git:修改维护分支并将补丁应用到另一个分支的正确合并或变基工作流是什么?

8
目标: 我需要对上游项目的以前版本进行自定义补丁,并希望能够将这些补丁应用于较新的版本。 问题: 从维护分支到较新版本的变基会导致未在维护分支中修改的文件产生冲突。 怀疑: 我应用合并或变基的方式不正确,无法达到我的目的。
例如:这是我想要实现的逻辑示例,但请注意,已标记的发布 v1.0 和 v2.0 之间的提交历史可能有数百个提交。
我使用多个标记发布(v1.0 和 v2.0)的上游 repo 进行 fork,其主提交历史为 A 到 D。B 是标记 v1.0 的最后一个提交。C 是标记 v2.0 的最后一个提交。D 是正在开发中。
$ git clone git@server:repo

A<--B(v1.0)<--C(v2.0)<--D Master

我从早期的发布标签(v1.0)分支出来,进行了一些自定义修改,如下所示:

$ git checkout v1.0 -b v1.1

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F v2.1

我现在想要做的是从一个后续发布标签(v2.0)分支,并将我在v1.1分支上制作的补丁提交(G'和H')应用到v2.0分支上。我需要保留在v1.0中进行的各个提交在v2.0日志中的记录。

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F<--G'<--H' v2.1

问题: 如何正确地应用这些更改并避免冲突?我尝试了许多合并和变基的组合(包括--onto),但都失败了。git rebase想要默认进行三方合并,并与无关文件发生冲突。

$ git rebase v2.1 v1.1
Falling back to patching base and 3-way merge
...
Failed to merge in the changes.

$ git checkout v2.1
$ git rebase v1.1
Falling back to patching base and 3-way merge
...
Failed to merge in the changes.
2个回答

7

我建议您编辑问题,包括您尝试重新定位的确切命令。这应该是您实现所需内容的方式,但如果您以触发比实际必要更多的重新定位的方式运行命令,则可能会导致许多不必要的痛苦。

如果您的重新定位命令重写了v1.0v.2.0之间的所有内容,那么如果该历史记录包括通过非快进合并解决的冲突,则可能会导致很多不必要的痛苦。

为了清晰起见,我已将有关合并冲突和重新定位的说明移至此答案的底部。但是,该部分仅是猜测,看到您尝试的rebase --onto示例将很有帮助。现在没有可用的内容,我将提供我认为您应该执行的操作。有了这个前提,让我们开始解决方案。


解决方案


Rebase --onto

我喜欢倒着阅读--onto的参数以更好地理解。倒着读,--onto <1> <2> <3>读作-获取在<3>上的任何提交,不在<2>上,并将其应用于<1>。提交并没有“移动”,它们是“克隆”的,因此您的旧提交仍然在原地-重新定位--onto只是创建了它们的副本并将它们应用于<1>之后。

重要的是要知道,在执行rebase --onto之后,您可能会处于无头状态。新提交按上述方式应用,但它们不会立即更改您的分支状态。这增加了一个额外的步骤,但也为您提供了额外的安全保障,以确保重新定位不会破坏您的分支-您将有机会在将这些更改应用于您的分支之前审查已更改的历史记录。

从这张图开始。

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F v2.1

为了让只有GH跟随F,而不包括E,这是根据您的描述看起来的情况,那么您应该尝试以下命令。
git rebase --onto F G^ v1.1

假设你的情况我了解得很少,以下是我的翻译:

这将获取在v1.1上存在但在紧随G提交的提交中不存在的所有提交,并在F之后应用它们。由于实际被重写的提交只有GH,因此您不应该遇到任何与这两个提交无关的冲突。


无头状态

如我上面所述,您可能会进入无头状态。这意味着您已经不在分支上了。此时,您的图表实际上看起来像这样...

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F v2.1
          \
           G'<--H' (currently checkout out headless state)

正如您所看到的,版本v2.1仍然停留在F,但是您已经创建了一个新的历史记录A<--B<--C<--F<--G'<--H'。这正是您想要的,但它不在您的v2.1分支中。因此,请检查您的历史记录并验证其是否是您想要的。如果需要,可以进行测试。一旦验证完成,您只需要切换到v2.1并运行以下命令...

git checkout v2.1
git merge H'

假设你在v2.1上没有新的提交不在 H' 中。为确保,请在合并时使用--ff-only标志,以便它将拒绝合并而不是创建合并提交。

如我上面所说,这是一个需要注意的额外步骤,但由于这个步骤,您可以放心地重置git rebase --onto,而不会弄乱您的实际分支。如果您发现重新基础不按预期工作 - 您可以简单地检出v2.1并查看未造成任何伤害。


