为什么 git cherry-pick 产生的冲突比 git rebase 产生的冲突少?

Why would git cherry-pick produce fewer conflicts than git rebase?

我经常变基。有时 rebase 特别有问题(很多合并冲突),在这种情况下我的解决方案是 cherry-pick 个人提交到 master 分支。我这样做是因为几乎每次我这样做时,冲突的数量都会少得多。

我的问题是为什么会这样。

为什么 cherry-pick 时的合并冲突比 rebase 时少?

在我的心智模型中,rebasecherry-pick 正在做同样的事情。

变基示例

A-B-C (master)
   \
    D-E (next)

git checkout next
git rebase master

产生

A-B-C (master)
     \
      D`-E` (next)

然后

git checkout master
git merge next

产生

A-B-C-D`-E` (master)

樱桃采摘示例

A-B-C (master)
   \
    D-E (next)

git checkout master 
git cherry-pick D E

产生

A-B-C-D`-E` (master)

根据我的理解,最终结果是一样的。 (D 和 E 现在在 master 上,具有干净的(直线)提交历史。)

为什么后者 (cherry picking) 产生的合并冲突比前者 (rebase) 更少?

更新更新更新

我终于能够重现这个问题,现在我意识到我可能过于简化了上面的例子。以下是我如何重现...

假设我有以下内容(注意额外的分支)

A-B-C (master)
   \
    D-E (next)
       \
        F-G (other-next)

然后我执行以下操作

git checkout next
git rebase master
git checkout master
git merge next

我得到以下结果

A-B-C-D`-E` (master)
   \ \
    \ D`-E` (next)
     \
      D-E
         \
          F-G (other-next)

从这里开始,我要么变基要么挑选

变基示例

git checkout other-next
git rebase master 

产生

A-B-C-D`-E`-F`-G` (master)

樱桃采摘示例

git checkout master
git cherry-pick F G

产生相同的结果

A-B-C-D`-E`-F`-G` (master)

但合并冲突比变基策略少得多。

终于重现了一个类似的例子,我想我明白了为什么与 cherry picking 相比,rebase 有更多的合并冲突,但我会把它留给其他人(他们可能会做得更好(更准确) ) 工作比我会) 回答。

更新的答案(见问题更新)

我认为这里发生的事情与选择要复制的提交有关

让我们注意,然后搁置,git rebase 可以使用 git cherry-pickgit format-patchgit am 来复制一些提交。在大多数情况下,git cherry-pickgit am 应该会获得相同的结果。 (git rebase documentation 特别指出上游文件重命名是 cherry-pick 方法的一个问题,而不是默认的基于 git am 的非交互式 rebase 方法。另请参见下面原始答案中的各种括号注释,和评论。)

这里主要考虑的是要复制哪些提交。在手动方法中,首先手动将提交 DE 复制到 D'E',然后手动复制 FGF'G'。这是最少的工作量,也是我们想要的;这里唯一的缺点是我们必须做的所有手动提交识别。

当您使用命令时:

git checkout <branch> && git rebase <upstream>

您使 Git 自动执行查找要复制的提交的过程。当 Git 做对时这很好,但如果 Git 做错了就不行了。

那么Git如何选择这些提交?简单但有些错误的答案在这句话中(来自同一文档):

All changes made by commits in the current branch but that are not in <upstream> are saved to a temporary area. This is the same set of commits that would be shown by git log <upstream>..HEAD; or by git log 'fork_point'..HEAD, if --fork-point is active (see the description on --fork-point below); or by git log HEAD, if the --root option is specified.

--fork-point 复杂化有点新,因为 git 2.something,但在这种情况下它不是 "active",因为您指定了一个 <upstream> 参数并且没有指定 --fork-point。实际的 <upstream> 两次都是 master

现在,如果你真的 运行 每个 git log(使用 --oneline 使它更好):

git checkout next && git log --oneline master..HEAD

和:

git checkout other-next && git log --oneline master..HEAD

您会看到第一个列出了提交 DE——太棒了!——但是第二个列出了 DEF,以及 G。哦哦,DE 出现了两次!

事实是,有时 有效。好吧,我上面说了"somewhat wrong"。这是错误的原因,仅比前面的引述少了两段:

Note that any commits in HEAD which introduce the same textual changes as a commit in HEAD..<upstream> are omitted (i.e., a patch already accepted upstream with a different commit message or timestamp will be skipped).

请注意,这里的 HEAD..<upstream>git log 命令中 <upstream>..HEAD 的反面,我们刚刚 运行,我们在其中看到了 D-through- G.

