本节摘要:blob 是 Git 对象库里最底层的证据单位——文件内容的原样快照。本节从一条最不起眼的命令
git hash-object出发,看清内容如何被 SHA-1 指纹唯一标识、为何相同内容只存一份、以及"指纹"这个比喻为什么在工程上站得住脚。
多数教程让你从 git init 开始,我们要先做一件更彻底的事:不建仓库,直接把一段内容塞进 Git 的对象库。在一个空目录里执行:
echo "hello detective" | git hash-object --stdin # 输出:ce8fea0fba3a90a8b0fbb5b1c7c1a2e19b3ff5f0
这一串 40 位十六进制就是 SHA-1 指纹。注意此刻我们还没执行 git init,上面只是"算了哈希但没存档"。加上 -w 参数(write)才能真正写入对象库,而写入需要先有仓库:
git init lab && cd lab echo "hello detective" | git hash-object -w --stdin # ce8fea0fba3a90a8b0fbb5b1c7c1a2e19b3ff5f0 find .git/objects -type f # .git/objects/ce/8fea0fba3a90a8b0fbb5b1c7c1a2e19b3ff5f0
证据出现了:对象库在 .git/objects 下,按哈希前两位分成子目录,剩下 38 位做文件名。这个文件的内容是 zlib 压缩的二进制,直接打开是乱码,得用 Git 自带的"验尸工具"读取:
git cat-file -t ce8fea0 # 对象类型:blob git cat-file -p ce8fea0 # 内容:hello detective git cat-file -s ce8fea0 # 大小:16 字节
三个子命令分别回答"它是什么、里面有什么、多大"——这就是侦探的验尸三连。从此以后,凡是你想知道 Git 内部到底存了什么,第一反应就该是 cat-file。
SHA-1 是单向哈希函数:输入任意内容,输出 160 位(40 个十六进制字符)摘要,输入变一个字节、输出就面目全非。试试看:
printf "hello detective" | git hash-object --stdin # 不带换行 printf "hello detective\n" | git hash-object --stdin # 带换行 # 两个输出完全不同
这带来第一个重要推论:blob 的身份完全由内容决定,与文件名、路径、时间戳统统无关。你可以做个实验:把同一段内容放进两个不同名字的文件,分别 git add,对象库里只会多出一个 blob。
printf "same content\n" > a.txt printf "same content\n" > b.txt git add a.txt b.txt git ls-files -s # 100644 3353b11b6d1a5b4cf42d1aa8b6ff6a1e5e0d7a22 0 a.txt # 100644 3353b11b6d1a5b4cf42d1aa8b6ff6a1e5e0d7a22 0 b.txt find .git/objects -type f | wc -l # 仍然只有 1 个新对象
git ls-files -s 列出暂存区里的登记项,两个文件指向同一个 blob 哈希。这个"相同内容去重"的特性在大型仓库里非常省钱:十份一模一样的配置文件只占一份对象的空间,分支再多,未变更的文件也从不重复存储。Git 的对象库因此更像一间按指纹归档的证物室,而不是按名字归档的档案柜。
git cat-file -p 输出的 blob 内容里没有文件名,也没有权限位。名字和权限记录在另一种对象——tree 对象里(下一节的主角)。这不是设计偷懒,而是职责分离:内容归 blob 管,结构归 tree 管。由此可以推出几个容易被新手误解的行为:
权限位变更不入档。 Git 只区分普通文件和可执行文件(100644 与 100755),chmod 644 之后 git status 毫无反应,因为 blob 内容没变,tree 里的模式位也没变。只有从"不可执行"翻到"可执行"才会被记录。
换行符会被动过手脚。 Windows 上默认配置 core.autocrlf=true 时,工作区是 CRLF、入库时转成 LF。也就是说 blob 存的内容可能和你磁盘上的字节不完全一致——同一份文件在两个不同配置的机器上算出的哈希可能不同。团队里如果有人抱怨"我什么都没改,diff 却显示整文件变更",第一怀疑对象就是换行符配置。
空目录在对象库中没有代言人。 blob 只能代表文件,tree 只在有内容时才会被创建,所以 Git 无法追踪空目录。想保留目录结构,惯例是放一个占位文件(常命名为 .gitkeep,其实叫什么都行——反正 Git 只认 blob 不认名字)。
看懂了 blob,git add 的真身就露出来了:它把工作区文件的内容写成(或复用)一个 blob 对象,并在暂存区的 index 文件里登记"这个路径对应这个哈希"。git add 并不提交任何东西,它只是采集证据、贴上指纹、放进证物袋。
⚠️ 常见坑:把
git add理解成"保存"。对象一旦写入就是只读的,"重新 add 同一个文件"会生成新 blob 并更新 index 里的登记,旧 blob 仍在对象库里,等待垃圾回收。所以反复修改再 add 不会丢任何中间状态——但也别指望它们出现在git log里,没有 commit 引用的对象是查不到的悬空证物。
还有一句直觉值得记牢:SHA-1 指纹的碰撞概率小到可以忽略——随机两个不同内容撞出同一哈希的概率约为 2 的 80 次方分之一量级。工程上可以把"哈希相同"与"内容相同"划等号,这也是 Git 用哈希做全局对象名、甚至跨机器同步对象时不需要中央编号的原因。
git cat-file -t 看类型、-p 看内容、-s 看大小,是勘查对象库的基本功。下一节我们看这些孤立的 blob 如何被 tree 组织成目录快照、再被 commit 挂到历史链上——证据将开始成卷。