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 哈希 IDdeadbeef
或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
。请注意,合并提交的消息还说明了哪个分支名称已合并到哪个其他分支名称中——但是您有机会在进行提交时编辑它,因此您可以更改它以适应您可能有的任何想法。:-)
(事实上,这与无冲突的情况没有什么不同:两者都运行git commit
以进行最终的合并提交,并且都让您有机会编辑消息。)
坏消息
坏消息是这些图表忽略了一些对你来说可能很重要的东西。Git 有第一个父级的概念:像这样的合并提交M
有两个父级,所以其中一个是第一个父级,一个不是。1 第一个父级是您在进行合并时如何判断您所在的分支。
因此,虽然这些图和作为动词的合并过程表明并不完全有“合并方向”,但这个第一父概念证明了存在。 如果你曾经使用过这个第一父母的概念,你就会关心这个,并且想要把它做对。幸运的是,有一个解决方案。(事实上,有多种解决方案,但我将首先展示笨拙但直截了当的解决方案。)
1显然,另一个是第二个父母:只有两个父母。但是,Git 允许将提交与三个或更多父级合并。您可以在必要时枚举所有父母;但是Git认为第一个特别重要,并且--first-parent
对各种命令都有标志,以便遵循“原始分支”。
得到我们想要的合并
让我们继续进行“错误”的合并提交:
# git add ... (if needed)
git commit
现在我们有:
o--o--...--o <-- [old branch1 tip]
/ \
...--o--* M <-- branch1
\ /
o--o--...--o <-- branch2
whereM
的第一个父母是旧的branch1
小费。
我们现在想要的是进行一个新的合并提交(具有不同的 ID),它与以下内容相同M
:
branch2
指向它
- 它的第一个父母是
branch2
- 它的第二个父节点是
branch1
诀窍是以某种方式保存提交M
——这很简单,我们将使用一个临时名称,这样我们就不必复制 downM
的哈希 ID——然后重置branch1
:
git branch temp # or git tag temp
git reset --hard HEAD~1 # move branch1 back, dropping M
(请注意,到目前为止,这与brehonia 的答案相同)。我们现在有了这个图表,除了每个提交附加的标签之外,它几乎没有变化:
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
——但一旦我们做出,它的第一个父项将是 的当前提示branch2
,而它的第二个父项将是 的当前提示branch1
。我们只需要获得正确的资源来完成我们将要进行的提交。
现在我们使用名称保存的合并结果temp
。这假设您位于树的顶层,因此.
命名所有内容:
git rm -rf -- . # remove EVERYTHING
git checkout temp -- . # get it all back from temp
我们现在正在使用我们之前提交的合并结果。我们甚至不需要做git add
任何事情,因为这种形式会git checkout
更新索引,所以:
git commit
并且我们根据需要M2
在 branch 上有了新的 merge branch2
:
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
并运行git commit
了,我们就有了正确的树,我们只是有一个具有错误的第一个和第二个父 ID 的提交。您可能还希望编辑提交消息,以便看起来您正在将消息中的 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
步骤创建了一个新的合并提交,它是我们刚刚创建的那个的副本,但是交换了两个父级。然后branch2
,我们使用git branch -f
. 确保branch2
在此步骤中获得正确的名称 ( )。HEAD^1
和HEAD^2
名称指的是当前提交的两个(第一个和第二个)父级 ,这当然是我们刚刚进行的合并提交。具有正确的树和正确的提交消息文本;它只是有错误的第一个和第二个父哈希。
一旦我们将新的提交安全地添加到branch2
,我们只需将当前 ( branch1
) 分支重置回来以删除我们所做的合并提交。