值得注意的是,提交不是,也不包含,更改。提交包含完整的快照。提交 viewers 故意对您撒谎(您通常希望他们这样做)以便将提交 显示作为一组更改。
这背后的机制很重要。提交是一个快照——例如告诉你当前温度为 20˚C 的天气预报——并且你想知道它现在有多么不同。你必须选择另一个提交——例如“昨天”——when 你想要比较。那么如果昨天是 19˚C,今天是 20˚C,那么区别是高了 1˚C。但要得到它,您必须选择前一天进行比较。
在 Git 中,每个提交都由其哈希 ID 唯一标识。哈希 ID 是 Git 如何获取快照中的所有文件以及有关提交的元数据,例如提交者、时间和原因(日志消息)。每个提交中保存的一项是 previous 提交的哈希 ID。这就是 Git 提交查看器用来构建差异的原因。
提交查看器不会用哈希 H 显示提交中每个文件的每一行。相反,它会找到提交的前身——它的 parent,用 Git 的术语来说。那个父母有一些不同的散列G。查看器提取 两个 提交,比较它们,然后告诉您:在 G 和 H 之间,这些文件是不同的,只是对这些行进行了这些更改。 这通常要短得多——而且比 H 中的完整快照更有用。
但这在合并时会崩溃。如果我们画出一组漂亮的线性提交:
... <-F <-G <-H <-- you-are-here
(箭头指向后方,因为每个提交都记录其父级;父母不记得他们的子级)很容易比较 G 与 H。但最终你将两条开发线结合起来:
o--...--o--K
/ \
...--* M <-- mainline
\ /
o--o--...--L <-- branch
主线在某个时候分裂,两个不同的人或团体发展。然后我们——或者无论如何——使用git checkout mainline; git merge branch并经历了整个可怕的1和神奇的2合并操作过程,这导致了这个合并提交 M.
提交M 就像任何其他提交一样,它有一个快照和一些元数据。快照就像任何其他快照一样。 M 的唯一特别之处在于,在其元数据中,它不只是将提交 K 列为其父项。相反,它将 both 提交(K 和 L)列为其两个父项。
1其实并不可怕。
2这也不神奇;见下文。
Git 的自动合并是如何工作的
让我们快速了解一下git merge 和合并冲突。如果没有冲突,Git 会自行完成整个合并。通常这些情况不会导致这种困惑,所以让我们看看当有冲突时会发生什么。
要开始合并,Git 只需比较 * 和 K——就像 Git 总是比较任何简单的提交对一样——以找出不同之处。然后,Git 比较 *-vs-L,找出不同之处。然后 Git 将这两组更改结合起来。这就是要合并,或者我喜欢称之为作为动词的合并,是合并过程的一部分。合并后的更改将应用于提交*中的快照。
请记住,每个提交都包含一个快照。提交* 的所有文件都处于他们在某人创建* 时的状态。提交K 的所有文件都处于其他状态,而提交L 的所有文件都处于第三种状态。 K 中甚至可能有不在* 中的文件,和/或在L 中但不在* 中的文件,等等,但通常大多数文件大部分都在所有三个输入中。
假设“我们”是指在K 线上工作的人,而“他们”是指在L 线上工作的人。我们更改了文件 A、B 和 C。他们更改了文件 B、C 和 D。然后 Git 只接受我们对 A 的所有更改,以及对 D 的所有更改。这部分很容易,因为我们没有触及 D 和他们没有碰 A。合并的那部分已经完成。
现在,Git 找出我们在文件 B 中更改了 的哪些 行,以及在同一个文件中更改了哪些行。如果我们的行根本不与它们的行重叠——请注意,Git 有时认为“只是接触”是重叠的——那么 Git 可以将提交 * 中的 both 更改应用到文件 B。合并的那部分现在也完成了。
Git 计算出我们在 C 中更改了哪些 行,以及它们更改了哪些行。哦哦,这次我们都改变了相同行。 Git 使用冲突标记将更改的组合写入工作树,并将合并声明为冲突。
由于合并 发生冲突,Git 停止并从进行合并的人那里获得帮助。 他们的工作就是解决这个问题。有很多方法可以修复它,但它们都以相同的方式结束:进行修复的人将文件 C 的 正确 版本写入工作树并运行 git add C 到告诉 Git:这是正确的结果。
Git 不会检查他们写的内容,它只是将他们放入最终文件中的任何内容。如果他们把所有东西都搞砸了,例如完全扔掉你的代码,Git 没问题! Git 假设他们知道他们在做什么。
它们现在运行git commit 或git merge --continue,Git 使用已完成的合并快照进行合并提交M,看起来就像我们绘制的那样。
回到你手头的问题
让我们回到我们的提交查看器。你要求它查看提交M。它像往常一样向您显示元数据——提交者的姓名,等等。它可能会或可能不会向您显示两个父哈希 ID,具体取决于查看器。它可能会向您显示运行git merge 的人用来记录为什么他们进行合并的日志消息,并保存任何重要的注释。如果这个人超级勤奋,那么日志消息甚至可能会很有用......但是,唉,大多数人使用自动生成的、几乎毫无价值的日志消息:“合并分支......”。
现在您的查看器应该继续向您展示此提交中的更改。但现在有一个问题。为了显示更改,查看者必须查看父提交并进行比较。没有一个父母。有两个父母。观众会使用哪一个?
这里的实际答案取决于观众。有些观众只是完全放弃了,什么都没有。例如,git log -p 就是这样做的。听起来您可能正在使用这种查看器。另一个查看器,git show 运行的查看器试图发挥作用:它实际上将合并 M 与 both 父级 K 和 L 进行比较。但可惜的是,这个查看器试图 提供帮助。它关注合并可能有合并冲突的地方,因此它不会显示M 中的文件完全匹配的任何文件任何一个 K 中的那个,或者L 中的那个。
如果进行合并的人错误地丢弃一些应该在M中的文件更改,这种查看器同样会从显示中丢弃这些更改。在这种情况下,文件 C 与来自提交 L 的 他们的 副本完全匹配。所以git show,作为合并查看器,不会显示文件 C。
(当然,使用git log 作为提交查看器更糟糕:它不会向您显示 A、B、C 或 D 中的任何一个,即使这四个文件有一些更改。)
您可以指示git log(和git show)将一个合并提交分解为两个虚拟提交。也就是说,给定:
...--K
\
M
/
...--L
你可以让他们假装他们有:
...--K--M1
...--L--M2
首先显示K-vs-M1,然后显示L-vs-M2。这通常对这些情况有些用处。为此,请将-m 添加到git log 或git show。 (请注意,M1 和 M2 永远不会进入存储库,它们只是在查看合并提交的“显示差异”部分期间的假装提交。)
底线,因为它是
如果有人制作了糟糕的合并快照,许多观众不会向您展示。找到它的方法是查看合并前后的提交。如果有人继续这样做,您需要教他们如何正确合并。 将他们的更改扔掉并使用我的更改是很少见的是正确的。 Git 提供了此选项,但他们应谨慎使用该选项,而不仅仅是因为它解决了他们的冲突。