← 首页

XversePro 知识库 · 工具

Git 的对象模型:blob、tree、commit

Git 用内容的 SHA-1 当键存三类对象,提交就是一棵指向文件快照的树。

结论:Git 的底层是一个按内容寻址的键值库。键是内容的 SHA-1 哈希,值是对象。blob 存文件内容,tree 存目录,commit 指向一棵根 tree 和上一个 commit。

分支只是一个写着 commit 哈希的文件。没改过的文件和目录在新提交里直接复用旧对象,每次提交只新增变了的那部分。

1. 内容寻址:哈希就是地址

《Pro Git》的 Git Objects 一节开篇就说,Git 的核心是一个简单的键值数据库。存进去一段内容,Git 返回一个键,以后用这个键取回内容。

这个键怎么来:Git 先拼一个头部,格式是类型名、空格、内容字节数、一个空字节(\0),再接上原始内容,对整体算 SHA-1。以文本 hello 加换行为例,内容 6 字节,参与计算的是 blob 6\0hello\n,结果是 ce013625030ba8dba906f756967f9e9ca394464a。

对象用 zlib 压缩后存成文件。路径取哈希前 2 个字符做子目录,后 38 个字符做文件名。内容相同,哈希就相同,同一份内容只存一次。

blob 6\0hello\n 头部(蓝色)加原始内容 ce013625030b…394464a SHA-1,40 个十六进制字符 .git/objects/ce/013625030b…394464a zlib 压缩后的对象文件,文件里不存文件名 SHA-1 存盘
图 1 一个 blob 的哈希与存放位置(示意)。哈希值由 git hash-object --stdin 实际计算,并用 shasum 对拼好的字节复算一致。

2. 三种对象各存什么

blob 只有文件内容,没有文件名。《Pro Git》特意指出这一点:文件名存在 tree 里。

tree 相当于一个目录清单。用 git cat-file -p 查看时,每一行是模式、类型、哈希和名字。模式 100644 是普通文件,100755 是可执行文件,120000 是符号链接,040000 是子目录。

commit 记录根 tree 的哈希、父提交的哈希(第一个提交没有父提交)、作者和提交者信息、时间戳,空一行后是提交说明。除了这三种,还有第四种对象 tag,用于附注标签,见 《Pro Git》的 Git References 一节。

类型内容示例(用 git cat-file -p 查看)
blob文件的原始字节hello
tree每行一个条目:模式、类型、哈希、名字100644 blob 94bab17… README.md
040000 tree 37a4b05… src
commit根 tree、父提交、作者、提交者、说明tree 33c0088…
parent 9009e36…
author demo …
tag附注标签:指向的对象、标签名、说明本文示例仓库未创建

表 1 四种对象。示例取自下一节的演示仓库,哈希只显示前 7 位。

3. 两次提交的对象图

下面是一个真实的小仓库。第一次提交有 README.md 和 src/main.py 两个文件。第二次提交只改了 README.md。

第二次提交生成了新的 README.md blob,也因此生成了新的根 tree。src 目录没变,它的 tree 哈希还是 37a4b05,两个根 tree 都指向同一个对象,main.py 的 blob 也没有重复存储。这就是没改的部分直接复用。

提交链靠 parent 串起来。改动历史上任何一个字节,那个对象的哈希就变,指向它的 tree 和 commit 的哈希也跟着变,所以历史很难被悄悄篡改。

committreeblob / 子 tree e45f2feedit readme 9009e36first commit 33c0088根目录(新) 1b94a78根目录(旧) 94bab17README.md(新) 37a4b05src/ 两次共用 b80e322main.py ce01362README.md(旧) parent
图 2 演示仓库的完整对象图,哈希取前 7 位。数据来自 git cat-file -p 和 git ls-tree 的实际输出。绿框是两次提交共用的对象。

4. 分支和 HEAD 只是指针

记 40 位哈希不现实,所以 Git 允许给哈希起名字,这些名字叫引用(ref),存在 .git/refs 目录下,见 《Pro Git》的 Git References 一节。分支 main 就是文件 .git/refs/heads/main,里面只有一行 commit 哈希。

HEAD 通常指向当前分支的名字,而不是直接指向 commit。提交时 Git 生成新的 commit 对象,再把当前分支文件里的哈希改成新值。所以新建分支几乎不花时间,只是多写一个小文件。

HEADrefs/heads/maine45f2fe9009e36 内容:ref: refs/heads/main内容:一行 commit 哈希parent
图 3 HEAD、分支和提交之间的指向关系(示意)。

5. 存储方式与哈希算法

每个对象一开始单独存成一个文件,叫松散对象(loose object)。对象多了以后,或者手动运行 git gc、推送到远端时,Git 会把它们打进打包文件(packfile)。打包时相似的对象只存一份完整内容,其余存差异,见 《Pro Git》的 Packfiles 一节。

SHA-1 已经不够安全。Git 官方文档 《hash-function-transition》提到,2017 年 2 月 23 日公开的 SHAttered 攻击做出了实际的 SHA-1 碰撞,Git 从 2.13.0 起默认改用能抵御这一攻击的加固版 SHA-1,同时在推进向 SHA-256 迁移。按 git init 文档,新建仓库时可以用 --object-format 选 sha1 或 sha256,默认仍是 sha1。

方面松散对象打包文件
存放每个对象一个文件,zlib 压缩许多对象合进一个 .pack,配一个 .idx 索引
相似内容各存完整一份一份完整,其余存差异
何时出现新写入的对象对象过多、git gc、推送时
对哈希的影响无。对象哈希只由类型、长度和内容决定,与怎么存无关

表 2 两种存储方式的对比,依据 《Pro Git》Packfiles 一节。

参考资料

  1. 《Pro Git》Git Internals: Git Objects https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
  2. 《Pro Git》Git Internals: Git References https://git-scm.com/book/en/v2/Git-Internals-Git-References
  3. 《Pro Git》Git Internals: Packfiles https://git-scm.com/book/en/v2/Git-Internals-Packfiles
  4. 《hash-function-transition》,Git 文档 https://git-scm.com/docs/hash-function-transition
  5. 《git-init》,Git 文档 https://git-scm.com/docs/git-init
  6. 《git-cat-file》,Git 文档 https://git-scm.com/docs/git-cat-file