理解git merge 的关键是Git 不会比较两个 的东西。 Git 比较 三个 事物。
Git 无法直接比较所有三个。它必须一次比较它们两个。其中两件事是文件的两个分支提示版本(或分支提示提交;我稍后会详细讨论),但 Git 不会将它们相互比较。这是第三个出现的地方:第三个文件是文件的 merge base 版本。
请记住,合并的目标是合并更改。但是 Git 不存储更改。 Git 存储快照。每个提交都完整且完整地存储每个文件:给定一个提交,Git 获取整个 README.md、整个 main.py,无论此特定提交中的其他文件是什么,这就是提交中的版本。
要从快照中获取更改,我们需要 两个 快照:旧的和新的。然后我们玩Spot the Difference的游戏。对于 Git,它是 git diff:你给它旧提交的哈希 ID 和新提交的哈希 ID,它会为两者之间更改的每个文件创建一个差异。 git diff 的输出是一系列指令:删除这些行,添加这些其他行。如果您拍摄原始快照并应用说明,您将获得新快照。
但是,当我们合并时,我们希望将(比如说)Alice 所做的工作与 Bob 所做的工作结合。所以 Git 所做的是:
- 找到最佳的共享提交,爱丽丝和鲍勃都开始使用。
- 比较 shared 提交的文件和 Alice 的文件。这就是爱丽丝改变的地方。
- 比较 shared 提交的文件和 Bob 的文件。这是鲍勃改变的地方。
我们将共享提交(Alice 和 Bob 都开始使用的提交)称为 合并库。这是合并的第三个输入。 Git 使用您的存储库中的历史记录(提交)自动找到此合并基础提交。这意味着您需要同时拥有 Alice 的 和 Bob 的提交,以及导致这两个分支提示的所有提交,以便您还拥有共同的起点提交。
请记住,每个提交及其快照都会记录一些关于快照的信息:例如,制作者的姓名和电子邮件地址。 当他们制作它时有一个日期和时间戳,以及一个他们可以用来解释他们制作它的为什么的日志消息。它还存储其直接父提交的原始哈希ID:他们使用的提交,通过git checkout,从他们进行他们的提交之前开始。这些父哈希 ID 形成一个向后看的链:如果 Alice 和 Bob 都从提交 H 开始,并且 Alice 做了两个提交 I 和 J 并且 Bob 做了两个提交 K 和 L,则向后链看起来像这样:
I <-J <-- (Alice's latest)
/
... <-F <-G <-H
\
K <-L <-- (Bob's latest)
Git 会自动找到H,Alice 和 Bob 都是从这里开始的。1
找到H,Git 现在实际上运行这两个git diff 命令:
-
git diff --find-renames <em>hash-of-H</em> <em>hash-of-J</em>:爱丽丝改变了什么
-
git diff --find-renames <em>hash-of-H</em> <em>hash-of-L</em>: Bob 改变了什么
合并过程现在结合了这些更改。对于H中的每个文件:
- Alice 是否更改了文件? Bob 是否更改了文件?
- 如果两者都没有更改文件,请使用文件的任何副本:所有三个都相同。
- 如果 Alice 更改了文件而 Bob 没有更改,请使用 Alice 的版本。
- 如果 Bob 更改了文件而 Alice 没有更改,请使用 Bob 的版本。
- 如果双方都更改了文件,合并他们的更改。这就是可能发生合并冲突的地方。
[Git] 合并时是否逐行比较?
这个问题的答案是“不”和“是”。如您现在所见,Alice 的版本与 Bob 的版本没有可比性。 有一个比较——一种逐行的比较;这是git diff 所做的比较——base 版本与 Alice 的比较,base 版本与 Bob 的比较。整个过程通过对两对 commits 进行完整的提交范围比较来开始。在该提交范围的比较中,发现 Alice 和 Bob 都更改了某些特定文件,现在逐行更改,或者真正的 diff-hunk- by-diff-hunk,比较很重要。但它们来自第三个版本。
我不想每次都使用“git diff”手动检查。
您不必这样做。如果您想要可以这样做,但要做到这一点,您需要找到合并基础提交,也许使用git merge-base。但如果你不想,那么……不要。 Git 会找到基于合并的提交; Git 将执行两个独立的git diff 操作; Git 会将 Alice 的更改与 Bob 的更改结合起来,如果更改的行重叠,或者在某些情况下,abut,或者如果两者都跨越到文件末尾,则会声明冲突。
(对于 Git,如果 Alice 和 Bob exactly 对 exactly 相同的行进行了相同的更改,Git 只需复制一份更改。其他 VCS 可能会声明这里有冲突,要么是因为懒惰——他们不检查更改是否相同,只是因为它们重叠——要么是偏执狂:如果两者都更改了相同的行,那么正确的结果可能是 not使用更改的一份副本。Git 只是说“正确的结果是更改的一份副本”。)
在任何情况下,Git 都会将 combined 更改应用到文件的 merge base 版本。这就是结果,可能存在合并冲突(以及文件的工作树副本中的合并冲突标记)。
最后,注意两个git diff 命令中的--find-renames。 Git 将尝试判断 Alice 和/或 Bob 是否重命名合并基础提交中的任何文件。如果是这样,Git 将尝试在最终结果中保留重命名。无论是 Alice 还是 Bob 进行了重命名,这都是正确的。如果 Alice 和 Bob 都重命名了文件,Git 不知道要使用哪个最终名称,并声明 rename/rename 冲突。如果 Alice 或 Bob deletes 文件而另一个人修改它,则会出现类似的问题,并且如果 Alice 和 Bob 添加一个 new 文件,则会发生最后一个冲突一样的名字。这些类型的冲突就是我所说的高级冲突:它们影响整个文件(和/或它们的名称),而不是文件中的单个行。当您使用-Xours 或-Xtheirs 选项时,低级别冲突(文件中的行)和高级别冲突之间的差异很重要。
1即使 Alice 只在 J 上进行了 一个 提交,也可以在 Carol 的一次提交 I 之上(比如)Carol 在 H 之上提交.共同的起点仍然是H。 Git 甚至不查看每个提交的作者身份:它只是从两个分支提示向后工作。