“合并前文件还在的,合并后就不见了”、“我遇到Git合并的bug了” 是两句常常听到的话,但真的是Git的bug么?或许只是你的预期不对。本文经过讲解三向合并和Git的合并策略,step by step介绍Git是怎么作一个合并的,让你们对Git的合并结果有一个准确的预期,而且避免发生合并事故。html
这是一个系列的文章,计划包括三篇:git
- 这才是真正的Git——Git内部原理
- 这才是真正的Git——分支合并【当前这篇文章】
- 这才是真正的Git——Git实用技巧(暂未完成)
在开始正文以前,先来听一下这个故事。算法
以下图,小明从节点A拉了一条dev分支出来,在节点B中新增了一个文件http.js,而且合并到master分支,合并节点为E。这个时候发现会引发线上bug,赶忙撤回这个合并,新增一个revert节点E'。过了几天小明继续在dev分支上面开发新增了一个文件main.js,并在这个文件中import了http.js里面的逻辑,在dev分支上面一切运行正常。可当他将此时的dev分支合并到master时候却发现,http.js文件不见了,致使main.js里面的逻辑运行报错了。但此次合并并无任何冲突。他又得从新作了一下revert,而且迷茫的怀疑是Git的bug。测试
两句常常听到的话:3d
—— ”合并前文件还在的,合并后就不见了“code
—— ”我遇到Git的bug了“cdn
相信不少同窗或多或少在不熟悉Git合并策略的时候都会发生过相似上面的事情,明明在合并前文件还在的,为何合并后文件就不在了么?一度还怀疑是Git的bug。这篇文章的目的就是想跟你们讲清楚Git是怎么去合并分支的,以及一些底层的基础概念,从而避免发生如故事中的问题,并对Git的合并结果有一个准确的预期。视频
在看怎么合并两个分支以前,咱们先来看一下怎么合并两个文件,由于两个文件的合并是两个分支合并的基础。htm
你们应该都据说过“三向合并”这个词,不知道你们有没有思考过为何两个文件的合并须要三向合并,只有二向是否能够自动完成合并。以下图blog
很明显答案是不能,如上图的例子,Git无法肯定这一行代码是我修改的,仍是对方修改的,或者以前就没有这行代码,是咱们俩同时新增的。此时Git没办法帮咱们作自动合并。
因此咱们须要三向合并,所谓三向合并,就是找到两个文件的一个合并base,以下图,这样子Git就能够很清楚的知道说,对方修改了这一行代码,而咱们没有修改,自动帮咱们合并这两个文件为 Print("hello")。
接下来咱们了解一下什么是冲突?冲突简单的来讲就是三向合并中的三方都互不相同,即参考合并base,咱们的分支和别人的分支都对同个地方作了修改。
了解完怎么合并两个文件以后,咱们来看一个使用 git merge 来作分支合并。如上图,将master分支合并到feature分支上,会新增一个commit节点来记录此次合并。
Git会有不少合并策略,其中常见的是Fast-forward、Recursive 、Ours、Theirs、Octopus。下面分别介绍不一样合并策略的原理以及应用场景。默认Git会帮你自动挑选合适的合并策略,若是你须要强制指定,使用git merge -s <策略名字>
了解Git合并策略的原理可让你对Git的合并结果有一个准确的预期。
Fast-forward是最简单的一种合并策略,如上图中将some feature分支合并进master分支,Git只须要将master分支的指向移动到最后一个commit节点上。
Fast-forward是Git在合并两个没有分叉的分支时的默认行为,若是不想要这种表现,想明确记录下每次的合并,可使用git merge --no-ff
。
Recursive是Git分支合并策略中最重要也是最经常使用的策略,是Git在合并两个有分叉的分支时的默认行为。其算法能够简单描述为:递归寻找路径最短的惟一共同祖先节点,而后以其为base节点进行递归三向合并。提及来有点绕,下面经过例子来解释。
以下图这种简单的状况,圆圈里面的英文字母为当前commit的文件内容,当咱们要合并中间两个节点的时候,找到他们的共同祖先节点(左边第一个),接着进行三向合并获得结果为B。(由于合并的base是“A”,下图靠下的分支没有修改内容仍为“A”,下图靠上的分支修改为了“B”,因此合并结果为“B”)。
但现实状况老是复杂得多,会出现历史记录链互相交叉等状况,以下图
当Git在寻找路径最短的共同祖先节点的时候,能够找到两个节点的,若是Git选用下图这一个节点,那么Git将没法自动的合并。由于根据三向合并,这里是是有冲突的,须要手动解决。(base为“A“,合并的两个分支内容为”C“和”B“)
而若是Git选用的是下图这个节点做为合并的base时,根据三向合并,Git就能够直接自动合并得出结果“C”。(base为“B“,合并的两个分支内容为”C“和”B“)
做为人类,在这个例子里面咱们很天然的就能够看出来合并的结果应该是“C”(以下图,节点四、5都已是“B”了,节点6修改为“C”,因此合并的预期为“C”)
那怎么保证Git可以找到正确的合并base节点,尽量的减小冲突呢?答案就是,Git在寻找路径最短的共同祖先节点时,若是知足条件的祖先节点不惟一,那么Git会继续递归往下寻找直至惟一。仍是以刚刚这个例子图解。
以下图所示,咱们想要合并节点5和节点6,Git找到路径最短的祖先节点2和3。
由于共同祖先节点不惟一,因此Git递归以节点2和节点3为咱们要合并的节点,寻找他们的路径最短的共同祖先,找到惟一的节点1。
接着Git以节点1为base,对节点2和节点3作三向合并,获得一个临时节点,根据三向合并的结果,这个节点的内容为“B”。
再以这个临时节点为base,对节点5和节点6作三向合并,获得合并节点7,根据三向合并的结果,节点7的内容为“C”
至此Git完成递归合并,自动合并节点5和节点6,结果为“C”,没有冲突。
Recursive策略已经被大量的场景证实它是一个尽可能减小冲突的合并策略,咱们能够看到有趣的一点是,对于两个合并分支的中间节点(如上图节点4,5),只参与了base的计算,而最终真正被三向合并拿来作合并的节点,只包括末端以及base节点。
须要注意Git只是使用这些策略尽可能的去帮你减小冲突,若是冲突不可避免,那Git就会提示冲突,须要手工解决。(也就是真正意义上的冲突)。
Ours和Theirs这两种合并策略也是比较简单的,简单来讲就是保留双方的历史记录,但彻底忽略掉这一方的文件变动。以下图在master分支里面执行git merge -s ours dev
,会产生蓝色的这一个合并节点,其内容跟其上一个节点(master分支方向上的)彻底同样,即master分支合并先后项目文件没有任何变更。
而若是使用theirs则彻底相反,彻底抛弃掉当前分支的文件内容,直接采用对方分支的文件内容。
这两种策略的一个使用场景是好比如今要实现同一功能,你同时尝试了两个方案,分别在分支是dev1和dev2上,最后通过测试你选用了dev2这个方案。但你不想丢弃dev1的这样一个尝试,但愿把它合入主干方便后期查看,这个时候你就能够在dev2分支中执行git merge -s ours dev1
。
这种合并策略比较神奇,通常来讲咱们的合并节点都只有两个parent(即合并两条分支),而这种合并策略能够作两个以上分支的合并,这也是git merge两个以上分支时的默认行为。好比在dev1分支上执行git merge dev2 dev3
。
他的一个使用场景是在测试环境或预发布环境,你须要将多个开发分支修改的内容合并在一块儿,若是不用这个策略,你每次只能合并一个分支,这样就会致使大量的合并节点产生。而使用Octopus这种合并策略就能够用一个合并节点将他们所有合并进来。
git rebase
也是一种常常被用来作合并的方法,其与git merge的最大区别是,他会更改变动历史对应的commit节点。
以下图,当在feature分支中执行rebase master时,Git会以master分支对应的commit节点为起点,新增两个全新的commit代替feature分支中的commit节点。其缘由是新的commit指向的parent变了,因此对应的SHA1值也会改变,因此没办法复用原feature分支中的commit。(这句话的理解须要这篇文章的基础知识)
对于合并时候要使用git merge仍是git rebase的争论,我我的的见解是没有银弹,根据团队和项目习惯选择就能够。git rebase能够给咱们带来清晰的历史记录,git merge能够保留真实的提交时间等信息,而且不容易出问题,处理冲突也比较方便。惟一有一点须要注意的是,不要对已经处于远端的多人共用分支作rebase操做。
我我的的一个习惯是:对于本地的分支或者肯定只有一我的使用的远端分支用rebase,其他状况用merge。
rebase还有一个很是好用的东西叫interactive模式,使用方法是git rebase -i
。能够实现压缩几个commit,修改commit信息,抛弃某个commit等功能。好比说我要压缩下图260a12a五、956e1d18,将他们与9dae0027合并为一个commit,我只需将260a12a五、956e1d18前面的pick改为“s”,而后保存就能够了。
限于篇幅,git rebase -i 还有不少实用的功能暂不展开,感兴趣的同窗能够本身研究一下。
如今咱们再来看一下文章开头的例子,咱们就能够理解为何最后一次merge会致使http.js文件不见了。根据Git的合并策略,在合并两个有分叉的分支(上图中的D、E‘)时,Git 默认会选择Recursive策略。找到D和E’的最短路径共同祖先节点B,以B为base,对D,E‘作三向合并。B中有http.js,D中有http.js和main.js,E’中什么都没有。根据三向合并,B、D中都有http.js且没有变动,E‘删除了http.js,因此合并结果就是没有http.js,没有冲突,因此http.js文件不见了。
这个例子理解原理以后解决方法有不少,这里简单带过两个方法:1. revert节点E'以后,此时的dev分支要抛弃删除掉,从新从E'节点拉出分支继续工做,而不是在原dev分支上继续开发节点D;2. 在节点D合并回E’节点时,先revert一下E‘节点生成E’‘(即revert的revert),再将节点D合并进来。
Git有不少种分支合并策略,本文介绍了Fast-forward、Recursive、Ours/Theirs、Octopus合并策略以及三向合并。掌握这些合并策略以及他们的使用场景可让你避免发生一些合并问题,并对合并结果有一个准确的预期。
但愿这篇文章对你们有用,感兴趣的同窗能够逛一逛个人博客 www.lzane.com 或看看个人其余文章。