git merge的原理(递归三路合并算法)

git merge

merge 常见误区


git merge 是用时间前后决定merge结果的,后面会覆盖前面的?git

答 :git 是分布式的文件版本控制系统,在分布式环境中时间是不可靠的,git是靠三路合并算法进行合并的。算法

git merge 只要两行不相同就必定会报冲突,叫人工解决?


答:git 尽管两行内容不同,git 会进行取舍,当git没法进行取舍的时候才会进行人工解决冲突分布式

merge 基础


咱们知道git 合并文件是以行为单位进行一行一行进行合并的,可是有些时候并非两行内容不同git就会报冲突,由于smart git 会帮咱们自动帮咱们进行取舍,分析出那个结果才是咱们所指望的,若是smart git 都没法进行取舍时候才会报冲突,这个时候才须要咱们进行人工干预。那git 是如何帮咱们进行Smart 操做的呢?工具

二路合并


二路合并算法就是讲两个文件进行逐行对别,若是行内容不一样就报冲突。
布局

Mine 表明你本地修改
Theirs 表明其余人修改
假设对于同一个文件,出现你和其余人一块儿修改,此时若是git来进行合并,git就懵逼了,由于Git既不敢得罪你(Mine),也不能得罪他们(Theirs) 无理无据,git只能让你本身搞了,可是这种状况太多了并且也没有必要…this

三路合并


三路合并就是先找出一个基准,而后以基准为Base 进行合并,若是2个文件相对基准(base)都发生了改变 那git 就报冲突,而后让你人工决断。不然,git将取相对于基准(base)变化的那个为最终结果。
spa

Base 表明上一个版本,即公共祖先
Mine 表明你本地修改
Theirs 表明其余人修改
这样当git进行合并的时候,git就知道是其余人修改了,本地没有更改,git就会自动把最终结果变成以下,这个结构也是大多merge 工具的常见布局,好比IDEA
.net

若是换成下面的这样,就须要人工解决了:
翻译

上面就是git merge 最基本的原理 “三路合并”。版本控制


下面面的合并就是咱们常见的分支graph,结合具体分析。

上面①~⑨表明一个个修改集合(commit)每一个commit都有一个惟一7位SHA-1惟一表示。
①,②,④,⑦修改集串联起来就是一个链,此时用master指向这个集合就表明master分支,分支本质是一个快照,其实类比C中指针
一样dev分支也是由一个个commit组成
如今在dev分支上因为各类缘由要运行git merge master须要把master分支的更新合并到dev分支上,本质上就是合并修改集 ⑦(Mine) 和 ⑧(Theirs) ,此时咱们要 利用DAG(有向无环图)相关算法找到咱们公共的祖先 ②(Base)而后进行三方合并,最后合并生成 ⑨

git merge-base –all commit_id1(Yours/Theirs) commit_id2(Yours/Theirs) 就能找出公共祖先的commitId(Base)


图虽然复杂 可是核心原理是不变的,下面咱们看 另一个稍微高级一点的核心原理”递归三路合并” 也是咱们很常见看到 git merge 输出的 recursive strategy

例一

递归三路合并

下图中咱们若是要合并 ⑦(source) -> ⑥(destination):

简短描述下 如何会出现上面的图:

  • 在master分支上新建文件foo.c ,写入数据”A”到文件里面
  • 新建分支task2 git checkout -b task2 0,0 表明commit Id
  • 新建并提交commit ① 和 ③
  • 切换分支到master,新建并提交commit ②
  • 新建并修改foo.c文件中数据为”B”,并提交commit ④
  • merge commit ③ git merge task2,生成commit ⑥
  • 新建分支task1 git chekcout -b ④
  • 在task1 merge ③ git merge task2 生成commit ⑤
  • 新建commit ⑦,并修改foo.c文件内容为”C”
  • 切换分支到master上,并准备merge task1 分支(merge ⑦-> ⑥)

从上面咱们DAG图能够知道公共祖先有③和④,那到底选择哪一个呢,咱们分别来看:

若是选择③做为公共祖先 根据最基本的三路合并,能够看到最终结果⑧ 将须要手动解决冲突 /foo.c = BC???

若是选择④做为公共祖先 根据最基本的三路合并,能够看到最终结果⑧ 将获得 /foo.c=C

