Git:合并两个分支:合并方向是什么?

80

我们有以下情况:

             A --- B --- C --- ... --- iphone
           /
  ... --- last-working --- ... --- master

在 last-working 和 iPhone 之间进行了32次提交,在 last-working 和 master 之间进行了许多次提交。

我现在想要一个新的分支,将iphone和当前的master合并在一起。稍后,这应该合并到master中。

首先,我计划执行以下操作:

git checkout iphone -b iphone31
git merge master

但是后来我想,如果这样做会不会更好:

git checkout master -b iphone31
git merge iphone

现在我在思考。 结果会有什么不同呢?合并会有所不同吗?

我已经尝试了两种方式,正如我所预料的那样,因为iphone相对于master而言真的很旧,所以我遇到了许多冲突。现在我在想最简单的合并方法。

也许甚至从master开始,将iphone的每个提交合并到其中会更容易?像这样做:

git checkout master -b iphone31
git merge A
git merge B
git merge C
...
git merge iphone

在最后合并完成之后(即所有冲突都已解决且它正在工作时),我想要这样做:

git checkout master
git merge iphone31

与我相关的内容(至少对我而言):https://dev59.com/vnM_5IYBdhLWcg3ww2AQ - cregox
1
“git checkout -b iphone31” 是否等同于 “git checkout iphone -b iphone31”? - Gerald
git log --first-parent 的行为将会有所不同。 - Chuck Lu
5个回答

49

关于替代方案

git checkout iphone -b iphone31
git merge master

git checkout master -b iphone31
git merge iphone

他们将拥有相同的容易或困难,这就像争论一个玻璃杯是半满还是半空。

版本树感知

我们观察版本树的方式在某种程度上只是我们的任意感知。假设我们有一个如下所示的版本树:

    A----------+
    |          |
    |          |
   \_/        \_/
    B          X
    |          |
    |          |
   \_/        \_/
    C          Y
    |
    |
   \_/
    D
    |
    |
   \_/
    E

假设我们想基于从C到E的更改创建一个新版本Z,但不包括从A到C所做的更改。

"哦,那会很困难,因为没有共同的起点。"其实不是这样。如果我们只是稍微改变图形布局中的对象位置,就像这样:

      /
    C+---------+
    | \        |
    |          |
   \_/         |
    D          B
    |         / \
    |          |
   \_/         |
    E          A
               |
               |
              \_/
               X
               |
               |
              \_/
               Y

现在情况开始看起来很有前途。请注意,我没有改变任何关系,箭头仍然指向上一个图片中的同样方向,版本A仍然是公共基础。只有布局被改变了。

但现在很容易想象出一棵不同的树

    C'---------+
    |          |
    |          |
   \_/        \_/
    D          B'
    |          |
    |          |
   \_/        \_/
    E          A
               |
               |
              \_/
               X
               |
               |
              \_/
               Y

任务只是按照正常方式合并版本E。

因此,您可以合并任何您想要的内容,唯一影响易用性或难度的是在选择起点或公共基础和合并位置之间所做更改的聚合。您不限于使用版本控制工具建议的自然起点。

对于某些版本控制系统/工具可能并不简单,但如果所有其他方法都失败了,您可以通过手动检出版本C并将文件保存为file1、检出版本E并将文件保存为file2、检出版本Y并将文件保存为file3,并运行kdiff3 -o merge_result file1 file2 file3来执行此操作。

答案

对于您的具体情况,很难确定哪种策略会产生最少问题,但如果有许多更改会创建某种冲突,则拆分并合并较小的部分可能更容易。

我的建议是,由于最后一个工作版本和iphone之间有32个提交,您可以从主分支分支,然后合并前16个提交。如果那太麻烦了,请还原并尝试合并前8个提交。以此类推。最坏的情况是您最终需要逐个合并这32个提交,但这可能比处理所有累积的冲突更容易,并且在这种情况下,您正在使用一个真正分散的代码库。

提示:

在纸上绘制版本树,并用箭头注明您要合并的内容。如果将过程拆分为几个步骤,则将其划掉。这将使您更清楚地了解您想要实现什么,迄今为止已经做了什么以及还剩下什么。

我真的很推荐KDiff3,它是一款优秀的差异/合并工具。


