10 --- 对象模型:.git/objects 里到底装了什么
摘要: 本章深入解析 Git 对象模型,揭示 .git/objects 目录中哈希文件的本质。你将理解 Git 存储的四种核心对象:blob(文件内容)、tree(目录结构)、commit(提交)和 tag(标签),掌握它们如何通过内容寻址构成不可变的快照链。通过动手实践 git cat-file 等底层命令,你将直观看到「改一字节即生新对象」的原理,理解路径信息存储在 tree 而非 blob 中,并建立「Git 历史是对象链而非差异日志」的思维模型。最终,你将能亲手查看对象、绘制对象关系图,并为后续理解 Git 命令如何操作对象打下坚实基础。
写在前面:这一章要解决什么
上一章你打开了 .git,看到 objects/ 里有一堆哈希命名的文件。这一章就告诉你 那堆东西是什么------
- 提交(commit)到底是什么
- 为什么说「Git 存的是快照不是差异」
- blob、tree、commit、tag 四类对象各装什么
- 怎么用
git cat-file亲自看
学完后,你应该能:
- 用大白话解释四类对象
- 用
git cat-file亲手查看对象内容 - 理解「改一字节就变新哈希」
- 不再觉得 Git 历史是玄学
读者设定: 大一同学,看过 .git 地图,对「哈希」有概念即可。
1. 定位:为什么要讲对象
1.1 一句话先记住
Git 的历史就是四种对象拼起来的:blob(文件内容)、tree(目录结构)、commit(提交)、tag(附注标签)。
每类对象都有自己的格式,用一个哈希当「身份证号」。
1.2 看了对象能懂什么
| 不看 | 看了 |
|---|---|
| 提交是「一次改动日志」 | 提交指向当时整棵目录树 |
| 改文件就改了提交 | 改文件 = 新 blob + 新 tree + 新 commit |
| 分支是文件夹拷贝 | 分支只是指向某 commit 的名字 |
rebase/cherry-pick 像魔法 |
都是对象复制 + 重连 |
1.3 和你已经会的对比
| 你已经会的 | 对象对应 |
|---|---|
| 文件内容 | blob(只存内容,不存路径) |
| 文件夹里的清单 | tree(路径 → 哪个 blob) |
| 一次存档 + 说明 | commit(指向 tree + 作者 + 说明) |
| 给存档贴标签 | tag(附注标签是个对象) |
1.4 本章内容目标
| 目标 | 你能做到 |
|---|---|
| 说清四类 | 用白话讲清 blob/tree/commit/tag |
| 查看 | 用 cat-file 亲手看对象 |
| 理解哈希 | 改一字节 = 新对象 |
| 画图 | 画一次提交的对象关系图 |
2. 本质:四类对象各装什么
2.1 一张图先建立感觉

