Git学习笔记——4. 底层原理
仓库配置信息
.git/logs/
和.git/info/
,顾名思义是一些日志和辅助信息。
.git/hooks/
存储一些 Git
提供的钩子脚本的模板,例如(pre-push.sample
),可以创建名为pre-push
的钩子脚本,这个脚本在特定行为(push
之前)会自动执行,其他名称的钩子也是类似的。
.git/config
是仓库级的 Git
配置文件,优先级最高,但是配置只是局限在当前仓库中,可能包括如下内容:
1 | [core] |
可以发现这里的配置还包括了远程仓库的信息,以及当前仓库和远程仓库的绑定关系。
数据存储原理
在.git/
文件夹内部包括一个基于哈希值的简易数据库.git/object/
,关于这个内部数据库,有如下要点:
- Git对文件的保存都是基于单个文件的完整快照,而不是基于差异(这也导致
Git
不适合大文件的版本控制),每一个文件快照在
git add
之后(而不是提交时)都会压缩和生成哈希值,然后以哈希值为索引存储到数据库之中,这种数据项称为 blob 对象 - 数据库中除了基本的 blob 对象(对应一个文件快照),还有体现目录结构的 tree 对象,以及代表一次提交的 commit 对象等,所有的项都以哈希值作为在数据库的索引,并且哈希值的前两个作为文件夹名称,剩下的作为实际的文件名,查找时不需要提供完整的哈希值,通常前六个数据就足够了
- 数据库除了这些原始的零散文件,还会不定期地进行自动的整理(在推送到远程时必然会先整理数据库),此时这些对象会被打包收集到
.git/object/pack/
中,一些打包信息在.git/object/info/
,打包是数据库内部的优化行为,对外部没有影响 - Git
可能因为某些撤销操作,产生悬挂对象(没有任何对象指向它),包括对
git add
的撤销,或者对正式提交后的回滚等,Git 并不会自动删除这些悬挂对象,即使手动清理也不会删除它们。可以使用如下命令查询所有的悬挂对象1
git fsck --full
index 原理
Git 的一个核心概念是
index(缓冲区,暂存区),它在逻辑上位于工作目录和 Git
版本控制之间,index
的实现是通过内部文件.git/index
,它是二进制格式的文件,不可读。
index
可以被视作一个列表,列表里的每一条目包括:权限+哈希值+名称,可以通过git ls-files --stage
命令查看,结果大致形如
1 | 100644 0c3acb5db2e121d819f1af9c0b97b8baaa12021b 0 a.txt |
查看到的结果可能是一个很大的列表,包括了所有的被 Git 追踪的项,它们的最新快照的哈希值。这个命令显示最终每一层目录下的所有文件,但仍然可以理解为 index 中存在树结构。
index 是下一次提交时用于生成顶层 tree 对象的草稿(提交生成 commit
对象指向顶层的 tree 对象),但注意它记录的内容并不是当前相比于前一次
commit
的变化量,即使刚刚提交后git status
显示完全干净时,index
也不是空的!它的列表仍然记录所有的项 。
在每一次执行git add
之后,对应的快照会立刻存储到数据库中,生成相应的
blob 对象(必要时生成 tree 对象),然后更新 index:
- 对于新添加的文件,index 增加一个条目
- 对于修改的文件,index 修改哈希值为最新版本的
- 对于删除的文件,index 移除对应的条目
提交原理
使用git commit
操作会基于 index
的内容生成本次提交所对应的顶层 tree 对象和 commit 对象,commit
对象会指向顶层 tree 对象。所有的 commit
对象会组成一个版本链表,这是 Git
版本控制的核心数据结构。版本链表包括一个特殊的第一次提交的
commit,其他的 commit 对象都包括至少一个父对象(如果执行了 merge
操作则可能有两个父对象,但是它们不等价,执行 merge 时所在的 commit
对象是第一个父对象)
可以使用git cat-file -p
加上哈希值来查看具体对象的内容(-t
选项查看对象的类型),例如查看
commit 对象,得到的输出为
1 | tree 0e9f335d73965f44ea4772ce4126ab219de247fd |
这表明 commit 对象除了记录提交的信息(时间,作者/提交者,提交 message),还记录了顶层的 tree 对象,以及版本链表中的 commit 父对象,这就是一次提交的全部:
- index 的作用就是下一次提交的顶层 tree 对象的草稿
- 下一次提交的其他信息(时间,作者/提交者)会自动生成
- 提交所需要的 message 需要手动输入
如果查看 commit 指向的顶层 tree 对象,就可以得到对应的提交版本中每一个项的快照的哈希值
1 | 100644 blob fd1240c913208228cc9e9f22d870da226ffc9b46 .gitattributes |
分支原理
分支和标签功能的内部实现以文件形式存储在.git/refs/heads/
和.git/refs/tags/
中,它们在逻辑上是指向版本链表的某个
commit
对象的指针,区别在于标签不可移动而分支可以,并且分支会随着提交(链表的延伸)自动前移到最新的
commit 对象。
例如在.git/refs/heads/
通常有一个名为 main
的文件,代表主分支,它的内容只有一行,记录当前 main 分支指向的 commit
对象的哈希值,例如
1 | 5a0ca82eb0072623067f1d7dbd734354d72f9441 |
HEAD 是一个通常指向 branch 的指针(也可以直接指向标签,commit
对象或者其他,但这种游离的情况比较特殊且危险),HEAD
代表当前处于的位置,HEAD
在底层实现就是.git/HEAD
,这个文本文件只有一行,记录的就是
HEAD 指向的 branch 的名称,例如
1 | ref: refs/heads/main |
执行 commit 操作后,会生成一个新的 commit 对象指向当前的 commit 对象,而 HEAD 指向的 branch(例如 main)则会前移到新的 commit 对象上。如果 HEAD 指向的不是一个 branch,例如指向一个标签,此时提交后并不会自动前移到新的 commit 对象,需要额外的特殊处理。
分支的合并和变基都是针对版本链表进行的操作,可以通过git log --graph --all
进行检查。