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

时间:2016-07-07 17:36:10

标签: git git-rebase git-cherry-pick

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

我的问题是为什么会出现这种情况。

为什么我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现在使用干净的(直线)提交历史记录。)

为什么后者(采摘樱桃)产生的合并冲突少于前者(重新定位)?

更新更新更新

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

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

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)

但合并冲突远少于重新定位策略。

最后再现了一个类似的例子,我想我知道为什么与变装有更多的合并冲突而不是樱桃采摘,但是我会把它留给其他人(他们可能做得更好(更准确) )工作比我想回答。

1 个答案:

答案 0 :(得分:4)

更新了答案(请参阅相关更新)

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

让我们注意,然后放弃,git rebase可以使用git cherry-pickgit format-patchgit am来复制某些提交。在大多数情况下,git cherry-pickgit am应达到相同的效果。 (git rebase documentation专门调用上游文件重命名作为cherry-pick方法的问题,相对于基于默认git am的非交互式rebase方法。请参阅下面原始答案中的各种括号注释,和评论。)

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

使用命令时:

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

你让Git自动完成了复制提交的过程。当Git做对了,这很好,但是如果Git弄错了就不行。

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

  

当前分支中的提交所做的所有更改,但不在&lt; upstream&gt;中被保存到临时区域。这是git log <upstream>..HEAD所显示的同一组提交;或git log 'fork_point'..HEAD,如果--fork-point有效(请参阅下面--fork-point的说明);如果指定了git log HEAD选项,则按--root

--fork-point并发症有点新,因为git 2.something,但它没有&#34;活跃&#34;在这种情况下,因为您指定了<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出现两次!

问题是,这个有时有效。嗯,我说'有些错误&#34;以上。这是错误的原因,只比前面引用的两段:

  

请注意,HEAD中的任何提交都会引入与HEAD中的提交相同的文本更改。&lt; upstream&gt;被省略(即,将跳过已经在上游接受不同提交消息或时间戳的补丁)。

请注意,此处的HEAD..<upstream>与我们刚刚运行的<upstream>..HEAD命令中的git log相反,我们在其中看到了D - 通过 - G

对于第一个 rebase,git log HEAD..master中没有提交,因此没有可能被跳过的提交。这样做很好,因为没有提交要跳过:我们正在将EF复制到E'F',并且&#39;正是我们想要的。

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

&#34;可能跳过&#34;不是&#34;真的跳过&#34;

那么如何 Git决定哪些提交应该真正跳过?答案在git patch-id,虽然它实际上直接在git rev-list中实现,这是一个非常奇特和复杂的命令。然而,这些都没有真正描述得非常好,部分原因是它很难描述。无论如何,这是我的尝试。 : - )

Git在这里做的是在剥离识别行号之后查看差异,以防补丁进入稍微不同的位置(由于早期补丁在文件中上下移动线)。它使用与文件相同的技巧 - 将文件内容转换为唯一的哈希 - 将每个提交转换为&#34;补丁ID&#34;。 提交ID 是一个唯一的哈希,它标识一个特定的提交,并始终是同一个特定的提交。 修补程序ID 是一个不同的(但仍然是唯一到某些内容)哈希ID,始终标识&#34;相同&#34;补丁,即删除和添加相同差异的东西,即使它从不同位置移除和添加它们。

为每次提交计算了一个补丁ID后,Git可以说:&#34; Aha,commit D和commit D'具有相同的补丁ID!我应该跳过复制D,因为D'可能是复制D的结果。&#34;它可以对E vs E'执行相同的操作。这个通常有效 - 但只要DD的副本需要人工干预(修复合并冲突),D'就会失败,同样{只要EE的副本需要人工干预,就会{1}}。

更聪明的rebase

这里需要的是一种&#34;聪明的变种&#34;可以查看一系列分支并提前计算,这些分支承诺一次为所有待重新分支的分支进行复制。然后,在完成所有副本之后,这个&#34; smart rebase&#34;会调整所有分支名称。

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

G

接下来是:

$ 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'指出。由于next引用了分支HEADnext会调整git reset以指向提交refs/heads/next,并且我们已完成。

在更复杂的情况下,需要复制的提交 - 完全一次并不是完全线性的:

E'

如果我们想要&#34; multi-rebase&#34;所有这三个特性,我们可以独立于其他两个重新定义 A1-A2-A3 <-- featureA / ...--o--o--o--o--o--o--o <-- master \ *--*--B3-B4-B5 <-- featureB \ C3-C4 <-- featureC - 三个featureA提交中的任何一个都不依赖于任何东西&#34;非主要&#34;除了早先的A提交 - 但要复制五个A提交和四个B提交,我们必须复制两个提交的两个C提交 * B,但只复制一次,然后将剩余的三次和两次提交(分别)复制到复制的提交的提示上。

(它 可以编写这样一个&#34; smart rebase&#34;,但是将它正确地集成到Git中,以便C真正理解它,相当困难。)

原始答案

我很乐意看到一个可重复的例子。在大多数情况下,你的&#34; in-head&#34;模型应该工作。但是有一个已知的特例。

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

(另请注意,这两种rebase - 面向补丁的版本和基于cherry-pick的版本 - 将跳过--merge的提交 - 与上游已提交的提交相同,通过{{1请参阅the documentation for git rev-list,特别是关于git patch-idgit rev-list --left-only --cherry-pick HEAD...<upstream>的部分,我认为这一部分使我们更容易理解。但是,对于这两种rebase,这应该是相同的;你手动采摘樱桃,无论你是否这样做都取决于你。)

1 更准确地说,--cherry-mark需要相信那里有重命名。通常它会相信这个,如果有一个,但由于它通过比较树来检测它们,这并不完美。