假设我们想要基于从C到E的更改,但不包括从A到C所做的更改,从Y检出一个新版本Z。我不想排除任何提交。我真的看不出这与我的需求有什么关系。我只想将主分支与iPhone分支合并。 - Albert
我的建议是,因为在上一个可行版本和 iPhone 版本之间有 32 次提交,所以你可以从主分支创建一个新分支,然后将前 16 次提交合并进来。这样做是否更容易呢?但是难道我不需要针对每个提交都再次解决同样的冲突吗?比如我在一个提交中添加了一行,然后在另一个提交中紧接着添加了另一行。现在把这两个提交一起处理比分别处理两次更容易,不是吗? - Albert
1
他们将具有相同的容易或困难程度,这正是我的问题。所以我在一开始提出的两种方式都会产生完全相同的结果?因此,Git合并是100%对称的吗? - Albert
再次引用我的主要问题:“我现在在想。结果会有什么不同?合并是否会表现不同?” - Albert
1
“我不想排除任何提交。”是的,我理解这一点,我的回答部分比严格需要的更为普遍,很抱歉没有表达得更清楚。我的观点是,您可以合并任何您想要的内容,只需要指出起始/结束点即可。 - hlovdal
显示剩余2条评论

42

有所不同。


在合并时始终检查目标分支(即如果将A合并到B中,则检出B)。


人们经常说合并方向无关紧要,但这是错误的。尽管无论合并方向如何,生成的内容都将是相同的,但有几个方面是不同的:

  1. 取决于方向,结果合并提交中列出的差异将不同。
  2. 大多数分支可视化器将使用合并方向来确定哪个分支是“主要”分支。

为了解释清楚,想象一个夸张的例子:

  • 您从MASTER分支中分支出1000个提交,并将其命名为DEVELOP(或在跟踪分支方案中,您已经有一段时间没有进行提取)。
  • 您在DEVELOP中添加了一个提交。您知道此更改没有冲突。
  • 您想将更改推入MASTER。
  • 您错误地将MASTER 合并到 DEVELOP中(即在合并期间检出DEVELOP)。然后,您将DEVELOP作为新的MASTER推送。
  • 结果合并提交中的差异将显示MASTER中发生的所有1000个提交,因为DEVELOP是参考点。

这些数据不仅无用,而且难以理解发生了什么。大多数可视化器将使它看起来像您的DEVELOP线一直是主要的,并带入了1000个提交。

我的建议是:在合并时始终检查目标分支(即如果将A合并到B中,则检出B)。

  • 如果您正在平行分支上工作并希望定期引入主分支中的更改,请检出您的平行分支。 差异将有意义--您将看到主分支对于您的分支所做的更改。
  • 当您完成平行工作并希望将更改合并到主分支时,请检出主分支并与平行分支合并。差异再次有意义--它将显示您的平行更改相对于主分支的内容。

在我看来,日志的可读性很重要。


你所说的是git中的--first-parent概念,当你使用第一个父节点显示日志图时,它会表现不同。我还为此创建了一个示例仓库,链接为https://github.com/ChuckGitMerge/FirstParentTest。 - Chuck Lu

6
最好的方法取决于其他人是否拥有您代码的远程副本。如果“主”分支仅存在于您的本地计算机上,则可以使用rebase命令将特性分支中的提交交互式地应用到主分支中:
git checkout master -b iphone-merge-branch
git rebase -i iphone

注意,这会更改您的新 iphone-merge-branch 分支的提交历史记录,这可能会对稍后尝试将您的更改拉入其检出的任何其他人造成问题。相比之下,合并命令将更改应用为新提交,这在协作时更安全,因为它不会影响分支历史记录。请参见 this article 以获取有关使用变基的一些有用提示。
如果需要保持提交历史记录同步,则最好执行合并。您可以使用 git mergetool 使用可视差异工具逐个交互式修复冲突(有关此内容的教程可以 在此处找到):
git checkout master -b iphone-merge-branch
git merge iphone
git mergetool -t kdiff3

