Git 教学工作区 · 课程 0001 · 约 25 分钟 · 前置:无 · 2026-09-24

Git 里到底存了什么:四种对象

这节课的胜利:用四条命令把这个笔记库的 HEAD 一层层拆到最底 —— 看到 commit 里指向的 tree、tree 里指向的 blob,并用 hash-object 亲手验证 「内容决定哈希」。做完这一节,后面所有回滚命令都不再需要背。

为什么是这一步

使命说 Git 是两件事的共同底座:本库自己的版本管理,和给 Zephyr 提 PR 的协作流程。 而绝大多数人学 Git 的方式是"背命令",于是 reset / revert / checkout 永远分不清 —— 因为它们真正的区别在对历史做了什么,而历史是由对象构成的。

最小知识:三条

  1. Git 是"内容寻址"的。每个对象的名字就是它内容的哈希:同样的内容永远同一个名字,差一个字符名字就完全不同。 这也是"Git 能发现损坏"和"改历史会重写所有后续哈希"的根源。
  2. 三种对象,各管一件事。 blob = 文件内容(不含文件名);tree = 一层目录(名字 → 哈希的清单); commit = 一个 tree + 父提交 + 作者/时间 + 提交信息。
  3. HEAD 和分支都只是指针。HEAD 指向"你现在在哪";分支指向"某个提交"。 提交时 Git 只是把当前分支这个指针往前挪一格 —— 这也解释了为什么"创建分支"几乎不花时间。

动手:把这个库的 HEAD 拆开看

在库根目录执行(输出是真实的,来自本库当前状态):

cd /f/0-Note

# ① HEAD 是什么类型的对象?
git cat-file -t HEAD
# commit

# ② 看它的内容:指向哪个 tree、父提交是谁、谁在什么时候提交的
git cat-file -p HEAD
# tree a08b97b10fd8cb2b8c03be28d307840ab742e78f
# parent 6dad548ef5904dbeb4573fe59d945263f07c8c6f
# author Zzz210s <chenchen237038@qq.com> 1790263411 +0800
# committer Zzz210s <chenchen237038@qq.com> 1790263411 +0800
#
# docs(teach): 18个教学工作区各出课程地图

# ③ 看这个 tree:它列出顶层目录与文件,每行是 模式 + 类型 + 哈希 + 名字
git cat-file -p HEAD^{tree}
# 100644 blob a20a44a5e24047117fc6ade6293b9dfba1b0b697  .gitattributes
# 100644 blob ebe9b580d6bf0310cc236211e3ca01a384817dc7  .gitignore
# 040000 tree 773d8736400562b83472de8579e0f96885b74947  00-索引
# 040000 tree 822fab46d2f1dd3d78669cde4e1521f798b05e8c  10-项目
# …

# ④ 拿到某个文件的 blob 哈希,并确认它确实是个 blob
git rev-parse HEAD:README.md
# 1b015a320578e56ce63a1e548e9e7bf1cc4a6f78
git cat-file -t HEAD:README.md
# blob

看懂了什么:commit → tree → tree → … → blob,这就是"一次提交"的完整结构。 文件名不存在于 blob 里,它只存在于 tree 的清单里 —— 所以"同一个内容出现在两个目录"只占一份存储。

亲手验证「内容决定哈希」

下面两条命令不会写入任何东西(没有 -w),只是计算哈希:

printf 'hello\n' | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a
printf 'hello\n' | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a   ← 一模一样:内容相同,名字必然相同
printf 'hello!\n' | git hash-object --stdin
# 4effa19f4f75f846c3229b9dbdbad14eff362f32   ← 多一个感叹号,名字彻底不同

顺手看一眼家底:git rev-list --count HEAD → 185(这个库有 185 个提交); git count-objects -v → 松散对象 1023 个、打包对象 4046 个。这些数字以后会用到。

自测(选项一样长,别从长度上猜)

1. git cat-file -p HEAD 打印出的是?

对。它会打印 tree 哈希、parent 哈希、作者/提交者与提交信息 —— 也就是"一次提交"的全部内容。工作区差异要用 git diff。

2. tree 对象里存的是什么?

对。tree 是一层目录的清单:模式、类型、哈希、名字。文件内容在 blob 里,时间与作者在 commit 里。

3. HEAD 是什么?

对。HEAD 指向"你现在所在的提交"(通常经由当前分支)。分支与 HEAD 都只是指针 —— 所以切换分支几乎瞬间完成,它只是挪了个指针。