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)
但是合并冲突比重新调整策略少得多。
最终重现了一个类似的示例,我认为我知道为什么与重定基相比在合并中存在更多的合并冲突,但我将其留给其他人(他们可能会比我做得更好(更准确)的工作) ) 回答。
我认为这里发生的事情与选择要复制的提交有关。
让我们注意一下,然后撇开git rebase可能使用git cherry-pick,或git format-patch和git am复制某些提交的事实。在大多数情况下git cherry-pick,git am应达到相同的结果。(git rebase文档特别指出上游文件重命名是Cherry-pick方法的问题,而不是git am基于默认方法的非交互式rebase问题。另请参见以下原始答案中的各种括号和注释。)
这里要考虑的主要事情是要复制哪些提交。在手动方法中,首先手动复制提交D,并E以D'和E',那么你手动复制F和G以F'和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)
你会看到的第一个列表提交D和E-优良的! -但第二个清单D,E,F,和G。嗯哦,D并E发生两次!
问题是,这有时可行。好吧,我在上面说“有点不对劲”。这是错误的原因,与之前的引用相比,仅有两段文字:
请注意,在HEAD中引入与HEAD .. <upstream>中的提交相同的文本更改的所有提交都将被忽略(即,将跳过上游已接受的具有不同提交消息或时间戳的补丁程序)。
请注意,HEAD..<upstream>这与我们刚运行<upstream>..HEAD的git log命令D-through-中的相反G。
对于第一个基准,没有中的提交git log HEAD..master,因此没有可能被跳过的提交。这是很好的,因为没有提交跳过:我们所拷贝E和F向E'和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从复制D到D'所需的手动干预(修复合并冲突)的E任何时候都将失败,并且从复制E到E'所需的手动干预的每当同样失败。
这里需要的是一种“智能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 next,git 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-patch和git 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需要相信那里有一个重命名。通常,它会相信是否有一个,但是由于它是通过比较树来检测它们的,所以这并不是完美的。