Stop cherry-picking, start merging,第1部分:合并冲突

121 阅读6分钟

停止cherry-picking,开始合并,第1部分:合并冲突

原文链接: Stop cherry-picking, start merging, Part 1

原文作者:Raymond Chen,发布于2018年3月12日

Cherry-picking在git中是一种常见的操作,但这并不是一个好主意。有时它是一个中性的做法,但我至今还没有发现它真正有用的情况。

这是一个系列的开始,我将首先解释为什么cherry-picking不好,然后解释为什么cherry-picking更糟,接着我会设法让你冷静下来,向你展示如何通过合并而不是cherry-picking来达到相同的效果,展示如何将这种技术应用于需要进行追溯合并的情况,并总结如何将这种技术应用于已经犯了cherry-picking错误并希望在出现更糟糕情况之前修复它的情况。

这是一项艰巨的任务,但我一直想写这个内容很久了,该做的事情总要做。

进行cherry-pick需要两个分支,一个作为捐赠方,一个作为接收方。让我们称它们为master分支和feature分支。为了简化起见,假设被cherry-pick的提交是对单个文件的一行更改。每个提交都将带有该行内容的注释。

%%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}} }%%
gitGraph
   commit id: "A (apple)"
   branch feature
   checkout feature
   commit id: "F1 (apple)"
   commit id: "F2 (berry)"
   checkout master
   commit id: "M1 (apple)"
   commit id: "M2 (berry)" type: REVERSE

为了便于说明,我使用虚线表示cherry-pick操作。这条虚线在实际仓库中并不存在,我画出它只是为了帮助表达时间顺序。(最终,我也会停止绘制虚线。)

你有一个共同祖先A,在这个提交中,问题所在的行是单词apple。从这个共同祖先开始,两个分支分叉:提交F1发生在feature分支上,提交M1发生在master分支上。这些更改不影响问题所在的行,所以它仍然是apple。然后,你在feature分支中进行一些提交F2,将问题所在的行从apple更改为berry,并且你将提交F2 cherry-pick到master分支作为M2。

到目前为止,一切正常。

时间流逝,更多的提交发生,你的提交图看起来像这样:

%%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}} }%%
gitGraph
   commit id: "A (apple)"
   branch feature
   checkout feature
   commit id: "F1 (apple)"
   commit id: "F2 (berry)"
   commit id: "F3 (berry)"
   checkout master
   commit id: "M1 (apple)"
   commit id: "M2 (berry)" type: REVERSE
   commit id: "M3 (berry)"

你对master分支进行了另一个提交M3,对feature分支进行了另一个提交F3。这些提交都不影响问题所在的行,所以该行仍然是单词berry

现在是合并的时候了,由于两个分支中问题所在的行是相同的,所以合并很简单,最终合并的结果是berry

%%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}} }%%
gitGraph
   commit id: "A (apple)"
   branch feature
   checkout feature
   commit id: "F1 (apple)"
   commit id: "F2 (berry)"
   commit id: "F3 (berry)"
   checkout master
   commit id: "M1 (apple)"
   commit id: "M2 (berry)" type: REVERSE
   commit id: "M3 (berry)"
   merge feature id: "M4 (berry)"

这是理想情况。

但在活跃的代码库中,这种情况相对不常见。

考虑这个替代时间线:在cherry-pick之后,对master分支进行了额外的提交M3,对feature分支进行了额外的提交F3,但这次提交F3将问题所在的行更改为cherry。这可能是因为提交F2的人发现了一个改进(现在樱桃在打折),或者他们进行了一个更大的更改,需要从浆果切换到樱桃。

无论什么原因,提交图现在看起来像这样:

%%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}} }%%
gitGraph
   commit id: "A (apple)"
   branch feature
   checkout feature
   commit id: "F1 (apple)"
   commit id: "F2 (berry)"
   commit id: "F3 (cherry)"
   checkout master
   commit id: "M1 (apple)"
   commit id: "M2 (berry)" type: REVERSE
   commit id: "M3 (berry)"
   merge feature id: "💥 冲突!"

