从已经从主分支rebase的另一个分支上rebase一个分支

6

我有如下场景:

o---o---o---o---o  master
     \
      o---o---o---o---o  next
                       \
                        o---o---o  topic

如上所述,我在主分支上进行了一些提交,现在我想更新另外两个分支,并使它们保持一致:

o---o  master
     \
      o---o---o---o---o  next
                       \
                        o---o---o  topic

我已经在next分支上执行了git rebase master,但并没有考虑到此时topic分支缺少next分支的基础提交。

现在该怎么办?我立刻对中间分支进行变基操作是错误的吗?我应该怎么做?


我会使用 git checkout topic; git rebase next,但是我想知道是否有更好的解决方案。有时我需要解决一些我觉得本可以避免的问题。 - Jan Matějka
我已经尝试过了,但是git无法变基,因为“topic”不再具有“next”的基本提交,这是由变基主分支更改的。很难解释我想说什么,希望清楚!:D - Antonino Scarfì
不太清楚。有时我也会做这种变基,虽然不是最理想的方法,但对我来说还是有效的。 - Jan Matějka
1个回答

14

有两个相对容易的解决方案。一个是尝试rebase,这可能有效。但如果不行,还有另外一种更强大的方法。下面是第二种方法以及为什么要这样做:

git checkout topic; git rebase --onto next next@{1}

发生了什么:如何使用rebase

如果您以稍微不同的方式绘制图形,我认为视图会更清晰。以下是原始图形稍作重绘后的样子:

A---B---C---D---E                   <-- master
     \
      F---G---H---I---J             <-- next
                       \
                        K---L---M   <-- topic

使用git checkout next; git rebase master命令后,以下是您的代码所处的状态:

A---B---C---D---E                   <-- master
     \           \
     |            F'-G'-H'-I'-J'    <-- next
     \
      F---G---H---I---J             [was "next", but not anymore]
                       \
                        K---L---M   <-- topic
这是因为 rebase 通过 复制 提交来工作,并进行一些轻微(或重大)的更改。新提交 F' 是原始提交 F 的副本,G'G 的副本,等等。
如果您现在运行 git checkout topic; git rebase next,它通常可以正常工作,但并非总是如此。这是因为默认情况下,rebase 根据单个参数选择要复制的提交以及要复制到的位置。这有点复杂,因此让我们分两步来看。
当您说 git rebase next 时,单个参数是 next:副本将放置在 next 的尖端提交之后(现在是 J')。这部分完全简单明了:那个标识符 next 找到 --onto 符合复制的提交。(我拼写成 --onto 而不只是 "onto" 有一个原因 :-))
复制的内容比较棘手,有两种方式。首先,next 指定不应该复制的内容,而当前分支(topic)指定要复制的内容。Git 获取从 topic 可达到的所有提交列表 - 这是提交 ABFGH,以及一直到 L 的提交。然后它会排除从 next 可达的所有提交 - 这是提交 ABCDEF'(但不是 F)、G'(但不是 G)等等,直到 J'。一些被排除的提交不在包含的列表中并不重要:我们只是排除了那些在包含列表中的提交。
不幸的是,这使得提交 FGHIJ 在待复制的提交堆栈中。这就是事情出错的地方,正如您所注意到的那样。但是,在大部分时间里,它似乎仍然可以正常工作。当它正常工作时发生了什么?为什么它不总是有效?答案:rebase也会排除“补丁等价提交”。在进行变基时,Git会扫描“排除”列表,看是否有任何提交与“包括”列表中的提交执行相同操作。由于F'是F的副本,在补丁等价意义上很可能与F“相同”。G与G'、H与H'也是如此。
只要提交与其变基后的版本补丁相同,它们就会被排除。
当这个条件成立(通常情况下是成立的)时,你可以简单地输入git checkout topic; git rebase next并让它自动完成。只有当提交的修改量过大导致复制后的提交不再补丁等价时,才会失败。作为一个经验法则,如果Git能够单独完成第一次变基,那么它也能够单独完成第二次变基。主要是当它停止处理冲突并要求你手动解决时,补丁才会变得“非补丁等价”。但使用更长的git rebase格式处理这些问题很容易。
git rebase next

我们可以写成:

git rebase --onto next <something>

根据我们绘制的图表,这里需要使用<something>来指定提交J。也就是说,我们需要找到next在重新设置之前所指向的提交记录。现在它指向了J',但它曾经指向过J。我们需要Git排除那些next曾经能够到达的提交记录:ABFGHIJ。如果Git无法通过补丁等效性自动找到它们,我们可以手动告诉Git。

这仍然存在一个问题:如何拼写标识提交J的名称。一种方法是使用原始的哈希值,这总是有效的,但有点繁琐(不仅要找到,而且要复制,尽管剪切和粘贴对于复制哈希值非常有效)。不过再次查看重画的图表,看一下“是“next”,但不再是”的部分。如果我们只能告诉Git查找next的前一个值。

但等等!Git有reflogs!我们可以告诉它做到这一点!next的前一个值是next@{1}。(每当我们更改标签next,例如添加新的提交记录,该数字就会增加,但我们此时所做的仅是一个rebase,因此它仍然是next@{1}。)因此,此时我们只需要执行以下操作:

git checkout topic; git rebase --onto next next@{1}

(根据您的shell不同,如果要使用花括号,可能需要将其放在引号中,例如'next@{1}'next@\{1\}等)。


1有一些例外情况。例如,在这种情况下,如果 git diff E F'git diff B F 的产生的更改集不同,由于diff选择了不同的半相关匹配项,比如空行或闭合括号行,那么就会出现问题。然而,处理这些问题的答案与处理由于冲突导致手动应用变基的答案相同。


感谢您提供这个完整的解释!无论如何,我通过交互式变基并仅保留主题分支来解决了问题,但我理解您所解释的内容,并将记住它以备下次使用! :)非常感谢。 - Antonino Scarfì
是的,交互式变基是另一种处理这个问题的方法,因为你有机会删除已复制的提交。另一种方法是编写一个程序,通过找出共享和独特的部分,一次性进行多分支变基。当所有分支标签都包含在一个“主”变基中时(就像本例一样),你只需执行主变基,然后移动所有内部标签。 - torek
1
感谢@torek提供的出色答案!如果可以的话,我会点赞两次:)非常有效。我的做法是使用“next”进行交互式变基,其中我压缩了一些提交(git checkout next; git rebase -i HEAD~8)。然后我切换到“topic”,想要将其变基到来自“next”的新提交上(git checkout topic; git rebase next)。但是(多亏了您的解释),我现在明白了压缩的提交不再是补丁等效的了。因此,我执行了git rebase --onto next next@{1},现在我的历史记录很干净了。 - Simon Lang

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