【问题标题】:Why would git cherry-pick produce fewer conflicts than git rebase?为什么 git cherry-pick 会比 git rebase 产生更少的冲突?
【发布时间】:2016-07-07 17:36:09
【问题描述】:

我经常变基。有时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 现在在 master 上,具有干净(直线)的提交历史。)

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

更新更新更新

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

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

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)

但与变基策略相比,合并冲突要少得多。

终于重现了一个类似的例子,我想我明白为什么与挑选樱桃相比,变基的合并冲突更多,但我会把它留给其他人(他们可能会做得更好(更准确)比我愿意)回答。

【问题讨论】:

  • 这个问题可能问得太多了,但是你能提供一个MVCE吗?
  • 是的,重现案例有助于理解,因为 git-rebase 在后台使用 git-cherry-pick。 (无论是显式地还是作为补丁应用程序失败时的后备。)
  • 尽管如此,我认为在准备好 MVCE 后,您很有可能已经找到了自己问题的答案,并且能够与我们分享关于 rebase 与cherry-picking 的有用指南不同的场景。
  • @EdwardThomson - 非常有趣......这就是我一直在思考 rebase 与 cherry-pick 的方式......
  • @EdwardThomson:你的 cmets(连同@Leon's)让我怀疑我的问题的有效性......我会尝试创建一个 mvce(虽然这可能需要一段时间)......

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


【解决方案1】:

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

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

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

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

使用命令时:

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

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

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

所有在当前分支中提交但不在 中的更改都保存到临时区域。这与git log &lt;upstream&gt;..HEAD 显示的提交集相同;或git log 'fork_point'..HEAD,如果--fork-point 处于活动状态(参见下面--fork-point 的描述);或git log HEAD,如果指定了--root 选项。

--fork-point 复杂性有点新,因为 git 2.something,但在这种情况下它不是“活动的”,因为您指定了 &lt;upstream&gt; 参数并且没有指定 --fork-point。实际的&lt;upstream&gt; 两次都是master

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

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

和:

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

你会看到第一个列出了提交DE——非常好!——但是第二个列出了DEFG。哦哦,DE 出现两次!

问题是,这有时有效。好吧,我在上面说“有些错误”。这就是它的错误之处,仅比之前的引用低了两段:

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

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

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

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

“可能跳​​过”不是“真的跳过”

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

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

在为每个提交计算了一个补丁 ID 之后,Git 可以说:“啊哈,提交 D 和提交 D' 具有相同的补丁 ID!我应该跳过复制 D 因为 D' 可能是复制D 的结果。”它可以对EE' 做同样的事情。这经常有效——但只要从D 复制到D' 需要手动干预(修复合并冲突),D 就会失败,并且每当从EE' 需要人工干预。

更智能的变基

这里需要一种“智能变基”,它可以查看一系列分支并提前计算,它承诺为所有要变基的分支复制一次。然后,在所有副本完成后,这个“智能变基”将调整所有分支名称。

在这种特殊情况下——从 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',这是我们希望next 指向的地方。由于HEAD指的是分支next,所以git reset调整refs/heads/next指向提交E',我们就完成了。

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

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

如果我们想要“多重变基”所有三个功能,我们可以独立于其他两个变基featureA——三个A 提交中没有一个依赖于任何“非主控”,而不是早先的A提交——但是要复制五个B 提交和四个C 提交,我们必须复制两个* 提交,它们 B @ 987654415@,但只复制一次,然后将剩余的三个和两个提交(分别)复制到复制提交的尖端。

可以编写这样一个“智能变基”,但是将其正确集成到 Git 中,以便 git status 真正理解它,要困难得多。)


原答案

我很想看到一个可重现的例子。在大多数情况下,您的“头脑中”模型应该可以工作。但是有一种已知的特殊情况。

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

(另外,请注意,两种类型的 rebase(面向补丁的版本和基于cherry-pick的版本)都将跳过git patch-id的提交,与上游已经通过git rev-list --left-only --cherry-pick HEAD...&lt;upstream&gt;或等效的提交相同. 参见the documentation for git rev-list,尤其是--cherry-mark--left-right 部分,我认为这更容易理解。不过,这对于两种rebase 应该是相同的;如果您手动挑选樱桃,它将是是否这样做取决于您。)


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

【讨论】:

  • @Leon:啊,这很有道理。我暂时保留它,如果合适,稍后将其编辑掉...
  • @Leon:你是对的(在变基后合并)。很抱歉让您感到困惑。我稍微编辑了我的问题,希望能解决这个问题。
  • 感谢@torek 的回答!我在当天晚些时候重新审视了我的问题并进行了一些编辑,这可能有助于澄清为什么在 rebase 和cherry-picking 时合并冲突的数量存在差异。如果您有兴趣,我会欢迎后续答复。
  • 是的,重新回答。 :-) 我实际上有一个(不是很好,非常近似的尝试)“智能变基”脚本,我开始使用并变得一瘸一拐,我认为它可能会成为一个很好的程序来“编写”作为我停滞不前的书的一部分-about-Git-and-Mercurial 项目,我需要在示例项目中提交示例...
猜你喜欢
  • 2013-11-18
  • 2016-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-22
  • 1970-01-01
  • 1970-01-01
  • 2023-02-02
相关资源
最近更新 更多