使用git rebase交互式命令来编排一系列的git cherry-pick操作?

3

git cherry-pick 允许通过指定哪个合并父节点作为基线来简单地挑选合并。例如:

git cherry-pick -m 1 1234abcdef

我有一堆提交记录需要挑选,其中有些可能是合并记录,而其他的则不是。我想使用git rebase来挑选所有这些提交记录,类似于以下方式:

git rebase -i --onto myBranch myBranch 

把拾取列表放入交互文件中:
p 1234
p 3224
... a bunch more picks
p abcde

如果在这些提交中,git rebase 遇到了合并,则我希望指定类似于cherry-pick的-m 1选项来指示更改应针对第一个父级进行挑选。

我尝试了许多与rebase相关的合并选项,但最终都会遇到以下错误:

commit 3c4ffe04532 is a merge but no -m option was given.

即使我指定了-m来进行rebase,也是如此。
我知道我可以编写一个使用cherry-pick的脚本,但我喜欢现有的行为rebase -i(它会遍历命令列表,并在到达无法处理的内容时暂停)。我非常希望直接利用这个逻辑,但我还没有找到正确的方法来调整rebase的pick命令以填补这个空白。
有没有办法让rebase采用cherry-pick的-m #行为来选择pick?
换句话说,为了协调一系列git cherry-pick,以便手动解决过程中的任何合并冲突,然后可以使用--continue,--abort和/或--skip管理过程,我想使用git-rebase的-i功能。
这将很有用,因为一个简单的脚本包含以下内容:
git cherry-pick -m 1 e1bed15c97f3f
git cherry-pick -m 1 6b5e6060b0e99
....
git cherry-pick -m 1 1a625d6b45faa

很可能会出现以下错误导致中止:

error: could not apply 6b5e6060b0e99... Implement Something... 
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

d:\src>git cherry-pick -m 1   e1bed15c97f3f
error: Cherry-picking is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: cherry-pick failed

谢谢!


1
你尝试过使用 -r (--rebase-merges) 选项来运行交互式变基了吗? - mnestorov
是的,我做了;回答下面的问题,我使用的是 Git 版本 2.31.0。 - Brad
已添加到上面的问题,希望能够澄清。 - Brad
1个回答

4

mnestorov的评论提到使用--rebase-merges与你试图解决的实际问题相关,请考虑使用它(但你还没有真正描述出问题在哪里,我会在下面记录下似乎偏离轨道的地方)。如果-r有效,那么你已经完成了。如果你的git版本较旧,则可能没有-r/--rebase-merges选项。如果是这样,最好的答案是升级你的Git。

更多关于rebase的内容

让我们更多地谈论一般情况下的rebase,从这里开始:

有没有办法让rebase采用cherry-pick的-m#行为来进行pick

没有:即使有,也不会起作用,至少不是普遍的。下面是原因。

当您在此处使用-m选项将合并提交“复制”到非合并,普通提交中。 -m选项使Git将合并提交视为普通提交,具有单个父项,并且-m标志告诉它将哪个父项称为“the” 单一的父项。但是,合并提交的目的通常是从两个父项中结合工作1

同时,git rebase的目的是重复复制某些提交,随后放弃原始提交,转而使用新的副本。无法复制合并提交 - cherry-pick的-m并不这样做;它代替产生一个普通提交-因此rebase通常会丢弃合并提交。我将展示下面的如何以及为什么这是标准rebase运作的正确方法

git rebase -i --onto myBranch myBranch
请注意,--onto参数和git rebase文档中称为upstream的另一个参数默认相同,因此更简单的写法如下:
git rebase -i myBranch

此操作要复制的提交集合,不超过以下操作所产生的提交集合:
git log myBranch..HEAD

那就假设我们有以下情况,其中更新的提交向右,我们当前在分支topic上:
          G--H   <-- topic (HEAD)
         /
...--E--F--I--J   <-- myBranch

运行git rebase myBranch,无论是否带有--interactive参数,告诉Git:首先,列出那些从HEAD(即topic)可以到达的提交,减去任何同样可以从myBranch到达的提交。这会导致Git在内部列出提交GH。它们是复制的候选者
如果这些提交是最终要复制的提交,且其他简化假设成立,则结果将是:
          G--H   [abandoned]
         /
...--E--F--I--J   <-- myBranch
               \
                G'-H'  <-- topic (HEAD)

其中G'H'是原始提交GH的副本,这些副本各有两个重要的不同之处:

  • G'的父级是J,而不是FG'中的文件包含与G相对于F所做的更改相同的更改,因此G'中存储的快照与G中的快照不同。
  • H'的父级是G',而不是G,其快照以类似的方式不同。

也就是说,由于每个提交都保存了完整的快照,因此我们需要在复制的提交中让快照与原始提交的快照不同。 新快照中的差异使得G'J进行比较与GF进行比较产生了大致相同的差异(diff)。 当然,链接(在Git中总是向后)也不同,因此这些副本排在myBranch的最后一个提交之后。


1如果有章鱼合并(octopus-merge),则会从多个父级中合并工作,而罕见的-s ours合并将完全丢弃一个父级的内容,因此这些特殊情况更加特殊。一般来说,不应在这些情况下使用变基(rebase)。


变基(rebase)有意不做什么

假设在我们的原始两个提交GH中:

          G--H   <-- topic (HEAD)
         /
...--E--F--I--J   <-- myBranch

GH的更改与从IJ的更改完全相同。例如,这两个提交都修复了在README文件中拼写错误的相同单词,除此之外没有做任何其他更改。

当我们运行git rebase myBranch时,Git仍然列出提交记录GH。但它还会查看提交记录IJ,并针对每个提交记录计算出Git所谓的一个补丁ID(请参见git patch-ID文档)。该补丁ID告诉Git:提交H是提交J的重复提交。因此,Git会删除要复制的提交记录H

因此,当我们说rebase列出myBranch..HEAD以获取要复制的候选提交记录时,这些只是候选者。其中一些候选项会被有意自动排除。在这种特殊情况下,只有提交记录H是有意被排除的,最终rebase的结果将是:

          G--H   [abandoned]
         /
...--E--F--I--J   <-- myBranch
               \
                G'  <-- topic (HEAD)

Git基本上认为提交H已经应用。因此,它完全放弃了它。

Git还会与其称为"分岔点"的东西进行相当复杂的协作。分叉点代码的目标是发现被有意删除的提交并在变基期间自动删除它们。这段代码通常做正确的事情,但也可能出现问题。2 在这种情况下,修补程序ID和分叉点代码似乎都没有影响到你,但还有一个更大的特殊情况,值得单独讨论。


2它可能会出错的事实使我认为它不一定是正确的默认值。这也适用于“已应用上游”修补程序ID案例。特别是,交互式变基真的应该在其指令页中包括这些提交,预选操作应为“删除”,并注明为什么要删除它们。但是今天还没有这样。


合并

到目前为止,我们绘制的图片都很简单。但是假设我们的topic分支提交看起来像这样:

                 I--J
                /    \
            G--H      M--N   <-- topic (HEAD)
           /    \    /
          /      K--L
         /
...--E--F--------------O--P   <-- myBranch

当我们运行以下代码时:
git log myBranch..topic

我们将看到提交 NM,然后按某种顺序看到提交 IL,其中 IJ 后面显示,但与 KL 的顺序是随机的,而且 KL 后面显示,但对于 IJ 的顺序也是随机的。 然后我们将看到提交 H,接着是 G,这就是列表的结尾。
(如果我们添加 --topo-order,那么列表的顺序会更受限制。rebase 代码在内部添加 --topo-order。我们仍然不知道 LJ 谁会先出现,但一旦我们得到其中一个,我们将完成整个行,然后再去另一行。没有 --topo-order,我们可以看到 NMLJKIHG 这样的结果。) 你的问题有点偏离了。 git rebase 命令将自动完全删除合并提交 M,原因有两个:
  • cherry-pick(以及旧的 git format-patch / git am 方法)无法复制合并提交;
  • 标准 rebase 的结果不应复制合并提交。
因此,您将不会在提交 M 上看到 pick 命令。如果要看到它,您必须手动插入,这是一个错误的做法。为了理解这一点,让我们看看 Git 在不使用 pick <hash-of-M> 时如何处理这种情况,并进行常规(非 --rebase-merges)重新派生。
序列首先列出要复制的提交。假设它们按以下顺序显示,在谨慎地反转它们的同时舍弃合并提交:G-H-I-J-K-L-N。如果在复制阶段一切顺利,结果将是:
                 I--J
                /    \
            G--H      M--N   [abandoned]
           /    \    /
          /      K--L
         /
...--E--F--O--P   <-- myBranch
               \
                G'-H'-I'-J'-K'-L'-N'  <-- topic (HEAD)

换句话说,git rebase已经摊平了合并。但是合并代码M的目的是将I-JK-L分支上的工作合并在一起。我们不需要那个合并,因为将K复制到K'的过程如下:
  • 对于HK提交之间的每个变化,在从I'中提取的内容中进行相同的更改;
  • 然后将其作为新提交K'提交。
也就是说,提交K'不是基于HH',而是基于I'。它已经包含了另一个分支上的工作。同样地,当Git将L复制到L'时,它会将其复制到一个已经包含了另一个分支上的工作的提交上。因此,没有必要创建分支。重新定位操作可以完全将其摊平

3请记住,Git是反向工作的,因此列表总是首先显示N。我们需要将N放在最后,因此重新定位会将列表反转。


--rebase-merges选项

摊平合并的想法并不总是一个好主意。有时它不起作用。例如,一系列操作如下:

       I--J
      /
...--H
      \
       K--L

通常,两个分支上的更改相对较少,这使得"将分支平铺"变得容易且效果良好。但是如果该系列在每个分支上有大量提交:

       o--o--...(1000 commits)...--o--tip1
      /
...--o
      \
       o--o--....................--o--tip2

在这种情况下,合并两个tip提交的合并可能会有很多工作。删除合并是不可行的。
或者,也许我们只是喜欢保留合并。合并代表着重要的东西,我们希望未来的代码考古学家能够看到它。
好吧,“复制”合并确实是不可能的。Cherry-pick的-m选项不会这样做。如果我们在平整事情之后用cherry-pick -m“复制”合并:
                 I--J
                /    \
            G--H      M--N   <-- topic
           /    \    /
          /      K--L
         /
...--E--F--O--P   <-- myBranch
               \
                G'-H'-I'-J'-K'-L'  <-- HEAD

我们只需通过I-J或通过K-L获得已有的更改,因此再次引入这些更改是多余的。为了“正确地”复制合并操作,我们必须先创建一个分支

                 I--J
                /    \
            G--H      M--N   <-- topic
           /    \    /
          /      K--L
         /
...--E--F--O--P   <-- myBranch
               \
                \      I'-J'   <-- temp-label-1
                 \    /
                  G'-H'
                      \
                       K'-L'   <-- temp-label-2, HEAD

接着,我们必须选择正确的分支最新版本作为HEAD提交,并且要重新再次运行git merge以创建M'

reset-to temp-label-1
merge temp-label-2

如果合并成功,我们现在将拥有:
                 I--J
                /    \
            G--H      M--N   <-- topic
           /    \    /
          /      K--L
         /
...--E--F--O--P   <-- myBranch
               \
                \      I'-J'  <-- temp-label-1
                 \    /    \
                  G'-H'     M'  <-- HEAD
                      \    /
                       K'-L'  <-- temp-label-2

我们现在可以选择 N 的哈希值来生成 N':
                 I--J
                /    \
            G--H      M--N   <-- topic
           /    \    /
          /      K--L
         /
...--E--F--O--P   <-- myBranch
               \
                \      I'-J'  <-- temp-label-1
                 \    /    \
                  G'-H'     M'-N'  <-- HEAD
                      \    /
                       K'-L'  <-- temp-label-2

然后我们就完成了这个复杂的重新基础分支合并的操作,可以移动分支标签topic并且删除任何临时标签:

                 I--J
                /    \
            G--H      M--N   [abandoned]
           /    \    /
          /      K--L
         /
...--E--F--O--P   <-- myBranch
               \
                \      I'-J'
                 \    /    \
                  G'-H'     M'-N'  <-- topic (HEAD)
                      \    /
                       K'-L'

这是git cherry-pick --rebase-merges 命令的作用。为了达到这个结果,它需要一些额外的命令和插入临时标签的能力。(注意,在进行cherry-pick操作序列重置HEAD之前,K复制到K'时,也会有一个H'的临时标签。您将在指令说明书中看到所有这些标签和重置操作,该说明书需要知道何时创建各种标签以及如何移动HEAD。)

非常感谢您花时间详细描述rebase!我很感激您的努力,但似乎答案归结为"git不能"满足我的需求。"当您在此处使用-m选项来"复制"合并时,您将其复制到一个非合并的普通提交......只有一个父提交。" <- 实际上,这正是我的目标。我对提交/分支历史了解得足够清楚,希望出现这种行为,并且知道它应该适用于相关的提交,而您描述的更复杂的rebase场景都不是我所需要的结果。我想使用rebase来实现"脚本式挑选"。 - Brad
我在上面的问题中添加了一些内容,希望能够澄清我正在努力解决的实际问题。 - Brad
1
Rebase目前来说并不适合这个更新的问题。Git的顺序器(实现了用C重写的rebase)无法处理这个问题。你需要编写自己的脚本或程序,以正确的选项重复运行git cherry-pick,并复制旧的交互式rebase脚本所做的操作,或者修改Git的源代码。 - torek

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