首页 > 解决方案 > 使用 git rebase interactive 来编排一系列 git cherry-pick?

问题描述

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选项,以指示应针对第一个父项选择更改。

我已经尝试了许多与合并相关的选项来变基,但我总是最终得到错误:

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

(即使我指定 -m 来变基。)

我意识到我可以使用cherry-pick 编写一个脚本,但我喜欢现有的行为rebase -i(它会在命令列表中运行,如果遇到无法处理的问题就会暂停)。我非常想直接利用该逻辑,但我无法找出正确的方法来巧妙地利用 rebase 的pick命令来填补这一空白。

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

以另一种方式陈述我的目标并帮助澄清问题 - 我想使用 git-rebase 的--i功能来协调一系列git cherry-picks 以便可以手动解决流程中的任何合并冲突,然后可以使用--continue,--abort和/或来管理流程--skip.

这将很有用,因为一个简单的脚本包括:

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

谢谢!

标签: gitgit-rebasegit-cherry-pick

解决方案


mnestorov关于使用的评论--rebase-merges与此处相关;考虑一下您要解决的实际问题(您还没有真正描述过:我会在下面指出事情似乎已经脱轨的地方)。就目前而言,您可能正在做一些有点太难的git rebase事情。但我认为你正在做的正是-r设计的。

如果-r工作,你就完成了。如果您的 Git 较旧,您可能没有-r/--rebase-merges选项。如果是这样,最好的答案是升级你的 Git。

更多关于变基

让我们从这个开始讨论更多关于 rebase 的内容:

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

不:如果有,它无论如何都行不通,至少在一般情况下是行不通的。这就是为什么。

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

同时, 的目的git rebase是重复复制一些提交,随后放弃原始提交以支持新副本。无法复制合并提交——cherry-pick-m不会这样做;它会产生一个普通的提交——所以 rebase 通常会丢弃合并提交。我将在下面展示标准 rebase 的工作方式如何以及为什么这是正确的。

git rebase -i --onto myBranch myBranch

请注意, 的参数和文档调用--onto另一个参数默认为同一事物,因此更简单地写为:git rebaseupstream

git rebase -i myBranch

此操作要复制的提交集限制为不超过以下生成的提交集:

git log myBranch..HEAD

也就是说,假设我们有以下内容,其中较新的提交在右侧,并且我们当前在分支上topic

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

运行git rebase myBranch,无论有无--interactive,都会告诉 Git:首先,列出可以从HEADaka到达的提交topic,减去可以从myBranch. G这会导致 Git 在内部 列出提交H。这些是复制的候选人

如果这些是最终被复制的提交,并且其他简化假设成立,那么结果将是:

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

其中G'H'是原始提交的副本GH,副本各有两个重要区别:

  • 的父级G'J,而不是F。其中的文件包含对G'的相同更改,因此存储在中的快照与 中的不同。JGFG'G
  • 的父级H'G',而不是G,并且其快照的不同之处大致相同。

也就是说,由于每个提交都包含一个完整的快照,因此我们需要复制提交中的快照与原始提交中的快照不同。新快照的不同之处在于,比较 G'vsJ会产生相同的diff,或多或少,就像比较 Gvs F。当然,在 Git 中总是向后的链接也是不同的,因此副本在最后一次提交之后出现在myBranch.


1章鱼合并,如果你有的话,会合并两个以上父级的工作,而罕见的-s ours合并会完全丢弃一个父级的内容,因此这些特殊情况更加特殊;通常,不应在这些上使用 rebase。


rebase 不是故意做的

假设在我们最初的两个提交中,G并且H

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

从到的变化与从G到的变化H 完全相同。例如,两个提交都修复了文件中同一个拼写错误的单词的拼写,而不做其他任何事情。IJREADME

