Git Internals
Blob 对象与内容寻址教程
解释 blob 如何只按内容存储,以及哈希为什么成为对象身份。
- 想建立稳定 Git 心智模型的学习者
- 经常遇到历史、引用、恢复问题的开发者
- 会看基础命令输出
- 知道提交、分支、HEAD 这些名词
- 只背底层术语却不连接到实际命令
- 把对象、引用、工作区混成一层理解
引用与延伸阅读
学完这篇你会掌握什么
- 理解 blob 是什么:它只保存文件内容,不保存文件名、路径或历史
- 掌握内容寻址的含义:对象身份(哈希)由内容本身推导,而非外部编号
- 能用
git hash-object与git cat-file亲手验证「相同内容 → 相同 blob」 - 分清 blob、tree、commit 三者的分工,并据此解释 diff、重命名与去重现象
先想一个问题
你在两个目录里放了完全相同的 README.md,但 Git 只占用了「一份」存储;你把 a.txt 改名成 b.txt,Git 却说「没有改动内容」。为什么 Git 对「内容」和「文件名」分得这么清?读懂 blob 与内容寻址,这些现象背后的机制就一目了然了。
一句话理解
blob 是 Git 里最“纯粹”的对象类型之一:它只保存文件内容本身,不保存文件名、不保存路径,也不关心这个内容属于哪个分支。
1. 为什么先理解 blob
很多人第一次接触 Git internals,会下意识把"文件"和"Git 对象"当成一回事。
但 Git 其实把这两件事拆开了:
- blob 负责内容
- tree 负责结构
- commit 负责把 tree 固定成历史节点
只要这层拆分建立起来,很多底层行为都会突然变得容易理解。
2. blob 到底保存什么
blob 保存的是文件字节内容本身。
这意味着:
- 同样内容的两个文件,可以对应同一个 blob
- 文件名变了,不代表 blob 一定变了
- 路径变了,也不代表 blob 一定变了
Git 真正关心的是“内容长什么样”,不是“你给这个内容起了什么名字”。
3. 为什么 Git 说自己是内容寻址
Git 写入对象时,不是先随便分配一个编号,而是:
- 先取对象内容
- 给内容加上对象类型和长度头部
- 对这段数据计算哈希
- 用这个哈希作为对象身份
所以对象 ID 不是外部贴上的标签,而是内容本身推导出来的身份。
4. 相同内容为什么会得到相同 blob
因为 Git 对 blob 的身份判断只看内容。
例如:
README.mddocs/readme-copy.md
如果这两个文件内容完全一样,那么它们底层完全可能指向同一个 blob。
这也是 Git 能高效复用内容的原因之一。
5. blob 为什么不保存文件名
这是 Git 对象模型里非常关键的一次分工:
- blob 只管“内容是什么”
- tree 才管“这个内容在目录里叫什么名字、放在哪个位置”
这种拆分的好处是:
- 内容复用更自然
- 重命名和移动路径不必强行改变内容对象
- 对象模型更稳定
6. 从 blob 的角度看常见命令
git add
当工作区里的文件内容变化后,Git 最终会让这些内容朝“新的 blob / 新的暂存快照”靠近。
git commit
commit 不只是“记一条说明”,它会让一组内容最终通过 tree 和 commit 对象固化下来,其中底层文件内容就可能对应若干 blob。
git mv
如果只是改路径而内容不变,那么变化更多发生在 tree 结构层,而不一定是 blob 内容层。
7. blob 和“版本”不是同一个概念
一个 blob 只是某一份文件内容。
它本身不是“完整版本”,也不是“一个提交”。
真正能表达“某个时刻整个项目长什么样”的,不是 blob,而是:
- 一组 tree
- 一个根 tree
- 一个指向这个根 tree 的 commit
8. 为什么这能帮助你理解 diff 和历史
当你知道 Git 会把内容拆成 blob,对很多现象就不会再误会:
- 重命名不等于重新创造内容
- 同内容复用是自然结果
- 文件内容和目录结构是两个层次的问题
9. 用命令亲手验证内容寻址
光看概念容易忘。下面这几条底层命令,能让你真正「看见」一个 blob 是怎么被算出来的。
计算任意一段内容的 blob 哈希
# 只算哈希,不写入对象数据库
echo "hello git" | git hash-object --stdin
# 输出示例:b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0
# 写入对象数据库,再返回哈希
echo "hello git" | git hash-object --stdin -w
注意:哈希只由「内容」决定。哪怕只是把 hello git 改一个字母大小写(Hello Git),得到的哈希也完全不同。
相同内容 → 同一个 blob
mkdir blob-demo && cd blob-demo && git init -q
echo "same content" > a.txt
echo "same content" > b.txt
git add a.txt b.txt
git write-tree
用底层命令查看 tree,会看到两个文件其实指回同一个 blob 哈希——这是「内容寻址」最直观的证据:a.txt 与 b.txt 名字不同没关系,内容相同就复用同一个 blob。
git ls-tree $(git write-tree)
# 100644 blob <同一个哈希> a.txt
# 100644 blob <同一个哈希> b.txt
blob 里到底存了什么
git cat-file -t <上面的 blob 哈希> # 输出 blob
git cat-file -p <上面的 blob 哈希> # 输出 hello git
git cat-file -s <上面的 blob 哈希> # 输出内容字节数
对象就安静地躺在 .git/objects/ 下,路径由哈希前两位决定:
ls .git/objects/b6/ # 看到 fc4c620b67d95f953a5c1c1230aaab5db5a1b0 这个目录
- 新建一个空仓库,写两条内容相同、名字不同的文件并
git add - 用
git ls-tree $(git write-tree)确认两个文件指向同一个 blob 哈希 - 用
git cat-file -t / -p / -s比对同一个哈希,体会「内容即身份」
常见误区
blob 等于文件
不准确。blob 更准确地说是“文件内容对象”,而不是工作区里的那个文件实体。
同名文件一定对应同一个 blob
不一定。名字相同但内容不同,就是不同 blob。
改文件名一定会改 blob
不一定。只改名字、不改内容,往往主要影响 tree,而不是 blob。
一个最值得记住的结论
如果只记一句话,可以记:
blob 只回答“内容是什么”,从来不回答“它叫什么、放在哪里”。
给你的练习
- 在空仓库里用
git hash-object --stdin -w写入README.md内容,再用git cat-file -p读回来,确认看到的就是原始字节 - 故意把内容改一个字符,
git hash-object --stdin再算一次,对比哈希为什么变了 - 用
git ls-tree $(git write-tree)在「两个同名内容不同」和「内容相同名字不同」两种场景里,观察 blob 哈希是否复用
延伸阅读
沿着同一主题继续深入: