Git中文社区

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.js

git add 做的事情就是:把文件内容写成 blob 对象,然后更新 index 中该文件对应的哈希。git commit 则是根据 index 生成 tree 对象,再创建 commit 对象。

数据完整性与垃圾回收

没有被任何引用(包括 reflog)指向的对象称为悬空对象。执行 git gc 时,超过一定期限的悬空对象会被清理。这就是为什么被 reset 掉的提交在一段时间内仍能通过 reflog 找回。

# 检查仓库完整性并列出悬空对象
git fsck --lost-found

延伸阅读