当我们运行时git rebase myBranch,Git 仍然列出提交GH. 但它也会查看提交IJ并且对于每个提交,Git 都会计算它所谓的补丁 ID(请参阅文档git patch-ID。这个补丁 ID 告诉 Git:CommitH是 commit 的副本J 然后 Git从要复制的提交列表中删除提交H

因此,当我们说 rebase 列出了myBranch..HEAD要复制的候选提交时,这些只是候选。其中一些候选人被故意自动淘汰。在这种特殊情况下,仅H故意消除,rebase 的最终结果将是:

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

Git 基本上认为 commitH已经被应用了。所以它完全放弃了它。

Git 也用它称为分叉点代码的东西做了一个相当复杂的舞蹈。fork-point 代码的目标是发现故意丢弃的提交,并在变基期间自动丢弃它们。这段代码通常会做正确的事情,尽管它可能会出错。2 在这种情况下,patch-ID 和 fork-point 代码似乎都不会对您不利,但还有一个更大的特殊情况,值得单独讨论。


2它可能失火的事实让我认为它不一定是正确的默认值。这也适用于“已经应用的上游”补丁 ID 案例。特别是,交互式 rebase 确实应该在其指令表中包含这些提交,预先选择的操作是“drop”,并说明它们被删除的原因。今天不是这种情况。


合并

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

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

当我们运行时:

git log myBranch..topic

我们将看到提交NM,然后——以某种顺序——I通过提交LI显示在之后J但相对于Kand随机排序L,以及K显示在之后但相对于andL随机排序。然后我们会看到 commit ,然后是 ,这就是列表的结尾。IJHG

(如果我们添加--topo-order,列表的顺序会受到更多限制。rebase 代码在内部添加--topo-order。我们仍然不知道是否LJ先出现,但一旦我们得到其中一个,我们将完成整个行,然后再去另一行。没有--topo-order我们可以看到N, M, L, J, K, I, H,G实例。)

这是您的问题有点偏离轨道的地方。git rebase命令将自动完全删除合并提交M,原因有两个:

  • cherry-pick (以及扩展,旧的git format-patch/git am基于方法)不能复制合并;和
  • 标准变基的结果无论如何都不应该复制合并。

所以你不会pickcommit的命令M。要获得一个,您必须手动插入自己的,这是一个错误。要了解原因,让我们看看 Git 如何在没有pick <hash-of-M>in 的情况下使用常规(非--rebase-merges)rebase 来处理这个问题。

该序列首先列出要复制的提交。假设它们按此顺序出现,在删除合并时git rebase小心地反转它们3G-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-LKK'

  • 对于H-vs-K提交中的每个更改,对取自的内容进行相同的更改I'
  • 现在将其提交为 new commit K'

也就是说,提交K'不是基于Hor H',而是基于I'。它已经包含了另一个分支的工作。同样,当 Git 复制L到时L',它会复制到已经包含另一个分支的工作的提交上。所以不需要分支。变基操作只是将其完全展平。


3请记住,Git 是逆向工作的,所以列表总是排在N第一位。我们需要N最后一个,所以 rebase 会反转列表。


--rebase-merges选项_

这种扁平化合并的想法并不总是一个好的想法。有时它不能很好地工作。当然,像这样的系列:

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

通常两个分支上的更改相对较少,因此“将分支扁平化”很容易并且进展顺利。但是,如果该系列在每个分支中都有大量提交怎么办:

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

在这种情况下,合并两个提示提交的合并可能有很多工作要做。压平合并是不切实际的。

或者,也许我们只是喜欢合并存在。合并代表了一些重要的东西,我们希望未来的代码考古学家能看到它。

好吧,“复制”合并确实是不可能的。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

我们只是重新引入我们已经通过 anyI-J或 via获得的更改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

我们现在可以制作:pick hash-of-NN'

                 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

然后我们完成了这个花哨的 rebase-that-re-does-the-merge,并且可以移动分支标签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做的。为了达到这个结果,它需要一些额外的命令和插入临时标签的能力。(请注意,还有一个临时标签,因为在复制到之前H',樱桃采摘操作的顺序必须在那里重置。您将在说明书中看到所有这些标签和重置,需要知道何时制作各种标签以及在哪里移动。)HEADKK'HEAD


推荐阅读