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

sfl*_*che 4 git git-rebase git-cherry-pick

我经常变本加厉。有时,这rebase特别有问题(很多合并冲突),在这种情况下,我的解决方案是cherry-pick将单个提交提交到master分支上。我这样做是因为几乎每次我所做的冲突数量都大大减少了。

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

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

在我的心理模型中,a rebase和a cherry-pick在做同样的事情。

变基示例

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

git checkout next
git rebase master
Run Code Online (Sandbox Code Playgroud)

产生

A-B-C (master)
     \
      D`-E` (next)
Run Code Online (Sandbox Code Playgroud)

然后

git checkout master
git merge next
Run Code Online (Sandbox Code Playgroud)

产生

A-B-C-D`-E` (master)
Run Code Online (Sandbox Code Playgroud)

樱桃精选示例

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

git checkout master 
git cherry-pick D E
Run Code Online (Sandbox Code Playgroud)

产生

A-B-C-D`-E` (master)
Run Code Online (Sandbox Code Playgroud)

根据我的理解,最终结果是相同的。(D和E现在掌握了干净的(直线)提交历史记录。)

为什么后者(樱桃采摘)产生的合并冲突会比前者(变基)产生的合并冲突少?

更新更新更新

我终于能够重现此问题,现在我意识到我可能已经简化了上面的示例。这就是我能够复制的方式...

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

A-B-C (master)
   \
    D-E (next)
       \
        F-G (other-next)
Run Code Online (Sandbox Code Playgroud)

然后我执行以下操作

git checkout next
git rebase master
git checkout master
git merge next
Run Code Online (Sandbox Code Playgroud)

我最终得到以下结果

A-B-C-D`-E` (master)
   \ \
    \ D`-E` (next)
     \
      D-E
         \
          F-G (other-next)
Run Code Online (Sandbox Code Playgroud)

从这里开始,我将变基或摘樱桃

变基示例

git checkout other-next
git rebase master 
Run Code Online (Sandbox Code Playgroud)

产生

A-B-C-D`-E`-F`-G` (master)
Run Code Online (Sandbox Code Playgroud)

樱桃采摘示例

git checkout master
git cherry-pick F G
Run Code Online (Sandbox Code Playgroud)

产生相同的结果

A-B-C-D`-E`-F`-G` (master)
Run Code Online (Sandbox Code Playgroud)

但是合并冲突比重新调整策略少得多。

最终重现了一个类似的示例,我认为我知道为什么与重定基相比在合并中存在更多的合并冲突,但我将其留给其他人(他们可能会比我做得更好(更准确)的工作) ) 回答。

tor*_*rek 5

更新的答案(请参阅有问题的更新)

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

让我们注意一下,然后撇开git rebase可能使用git cherry-pick,或git format-patchgit am复制某些提交的事实。在大多数情况下git cherry-pickgit am应达到相同的结果。(git rebase文档特别指出上游文件重命名是Cherry-pick方法的问题,而不是git am基于默认方法的非交互式rebase问题。另请参见以下原始答案中的各种括号和注释。)

这里要考虑的主要事情是要复制哪些提交。在手动方法中,首先手动复制提交D,并ED'E',那么你手动复制FGF'G'。这是要做的最少工作,而这正是我们想要的。这里唯一的缺点是我们必须要做的所有手动提交识别。

使用命令时:

git checkout <branch> && git rebase <upstream>
Run Code Online (Sandbox Code Playgroud)

您可以使Git自动执行查找要复制的提交的过程。当Git正确时,这很棒,但是如果Git错误,则不是。

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

当前分支中的所有提交但未在<upstream>中的提交所做的更改都保存到一个临时区域。这与将显示的提交相同git log <upstream>..HEAD。或通过git log 'fork_point'..HEAD,如果--fork-point处于活动状态(请参阅--fork-point下面的说明);或按git log HEAD,如果--root指定了选项。

--fork-point从git 2.something开始,这种复杂性有些新,但是在这种情况下它不是“活动的”,因为您指定了一个<upstream>参数而未指定--fork-point。实际<upstream>master两次。

现在,如果您实际运行它们git log--oneline使它们更好用):

git checkout next && git log --oneline master..HEAD
Run Code Online (Sandbox Code Playgroud)

和:

git checkout other-next && git log --oneline master..HEAD
Run Code Online (Sandbox Code Playgroud)

你会看到的第一个列表提交DE-优良的! -但第二个清单DEF,和G。嗯哦,DE发生两次!

问题是,这有时可行。好吧,我在上面说“有点不对劲”。这是错误的原因,与之前的引用相比,仅有两段文字:

请注意,在HEAD中引入与HEAD .. <upstream>中的提交相同的文本更改的所有提交都将被忽略(即,将跳过上游已接受的具有不同提交消息或时间戳的补丁程序)。

请注意,HEAD..<upstream>这与我们刚运行<upstream>..HEADgit log命令D-through-中的相反G

对于第一个基准,没有中的提交git log HEAD..master,因此没有可能被跳过的提交。这是很好的,因为没有提交跳过:我们所拷贝EFE'F',这就是我们想要什么。

但是,对于第二次重新设置,发生在第一次重新设置之后,git log HEAD..master它将显示commits E'F':我们刚刚制作的两个副本。这些有可能被跳过:它们是考虑跳过的候选对象

“可能跳过”不是“确实跳过”

那么 Git 如何确定应该真正跳过的提交呢?答案在中git patch-id,尽管实际上是直接在中实现的git rev-list,这是一个非常复杂的命令。但是,这些都不能很好地描述它,部分原因是很难描述。无论如何,这是我的尝试。:-)

Git在这里所做的是在去除标识行号之后查看差异,以防补丁位于稍微不同的位置(由于早期的补丁在文件中上下移动行)。它使用与文件相同的技巧-将文件内容转换为唯一的哈希-将每个提交转换为“补丁ID”。所述提交ID是唯一的散列识别一个特定的提交,并且总是相同的一个具体的提交。该补丁ID是不同的(但仍然是独一无二的对某些内容)哈希ID总是标识“相同的”补丁,即东西去除,并添加相同DIFF-帅哥,即使它删除,并从不同的增加了他们位置。

计算完每个提交的补丁程序ID后,Git可以说:“啊哈,提交D和提交D'具有相同的补丁程序ID!我应该跳过复制,D因为D'可能是复制的结果D。” Evs 可以做同样的事情E'。这通常是可行的,但对于D从复制DD'所需的手动干预(修复合并冲突)的E任何时候都将失败,并且从复制EE'所需的手动干预的每当同样失败。

更聪明的基础

这里需要的是一种“智能rebase”,它可以查看一系列分支并预先进行计算,从而承诺一次复制所有要重新建立基础的分支。然后,在完成所有副本之后,此“智能变基”将调整所有分支名称。

在这种特殊情况下(D通过复制),G这实际上非常简单,您可以使用以下方法手动进行操作:

$ git checkout -q other-next && git rebase master
[here rebase copies D, E, F, and G, perhaps with your assistance]
Run Code Online (Sandbox Code Playgroud)

其次是:

$ 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
Run Code Online (Sandbox Code Playgroud)

之所以other-next可行G',是因为名称commit ,其父代为F',其父代为E',而这正是我们要next指向的地方。由于HEAD指向branch nextgit reset调整refs/heads/next为指向commit E',我们就完成了。

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

                A1-A2-A3  <-- featureA
               /
...--o--o--o--o--o--o--o   <-- master
         \
          *--*--B3-B4-B5   <-- featureB
              \
               C3-C4       <-- featureC
Run Code Online (Sandbox Code Playgroud)

如果我们想对所有三个功能进行“多基础化”,我们可以featureA独立于其他两个基础进行基础化(三个A提交中的任何一个都不依赖于除先前A提交以外的任何“非主”内容),而是复制五个B提交和四个C提交我们必须复制两个*提交是 B C,但复制它们只是一次,然后剩下的三个和两个提交(分别)复制到的尖端复制承诺。

(这是可能写出这样的“聪明重订”,但该整合到Git的正常,从而使git status真正了解它,是相当困难。)


原始答案

我希望看到一个可复制的示例。在大多数情况下,您的“头脑中”模型应该可以工作。但是,有一种已知的特殊情况。

一个互动变基,或添加-m--merge平淡git rebase,实际上确实使用git cherry-pick,而默认的非交互式的底垫的用途git format-patchgit am替代。后者不适合重命名检测。特别是,如果上游有一个文件重命名,则可以期望交互式或变基1--merge行为不同(通常更好)。

(另外,请注意,两种基于rebase的git patch-idrebase- 面向补丁的rebase和基于cherry-pick的版本-都将跳过与通过/ git rev-list --left-only --cherry-pick HEAD...<upstream>或等效方法已经在上游的提交相同的提交。请参阅有关文档git rev-list,尤其是本节在--cherry-mark和上--left-right,我认为这更容易理解。不过,对于两种变基,这应该是相同的;如果您手动挑选,则是否执行此操作取决于您。)


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