我们会把 Git 提交理解为 diff、快照,还是历史?
如何把 Git 提交理解为 diff、快照,还是完整历史,会切实影响人们如何学习、教学,以及如何安全地使用 Git。评论者将 Git 实际的存储模型(DAG 中的内容寻址快照,delta 压缩只是实现细节)与核心命令的行为进行对比,指出许多操作(rebase、cherry-pick、merge)在概念上都把提交当作快照之间的变更来处理。一个反复出现的主题是:简单或误导性的心智模型——尤其是“提交就是 diff”——会让高级工作流和冲突解决更难推理,因此谨慎选择并解释抽象被视为提升开发者生产力的关键。
心理模型:diff、快照、历史
- 许多人认为,从概念上说,提交是快照:完整的仓库状态(树)加上父指针,形成一个 DAG。
- 也有人本能地把提交看作diff/变更,因为他们是在编写“一次变更”,而且像
git show、rebase、cherry-pick以及评审这类工具,都会展示或处理 diff。 - “历史”被描述为一个提交加上它的全部祖先(其他系统里称为分支);有些人觉得“历史 vs 快照”的区分很混乱,因为每个快照都链接进那段历史。
- 还有人建议采用双重(或三重)视角:提交既是快照,也是 diff,同时也是历史的一部分;“正确”的模型取决于具体操作。
实现 vs 抽象
- 对于“git 是如何实现的”是否是一个好的教学模型,存在很强分歧。
- 一方认为:把提交理解为快照,以及理解对象存储(trees、blobs、内容寻址)对开发者避免在合并/变基中产生困惑至关重要。
- 另一方认为:实现细节(包括 packfiles 和 delta 压缩)只是优化,常常会分散初学者注意力;UI 关注的是随时间变化的状态,而 diff 是按需推导出来的。
- 更广泛的争论用了油门踏板作类比:一些人强调直观抽象,另一些人则认为有泄漏的抽象和调试需求,使得了解真实实现更有价值。
Rebase、merge,以及 DAG
- Rebase 被描述为“重置到新基底 + 对每个提交做 cherry-pick”,在概念上是通过 3 路合并逐个应用每个提交的 diff。
- 也有人指出,在 rebase 过程中纯粹用 diff 来思考会造成困惑,尤其是提交顺序变化或涉及 merge commit 时。
- Merge commit 可以引入任意新的变更(包括冲突解决产生的变更),并且可能破坏工作中的父提交;“merge history = safe history” 被标记为不安全。
存储与性能
- 有人澄清,git 的逻辑模型是快照;物理存储可能在 packfile 内使用 delta 压缩(“deltas”),但这对用户是不可见的。
- 解释中提到 copy-on-write 树、通过哈希进行的重度去重,以及为什么即使提交很多,这样仍然保持快速且节省空间。
- 有人认为,因为 packfile delta 就把提交称为“diff”是误导性的;也有人觉得如果把“diff”宽泛地定义,这样说也可以接受。
教学、可用性,以及其他工具
- 许多人表示,“diff 心智模型”是日常混乱的主要来源,并主张总是先教快照。
- 另一些人则表示长期以来一直用 diff 思考,并声称自己几乎从不需要显式地调用快照模型。
- Git 的 UI 和术语被广泛批评为令人困惑,并且与内部实现绑得过紧;Mercurial 经常被提到拥有更清晰、更友好的模型(尽管现在较小众)。
Diff 及其非唯一性
- 评论者强调,不存在唯一的规范 diff:算法和选项(
--word-diff、patience、histogram、语言感知的 hunks)会改变用户看到的内容。 - 这可能与作者对自己编辑内容的概念化方式不一致,但大多数人接受“是一个 diff,而不是那个 diff”,作为实际使用上的合理做法。