合并一个基于另一个特性分支的特性分支

20
几天前,我有一个完全线性历史记录的主分支(master branch)。然后我创建了一个特性分支,我们称之为feat/feature-a。我在该分支上工作,然后将其提交进行代码审核以合并到主分支master中。
当feat/feature-a正在审核时,我想要开发另一个依赖于feat/feature-a引入的一些代码的功能。因此,我从feat/ feature-a分支创建了一个feat/feature-b分支。
在我工作于feat/feature-b期间,feat/feature-a被合并到了master中。所以现在master拥有由feat/feature-a引入的代码。我现在想将feat/feature-b合并到master中,但我遇到了很多合并冲突,看起来像这样:
<<<<<<< HEAD
=======
    // Some code that was introduced by feat/feature-b
>>>>>>> c948094... My commit message from feat/feature-b

我猜测是因为我将feat/feature-a的更改合并到了我的feat/feature-b分支中,现在我正在尝试“复制”这些更改,导致合并冲突。

我可以手动解决这些冲突,但它们存在于数十个文件中多次出现,所以如果有更好的解决方案,我想知道。


1
这是一个实际的合并(merge),还是压缩(squash),还是变基(rebase),还是其他什么东西? - Useless
现在我正在通过git rebase -i masterfeat/feature-b与主分支进行变基。我正在将feat/feature-b上的所有提交压缩为1个提交。 - Chris White
我的意思是问,feature-a 的合并是真正的合并吗?但是,如果 feature-b 的合并不是真正的合并,那也会出问题。 - Useless
它是这样的。feat/feature-a 被压缩成一个提交,然后合并到主分支,产生了一个合并提交。 - Chris White
那个被压缩的提交在 feat/feature-a 分支上还在吗?这么说它是 feat/feature-b 的父分支? - Useless
显示剩余4条评论
2个回答

38

摘要:使用git rebase --onto <target> <limit>

正如Useless在评论中建议的那样,如果您进行了真正的合并,则不应发生这种情况。以下是我所说的“真正的合并”,以及有关贡献记录图形的示意图。我们从类似于这样的东西开始:

...--E---H         <-- master
      \
       F--G        <-- feat/feature-a
           \
            I--J   <-- feat/feature-b

这里有两个提交(虽然确切的数量并不重要),它们仅存在于feat/feature-b分支上,这里称为IJ;有两个提交在两个特性分支上,称为FG;还有一个提交仅存在于master分支上,称为H。(提交E及之前的提交存在于所有三个分支上。)

假设我们在master上进行实际合并以引入FG。以图形形式表示如下:

...--E---H--K      <-- master
      \    /
       F--G        <-- feat/feature-a
           \
            I--J   <-- feat/feature-b
请注意真正的合并提交在其父提交历史指针中有两个提交H(位于master分支上)和G(位于feat/feature-a分支上)。因此,Git稍后知道合并J意味着“从G开始”。更准确地说,提交G将成为此后合并的合并基础。
这种合并方式本可以直接使用。但以前发生的事情并非如此:相反,执行合并的人使用了所谓的“压缩合并”功能。虽然压缩合并带来了与实际合并相同的更改,但它根本不会产生任何合并结果。相反,它会产生一个重复合并了多少次提交工作的单一提交。在我们的例子中,它会重复执行从FG进行的工作,因此看起来像这样:
...--E---H--K      <-- master
      \
       F--G        <-- feat/feature-a
           \
            I--J   <-- feat/feature-b

请注意,K 没有从 G 回指的指针。

因此,当您尝试合并(真正的合并或压缩而不是真正意义上的“合并”)feat/feature-b 时,Git 认为它应该从 E 开始。 (在较早的真正合并情况中,技术上说 E 是合并基础,而不是像之前那样的 G。)正如您所看到的,这会导致合并冲突。(通常它仍然可以“正常工作”,但有时就像在这种情况下一样,它不行。)

也许对于未来来说这很好,但现在问题是如何解决它。

在这里要做的是将专门针对 feat/feature-b 的提交复制到位于 K 后面的提交中。也就是说,我们希望图片看起来像这样:

              I'-J'  <-- feat/feature-b
             /
...--E---H--K        <-- master
      \
       F--G          <-- feat/feature-a
           \
            I--J     [no longer needed]

最简单的方法是使用变基(rebase)这些提交,因为变基意味着复制。问题在于一个简单的git checkout feat/feature-b; git rebase master会复制太多的提交。

解决方案是告诉git rebase要复制哪些提交。你可以通过将参数从master更改为feat/feature-a(或提交G的原始哈希ID——基本上是任何标识第一个1不需要复制的提交的东西)。但是这会告诉git rebase将它们复制到它们已经存在的地方,所以这样做是没有用的。所以解决新的问题的方法是添加--onto,它允许您将“副本的去处”部分与“要复制”的部分分开:

git checkout feat/feature-b
git rebase --onto master feat/feature-a

(假设您仍然拥有名称为feat/feature-a指向提交G;如果没有,您将不得不找到其他命名提交G的方法 - 您可能希望绘制自己的图形和/或仔细查看git log输出以查找提交哈希)。


1“首先”是Git风格的倒序方式。我们从最近的提交开始,向后跟随连接以查找旧的提交。Git在一切方面都是倒序的,因此在这里反向思考会有所帮助。:-)


这是一个完美的解释。谢谢! - Chris White

1
这是英语文本,翻译为中文:

这个简单的模型长这样:

X  -> MA  <master
  \  /
   A      <feature-a

在这里,feature-a可能已经通过变基压缩成了一个单独的提交,但合并仍然是真正的合并。然后,你有

X -> MA -> MB  <master
 \  /     /
  A ---> B     <feature-b

feature-b是在任何压缩后基于feature-a,同时正常合并。这种情况应该是可以正常工作的,因为git可以看到AB的祖先,并且你已经将其合并。

相比之下,以下内容无法干净地工作:

X -> MA -> ...                  <master
|\  /      
| As                            <feature-a
|  |
|  ^--squash------<--
 \                   \
  A0 -> A1 -> ... -> An -> B    <feature-b

在合并 feature-a 之前,您将 A0..n 压缩成 As,但 feature-b 是从 An 分支出来的。

现在 Git 不知道 As 和 A0..n 之间的关系,因此合并和(简单)变基都无法自动工作。如果要使用 rebase --onto 解决此问题,请参见 torek 的卓越答案。


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