图:commit 指向 tree,tree 列出路径 → blob。分支名指向 commit。
2.2 用白话拆开四类
| 类型 | 装什么 | 谁的身份证 |
|---|---|---|
| blob | 文件的内容字节(不含路径、不含名字) | 内容的哈希 |
| tree | 目录清单:哪个路径对应哪个 blob/tree | 清单内容的哈希 |
| commit | 指向一棵 tree + 父提交 + 作者 + 说明 | 提交内容的哈希 |
| tag | 指向某对象 + 标注者 + 说明(附注标签) | 标签对象内容的哈希 |
2.3 关键性质
| 性质 | 白话 |
|---|---|
| 内容寻址 | 哈希是根据内容算的;同内容 = 同哈希 |
| 不可变 | 改一字节就变新对象;旧对象还在 |
| 去重 | 同样内容的 blob 只存一份 |
| 路径在 tree 里 | blob 不知道自己叫啥 |
2.4 几条硬规矩
| 规矩 | 白话 |
|---|---|
| 哈希是身份证 | 同内容同哈希;不同内容必不同 |
| 提交指向 tree,不是直接指向文件 | 「快照」结构 |
| 改文件 = 新对象链 | blob → tree → commit 都变 |
| 轻量标签不是对象 | 只是个引用;附注标签才是对象 |
2.5 新手最常踩的坑
| 坑 | 表现 | 办法 |
|---|---|---|
| 以为提交存的是「差异」 | 期待 diff 输出 | 提交存的是整棵树快照 |
| 改文件以为「改了提交」 | 困惑哈希变了 | 改文件会造新 commit |
| 以为 blob 知道路径 | 想从 blob 找文件名 | 路径在 tree 里 |
| 背哈希 | 记不住 | 看说明,不背哈希 |
3. 建议学习顺序
text
先看一张图
→ 建文件 + add + commit
→ 用 cat-file 一步步看
→ 画对象关系图
→(预告)下一章看命令动了哪些对象
4. 动手准备
bash
mkdir lab-objects
cd lab-objects
git init -b main
git config user.name "Ada Example"
git config user.email "ada@example.com"
5. 跟着做:亲手查看对象
5.1 建文件 + add,看 blob
bash
printf 'alpha\n' > a.txt
git add a.txt
git rev-parse :a.txt
输出一串哈希,类似:
text
4a58007052a65fbc2fc3f910f2855f45a4058e74
这是 a.txt 内容(alpha\n)对应的 blob 哈希 。git rev-parse :a.txt 的意思是「暂存区里 a.txt 那个对象的哈希」。
看对象类型和内容:
bash
git cat-file -t 4a58007052a65fbc2fc3f910f2855f45a4058e74
git cat-file -p 4a58007052a65fbc2fc3f910f2855f45a4058e74
text
blob
alpha
白话翻译:
-t看类型:blob-p看内容:就是alpha\n(文件内容本身)- blob 不含路径,只含内容字节
5.2 commit,看 commit 和 tree
bash
git commit -m "add a.txt"
拿到 commit 哈希:
bash
git rev-parse HEAD
类似:
text
bf63851b0799854999c00cbb38cc495e1d6f0e41
看 commit 内容:
bash
git cat-file -p HEAD
text
tree 42d4c5245460645340a0b5b189f055b93cca0f7e
author Ada Example <ada@example.com> 1784883528 +0800
committer Ada Example <ada@example.com> 1784883528 +0800
add a.txt
白话翻译:
- commit 指向一棵 tree(哈希
42d4c52...) - 有作者、提交者、时间
- 有说明
add a.txt - 注意:没有直接列文件,文件在 tree 里
看 tree:
bash
git cat-file -p 'HEAD^{tree}'
text
100644 blob 4a58007052a65fbc2fc3f910f2855f45a4058e74 a.txt
白话翻译:
- tree 是一张清单:路径
a.txt、模式100644、指向哪个 blob - 这就是「路径」住在的地方
也可以用更顺手的:
bash
git ls-tree HEAD
5.3 改文件再提交,看哪些哈希变了
bash
printf 'alpha\nbeta\n' > a.txt
git add a.txt
git commit -m "update a.txt"
git log --oneline
text
b6a0b1b update a.txt
bf63851 add a.txt
对比:
| 对象 | 第一次 | 第二次 |
|---|---|---|
| a.txt 的 blob | 4a58007...(内容 alpha\n) |
新哈希(内容 alpha\nbeta\n) |
| tree | 42d4c52... |
新哈希(清单里 blob 变了) |
| commit | bf63851... |
b6a0b1b...(指向新 tree + 父提交) |
白话翻译: 改文件 → 新 blob → 新 tree → 新 commit。旧对象还在,所以你能回看老版本。
5.4 不写文件也能算哈希
bash
printf 'hello\n' | git hash-object --stdin
text
ce013625030ba8dba906f756967f9e9ca394464a
白话翻译: git hash-object --stdin 把 stdin 的内容算成 blob 哈希,但不一定写入对象库。这是 Git 「内容寻址」的最直观体验------同内容永远同哈希。

