Git 的 .git/objects 里到底藏了什么?

大部分人对 Git 的理解停在 git addgit commitgit push 这个流水线上,但如果你 ls .git/objects/ 看一眼,会发现一堆两级目录——前两位是 SHA-1 的前缀,剩下的在子目录里。这些就是 Git 的全部家当,所谓 content-addressable storage 的实体。

核心只有三种对象。blob 存文件内容,纯原文,连文件名都没有。tree 存目录结构,每条记录指向一个 blob 或子 tree,附带文件名和权限。commit 存快照元信息:一个 tree 指针、零或多个 parent 指针、作者信息和提交消息。所谓分支不过是 .git/refs/heads/main 里的一行四十字符,指向某个 commit 而已。

值得看的是压缩方式。Git 不存裸文本,而是用 zlib 的 deflate 压缩。随便找个文件用 python -c "import zlib, sys; sys.stdout.buffer.write(zlib.decompress(open(sys.argv[1],'rb').read()))" .git/objects/xx/yyyy… 就能还原。你会看到开头是一行 header,格式是 类型 长度\0,比如 blob 158\0,然后是原始内容。这就是 Git 的寻址方式——对 header+content 算 SHA-1,前两位做目录名,后 38 位做文件名,内容 zlib 压缩后写入。碰撞概率在可预见的未来可以忽略,所以这个哈希就是对象的唯一身份证。

再往深处走一层:packfile。当你有上千次提交时,松散对象泛滥,git gc 会把它们打包成 .git/objects/pack/ 下的 .pack.idx 文件。pack 内部对相似对象做 delta 压缩——只存与前一版本的差异,解压时链式还原。这也是为什么 git clone 大仓库比想象中快,网络传输的是 pack 格式而不是逐个对象。



下次用 git cat-file -pgit ls-tree 查看对象时,想想这些命令背后不过是对一个 zlib 压缩文件的解压和解析。整个版本控制系统建立在三个朴素的数据结构和一个哈希函数之上,没有数据库,没有服务器——这个设计判断本身就是最大的工程智慧。

#CS