TL;DR:看到结尾
最后有一个“食谱”;向下滚动找到它(然后向上滚动一点找到描述和解释以及稍微安全一点的方法)。
好消息和坏消息
从某种意义上说,合并并不完全有一个“方向”。 (当你看到一个合并时,你“看到”的方向是你想象的产物,加上合并提交的消息,再加上一个更重要的事情,这将是我们的“坏消息”——但最终它并不是致命的坏消息。 )
考虑这个以提交* 为合并基础的一对分支的图表:
o--o--...--o <-- branch1
/
...--o--*
\
o--o--...--o <-- branch2
合并(“合并为动词”)branch1和branch2的过程是通过:
- 比较,即
git diff,合并基础提交*与branch1的提示提交;
- 将合并基数与
branch2的尖端进行比较;
- 然后,从基础开始,结合两组更改。
因此实际的合并本身应该是这样的:
o--o--...--o
/ \
...--o--* M
\ /
o--o--...--o
(我将合并提交命名为M,因为我们不知道或真正关心的是什么疯狂的 Git 哈希 ID deadbeef 或 badc0ffee 或其他任何东西。)最终的合并提交 M 是合并(“作为名词合并”);它基于 merge-as-a-verb 组合过程存储最终的源树。
这次合并是哪个“方向”?好吧,这至少部分取决于我们贴上的标签,不是吗? :-) 请注意,我小心地剥离了branch1 和branch2。让我们把它们放回去:
o--o--...--o <-- branch1?
/ \
...--o--* M <-- branch1? branch2?
\ /
o--o--...--o <-- branch2?
这可能看起来令人困惑。它可能是令人困惑。这是整个问题的很大一部分。 坏消息是,这不是全部的事情。有些东西我们无法在这些图纸中完全捕捉到。这对您来说可能无关紧要,如果有,那也没什么大不了的;我们将通过再进行一次合并提交来修复它。但是现在,让我们继续前进吧。
进行合并提交
一旦我们自己做出合并提交 M,我们必须选择——或者让 Git 选择——它在哪个分支上。在一个简单的、无冲突的合并中,我们总是让 Git 选择这个。 Git 所做的 很简单:无论我们在哪个分支上,当我们启动git merge 命令时,都是获取 合并提交的分支。所以:
git checkout branch1
git merge branch2
表示最终的图片是:
o--o--...--o
/ \
...--o--* M <-- branch1
\ /
o--o--...--o <-- branch2
我们可以重新绘制为:
...--o--*--o--...--o--M <-- branch1
\ /
o---...---o <-- branch2
这表明我们将 branch2 合并到 branch1 中,反之亦然。尽管如此,提交M 所附的源代码并不依赖于这个“合并方向”。
侧边栏:合并冲突
就最终结果而言,冲突合并就像非冲突合并一样。只是 Git 不能自己进行合并。它停止并让你解决冲突,然后运行git commit 进行合并提交。最后的git commit 步骤使合并提交M。
新的合并提交继续当前分支,就像任何其他提交一样。这就是M 在branch1 上的结束方式。请注意,合并提交的 message 还说明了哪个分支名称被合并到哪个其他分支名称中 - 但是您有机会在提交时编辑它,因此您可以更改它以适应任何你可能心血来潮。 :-)
(实际上,这与无冲突的情况没有什么不同:都运行git commit 来进行最终的合并提交,并且都给了你编辑消息的机会。)
坏消息
坏消息是这些图表忽略了一些可能对你很重要的东西。 Git 有一个 first parent 的概念: 像 M 这样的合并提交有两个父级,所以其中一个是 first 父级,一个不是。1 first parent 是您在进行合并时如何判断您在哪个分支上的方式。
因此,虽然这些图和作为动词的合并过程表明并不完全有“合并方向”,但这个第一父概念证明了存在。 如果您曾经使用过这种第一父母的概念,您会关心这一点,并希望把它做好。幸运的是,有一个解决方案。 (事实上,有多种解决方案,但我会先展示一些笨拙但简单的解决方案。)
1显然,另一个是第二个父母:只有两个父母。但是,Git 允许将提交与三个或更多父级合并。您可以在必要时枚举所有父母;但 Git 认为 first 尤为重要,对各种命令都有--first-parent 标志,以便遵循“原始分支”。
得到我们想要的合并
让我们继续进行“错误”的合并提交:
# git add ... (if needed)
git commit
现在我们有了:
o--o--...--o <-- [old branch1 tip]
/ \
...--o--* M <-- branch1
\ /
o--o--...--o <-- branch2
其中M 的第一个父母 是旧的branch1 提示。
我们现在想要的是创建一个 新的 合并提交(具有不同的 ID),它与 M 相同,除了:
-
branch2 指向它
- 它的第一个父节点是
branch2 的尖端
- 它的第二个父节点是
branch1 的上一个提示
诀窍是以某种方式保存提交M——这很简单,我们将使用一个临时名称,这样我们就不必复制M的哈希ID——然后重置branch1:
git branch temp # or git tag temp
git reset --hard HEAD~1 # move branch1 back, dropping M
(请注意,到目前为止,这与brehonia's answer 相同)。我们现在有了这个图表,除了每个提交附加的 标签 之外,它几乎没有变化:
o--o--...--o <-- branch1
/ \
...--o--* M <-- temp
\ /
o--o--...--o <-- branch2
现在查看branch2 并再次运行合并;它会像以前一样因冲突而失败:
git checkout branch2
git merge branch1 # use --no-commit if Git would commit the merge
从某种意义上说,我们尚未进行的提交与合并提交 M 相同——但一旦我们完成,它的 first 父级将成为 @987654368 的当前提示@,其 second 父级将是 branch1 的当前提示。我们只需要获得正确的source 来完成我们将要进行的提交。
现在我们使用名称temp 保存的合并结果。这假设您位于树的顶层,因此 . 命名所有内容:
git rm -rf -- . # remove EVERYTHING
git checkout temp -- . # get it all back from temp
我们现在正在使用我们之前提交的合并结果。我们甚至不需要 git add 任何东西,因为这种形式的 git checkout 会更新索引,所以:
git commit
我们根据需要在分支branch2 上进行了新的合并M2:
o--o--...--o__ <-- branch1
/ \ \
...--o--* M \ <-- temp
\ / \
o--o--...--o-----M2 <-- branch2
现在我们可以删除临时分支或标签了:
git branch -D temp # or git tag -d temp
我们都完成了。随着临时名称的消失,我们再也无法看到原来的合并 M。
偷偷摸摸的管道方法
实际上有一个更短的方法来完成这一切,使用 Git 的“管道”命令,但它有点棘手。一旦我们拥有所有git add-ed 并运行git commit,我们就有了正确的树,我们只是有一个具有错误的第一和第二父ID 的提交。您可能还希望编辑提交 message,以便看起来您正在将消息中的 branch1 合并到 branch2 中。那么:
# git add ... (if / as needed, as above)
# git commit (make the merge)
git branch -f branch2 $(git log --pretty=format:%B --no-walk HEAD |
git commit-tree -p HEAD^2 -p HEAD^1 -F -)
git reset --hard HEAD^1
git commit-tree 步骤创建了一个新的合并提交,它是我们刚刚创建的那个的副本,但是交换了两个父级。然后我们使用git branch -f 使branch2 指向这个提交。 确保您在这一步得到了正确的名称 (branch2)。 HEAD^1 和 HEAD^2 名称指的是 当前 的两个(第一和第二)父母em> commit,这当然是我们刚刚做的合并提交。具有正确的树和正确的提交消息文本;它只是有错误的第一个和第二个父哈希。
一旦我们将新提交安全地添加到 branch2,我们只需将当前 (branch1) 分支重置回来以删除我们所做的合并提交。