对于 first 变基,git log HEAD..master 中没有提交,因此没有可能被跳过的提交。这很好,因为没有要跳过的提交:我们正在将 EF 复制到 E'F',这正是我们想要的。

但是,对于 second rebase,它发生在第一次 rebase 完成之后,git log HEAD..master 将显示提交 E'F':我们刚刚制作的两个副本。这些 可能 被跳过:它们是 考虑跳过的候选人

"Potentially skipped" 不是 "really skipped"

那么 Git 如何决定应该 真正 跳过哪些提交?答案在 git patch-id, although it's actually implemented directly in git rev-list 中,这是一个非常花哨和复杂的命令。但是,这些都没有真正很好地描述它,部分原因是它很难描述。无论如何,这是我的尝试。 :-)

这里 Git 所做的是在剥离识别行号后查看差异,以防补丁位于略有不同的位置(由于早期补丁在文件中上下移动行)。它使用与文件相同的技巧——将文件内容转换为唯一的哈希值——将每个提交转换为 "patch ID"。 提交 ID 是一个唯一的哈希值,用于标识一个特定的提交,并且始终是同一特定的提交。 补丁 ID 是一个不同的(但仍然是某些内容唯一的)哈希 ID,它始终标识 "the same" 补丁,即删除和添加相同差异的东西大块头,即使它从不同的位置删除和添加它们。

计算出每个提交的补丁 ID 后,Git 可以说:"Aha, commit D and commit D' have the same patch-ID! I should skip copying D because D' is probably a result of copying D." 它可以对 EE' 做同样的事情。这 通常 有效——但是当从 DD' 的副本需要手动干预(修复合并冲突)时,它对 D 失败,同样每当从 E 复制到 E' 需要手动干预时,E 就会失败。

更智能的 rebase

这里需要的是一种 "smart rebase" 可以查看一系列 b运行ches 并预先计算,它承诺为所有未来的人复制一次-rebased b运行ches。然后,在完成所有副本后,此 "smart rebase" 将调整 all b运行ch-names.

在这种特殊情况下——通过 G 复制 D——实际上非常简单,您可以手动执行此操作:

$ git checkout -q other-next && git rebase master
[here rebase copies D, E, F, and G, perhaps with your assistance]

其次是:

$ git checkout next
[here git checks out "next", so that HEAD is ref: refs/heads/next
 and refs/heads/next points to original commit E]
$ git reset --hard other-next~2

这是可行的,因为 other-next 命名提交 G',其父项是 F',其父项又是 E',这就是我们想要的 [=87] =] 来点。由于 HEAD 引用 b运行ch nextgit reset 调整 refs/heads/next 指向提交 E',我们就完成了。

在更复杂的情况下,需要精确复制一次的提交并不都是线性的:

                A1-A2-A3  <-- featureA
               /
...--o--o--o--o--o--o--o   <-- master
         \
          *--*--B3-B4-B5   <-- featureB
              \
               C3-C4       <-- featureC

如果我们想要 "multi-rebase" 所有三个功能,我们可以 rebase featureA 独立于其他两个 - none 三个 A 提交取决于任何 "non-master" 不同于之前的 A 提交——但是要复制五个 B 提交和四个 C 提交,我们必须复制两个 * 提交 both B and C,但只复制一次,然后将剩余的三个和两个提交(分别)复制到复制提交的提示。

(可以写出这样的"smart rebase",但是把它适当地整合到Git中,这样git status才能真正理解它,要难得多。)


原回答

我很想看到一个可重现的例子。在大多数情况下,您的 "in-head" 模型应该可以工作。不过有一种已知的特殊情况。

一个 interactive 变基,或者添加 -m--merge 到普通的 git rebase,实际上 使用 git cherry-pick,而默认的非交互式变基使用 git format-patchgit am。后者不适合重命名检测。特别是,如果上游有文件重命名,1 交互式或 --merge rebase 可能会有不同的行为(通常,更好)。

(另外,请注意,两种变基——面向补丁的变基和基于 cherry-pick 的变基——将跳过与上游中已经存在的提交相同的 git patch-id 提交,通过 git rev-list --left-only --cherry-pick HEAD...<upstream> 或等效项。参见 the documentation for git rev-list,特别是关于 --cherry-mark--left-right 的部分,我认为这更容易理解。这对于两种变基应该是相同的,尽管; 如果您是手动挑选,是否这样做将取决于您。)


1更准确地说,git diff --find-renames 需要相信 那里有重命名。通常如果有的话它会相信这一点,但由于它是通过比较树来 检测 它们,所以这并不完美。