Git是团队协作开发的神兵利器,它最强势的地方是让我们可以极其快速的创建、切换代码分支。开发新功能了,从主干上切一个Feature分支,开发到一半正准备去厕所划个水,测试过来提了一个主干代码上的闪退Bug,一点不慌,先憋住,从主干上另切一个BugFix分支,修改完了再切回Feature分支,于是键盘前推,座椅后移,站起转身,甩一下衣角,碎步猫步疾步向厕所走去,深藏功与名。(对Git分支的详细管理参看这篇《使用Git必须要理解的GitFlow》)
Git方便了我们在不同的分支上撸代码,但最后要合到主干上。有两种方式,合并(merge)和衍合(rebase),衍合又称变基。合并好理解,也比较常用,衍合是什么鬼,光看名字有点不明觉厉的样子。既然两者都能将一个分支的代码合到另一个分支上,那区别是什么?什么情况下该用哪种?
如果你是Git之父/母,怎么设计代码整合
在讲合并和衍合的区别之前,先来思考一个问题:假如我们现在有master和experiment两条分支代码如下
现在想把experiment上C4提交的代码合到master上来,如果你是git设计者,你会怎么设计?
一种,我们拿到C4、C3以及分支最近分叉点C2,通过三方比较,能知道experiment相对于master所不同的代码,然后提取这些不同的地方,提交到master就行了。这样,也就在master上生成了一次新的提交。
这就是合并。可以看出,合并的核心在于“差异比较”。
Git官方对于合并的解释:它是把两个分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。
那我能不能不通过这种差异比较呢?换种思路,还是刚才的master和experiment分支图:
虽然C3,C4分别在不同的分支上提交的,但如果这两次提交都是在其中一条分支上依次进行,那不就获得我们想要的最终代码了吗。这就是衍合的思想。
现在想要把C4改动的代码合到master上,看一下Git官方推荐的具体做法:
1.检出experiment分支,对master进行衍合
$ git checkout experiment
$ git rebase master
2.回到 master 分支,进行一次快进合并
$ git checkout master
$ git merge experiment
它的原理是首先找到这两个分支(即当前分支 experiment、衍合操作的目标分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。注意,也就是说这个过程中,如图experiment会先抛弃之前的C4提交,然后把C4变动的代码作为新的C4’重新再提交一次。
可以看出,衍合的核心在于“过程重演”。
衍合的雷区
从上面的栗子中,可以感受到,虽然合并和衍合两种整合方式最终的结果是一样的,但衍合使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。那我们是不是只用衍合就好了?别急,这里有个雷区!
在Git推荐的衍合栗子中,目标分支 master上的提交(C3)是没有变化的,experiment分支则丢弃一些现有的提交(C4),然后相应地新建一些内容一样但实际上不同的提交(C4’)。在这次的衍合过程中,给master起个名字叫被动分支吧,experiment叫主动分支。 如果在进行衍合前,你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。
所以不要将公共分支作为“主动分支”使用衍合,例如GitFlow中的develop分支,因为这样容易破坏其他人的提交记录,从而影响团队协作。不过你可以把公共分支作为“被动分支”进行衍合,比如你独立开发一个新功能,从develop中检出了一个feature分支,在feature上开发完成后,合到develop,那就可以将feature作为“主动分支”,develop作为“被动分支”进行衍合。
使用场景
如果你是普通青年,像我一样,那就只玩合并,不会出错。如果你是文艺青年,像我一样,也玩衍合,just 避雷。
衍合的官方解说
https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%8F%98%E5%9F%BA