使用 git rebase interactive 来编排 git cherry-pick 系列?

Using git rebase interactive to orchestrate series of 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 为 pick 采用 cherry-pick 的 -m # 行为?

以另一种方式陈述我的目标并帮助澄清问题 - 我想使用 git-rebase 的 --i 功能来协调一系列 git cherry-pick,以便任何合并冲突可以手动解决流程中的问题,然后可以使用 --continue--abort and/or --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

谢谢!

关于使用 --rebase-merges 在这里是相关的;考虑一下您要解决的实际问题(您还没有真正描述过:我会在下面指出事情似乎已经脱离 rails 的地方)。对于 git rebase 今天的情况,您可能正在做的事情有点太难了。但我 认为 你所做的正是 -r 的设计目的。

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

关于变基的更多信息

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

Is there a way to get rebase to adopt cherry-pick's -m # behavior for pick?

否:如果有,无论如何也行不通,至少一般情况下行不通。原因如下。

当您在此处使用 -m 选项“复制”合并时,您将其复制到 non-merge 普通提交中。 -m 选项使 Git 将合并提交视为 普通提交,具有单个 parent 和 -m 标志告诉它哪个 parent 调用“the” single parent。但是合并提交的目的通常是合并两个parents.的工作 1

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

git rebase -i --onto myBranch myBranch

请注意 --onto 的参数和 the git rebase documentation 调用 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'的parent是J,而不是FG' 中的文件包含 GF 所做的 J 的相同 更改 ,因此 存储在G'中的快照与G中的不同。
  • H' 的 parent 是 G',而不是 G,它的快照也有很大的不同。

也就是说,由于每个提交都持有一个完整快照,我们需要复制提交中的快照与原始提交中的快照不同。新快照的差异在于 比较 G'J 或多或少产生相同的 diff,作为 比较 GF。当然,链接(在 Git 中总是向后)也是不同的,因此副本出现在 myBranch 中的最后一次提交之后。


1一个 octopus merge,如果你有的话,它结合了来自两个以上 parent 的工作,并且rare -s ours 合并完全丢弃了 one parent 的内容,因此这些特殊情况更加特殊;一般来说,rebase 不应该用在这些上面。


rebase 没有故意做什么

假设在我们最初的两个提交中,GH:

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

G更改为H 完全相同I 更改为 J。例如,两个提交都修复了 README 文件中同一个拼写错误的单词的拼写,并且什么都不做。

当我们 运行 git rebase myBranch 时,Git 仍然列出提交 GH。但它也会查看提交 IJ,对于每个提交,Git 计算它所谓的 补丁 ID(参见 the git patch-ID documentation).这个补丁 ID 告诉 Git:Commit H 是提交 J. Git then drops 从要复制的提交列表中提交 H

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

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

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

Git 还使用一种叫做 fork point 代码的东西来处理相当复杂的舞蹈。 fork-point 代码的目标是发现 被故意删除的提交 并在变基期间自动删除它们。这段代码通常会做正确的事情,尽管它可能会失败。2 在这种情况下,patch-ID 和 fork-point 代码似乎都没有问题,但是有又一个大的特例,值得单独一节。


2它可能失灵的事实让我认为它不一定是正确的默认设置。这也适用于“已在上游应用”patch-ID 的情况。特别是,交互式 rebase 确实应该在其指令 sheet 中包含这些提交,其中 pre-selected 操作是“drop”,以及关于它们被删除的原因的评论。今天不是这样。


合并

至此,我们画的图很简单。但是假设我们的 topic 分支提交看起来像这样:

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

当我们运行:

git log myBranch..topic

我们将看到提交 NM,然后按某种顺序提交 IL,之后显示 I J 但 randomly-ordered 相对于 KLK 显示在 L 之后,但 randomly-ordered 相对于 IJ。然后我们将看到提交 H,然后是 G,这就是列表的末尾。

(如果我们添加 --topo-order,列表的顺序会受到更多限制。rebase 代码在内部添加 --topo-order。我们仍然不知道是 L 还是 J 将排在第一位,但是一旦我们得到其中一个,我们将在转到另一行之前完成整行。没有 --topo-order 我们可以看到 NMLJKIHG实例。)

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

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

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

序列从列出要复制的提交开始。假设它们按此顺序出现,在 git rebase 小心地颠倒它们后 3 同时删除合并: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 已经 将合并变平了 。但是 merge M 的目的是 合并 I-JK-L 分支上的工作 。我们 不需要 合并,因为将 K 复制到 K' 的过程是:

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

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


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

我们只是 re-introducing 我们已经通过 I-JK-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

然后我们必须选择正确的分支 tip 作为 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

我们现在可以选择<em>hash-of-N</em>来制作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

然后我们完成了这个花哨的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' 也会有一个临时标签,因为 cherry-picking 操作序列必须在将 K 复制到 K' 之前重置 HEAD。您将在指令 sheet 中看到所有这些标签和重置,它需要知道何时制作各种标签以及将 HEAD 移动到哪里。)