从一个基本意义上来说,合并操作不会真正影响任何 分支。 (它当然会做出一个新的提交,这会以通常的方式影响该分支。)Git 的诀窍是在你的脑海中同时保留以下五个想法:
在 Git 中重要的是提交及其父链接。分支名称大多只是分散注意力(但请参阅第 2 点和第 3 点)。
分支名称只是特定提交的名称,我们称之为该分支的提示提交。
进行新提交时,Git 将当前提交作为其父提交写入新提交。1 如果新提交有多个父提交(请参阅下一点),则当前提交变为它的第一个父级。在任何情况下,Git 都会更新分支名称以指向新的提交。这就是分支“生长”的方式。
合并提交 是具有两个(或更多)父提交的提交。这实际上是“作为名词合并”。
进行合并的行为——我的意思是合并提交——包括进行三向合并操作,然后进行新的提交像往常一样,除了新提交有两个(或更多)父母。 “额外”的父母是合并的提交。2
合并动作——“作为动词合并”——使用通过上述五点建立的历史。 Git 找到三个提交:
- 当前提交,又名
HEAD。 (这很简单。)
- 要合并的提交:
git rev-parse 为您传递给git merge 的参数提供的任何 ID。分支名称只是找到分支提示提交。
-
合并基地。这就是提交历史的来源,这就是您需要绘制图形片段的原因。
任何两个提交的合并基础被松散地定义为“图形重新组合在一起的(第一个)点”:
...--o--*--o--o--o <-- branch1
\
o--o--o--o <-- branch2
名称branch1 指向顶行的提示(最右边)提交。名称branch2 指向底部的提示提交。这两个提交的合并基数是标记为*的那个。3
为了执行合并操作,Git 然后将合并基础提交 * 与两个提示进行比较(如在git diff 中),给出两个差异。然后,Git 合并差异,每次更改仅获取一份副本:如果您(branch1)和他们(branch2)在README 中将单词color 更改为colour,Git 只接受换一次。
存储在工作树中的结果 source 成为合并提交的树。请注意,到目前为止,我们是将branch2 合并到branch1 还是将branch1 合并到branch2 都无关紧要:我们将获得相同的合并基数,并具有相同的两个提示提交,因此获得相同的两个差异并以相同的方式组合这两个差异以达到相同的工作树。但是现在我们进行实际的合并commit,它的两个父级,并且现在我们在哪个分支上很重要。如果我们在branch1,我们将新的提交on branch1,并推进名称branch1:
...--o--o--o--o--o---M <-- branch1
\ /
o--o--o--o <-- branch2
新的合并提交有两个父节点:一个是branch1 的旧提示,另一个是branch2 的提示。
因为我们现在有一个新图,所以稍后的git merge 将找到一个新的合并基数。假设我们在两个分支上都进行了多次提交:
...--o--o--o--o--o---M--o--o <-- branch1
\ /
o--o--o--*---o--o--o <-- branch2
如果我们现在要求合并这两个分支,Git 首先会找到基础。这是在 both 分支上的提交,最靠近两个提示。我再次将此提交标识为*,并查看它的位置:它曾经是branch2 的提示,当我们进行合并时。
请注意,无论我们以哪种方式进行合并,情况仍然如此。
然而,进行实际的合并提交至关重要。如果我们使用git merge --squash,它不会进行合并提交,我们将不会得到这种图表。同样重要的是,合并后两个分支都不会“重新定位”,因为git rebase 通过复制 提交工作,而git merge 在提交身份和跟随父指针的基础上工作。任何副本都是不同的提交,因此任何旧提交都不会指向新复制的提交。 (在这些图中,可以在合并点之后将提交变基——向右,在这些图中;不可以复制左侧的提交。)
如果您不禁止git merge 执行“快进”操作,git merge 也可以跳过合并提交,而只是移动分支标签。在这种情况下,两个分支标签——一个你刚刚移动的,一个你要求合并的——最终指向同一个提交。一旦发生这种情况,就没有办法“解开”这两个分支,除非将标签移回。要防止git merge 执行此快进而不是实际合并,请使用--no-ff。
这是一个快进“合并”的示例(在引号中,因为没有实际的合并)。像往常一样,我们从分歧的分支开始——但是在 current 分支上没有提交,branch1,在 other 上还没有提交/em> 分支,branch2:
...--o--* <-- branch1
\
o--o--o <-- branch2
如果我们坐在branch1 上运行git merge branch2——注意缺少--no-ff——Git 注意到不需要实际的合并。相反,它执行标签“快进”操作,将名称branch1 向前滑动,直到遇到branch2 上的提示提交:
...--o--o
\
o--o--o <-- branch1, branch2
这张图没有地方记录两个分支之间的任何“分离”,所以我们不妨理顺一下这个扭结:
...--o--o--o--o--o <-- branch1, branch2
直到我们在 branch2 上做出新的提交:
...--o--o--o--o--* <-- branch1
\
o <-- branch2
这并没有错误,但请注意,现在无法判断我们“向上”移动到第一行的三个提交是否已合并。
1这适用于普通的git commit 和git merge,但不适用于git commit --amend。提交的“修改”变体像往常一样进行新提交,但不是将 current 提交设置为新提交的父提交,而是设置当前提交的 父提交(如他们中的许多人,甚至可能根本没有父母)作为新提交的父母。效果是将当前提交推到一边,使其看起来好像提交已更改,而实际上旧提交仍在存储库中。
2超过两个父母的情况称为“章鱼合并”,我们可以在这里忽略它。 (它不会做任何重复的成对合并无法做到的事情。)
3在复杂的图形中,这样的“第一点”可能不止一个。在这种情况下,所有最低公共祖先节点是合并基础,对于 Git,-s <em>strategy</em> 合并策略参数决定如何处理这种情况。 (当然还有-s ours策略,它忽略了所有其他提交,完全绕过了三路合并代码。但我这里假设正常合并。)