1.2 tree与commit对象破案:快照如何成链


1.2 tree与commit对象破案:快照如何成链

本节摘要:tree 对象是目录快照,commit 对象是"快照加元数据加父指针"的卷宗。本节动手解剖一个真实提交,把 commit → tree → blob 的哈希链条完整走一遍,并解释 Git 历史为什么是"快照链"而不是"差异链"——这是 Git 与前代版本控制系统最深的分野。

学习目标

  1. 能用 git ls-treegit cat-file -p 逐层解剖提交
  2. 能说出 commit 对象里六个关键字段的含义
  3. 能论证"快照模型"与"差异模型"的区别及工程影响
  4. 能用对象模型解释"为什么切换到旧提交,文件瞬间就变回去了"

一、解剖一个真实提交

接着上一节的实验仓库,制造一个带子目录的提交:

printf "readme v1\n" > README.md mkdir src printf "print('hi')\n" > src/app.py git add . git commit -m "first case" # [master(根提交) a1b2c3d] first case

提交后立刻取证。git rev-parse HEAD 拿到当前提交哈希,然后验尸:

git cat-file -p HEAD # tree 4f2a9c1d8e3b7f6a5d4c3b2a1908f7e6d5c4b3a2 # author 侦探 <det@example.com> 1724000000 +0800 # committer 侦探 <det@example.com> 1724000000 +0800 # # first case

这就是 commit 对象的全部真身:一个 tree 指针、作者与提交者信息、提交说明;首个提交没有 parent 字段,后续提交会多出一行 parent <父提交哈希>。字段虽少,信息量极大——注意里面没有 diff、没有"改了哪些行",只有一个 tree 哈希

顺着 tree 指针往下挖:

git ls-tree HEAD # 100644 blob 2b3c4d5... README.md # 040000 tree 9e8f7a6... src git ls-tree HEAD src # 100644 blob 7d6c5b4... app.py

ls-tree 列出每条登记的模式(100644 普通文件、100755 可执行、040000 子目录、120000 符号链接)、对象类型、哈希和名字。至此链条闭合:commit 指向根 tree,tree 里的条目要么是 blob(文件)要么是子 tree(目录),一整棵树就是项目在该时刻的完整快照。文件名在哪?在 tree 里——上一节留下的"blob 不记名字"的缺口在这里补上了。

二、快照链,不是差异链

CVS、SVN 等前代系统存的是"相对上一个版本的差异",要看某个旧版本,得从起点一路重放补丁。Git 每次提交都存整棵树,但你不必担心膨胀:树里未变更的文件条目仍然指向旧 blob,真正新写入的只有变更过内容的 blob 和路径变化过的 tree。做个实验最直观:

printf "readme v2\n" > README.md git commit -am "second case" git cat-file -p HEAD^{tree} # 100644 blob 5a4b3c2... README.md <- 新 blob # 040000 tree 9e8f7a6... src <- 哈希没变,复用旧 tree

src 目录没动,新的根 tree 直接复用了旧子树的哈希。这个设计把"全量快照的语义"和"增量的存储成本"同时拿到了,是 Git 性能的根基。也解释了一个日常现象:git checkout <旧提交> 后文件"瞬间"变回旧样子——因为根本不需要重放历史,直接按旧 tree 的登记把对象库里的 blob 取出来写入工作区即可,一千年前的快照和一秒前的快照取用成本相同。

三、commit 的元数据:作者与提交者为什么是两个人

commit 对象里 author 与 committer 分开,常被误认为是冗余。实际用意在于协作场景:当你用 git rebasegit cherry-pick 把别人的提交搬到新位置时,内容与原作者信息保留,但"执行搬运的人"记在 committer 字段里,时间戳也更新为搬运时刻。审计时这条线索很有用——一条提交链上 author 时间早于 committer 时间好几分钟,多半经历过变基或拣选。

第一个 parent 之外,merge commit 会有两个 parent(第 2 章 2.2 节会用到这个证据);tag 对象则是指向 commit 的带签名注解,放在第 4 章发布流程里细说。

哈希即地址,链即历史。 每个 commit 通过 parent 指针指向前一个,形成单向链表;两条链在 merge commit 处汇合,就是分支拓扑。Git 的所有历史操作——log、diff、merge、rebase——都只是在这些链上做图遍历。没有中心数据库给提交编号,哈希本身就是全局唯一地址,这就是分布式协作不需要"先到服务器申请版本号"的原因。

提交链与快照复用示意

提交链与快照复用示意

四、常见疑惑的结案陈词

为什么提交哈希只有 7 位也够用? 完整哈希 40 位,显示时截短只要在当前对象库内无歧义即可,git rev-parse 接受任何不冲突的前缀。

改写历史会破坏链条吗? 会生成新对象新链条,旧链条仍在(第 2 章详述)。对象不可变是铁律,所有"修改"都是新建。

空提交存在吗? git commit --allow-empty 会创建一个 tree 与父提交完全相同、仅说明不同的提交,常用作标记节点(比如发布标记占位)。

本节要点回顾

  • 解剖链条cat-file -p 提交 → ls-tree 根树 → 逐层到 blob,是本教程反复使用的勘查动作。
  • commit 六要素:tree、parent(0/1/2 个)、author、committer、时间戳、说明。
  • 快照模型:每提交存整棵树,未变更对象复用,兼顾语义与成本。
  • author 与 committer 分离:是审计变基与拣选的线索。
  • 文件名在 tree 里:重命名即 tree 条目路径变化,blob 可以纹丝不动。

下一节把镜头拉远:这些对象在"工作区 → 暂存区 → 仓库"三区之间如何流转。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U