.git/objects 里那些名字像乱码的文件到底是什么?这篇用 zlib 把松散对象逐个解压,看原始字节:blob 的头部与内容、tree 里没有分隔符的二进制条目、commit 的纯文本结构,最后不用 git 命令,自己从 HEAD 走到每个文件。
$ git log --oneline
f9b1362 改一下 README
4591541 第一次提交
$ find .git/objects -type f | sort
.git/objects/01/52b8f966255cffca1c083a341f7c0d5809730b
.git/objects/25/7d8da2938464704dc06cfd2408017a1a607b66
.git/objects/37/a4b05693857e190d1ecbe45b4451bc36078274
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/45/91541c3541d409dc8b0a6c156d7566e88fd194
.git/objects/b8/0e3222ab264bd7cafb376749bd18814fd66776
.git/objects/f0/e9ea9cdcd1c7d022373365088a65e94c5ab13e
.git/objects/f9/b1362295e9b51a81b4adc680753b8f7e640a0f
$ find .git/objects -type f | wc -l
8每个对象是 .git/objects/ 下的一个文件,路径由 hash 决定:前 2 位十六进制当目录名,后 38 位当文件名。内容相同的对象 hash 相同,所以只存一份——第二次提交没碰 src/,它的 tree 与 blob 就直接复用,对象总数才只有 8 个。
src/main.py 的 blob = b80e3222ab264bd7cafb376749bd18814fd66776
文件 .git/objects/b8/0e3222ab264bd7cafb376749bd18814fd66776 的大小: 28 字节(压缩后);解压后 20 字节:
0000 62 6c 6f 62 20 31 32 00 70 72 69 6e 74 28 22 68 blob 12.print("h
0010 69 22 29 0a i").
repr: b'blob 12\x00print("hi")\n'
$ git cat-file -t b80e3222ab264bd7cafb376749bd18814fd66776; git cat-file -s b80e3222ab264bd7cafb376749bd18814fd66776; git cat-file -p b80e3222ab264bd7cafb376749bd18814fd66776
blob
12
print("hi")松散对象是 zlib 压缩的。解压后固定是“类型 空格 长度 NUL 内容”,blob 的内容就是文件的原始字节,不带文件名,也不带权限。很小的文件经过头部和压缩的固定开销,磁盘上的文件可以比内容还大。
根 tree = 0152b8f966255cffca1c083a341f7c0d5809730b,解压后 75 字节: 0000 74 72 65 65 20 36 37 00 31 30 30 36 34 34 20 52 tree 67.100644 R 0010 45 41 44 4d 45 2e 6d 64 00 f0 e9 ea 9c dc d1 c7 EADME.md........ 0020 d0 22 37 33 65 08 8a 65 e9 4c 5a b1 3e 34 30 30 ."73e..e.LZ.>400 0030 30 30 20 73 72 63 00 37 a4 b0 56 93 85 7e 19 0d 00 src.7..V..~.. 0040 1e cb e4 5b 44 51 bc 36 07 82 74 ...[DQ.6..t 头部: tree 67 条目 1: 模式=100644 名字=README.md 20 字节=f0e9ea9cdcd1c7d022373365088a65e94c5ab13e 条目 2: 模式=40000 名字=src 20 字节=37a4b05693857e190d1ecbe45b4451bc36078274 $ git ls-tree 0152b8f966255cffca1c083a341f7c0d5809730b 100644 blob f0e9ea9cdcd1c7d022373365088a65e94c5ab13e README.md 040000 tree 37a4b05693857e190d1ecbe45b4451bc36078274 src
tree 对象把目录里每个条目写成“模式 空格 名字 NUL 20 字节二进制 hash”,条目首尾相接。目录模式在这里是 40000,git ls-tree 显示时才补成 040000。因为 hash 是原始字节,直接用文本编辑器看 tree 对象会是乱码。
HEAD = f9b1362295e9b51a81b4adc680753b8f7e640a0f,解压后 226 字节: 头部: commit 215 | tree 0152b8f966255cffca1c083a341f7c0d5809730b | parent 4591541c3541d409dc8b0a6c156d7566e88fd194 | author Alice <alice@example.com> 1767261900 +0000 | committer Alice <alice@example.com> 1767261900 +0000 | | 改一下 README | $ git cat-file -p HEAD tree 0152b8f966255cffca1c083a341f7c0d5809730b parent 4591541c3541d409dc8b0a6c156d7566e88fd194 author Alice <alice@example.com> 1767261900 +0000 committer Alice <alice@example.com> 1767261900 +0000 改一下 README
commit 对象是几行头部加空行加提交说明:tree、parent(可以有零个、一个或多个)、author、committer。所谓“提交”就是这样一段文本,它指向某个目录快照和上一个提交。
.git/HEAD: ref: refs/heads/main
refs/heads/main: f9b1362295e9b51a81b4adc680753b8f7e640a0f
commit 的 tree 行: 0152b8f966255cffca1c083a341f7c0d5809730b
Python 走出来的:
100644 blob f0e9ea9cdcd1c7d022373365088a65e94c5ab13e README.md
100644 blob b80e3222ab264bd7cafb376749bd18814fd66776 src/main.py
与 git ls-tree -r HEAD 完全一致: TrueHEAD 是一个符号引用,指向 refs/heads/main 这个文件,文件里是 commit 的 hash;commit 里的 tree 行、tree 里的条目一层层往下,就能走到每个 blob。整个 git 的读取就是这条链。