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

merge基本原理


我们知道git 合并文件是以行为单位进行一行一行进行合并的,但是有些时候并不是两行内容不一样git就会报冲突,因为smart git 会帮我们自动帮我们进行取舍,分析出那个结果才是我们所期望的,如果smart git 都无法进行取舍时候才会报冲突,这个时候才需要我们进行人工干预。那git 是如何帮我们进行Smart 操作的呢?

二路合并


二路合并算法就是讲两个文件进行逐行对别,如果行内容不同就报冲突。
git1.png

Mine 代表你本地修改...
Theirs 代表其他人修改...
假设对于同一个文件,出现你和其他人一起修改,此时如果git来进行合并,git就懵逼了,因为Git既不敢得罪你(Mine),也不能得罪他们(Theirs) 无理无据,git只能让你自己搞了,但是这种情况太多了而且也没有必要…

三路合并


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

git2.png

Base 代表上一个版本,即公共祖先...
Mine 代表你本地修改...
Theirs 代表其他人修改...
这样当git进行合并的时候,git就知道是其他人修改了,本地没有更改,git就会自动把最终结果变成如下:
git3.png

如果换成下面的这样,就需要人工解决了:
git4.png

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

深入原理分析


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

git5.png

上面①~⑨代表一个个修改集合(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):
git6.png

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

  • 在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???
git7.png

如果选择④作为公共祖先 根据最基本的三路合并,可以看到最终结果⑧ 将得到 /foo.c=C
git8.png

最终期待的结果是什么?

  • 我们在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,如下:
git9.png

合并策略(git merge)


当项目中包含多条功能分支时,有时就需要使用 git merge 命令,指定将某个分支的提交合并到当前分支。Git 中有两个合并策略:fast-forward 和 no-fast-forward。

fast-forward

fast-forward(--ff) 意为快进式合并,如果当前分支在合并分支前,没有做过额外提交。那么合并分支的过程不会产生的新的提交记录,而是直接将分支上的提交添加进来,这称为 fast-forward 合并。
git13.gif

很多时候我们在找2个修改集合X,Y 公共祖先的时候,会发现公共祖先就是他们中的一个,此时我们进行merge 的时候,就是Fast-Forward即可,不会产生一个新的Commit 用于merge X和Y 。看懂下面这个例子你就明白了:
git10.png

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

no-fast-forward

no-fast-forward(--no-ff)意为非快进式合并,fast-forward的场景很少遇到,基本是:在当前分支分离出子分支后(比如分支dev),后续会有其他分支合并进来的修改,而分离出的dev分支也做了修改。这个时候再使用git merge,就会触发 no-fast-forward 策略了。

在 no-fast-forward 策略下,Git 会在当前分支(active branch)额外创建一个新的 合并提交(merging commit)。这条提交记录既指向当前分支,又指向合并分支。
git12.gif

看懂下面这个例子你就明白了:


git11.png

现在 f 提交是我们正在合并的提交

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

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

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

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

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

参考资料

git官方文档
git merge原理
git merge的合并原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容