为什么 git cherry-pick 产生的冲突比 git rebase 产生的冲突少?
Why would git cherry-pick produce fewer conflicts than git rebase?
我经常变基。有时 rebase
特别有问题(很多合并冲突),在这种情况下我的解决方案是 cherry-pick
个人提交到 master 分支。我这样做是因为几乎每次我这样做时,冲突的数量都会少得多。
我的问题是为什么会这样。
为什么 cherry-pick
时的合并冲突比 rebase
时少?
在我的心智模型中,rebase
和 cherry-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-pick
或 git format-patch
和 git am
来复制一些提交。在大多数情况下,git cherry-pick
和 git am
应该会获得相同的结果。 (git rebase
documentation 特别指出上游文件重命名是 cherry-pick 方法的一个问题,而不是默认的基于 git am
的非交互式 rebase 方法。另请参见下面原始答案中的各种括号注释,和评论。)
这里主要考虑的是要复制哪些提交。在手动方法中,首先手动将提交 D
和 E
复制到 D'
和 E'
,然后手动复制 F
和 G
到F'
和 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
您会看到第一个列出了提交 D
和 E
——太棒了!——但是第二个列出了 D
、E
、F
,以及 G
。哦哦,D
和 E
出现了两次!
事实是,有时 有效。好吧,我上面说了"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
中没有提交,因此没有可能被跳过的提交。这很好,因为没有要跳过的提交:我们正在将 E
和 F
复制到 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
." 它可以对 E
和 E'
做同样的事情。这 通常 有效——但是当从 D
到 D'
的副本需要手动干预(修复合并冲突)时,它对 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 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
如果我们想要 "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-patch
和 git 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
需要相信 那里有重命名。通常如果有的话它会相信这一点,但由于它是通过比较树来 检测 它们,所以这并不完美。
我经常变基。有时 rebase
特别有问题(很多合并冲突),在这种情况下我的解决方案是 cherry-pick
个人提交到 master 分支。我这样做是因为几乎每次我这样做时,冲突的数量都会少得多。
我的问题是为什么会这样。
为什么 cherry-pick
时的合并冲突比 rebase
时少?
在我的心智模型中,rebase
和 cherry-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-pick
或 git format-patch
和 git am
来复制一些提交。在大多数情况下,git cherry-pick
和 git am
应该会获得相同的结果。 (git rebase
documentation 特别指出上游文件重命名是 cherry-pick 方法的一个问题,而不是默认的基于 git am
的非交互式 rebase 方法。另请参见下面原始答案中的各种括号注释,和评论。)
这里主要考虑的是要复制哪些提交。在手动方法中,首先手动将提交 D
和 E
复制到 D'
和 E'
,然后手动复制 F
和 G
到F'
和 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 bygit log 'fork_point'..HEAD
, if--fork-point
is active (see the description on--fork-point
below); or bygit 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
您会看到第一个列出了提交 D
和 E
——太棒了!——但是第二个列出了 D
、E
、F
,以及 G
。哦哦,D
和 E
出现了两次!
事实是,有时 有效。好吧,我上面说了"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
中没有提交,因此没有可能被跳过的提交。这很好,因为没有要跳过的提交:我们正在将 E
和 F
复制到 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
." 它可以对 E
和 E'
做同样的事情。这 通常 有效——但是当从 D
到 D'
的副本需要手动干预(修复合并冲突)时,它对 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 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
如果我们想要 "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-patch
和 git 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
需要相信 那里有重命名。通常如果有的话它会相信这一点,但由于它是通过比较树来 检测 它们,所以这并不完美。