如果你想完全控制流程,第三个选择是使用git cherry-pick。你可以使用gitk(或你喜欢的历史查看器)查看iphone分支中的提交哈希值,记录下来,并逐个将它们挑选到合并分支中,解决冲突。有关此过程的说明可以在这里找到。这个过程将是最慢的,但如果其他方法不起作用,可能是最好的备选方案。
gitk iphone
<note down the 35 commit SHA hashes in this branch>
git checkout master -b iphone-merge-branch
git cherry-pick b50788b
git cherry-pick g614590
...

那个变基有什么优势比合并好呢?我不太明白。为什么git rebase比git merge更智能?但无论如何,在这里使用rebase不是一个选项。 - Albert
重新定位将提交按照时间顺序逐个应用到您的分支中,这显著减少了冲突,因为它们会在历史记录中适合的位置插入提交。考虑到重新定位对您来说不是一个选项,我建议您调查git-mergetool方法。 - seanhodges
但是我也可以这样做,逐个合并每个提交。那么与变基有什么区别呢? - Albert
1
当你合并时,你会将每个提交应用于HEAD之上,而HEAD已经与其他分支分叉 - 导致冲突。当你变基时,你实际上是倒回时间,并在它们被制作的位置应用(不是合并)提交。例如,在2010年1月12日在“iphone”中进行的提交将在2010年2月14日在“master”分支中进行的提交之前应用。 - seanhodges
当执行“git checkout iphone && git rebase master”时,与“git checkout master && git merge A && ...”有什么区别?据我所知,这应该完全相同。 - Albert
2
不,结果是非常非常不同的。唯一相同的是你最终的工作副本。我建议你阅读我在解决方案中提到的链接,了解一些关于rebase命令的背景知识:http://www.gitready.com/intermediate/2009/01/31/intro-to-rebase.html - seanhodges

2

您说:

与主分支相比,iPhone分支非常老

您真的想将它们合并成一个新的分支吗?

现在,主分支和iPhone分支的目的已经非常不同(因为iPhone非常老),是否考虑将iPhone分支与主分支的祖先合并成一个新的分支呢?

我强烈建议您阅读有关合并和分支目的的乐趣

在阅读了这篇文章之后,如果您仍然想合并iPhone和主分支,则@seanhodges会非常好地解决如何处理冲突的问题。


“the purpose”是什么意思?实际上,我想要的是将我在iPhone上所做的更改应用到主分支中。当我创建这个iPhone分支时,目的是为了添加iPhone支持。现在这个目标已经完成并且运行良好,因此我想将其合并到主分支中。我的问题是,我提出的两种方法是否有任何区别?或者是否有更简单的方法来实现我的需求? - Albert
那个链接开头深入地指出:“在git中合并分支时,所有分支都被视为相等。” 这通常是问题所在,不是吗?尽管请看看链接建议做与您建议的相反的事情:如果您开始在“主”发布版为1.0时就开始使用“frotz”,后来“主”代码库发生了重大变化,并且[分支“add-frotz”不再]干净地合并到“主”中...您可以选择更改“add-frotz”的目的...以一种更具体的方式“添加frotz功能,使其能够干净地合并到2.0代码库中”[并首先与主分支合并]。 - ruffin
这并没有回答OP关于“有什么区别”的问题。这个答案更像是“为什么不这样做呢?”而且,有时我们确实想要合并非常落后的东西。例如,合并gitignore文件更改。 - Ryuu

1

在尝试实验之前,您可能希望创建本地备份以便重新开始,如果您不再理解发生了什么。

如果想保留参考资料,请将您的工作创建备份分支,以免在rebase过程中丢失。

git checkout iphone -b iphone_backup

从主分支创建一个新的分支。
git checkout master -b iphone31

然后在其顶部进行变基。
git rebase -i --onto iphone31 iphone

Sean的观点是正确的,rebase一次只应用一个提交。但是不必担心你要rebase的分支上已经存在的提交-它们不会被更改。Rebase会将新的(经过调整的)提交放在它们的顶部。虽然每次只处理一个提交,但您可以更好地控制冲突。重要的是,您必须将您的工作重新基于主分支而不是相反,以便主分支的历史记录不会更改。

或者,您可以仅执行

git rebase -i master iphone

如果您不关心备份iPhone分支,请查看rebase文档以获取详细信息和替代方案http://git-scm.com/docs/git-rebase


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接