.git 文件结构
└── .git(版本库)
├── hooks目录 包含客户端或服务端的钩子脚本
├── info目录 包含一个全局排除文件
├── logs目录 保存日志信息
│ └── refs目录
│ └── stash文件 这里存放了所有stash入栈的信息
├── objects目录 存储所有数据内容,是git的数据库
│ ├── <hash前两位>/<hash剩余字符>
│ ├── info目录 存储Git 的一些特定操作生成的额外的对象信息
│ └── pack目录 存储新打包生成的更少的文件,以减小仓库的存储空间
├── refs目录 存储指向数据的提交对象的指针(分支)
│ ├── heads目录 存放分支名,每个分支文件的内容是一个commit-hash
│ │ ├── master文件 存放commit-hash
│ │ └── develop文件
│ ├── remotes目录 存放仓库别名
│ │ └── origin目录
│ │ └── master文件 存放commit-hash
│ ├── tags目录 存放版本名,每个版本文件的内容是一个commit-hash
│ │ └── v1.0文件
│ └── stash文件 这里存放了stash最后入栈的commit-hash
├── config文件 包含项目特有的配置选项
├── description文件 用来显示对仓库的描述信息
├── HEAD文件 指向目前被检出的分支,内部内容举例:ref:refs/heads/master,当处于分离HEAD状态时,是个commit对象对应的哈希值
└── index文件 保存暂存区信息(暂存区、索引)
git 两大核心概念
对象blob对象:存储文件内容tree对象:存储目录结构commit对象:存储提交信息
区域工作区暂存区版本库
blob对象(git对象)
git 的核心部分是一个简单的键值对数据库。你可以向该数据库插入任意类型的内容,会返回一个键值(哈希值),通过该键值可以在任意时刻再次检索该内容
blob对象可以通过底层命令git hash-object -w <file>创建
echo "hello world" | git hash-object --stdin [-w] # 将输入的内容计算哈希值,不加-w则不写入git数据库
# 输出示例:
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
git hash-object -w <file> #将文件内容计算哈希值,加-w会写入git数据库。
# 输出示例:
# 8b9a5477516339d8880604578f47122e57b45f7f
git hash-object <file> #将文件内容计算哈希值
# 输出示例:
# 8b9a5477516339d8880604578f47122e57b45f7f
find .git/objects -type f #列出git数据库的所有对象
# 输出示例:
# .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
# .git/objects/5e/9e67f0f5bc4a8e5ad3cf9e3073eed1f7f11d8b
查看内容或类型
git cat-file -t 3b18e512dba79e4c8300dd08aeb37f8e728b8dad #根据键值查看类型
# 输出示例:
# blob
# tree
# commit
git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad #根据键值查看内容
# 对于blob对象输出示例:
# hello world
# 对于tree对象输出示例:
# 040000 tree ad9a5477516339d8880604578f47122e57b45fdd bak
# 100644 blob 8b9a5477516339d8880604578f47122e57b45f7f README.md
# 100644 blob 5e9e67f0f5bc4a8e5ad3cf9e3073eed1f7f11d8b src/main.c
tree对象
构建tree对象
我们可以通过update-index、write-tree、read-tree等命令来构建
树对像并塞入到暂存区。
假设我们做了一系列操作之后得到一个树对象。
利用update-index命令为test.txt文件的首个版本创建一个
暂存区。并通过write tree命令生成tree对象。
示例:
echo 'hello world' > test.txt #创建文件
git hash-object -w test.txt #数据库写入blob对象。输出了3b18e512dba79e4c8300dd08aeb37f8e728b8dad
git update-index --add --cacheinfo 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad test.txt # 将git对象对应的内容写进test.txt文件,并加入暂存区
git write-tree #将暂存区的内容写入一个树对象,存到数据库里,并返回该对象的哈希值。这个哈希值可以用来标识该树对象,也可以用来创建一个新的提交对象。并不会清空暂存区。
git update-index --add --cacheinfo 并不能将未被版本库跟踪的内容对象(hash)直接添加到暂存区。想添加到暂存区,必须先生成blob对象存到数据库。
如果不用--cacheinfo命令,以上操作可以如下简写:
echo 'hello world' > test.txt #创建文件
git update-index --add text.txt
git write-tree
解释:
- 文件模式:
100644,这是一个普通文件;100:表示这是一个普通文件(regular file)。644:表示文件的权限设置(1 执行 2 写 4 读)。第一个数字(6)代表文件所有者的权限(读+写),后两个数字(44)分别代表组用户和其他用户的权限(仅读)。- 这是一个非可执行的普通文件。
100755,表示一个可执行文件;100:同样表示这是一个普通文件。755:第一个数字(7)代表文件所有者的权限(读+写+执行),后两个数字(55)分别代表组用户和其他用户的权限(读+执行)。- 这是一个可执行的普通文件。
120000,表示一个符号链接。120:表示这是一个符号链接(symbolic link)。000:对于符号链接来说,这个部分通常没有实际的意义,因为符号链接的权限是由它所指向的文件决定的
040000,这是一个目录(directory)040:实际上应该是八进制表示,但在这里通常写作 0100000(即十进制的 40000,但为了与 Git 的常规表示对齐,这里简化为 040000),表示这是一个目录(directory)。000:同样表示目录的权限,但目录的权限在 Unix/Linux 中主要用于控制对目录内容的访问,而不是对目录本身的访问。
add:因为此前该文件并不在暂存区中,首次添加需要加上cacheinfo:要添加的文件不是从工作区读取,而是从git数据库里某个blob对象读取,则需要加上
以下命令可以列出暂存区的文件信息
git ls-files -s # 不加-s,则仅输出文件名
# 输出示例:
# 100644 8b9a5477516339d8880604578f47122e57b45f7f 0 README.md
# 100644 5e9e67f0f5bc4a8e5ad3cf9e3073eed1f7f11d8b 0 src/main.c
-
-s或--stage: 在输出中显示暂存内容的模式位、对象名称和阶段编号。不加则仅输出暂存区的文件名:阶段编号(Stage Number):这通常是一个数字,表示文件在 Git 索引中的阶段。在正常的提交流程中,你通常只会看到阶段号为 0 的条目,这表示文件处于非冲突状态。然而,在解决合并冲突时,可能会看到多个阶段号(如 1、2、3),每个阶段号代表合并路径中的一个不同版本的文件。文件模式(Mode):同上解释。SHA-1哈希值:blob对象。
可以读取已有的tree对象,存到暂存区
git read-tree --prefix=bak a6a64baded89f229c2bbb4ae711b6477b9b5675b #将树对象存到暂存区
git ls-files -s # 列出暂存区的文件信息
# 输出示例:
# 100644 b3856de793e83d4decc64da893c5369af11b0638 0 bak/text1.txt
# 100644 608fe2f12aca4e255f425f2b79e0c1a6afecb903 0 text1.txt
# 100644 4761d3a6312cfb7fd8541b49892ead30933b2a26 0 text2.txt
commit对象
echo "commit desc v1" | git commit-tree a6a64baded89f229c2bbb4ae711b6477b9b5675b #写入commit对象到版本库
# 输出
# ed07d21240e43abba9d126abe43342c8d67e0e83
git cat-file -t ed07d21240e43abba9d126abe43342c8d67e0e83
# 输出
# commit
git cat-file -p ed07d21240e43abba9d126abe43342c8d67e0e83
# 输出
# tree a6a64baded89f229c2bbb4ae711b6477b9b5675b
# author caixukun <123456789@qq.com> 1720697222 +0800
# committer caixukun <123456789@qq.com> 1720697222 +0800
# commit desc v1
echo "commit desc v1" | git commit-tree <tree-hash> [-p <parent-commit-hash>] #写入commit对象到版本库,并指定父commit对象(第一次提交,没有父commit,不用写-p;)
# 合并提交时,[-p <parent-commit-hash>] 写两个
其输出格式是
tree <tree-hash>
parent <parent-commit-hash>
author <author-name> <author-email> <timestamp> +0000
committer <committer-name> <committer-email> <timestamp> +0000
<commit-message>
操作暂存区删除
git update-index --remove test.txt #如果指定的文件在索引中,但工作区丢失了,则会被移除。
git update-index --force-remove test.txt #从索引中删除文件,即使工作目录中仍有该文件。
列出可达的commit对象
git rev-list --all # 列出Git仓库中所有的commit哈希
git rev-list --author=<author> #列出指定作者的所有commit哈希
查看当前的分支名
git rev-parse --abbrev-ref HEAD # 查看当前分支名,“分离 HEAD”状态下,输出HEAD
# 对应的高层命令是
git branch --show-current # 查看当前分支名,“分离 HEAD”状态下,不输出
使用底层命令模拟高层命令
使用底层命令模拟高层命令 git add 和 git commit -m 的行为,并更新 HEAD 和当前分支的指向(假设当前分支名为 master):
# 将文件添加到暂存区
git update-index --add test.txt
# 写入树对象
tree_hash=$(git write-tree)
# 创建提交对象
commit_hash=$(echo "my commit" | git commit-tree $tree_hash)
# 这里git rev-parse --abbrev-ref HEAD 的结果是 HEAD,而不是master!
# 原因是HEAD并没有指向一个有效的分支引用(此时refs/heads/master文件不存在)
# 反而 git branch --show-current 打印了master
# 为了方便,直接简单写死了分支名
branch_name=master
git update-ref "refs/heads/$branch_name" "$commit_hash"
总结
blob对象是内容的快照。tree对象是项目结构的快照。commit对象是项目的一个版本(包含了各种描述信息)。
高层命令
对高层命令的内容大家都很熟悉了,简单挑一小部分复习一下:
#将修改添加到暂存区
git add ./
# 相当于调用了底层命令
git hash-object -w 文件名(修改了多少个工作目录中的文件此命令就要被执行多少次)
git update-index --add --cacheinfo <mode> <hash> <file>
#期间,写入git对象到版本库,并且更新到了暂存区
#将暂存区提交到版本库
git commit -m "注释内容"
git commit -a -m '注释内容' #将修改添加到暂存区,并提交到版本库,注意:未跟踪文件将不允许直接使用此命令,老老实实先用 git add
# 相当于调用了
git write-tree
git commit-tree
git status # 查看各文件状态
git diff # 比较工作目录和暂存区
git diff --staged # 比较暂存区和HEAD(不是版本库!!!经过原理的介绍,应该能明白版本库的数据一般会一直增加)
git diff HEAD # 比较工作目录和HEAD
git rm 文件名 # 删除文件,并提交到暂存区
git mv 文件名 新文件名 # 重命名文件,并提交到暂存区
文件状态有两种:
- 未跟踪
- 已跟踪
- 已暂存
- 已提交
- 已修改
HEAD
指向当前所在分支的尖端提交(commit),或者是一个指向某个具体提交的分离HEAD(detached HEAD)状态
# 进入分离HEAD状态的常见操作:
git checkout <commit-hash> # 指向某个具体的提交
git checkout <tag-name> # 指向某个具体的tag
git checkout <remote-name> # 指向某个具体的远程跟踪分支
#自动合并方式时内容产生冲突,等等
分支
本质:指向尖端 commit 对象的可变指针。
分支就是一个没有后缀名的文件,里面存放着一个 commit对象所对应的hash
git log --oneline --decorate --graph --all #查看所有的分支下的提交
git branch #查看所有分支
git branch 分支名 #创建分支
git branch -m 分支名 新分支名 #重命名分支
git branch -M 分支名 新分支名 #强制重命名分支
git branch -v #查看所有分支的最新 commit
git branch 分支名 哈希名 #基于某个 commit 创建分支
git branch --merged # 查看所有已经合并到当前分支的本地分支(一般都应该删除了,没啥用了)。但是,如果远程分支a有新提交没合并到当前分支,只是track它的本地分支a内容未同步过来(所以还是处于已合并到当前分支的状态),则本地分支a依旧会出现在这里。
git branch --no-merged #查看所有尚未合并到当前分支的分支
git checkout 分支名 #切换分支
git checkout -b 分支名 #创建并切换分支
git log --grep "啦啦啦" # 查看提交历史描述中包含 "啦啦啦" 的提交
git log -S "getUserInfo" [file] #查看提交历史内容中包含 "getUserInfo" 的提交
git log -2 # 查看最近2次提交
git log <file> # 查看某个文件的历史提交
git log -p <file> # 查看某个文件的历史提交,并显示每次提交的差异
git log --stat # 查看提交历史,并显示每次提交的文件修改统计信息
git log --oneline --decorate --graph --all # 查看提交历史,并显示分支和合并情况
git log --pretty=format:"%h - %an, %ar : %s" # 查看提交历史,并自定义输出格式
git config --global alias.loa "log --oneline --decorate --graph --all" #设置别名
git 切换分支的处理:
- 切换分支会改变三个地方: HEAD,工作区,暂存区
- 在当前分支 a 中,如果新增了文件在工作区未暂存,或者该新增文件已暂存但是从未提交,切换其他分支时可以成功切换,但是该文件在工作区和暂存区的情况会同步过来。
- 如果该文件已提交,对其做了相应修改后,未提交到版本库,切换分支不会成功。
最佳实践是:切换分支时,当前分支一定是干净的(所有文件都是已提交状态)。
merge
- fast-forward 快进合并,是不会产生冲突的
- auto-merging 自动合并,可能会产生冲突。产生冲突时,当前分支名变成了
当前分支名|MERGING
git merge --abort # 取消合并
后悔药
#工作区(Working Directory)
git checkout -- <file> #只动工作区,将其文件内容重置成暂存区的文件内容
#暂存区(Index)
git reset HEAD <file> #只动暂存区,将其文件内容重置为HEAD的文件内容
#HEAD(实际上版本库只是新增一个commit对象,并不会撤回,只是log日志好看了而已)
git commit --amend #修改HEAD最新提交分支的描述信息
# commit --amend原理就是使用了 git reset --soft HEAD~,重新提交一次
# 所以,下面这种操作可以修改东西,覆盖log最新的提交:
git add .
git commit --amend
git reset --soft <hash> #只动HEAD,指针移到指定版本,没有动Index 和 Working Directory
git reset [--mixed] <hash> #动HEAD、Index,不改变Working Directory
git reset --hard <hash> #动HEAD、Index、Working Directory (可以使用跨分支的commit提交进行重置)
#如果不写<hash>,则默认是HEAD
git reset的文件重置
# 带文件的重置都无法动HEAD区
git reset [--mixed] HEAD <file> #只动暂存区,将暂存区的文件重置为HEAD的文件(deprecated; use 'git reset -- <paths>' instead.)
# 可以写成 git reset <file>
#好奇的是,git reset [--mixed] HEAD~ <file>,虽然感觉没啥意义,但此效果该如何替代?
#只有--mixed有文件重置命令。
checkout
git checkout <commit hash> # 切换到分离的 commit 提交
- 这个命令里,git 会将 HEAD 指针移动到指定的提交上,更新工作目录和暂存区。
- 此时将不再处于任何分支上,而是处于“分离 HEAD”(detached HEAD)状态。
- 在这种状态下,如果你提交了新的更改,这些更改不会属于任何分支。
- 如果没有被合并到某个分支中,可能会在未来的 Git 操作中丢失,因为
git gc(垃圾回收)机制可能会清理掉这些不可达的提交。 - 如果你想要基于某个历史提交创建一个新的分支并继续工作,你可以在执行
git checkout <commit hash>之后,使用git switch -c <new-branch-name>或git checkout -b <new-branch-name>(如果你还在使用较旧的Git版本)来创建一个新的分支。
git checkout也可以进行文件的版本重置
git log <file> #列出文件的历史版本
git checkout <hash> <file> #动暂存区、工作区,功能就类似伪命令: git reset --hard <hash> <file>,只不过reset --hard这个命令不能跟文件名,会报错
tag
简单列举几个常见命令
git tag #查看标签
git tag 标签名 #创建标签
git tag -l "标签名*" #查看符合规则的标签
git tag -d 标签名 #删除标签
远程协作
- 本地分支
- 远程跟踪分支
- 远程分支
本地分支 track 远程跟踪分支
假设本地分支是 master,远程仓库别名是 origin,当创建并关联了远程分支 master (git push -u)后,
本地会存在 远程跟踪分支 origin/master,是本地分支跟远程分支交互的媒介。
(特殊:git checkout origin/master 进入此分支,将进入分离头部状态)。
git fetch 时,数据拉取到了远程跟踪分支 origin/master上,不会自动合并到本地分支 master。
之后通过 git merge origin/master,将内容合并到本地分支 master。
这两步就相当于 git pull.
在 git clone 的时候,本地分支是跟远程跟踪分支有同步关系的(已关联)。
要设置本地分支 track 一个远程跟踪分支,用命令
git branch -u 远程跟踪分支
如果远程分支不存在,此时此命令是执行不了的,需要先建立远程分支:
git push -u 远程仓库别名 本地分支
当想要新建一个本地分支并且直接 track 某个远程跟踪分支时,用
git branch 分支名 远程跟踪分支 #也能实现track效果
git checkout -b 分支名 远程跟踪分支 #如果分支名跟远程分支名同名,等同于
git checkout --track 远程跟踪分支
git branch -r # 列出本地所有的远程跟踪分支
git ls-remote --heads 远程仓库别名 # 列出远程仓库的所有分支
git remote prune 远程仓库别名 --dry-run #查看仍在远程跟踪但是远程分支已经不在的分支
git remote prune 远程仓库别名 #清除上面命令列出来的远程跟踪分支