图:再看一遍------commit 指向 tree,tree 列出路径→blob。分支名只是指向 commit 的便利贴。
6. 四类对象速查
6.1 各装什么
| 类型 | 装什么 | 怎么看 |
|---|---|---|
| blob | 文件内容 | git cat-file -p <哈希> |
| tree | 路径清单 → blob/tree | git ls-tree HEAD |
| commit | tree + 父 + 作者 + 说明 | git cat-file -p HEAD |
| tag | 指向对象 + 标注者 + 说明 | git cat-file -p <标签> |
6.2 上层 vs 底层命令
| 上层(日常) | 底层(探查) |
|---|---|
commit、log |
cat-file、hash-object、ls-tree、rev-parse |
底层命令(plumbing)给你看原始对象;上层命令(porcelain)给你友好输出。
7. 对照表:降低记忆负担
7.1 一次提交的对象关系
text
commit bf63851...
│
├─ tree 42d4c52...
│ └─ "a.txt" → blob 4a58007... ("alpha\n")
├─ parent: (根提交无)
└─ message / author
7.2 路径 vs 内容
| blob | tree | |
|---|---|---|
| 存路径吗 | 不存 | 存 |
| 存内容吗 | 存字节 | 存「路径→哈希」清单 |
| 改文件会变吗 | 内容变就变 | 清单变了就变 |
7.3 轻量 vs 附注标签
| 轻量标签 | 附注标签 | |
|---|---|---|
| 是对象吗 | 不是,只是引用 | 是,tag 对象 |
| 带说明吗 | 不带 | 带 |
| 怎么建 | git tag 名字 |
git tag -a 名字 -m "..." |
8. 安全习惯
8.1 建议这样做
| 习惯 | 原因 |
|---|---|
用 cat-file 看,别手编辑对象 |
对象是不可变的二进制 |
| 多次提交后画一画对象图 | 加深理解 |
备份 .git 就等于备份对象 |
历史都在对象库里 |
| 改文件后看哪些哈希变 | 直觉建立 |
8.2 别这样做
| 别 | 原因 |
|---|---|
手改 objects/ 里的文件 |
破坏完整性 |
| 以为删提交就没了 | 对象可能还在,直到 gc |
| 背哈希 | 没意义,每次都不同 |
9. 真实场景
9.1 「历史里那个文件长啥样」
bash
git show <老提交>:a.txt
底层就是:找到那个 commit → 它的 tree → 找到 a.txt 的 blob → 打印内容。
9.2 「为什么改一行提交哈希就全变」
因为改一行 → 新 blob → 新 tree → 新 commit。父链上的后续 commit 也会因为父指针变了而全部换哈希(这就是 rebase 会改哈希的原因,进阶章细讲)。
9.3 「内容相同会重复存吗」
不会。同内容 = 同哈希 = 同一个 blob 对象复用。
10. 稍微多懂一点点(可选)
- 哈希算法:现代 Git 默认 SHA-1;新版本支持 SHA-256。
- 对象存的是 zlib 压缩 :所以
.git不会因为重复内容膨胀。 - packfile :很多对象压缩打包放
objects/pack/(gc维护)。 git fsck:检查对象完整性,发现损坏或悬空对象。
11. 小实验(请一定动手)
实验甲:看一次提交的三个对象
- 建仓、建
a.txt、add、commit。 git cat-file -p HEAD拿到 tree 哈希。git ls-tree HEAD看 tree 清单。- 在纸上画:commit → tree → blob。
实验乙:改文件看哈希变
- 改
a.txt、add、commit。 - 对比两次的 blob 哈希、tree 哈希、commit 哈希。
- 确认旧 blob 还在:
git cat-file -p <旧 blob 哈希>。
实验丙:算哈希
printf 'hello\n' | git hash-object --stdin。- 改一个字符再算,确认哈希变。
通过标准: 能画一次提交的对象关系图,并解释「改一字节 = 新对象」。
12. 常见问题
问 1:blob 是文件吗?
不是。blob 是文件内容字节,不含路径。
问 2:路径存在哪?
在 tree 里。tree 是「路径 → blob/tree 哈希」的清单。
问 3:提交存的是差异吗?
不是。提交指向一棵 tree(整份快照)。差异是查询时算出来的。
问 4:哈希每次都不同吗?
同内容同哈希;改了就变。所以别背哈希。
问 5:附注标签和轻量标签啥区别?
附注标签是个对象,带说明;轻量标签只是个引用。
问 6:删了提交对象就没了吗?
短时间内可能还在,直到 gc。reflog 能看到。
问 7:cat-file 安全吗?
安全,只读。
问 8:.git 多大和对象有关吗?
有关。对象越多越大;gc 打包压缩。
13. 总结、学习路线与思维升华
13.1 这一章请记住的
| 点 | 记住什么 |
|---|---|
| 四类对象 | blob / tree / commit / tag |
| 内容寻址 | 同内容同哈希 |
| 不可变 | 改 = 新对象 |
| 提交指向 tree | 快照结构 |
| 路径在 tree | blob 不知路径 |
| 下一章 | 命令动了哪些对象/引用 |
13.2 在整个系列中的位置
text
09 .git 地图
10 对象模型 ← 当前
11 操作→文件
12 引用与 HEAD
13 暂存区深入
14 历史是图
13.3 思维升华
历史不是差异日志,而是不可变快照串成的链。
改一字节就长出新对象;旧对象还在,所以你能回看。
理解对象,
rebase和cherry-pick就不再是魔法,而是「复制对象 + 重连」。
13.4 参考资料
- Pro Git 中文版 --- Git 对象
- Pro Git 中文版 --- 包文件
- git cat-file 说明
- git hash-object 说明
- git ls-tree 说明
- git rev-parse 说明
- 本仓库图示署名:
assets/diagrams/ATTRIBUTION.md
命令输出样例验证环境:Git 2.43.0 ;演示作者信息为虚构:Ada Example <ada@example.com>。
13.5 本章检查清单
- 能用白话讲清四类对象
- 用
cat-file看过 blob/tree/commit - 改文件后对比过哈希变化
- 画过一次提交的对象关系图
- 知道路径在 tree 里,不在 blob
- 用过
hash-object --stdin算哈希 - 知道旧对象改后还在
理解了对象,Git 的历史就不再是玄学。下一章把命令和「动了哪些对象/引用」对上号。