Git 内部原理
从根本上讲,Git 是一个内容寻址文件系统,并在此之上提供了版本控制的用户界面。理解它的内部原理,能让你真正明白每个命令在做什么。
.git 目录结构
.git/
├── HEAD # 指向当前分支
├── config # 仓库级配置
├── description # 供 GitWeb 使用
├── hooks/ # 钩子脚本
├── index # 暂存区
├── info/ # 全局排除规则 exclude
├── logs/ # reflog 记录
├── objects/ # 所有数据对象
└── refs/ # 分支、标签、远程跟踪分支的指针
├── heads/
├── tags/
└── remotes/其中最核心的是 objects(存储所有内容)、refs(存储指向提交的指针)、HEAD(指向当前分支)和 index(暂存区信息)。
四种对象
Git 的对象数据库中只有四种对象:
| 对象 | 存储内容 | 类比 |
|---|---|---|
| blob | 文件内容(不含文件名) | 文件 |
| tree | 目录结构:文件名、权限以及指向 blob / tree 的指针 | 目录 |
| commit | 指向顶层 tree 的指针、父提交、作者、提交说明 | 快照 |
| tag | 指向某个对象的指针、标签名、打标签者、说明 | 附注标签 |
每个对象都以其内容的 SHA-1 哈希命名。相同内容的文件,无论出现在多少个提交中,都只存储一份。
动手查看对象
# 查看对象类型
git cat-file -t HEAD
# commit
# 查看提交对象内容
git cat-file -p HEAD
# tree 8f94139338f9404f26296befa88755fc2598c289
# parent 3c4e9cd789d88d8d89c1073707c3585e41b0e614
# author 张三 <[email protected]> 1742219531 +0800
# committer 张三 <[email protected]> 1742219531 +0800
#
# 修改版本号
# 查看 tree 对象
git cat-file -p HEAD^{tree}
# 100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README.md
# 040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 src手动创建一个 blob
echo "hello git" | git hash-object -w --stdin
# 8d0e41234f24b6da002d962a26c2495ea16a425f
git cat-file -p 8d0e41234f24b6da002d962a26c2495ea16a425f
# hello git对象被 zlib 压缩后存储在 .git/objects/8d/0e41234f...,前两位是目录名,后 38 位是文件名。
对象之间的关系
一次提交的对象关系如下:
commit ──▶ tree (根目录)
├── blob README.md
└── tree src/
├── blob index.js
└── blob app.js当你修改 src/app.js 并提交后,Git 会创建:一个新的 app.js blob、一个新的 src tree、一个新的根 tree 和一个新的 commit。而没有变化的 README.md 和 index.js,直接复用原来的 blob。
引用(References)
记住 SHA-1 太难了,所以 Git 用引用给提交起名字。引用就是存储在 .git/refs 下、内容为一个哈希值的简单文件:
cat .git/refs/heads/main
# ca82a6dff817ec66f44342007202690a93763949- 分支:
refs/heads/<分支名> - 标签:
refs/tags/<标签名> - 远程跟踪分支:
refs/remotes/<远程名>/<分支名>
HEAD 通常是一个符号引用,指向另一个引用:
cat .git/HEAD
# ref: refs/heads/main所以"提交"这个动作本质上就是:创建 commit 对象,然后把 HEAD 所指向的分支文件内容更新为新的哈希值。
包文件(Packfiles)
每个文件的每个版本都完整保存为一个对象,看起来很浪费空间。实际上,Git 会定期(或在推送时)把松散对象打包成包文件,并对相似的对象只保存差异(delta),大大压缩体积:
# 手动触发打包和垃圾回收
git gc
# 查看仓库对象统计
git count-objects -vH暂存区的真面目
.git/index 是一个二进制文件,记录了下次提交时的目录结构:每个文件的路径、权限、对应的 blob 哈希以及时间戳等信息。
git ls-files --stage
# 100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README.md
# 100644 8d0e41234f24b6da002d962a26c2495ea16a425f 0 src/app.jsgit add 做的事情就是:把文件内容写成 blob 对象,然后更新 index 中该文件对应的哈希。git commit 则是根据 index 生成 tree 对象,再创建 commit 对象。
数据完整性与垃圾回收
没有被任何引用(包括 reflog)指向的对象称为悬空对象。执行 git gc 时,超过一定期限的悬空对象会被清理。这就是为什么被 reset 掉的提交在一段时间内仍能通过 reflog 找回。
# 检查仓库完整性并列出悬空对象
git fsck --lost-found