这一次,当将feature分支合并回master分支时,出现了合并冲突。三向合并的基础包含apple,传入的feature分支有cherry,而现有的master分支有berry

<<<<<<< HEAD (master)
berry
||||||| merged common ancestors
apple
=======
cherry
>>>>>>> feature

冲突发生是因为cherry-pick的更改随后被其中一个分支再次更改。我们在图表中使用虚线来强调cherry-pick关系只存在于我们的头脑中,实际上并没有记录在提交图中的任何地方。

你必须希望解决这个合并冲突的人记住这行代码的历史,或者能够访问团队对这行代码的知识,以理解正确的解决方案是接受feature分支中的更改,而不是master分支中的更改。

在这种情况下,没有太多的变化,只涉及两个分支,希望在合并中没有太多其他冲突(这样解决合并的人就不会感到疲惫和精疲力竭),所以正确解决的机会相当大。但考虑这个三分支场景:

%%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel':true, 'mainBranchName': 'master'}} }%%
gitGraph
   commit id: "A (apple)"
   branch victim
   checkout victim
   commit id: "V1 (apple)"
   branch feature
   checkout feature
   commit id: "F1 (apple)"
   commit id: "F2 (berry)"
   commit id: "F3 (cherry)"
   checkout victim
   commit id: "V2 (apple)"
   commit id: "V3 (apple)"
   merge feature id: "V4 (cherry)"
   checkout master
   commit id: "M1 (apple)"
   commit id: "M2 (berry)" type: REVERSE
   commit id: "M3 (berry)"
   checkout victim
   merge master id: "💥 冲突!"

从一个提交A开始,其中问题所在的行是apple。我们基于提交A创建一个分支,不祥地命名为victim,并添加一个名为V1的提交,该提交不影响问题所在的行,所以它仍然是apple。从victim分支我们从提交V1创建feature分支,然后故事是一样的:对feature分支,我们添加与之前相同的提交F1,它不影响问题所在的行,所以它仍然是apple。同时,master分支添加了一个不影响问题所在的行的提交M1。

我们继续:feature分支添加了一个提交F2,将问题所在的行更改为berry,master分支将该提交作为M2 cherry-pick过来。feature分支进行了另一个更改F3,恰好将问题所在的行从berry更新为cherry,而master分支添加了一个不更改问题所在的行的提交M3,所以它仍然是berry

在整个过程中,victim分支完全不知道feature和master分支创建的cherry-picking灾难。它提交了与问题所在的行无关的更改V2和V3,所以该行仍然是apple

最终,feature分支将其更改合并回victim分支,产生提交V4,其中问题所在的行现在是cherry,这要归功于feature分支所做的更改。

这个定时炸弹现在已经进入了victim分支。

victim分支决定从master分支进行合并,这就是冲突被检测到的地方,因为这是原始更改F2第一次遇到其cherry-picked副本M2。被这个合并冲突困住的可怜人完全不知道feature和master分支在他背后与魔鬼达成的交易。此外,被合并冲突困住的人可能因处理由master分支合并引起的所有其他(有效的)冲突而精疲力竭,可能没有精力去逆向工程两个分支是如何最终以这种方式结束的,并找出哪一方是对的。

基本上,当你cherry-pick一个提交时,你现在在图中有两个提交副本。任何受该提交影响的代码行在两个分支中都必须保持不变,直到提交的两个副本最终合并。如果任一分支修改了cherry-pick触及的任何行,那么就会创建一个可以无限期静静坐着的火药桶。当有人试图将两个提交合并在一起时,爆炸就会发生,而这一点可能在一个与cherry-pick所涉及的分支没有直接关系的遥远地方。这意味着试图解决合并的人从未参与过cherry-pick疯狂,可能不知道该与谁交谈以找出发生了什么。

好的,这是一个漫长的故事,你可能已经知道了其中的大部分,但请相信,尽管这很糟糕,但情况可能会更糟糕:爆炸可能不会发生。

等等,为什么爆炸不发生会更糟糕?我们下次再说。

导航