在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

提交是快照

Git 提交是不可变对象,不是保存的补丁。提交引用由索引构造的完整树,记录父提交、作者、提交者、消息,再对序列化内容计算哈希。即使文件完全相同,只改变父提交也会改变提交 ID。

树可以看成照片,提交则是照片背后的记录卡。两张卡可以附同一张照片,却记载不同的上一张卡。Git 对整张记录卡计算哈希,不只对照片计算。

Git 提交快照如何工作

索引提供快照内容。每个暂存路径指向 blob,blob ID 来自文件的精确字节。树对象记录全部路径到对象的映射。提交再记录树 ID、父提交信息、作者与提交者行、消息。

  • blob 保存文件字节,文件名不属于它的身份。两个文件内容相同,可以共用 blob。
  • tree 保存名称、模式和对象 ID。一个文件改变后,新树仍能引用旧树中未变文件的 blob。
  • commit 保存树 ID 和历史元数据。父字段让相同项目快照成为特定历史中的新节点。

哈希格式有精确定义:对象正文前加类型、空格、字节长度和空字节,再对头部加正文计算哈希。本课采用 SHA-1 格式,空树 ID 固定为 4b825dc642cb6eb9a060e54bf8d69288fbee4904,这是实际 Git ID,不是画布自定义标签。

对象 ID 表示内容身份,不是可覆盖的存储槽。同样字节产生同样 ID,可以复用对象;字节改变就产生新对象,旧对象不被改写。git diff 比较两份快照来计算差异,不是读取提交内部保存的补丁。rebase、cherry-pick、git commit --amend 可能保持检出文件不变,但因祖先或元数据变化而生成新 ID。

提交快照逐步演示

默认使用 app.py、README.md 和线性 main。身份与时间固定,因此可以把最后的 ID 变化明确归因于父字段。

  1. 编辑 app.py 为 print("one"),只改变工作字节,尚无 blob。
  2. add 将这 12 字节计算为 blob 51ee69d...,保存对象并在索引写入 app.py -> 51ee69d...。
  3. 编辑 README 为 hello 并 add,生成 blob b6fc4c6...,索引已有两个路径。
  4. 用消息 base 提交,将两个索引条目序列化为树 d5ba943...。首提交没有父,载荷包含树、固定身份行、消息,得到 45ecc20...;对象写完后 main 才移动。
  5. 将 app.py 改成 print("two") 并 add,创建新的 app.py blob。README 未变,仍引用 b6fc4c6...。
  6. 用消息 update 提交。新树 ff8ad65... 引用新 app.py blob 和旧 README blob;提交以 45ecc20... 为父,得到 309aeee...。
  7. 用 --allow-empty 和相同消息 update 再提交。索引未变,复用树 ff8ad65...;消息、身份、时间也都相同。唯一变化是父变成 309aeee...,新载荷得到 ccf8be2...,main 再次前进。

第 6 步展示结构共享:README 字节不复制到新容器,新树直接引用原 blob。第 7 步则说明项目快照不变,历史仍可改变。

相同快照为何有不同提交 ID

默认 C2、C3 都引用树 ff8ad65...,但 C3 的父是 C2,C2 的父是 C1。提交 ID 对包含祖先的载荷作内容标识,因此不同历史产生不同 ID。

若删去父行,两份仓库即使经历不同前序提交,只要树、固定身份、时间、消息相同,就会产生相同载荷和 ID,无法从该对象证明它接在哪段历史之后。树回答保存什么,父回答接在哪段历史后,哈希把两者一起纳入身份。

--allow-empty 不会制造重复树,只是绕过普通提交的树相等检查,允许新提交引用已有树。新的父字段就足以改变载荷。它可用于事件标记、自动化触发或记录无文件变化的事件。

边界情况

  • 无暂存修改:普通提交比较索引树与 HEAD 树,相同则在组装提交载荷前停止。工作区变脏但未 add,不影响这个比较。
  • allow-empty:接受同一棵树,当前 HEAD 仍成为新父;即使固定元数据和消息都相同,也会有新提交。
  • 工作文件缺失:本课按路径 add 不创建 blob 或索引项。真实 Git 还处理删除暂存,本双文件引擎未实现。
  • 相同文件字节:再次 add 得到相同 blob ID,对象库无需第二份。
  • 短 ID 冲突:播放器使用至少七字符的最短唯一前缀,必要时加长;对象身份始终是完整哈希。
  • 哈希格式:本课用 SHA-1,Git 也支持 SHA-256 仓库,工具不应假设 ID 永远是 40 位十六进制。

常见错误

  • “提交保存修改行。” 差异由快照比较生成;提交引用完整树,未改路径仍在树中。
  • “改一个文件就复制全仓库。” 新树更新路径映射,未改条目复用对象 ID。
  • “提交 ID 只是文件哈希。” 它对带对象头的序列化提交内容计算,正文包括树 ID、父、身份、消息,文件通过树影响它。
  • “父只是导航提示。” 父字段参与哈希,改动它就产生不同提交,即使检出文件相同。
  • “allow-empty 创建空树。” 通常复用当前索引树,“空”表示相对父没有文件树变化,而非仓库没有文件。
  • “Git 从当前磁盘内容重算所有 ID。” 工作区在对象库之外,对象 ID 来自存储的字节,编辑文件不会改写它们。

模型范围

本课使用两个平面文件、模式 100644、线性历史、固定作者与提交者,以及可重复的 SHA-1 序列化。省略目录、可执行和符号链接模式、删除、合并父、签名、钩子、引用存储、索引元数据、SHA-256 仓库设置、pack、delta、reflog 和扩展。这些机制不改变提交内容包含树、祖先、元数据的原则。

完整格式与行为见 Git 对象内部机制、git commit-tree、哈希函数迁移计划。