结果

完成快进合并后,您将拥有以下历史记录...

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F<--G'<--H' v2.1


挑拣(cherry-picking)与重置基础(rebase --onto)

不会详细介绍挑拣,但我想要明确以下内容...

git checkout v2.1
git cherry-pick G^..H

完全等同于……

git rebase --onto v2.1 G^ H
git checkout v2.1
git reset --hard <hash> <-- were hash is the commit the rebase kicks you into.

樱桃挑选具有较少的步骤,可以在不检出“基础”(在这两种情况下都是v2.1)的情况下完成变基。 此外,正如上面所解释的那样,rebase --onto不会直接影响您的分支,如果出现问题,则更容易从中恢复。 它们都“克隆”它们带到基础的提交,使原始提交保持不变。
问题:
以上是关于如何实现您要求的操作的一般说明。以下是我对您描述的问题的怀疑。
非快进冲突解决和变基
我猜测,在v1.0和v2.0之间,您有一些非快进合并用于解决冲突。当在非快进合并期间解决冲突时,该冲突的解决方案存储在合并提交中,而不是在冲突提交本身中。合并提交在历史记录中稍后出现在合并点上,而不是在冲突提交本身上。
当您进行变基时,Git会逐个遍历每个提交并重新提交它-因此,您将重温所有由非快进合并引起的冲突,但是该冲突的解决方案在合并发生时直到历史记录后才可用。使用非快进合并解决的冲突对于将来重新基于分支而言是有害的,除非您愿意逐个重新解决所有这些冲突。
您可能犯的错误
如果我对您的问题的猜测是正确的,那么您可能已经执行了以下操作...
git rebase --onto v1.1 F v1.1

这项操作或其变体将导致获取F中所有不在v1.1上的提交,并将它们附加到v1.1的末尾。正如前面所述,这将导致BF之间的每个提交逐个重新提交。如果在这些提交中有用非快进合并解决的冲突,则在rebase步骤中会重现每个冲突。
合并而不是rebasing: 您的问题标题表明您可能愿意简单地合并这些历史记录。如果您不关心线性历史记录,则可以将v1.1简单合并到F中。这不应该导致任何奇怪的冲突,但它将显著混淆您的历史记录。

简而言之,我设法读完了第一部分(“非快进式冲突解决和变基”)。我必须说,我不知道你为什么要写这个?我理解v1.1只是通过进行几次提交而创建的,没有任何合并和冲突。如果重新设置正确执行,它应该只重放v1.0和v1.1之间的几个提交,所以所有关于重放具有冲突解决的合并提交的内容都是无关紧要的。 - Marcin Koziński
顺便提一下,你写的好像非快进合并与冲突有关 - 但是非快进合并而没有冲突是完全正常的。 - Marcin Koziński
1
如果你不打算仔细阅读,那么我不确定你为什么会评论说不理解。对于我来说很难知道你不理解哪一部分,因为我不知道你没有阅读哪一部分。 - eddiemoya
正如答案中所解释的那样 - 如果在非快进式合并期间解决了冲突,则解决方案将存储在合并提交中。当您进行变基时,git不会有该合并提交,因为它发生在实际提交之后的某个未来时间点。通过变基解决的冲突是在具有冲突的原始提交中解决的。这只是合并冲突的一个小问题。这很可能是OP在变基时遇到无关冲突的原因。如果有什么方法可以让我更清楚地表达,请告诉我。 - eddiemoya
关于你的“附注”,是的,非快进式合并并不总是会导致冲突。我所说的是,如果它们确实导致冲突,并且你选择在合并提交中解决这些冲突,那么在重新定位期间,你将会重新遇到这些冲突,而且你无法轻松解决,因为它们在未来的合并提交中。这一切都在答案中有所解释。 - eddiemoya

2

虽然 git cherry-pick 没有像 rebase 那样的细粒度控制,也没有像 merge 那样的易用性,但是将一系列提交传递给它似乎更适合将旧分支上的单个更改应用到当前分支上。

因此,如果 G 和 H 是 v1.1 中的最后两个提交,您应该能够通过以下方式将它们拾取到 v2.0 中:

git cherry-pick v1.1~1

(或手动提供提交哈希)

如果您已经尝试过这种方法,并且存在缺点,请告诉我。我仍在努力完善这种工作流程 : )


1
谢谢。到目前为止,我一直在使用cherry-pick,但我认为一个潜在的缺点是cherry-pick的分支可能会与上游进一步分歧。 - Brett Bonner

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