HEAD 一层层拆到最底 ——
看到 commit 里指向的 tree、tree 里指向的 blob,并用 hash-object 亲手验证
「内容决定哈希」。做完这一节,后面所有回滚命令都不再需要背。
使命说 Git 是两件事的共同底座:本库自己的版本管理,和给 Zephyr 提 PR 的协作流程。
而绝大多数人学 Git 的方式是"背命令",于是 reset / revert / checkout 永远分不清 ——
因为它们真正的区别在对历史做了什么,而历史是由对象构成的。
blob = 文件内容(不含文件名);tree = 一层目录(名字 → 哈希的清单);
commit = 一个 tree + 父提交 + 作者/时间 + 提交信息。在库根目录执行(输出是真实的,来自本库当前状态):
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 都只是指针 —— 所以切换分支几乎瞬间完成,它只是挪了个指针。