最终期待的结果是什么?

  • 咱们在Master上也是全部分支的起点定义了 /foo.c = A,在task2 分支上并无进行任何修改。
  • 最初修改 /foo.c = B 是在master 分支上,修改集④ 上修改成 /foo.c = B
  • 第一次经过 ③,④ 合并生成 ⑥, 最终使得Master分支上 ⑥ /foo.c = B
  • 第二次经过 ③,④ 又合并生成 ⑤, 最终使得task1分支上 ⑤ /foo.c = B
  • 在task1分支上不但愿 /foo.c = B ,因此在task1上新建一个⑦ /foo.c = C
  • 咱们知道 foo.c = B 是在 master分支上 ④ 进行修改的,其余的/foo.c = B 都是来自④此次修改。
  • 咱们能从图上能够知道 ⑦ 的修改必定是在 ④ 以后的,并非由于⑦ > ④ 而是 ④ 是 ⑦ 的祖先节点,因此咱们知道最终的修改合并以后就应该保留 /foo.c = C

因此 咱们的最佳公共祖先应该是4,最终结果应该是 /foo.c = C
git 如何选择公共祖先呢?

你可能会说用 git merge-base ⑥ ⑦ 输出的是 ④ 可是git 就真的是用 ④ 作祖先吗 ?答案是No

When the history involves criss-cross merges, there can be more than one best common ancestor for two commits. For example, with this topology:
---1---o---A
    \ / 
     X 
    / \
---2---o---o---B
both 1 and 2 are merge-bases of A and B. Neither one is better than the other (both are best merge bases). When the –all option is not given, it is unspecified which best one is output.

从git的解释中,咱们就知道 若是有2个都是最佳公共祖先时候,这个时候git 会随便输出一个不肯定公共祖先。

git 是这样进行合并的:

  • git 既不是直接用③,也不是用④,而是将2个祖先进行合并成一个虚拟的 X /foo.c = B, 由于③ 和 ④ 公共祖先是 〇/foo.c = A
  • git 用 X 作为 base 合并 ⑥ 和 ⑦ 结果就是 /foo.c = C

那什么又叫递归(recursive)合并呢 ? 咱们合并 ⑥ 和 ⑦ 的时候,咱们将其 2 个公共祖先③ 和 ④ 进行 merge 为 X ,在合并 ③ 和 ④时候 咱们又须要找到 他们的公共祖先,此时可能又有多个公共祖先,咱们又须要将他们先进行合并,如此就是递归了 也就是 recursive merge,以下:

Fast-Forward

Fast-Forward 翻译为快速前进,不少时候咱们在找2个修改集合X,Y 公共祖先的时候,会发现公共祖先就是他们中的一个,此时咱们进行merge 的时候,就是Fast-Forward便可,不会产生一个新的Commit 用于merge X和Y 。以下

当merge ② 和 ⑥时候 因为②是公共祖先,因此进行Fast-Forward 合并,直接指向⑥ 不用生成一个新的⑧进行merge了。

例二

如今 f 提交是咱们正在合并的提交

若是如今找 e 和 d 的共同祖先,你会发现并不惟一,b 和 c 都是。那么此时怎么合并呢?

git 会首先将 b 和 c 合并成一个虚拟的提交 x,这个 x 看成 e 和 d 的共同祖先。

而要合并 b 和 c,也须要进行一样的操做,即找到一个共同的祖先 a。

咱们这里的 a、b、c 只是个比较简单的例子,实际上提交树每每更加复杂,这就须要不断重复以上操做以便找到一个真实存在的共同祖先,而这个操做是递归的。这即是“递归三路合并”的含义。

这是 git 合并时默认采用的策略。

快进式合并

git 还有很是简单的快进式(Fast-Forward)合并。快进式合并要求合并的两个分支(或提交)必须是祖孙/父子关系。例如上面的 e 和 d 并不知足此关系,因此没法进行快进式合并。

在上面的例子合并出了 f 以后,若是将 t/walterlv 合并到 master,那么就可使用快进式合并。这时,直接将 master 分支的 HEAD 指向 f 提交即完成了合并。固然,能够生成也能够不生成新的 g 提交,但内容与 f 的内容彻底同样。

参考文章

https://blog.csdn.net/u012937...
https://blog.csdn.net/WPwalte...