我经常变身。偶尔rebase
特别有问题(很多合并冲突),在这种情况下我的解决方案是cherry-pick
个人提交到主分支。我这样做是因为几乎每次我都这样做,冲突的数量要少得多。
我的问题是为什么会出现这种情况。
为什么我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现在使用干净的(直线)提交历史记录。)
为什么后者(采摘樱桃)产生的合并冲突少于前者(重新定位)?
更新更新更新
我终于能够重现这个问题了,我现在意识到我可能已经过度简化了上面的例子。这是我如何重现...
说我有以下内容(注意额外的分支)
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)
但合并冲突远少于重新定位策略。
最后再现了一个类似的例子,我想我知道为什么与变装有更多的合并冲突而不是樱桃采摘,但是我会把它留给其他人(他们可能做得更好(更准确) )工作比我想回答。
答案 0 :(得分:4)
我认为这里发生的事情与选择要复制的提交有关。
让我们注意,然后放弃,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选择这些提交?这个简单但有些错误的答案在这句话中(来自同一文档):
当前分支中的提交所做的所有更改,但不在&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
您会看到第一个列出了提交D
和E
- 非常好! - 但第二个列出了D
,E
,F
,和G
。哦,D
和E
出现两次!
问题是,这个有时有效。嗯,我说'有些错误&#34;以上。这是错误的原因,只比前面引用的两段:
请注意,HEAD中的任何提交都会引入与HEAD中的提交相同的文本更改。&lt; upstream&gt;被省略(即,将跳过已经在上游接受不同提交消息或时间戳的补丁)。
请注意,此处的HEAD..<upstream>
与我们刚刚运行的<upstream>..HEAD
命令中的git log
相反,我们在其中看到了D
- 通过 - G
。
对于第一个 rebase,git log HEAD..master
中没有提交,因此没有可能被跳过的提交。这样做很好,因为没有提交要跳过:我们正在将E
和F
复制到E'
和F'
,并且&#39;正是我们想要的。
对于第二个 rebase,在第一个rebase完成后发生,git log HEAD..master
将显示提交E'
和F'
:两个副本我们刚刚做了。这些可能被跳过:他们是考虑跳过的候选人。
那么如何 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'
执行相同的操作。这个通常有效 - 但只要D
到D
的副本需要人工干预(修复合并冲突),D'
就会失败,同样{只要E
到E
的副本需要人工干预,就会{1}}。
这里需要的是一种&#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
引用了分支HEAD
,next
会调整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-pick
和git format-patch
代替。后者不适合重命名检测。特别是,如果在上游有一个文件重命名, 1 ,交互式或git am
rebase的行为可能会有所不同(通常,更好)。
(另请注意,这两种rebase - 面向补丁的版本和基于cherry-pick的版本 - 将跳过--merge
的提交 - 与上游已提交的提交相同,通过{{1请参阅the documentation for git rev-list
,特别是关于git patch-id
和git rev-list --left-only --cherry-pick HEAD...<upstream>
的部分,我认为这一部分使我们更容易理解。但是,对于这两种rebase,这应该是相同的;你手动采摘樱桃,无论你是否这样做都取决于你。)
1 更准确地说,--cherry-mark
需要相信那里有重命名。通常它会相信这个,如果有一个,但由于它通过比较树来检测它们,这并不完美。