XversePro 知识库 · 工具
Git 用内容的 SHA-1 当键存三类对象,提交就是一棵指向文件快照的树。
结论:Git 的底层是一个按内容寻址的键值库。键是内容的 SHA-1 哈希,值是对象。blob 存文件内容,tree 存目录,commit 指向一棵根 tree 和上一个 commit。
分支只是一个写着 commit 哈希的文件。没改过的文件和目录在新提交里直接复用旧对象,每次提交只新增变了的那部分。
《Pro Git》的 Git Objects 一节开篇就说,Git 的核心是一个简单的键值数据库。存进去一段内容,Git 返回一个键,以后用这个键取回内容。
这个键怎么来:Git 先拼一个头部,格式是类型名、空格、内容字节数、一个空字节(\0),再接上原始内容,对整体算 SHA-1。以文本 hello 加换行为例,内容 6 字节,参与计算的是 blob 6\0hello\n,结果是 ce013625030ba8dba906f756967f9e9ca394464a。
对象用 zlib 压缩后存成文件。路径取哈希前 2 个字符做子目录,后 38 个字符做文件名。内容相同,哈希就相同,同一份内容只存一次。
git hash-object --stdin 实际计算,并用 shasum 对拼好的字节复算一致。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.md040000 tree 37a4b05… src |
| commit | 根 tree、父提交、作者、提交者、说明 | tree 33c0088…parent 9009e36…author demo … |
| tag | 附注标签:指向的对象、标签名、说明 | 本文示例仓库未创建 |
表 1 四种对象。示例取自下一节的演示仓库,哈希只显示前 7 位。
下面是一个真实的小仓库。第一次提交有 README.md 和 src/main.py 两个文件。第二次提交只改了 README.md。
第二次提交生成了新的 README.md blob,也因此生成了新的根 tree。src 目录没变,它的 tree 哈希还是 37a4b05,两个根 tree 都指向同一个对象,main.py 的 blob 也没有重复存储。这就是没改的部分直接复用。
提交链靠 parent 串起来。改动历史上任何一个字节,那个对象的哈希就变,指向它的 tree 和 commit 的哈希也跟着变,所以历史很难被悄悄篡改。
git cat-file -p 和 git ls-tree 的实际输出。绿框是两次提交共用的对象。记 40 位哈希不现实,所以 Git 允许给哈希起名字,这些名字叫引用(ref),存在 .git/refs 目录下,见 《Pro Git》的 Git References 一节。分支 main 就是文件 .git/refs/heads/main,里面只有一行 commit 哈希。
HEAD 通常指向当前分支的名字,而不是直接指向 commit。提交时 Git 生成新的 commit 对象,再把当前分支文件里的哈希改成新值。所以新建分支几乎不花时间,只是多写一个小文件。
每个对象一开始单独存成一个文件,叫松散对象(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 一节。