Git Internals

Blob 对象与内容寻址教程

解释 blob 如何只按内容存储,以及哈希为什么成为对象身份。

适合谁看
  • 想建立稳定 Git 心智模型的学习者
  • 经常遇到历史、引用、恢复问题的开发者
前置知识
  • 会看基础命令输出
  • 知道提交、分支、HEAD 这些名词
常见风险
  • 只背底层术语却不连接到实际命令
  • 把对象、引用、工作区混成一层理解

引用与延伸阅读

  1. Git Internals Git Objects [书籍]

学完这篇你会掌握什么

  • 理解 blob 是什么:它只保存文件内容,不保存文件名、路径或历史
  • 掌握内容寻址的含义:对象身份(哈希)由内容本身推导,而非外部编号
  • 能用 git hash-objectgit 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 固定成历史节点

只要这层拆分建立起来,很多底层行为都会突然变得容易理解。

Blob → Tree → Commit 的链接关系文件内容先编码为 blob 对象,再由 tree 对象组织成目录结构,最终被 commit 对象指向,形成完整的快照。
内容层
blob: 文件 A 内容blob: 文件 B 内容blob: 文件 C 内容
结构层
tree: 根目录(含子目录)sub-tree: src/ 目录commit: 快照 + 作者 + 父提交
相同内容的文件会复用同一个 blob,blob 不保存文件名和路径。

2. blob 到底保存什么

blob 保存的是文件字节内容本身。

这意味着:

  • 同样内容的两个文件,可以对应同一个 blob
  • 文件名变了,不代表 blob 一定变了
  • 路径变了,也不代表 blob 一定变了

Git 真正关心的是“内容长什么样”,不是“你给这个内容起了什么名字”。

3. 为什么 Git 说自己是内容寻址

Git 写入对象时,不是先随便分配一个编号,而是:

  1. 先取对象内容
  2. 给内容加上对象类型和长度头部
  3. 对这段数据计算哈希
  4. 用这个哈希作为对象身份

所以对象 ID 不是外部贴上的标签,而是内容本身推导出来的身份。

4. 相同内容为什么会得到相同 blob

因为 Git 对 blob 的身份判断只看内容。

例如:

  • README.md
  • docs/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.txtb.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 这个目录
亲手验证一次内容寻址
  1. 新建一个空仓库,写两条内容相同、名字不同的文件并 git add
  2. git ls-tree $(git write-tree) 确认两个文件指向同一个 blob 哈希
  3. git cat-file -t / -p / -s 比对同一个哈希,体会「内容即身份」

常见误区

blob 等于文件

不准确。blob 更准确地说是“文件内容对象”,而不是工作区里的那个文件实体。

同名文件一定对应同一个 blob

不一定。名字相同但内容不同,就是不同 blob。

改文件名一定会改 blob

不一定。只改名字、不改内容,往往主要影响 tree,而不是 blob。

一个最值得记住的结论

如果只记一句话,可以记:

blob 只回答“内容是什么”,从来不回答“它叫什么、放在哪里”。

给你的练习

  1. 在空仓库里用 git hash-object --stdin -w 写入 README.md 内容,再用 git cat-file -p 读回来,确认看到的就是原始字节
  2. 故意把内容改一个字符,git hash-object --stdin 再算一次,对比哈希为什么变了
  3. git ls-tree $(git write-tree) 在「两个同名内容不同」和「内容相同名字不同」两种场景里,观察 blob 哈希是否复用

延伸阅读

沿着同一主题继续深入: