merge基本原理
我們知道git 合并文件是以行為單位進(jìn)行一行一行進(jìn)行合并的,但是有些時(shí)候并不是兩行內(nèi)容不一樣git就會(huì)報(bào)沖突,因?yàn)閟mart git 會(huì)幫我們自動(dòng)幫我們進(jìn)行取舍,分析出那個(gè)結(jié)果才是我們所期望的,如果smart git 都無(wú)法進(jìn)行取舍時(shí)候才會(huì)報(bào)沖突,這個(gè)時(shí)候才需要我們進(jìn)行人工干預(yù)。那git 是如何幫我們進(jìn)行Smart 操作的呢?
二路合并
二路合并算法就是講兩個(gè)文件進(jìn)行逐行對(duì)別,如果行內(nèi)容不同就報(bào)沖突。

Mine 代表你本地修改...
Theirs 代表其他人修改...
假設(shè)對(duì)于同一個(gè)文件,出現(xiàn)你和其他人一起修改,此時(shí)如果git來(lái)進(jìn)行合并,git就懵逼了,因?yàn)镚it既不敢得罪你(Mine),也不能得罪他們(Theirs) 無(wú)理無(wú)據(jù),git只能讓你自己搞了,但是這種情況太多了而且也沒(méi)有必要…
三路合并
三路合并就是先找出一個(gè)基準(zhǔn),然后以基準(zhǔn)為Base 進(jìn)行合并,如果2個(gè)文件相對(duì)基準(zhǔn)(base)都發(fā)生了改變 那git 就報(bào)沖突,然后讓你人工決斷。否則,git將取相對(duì)于基準(zhǔn)(base)變化的那個(gè)為最終結(jié)果。

Base 代表上一個(gè)版本,即公共祖先...
Mine 代表你本地修改...
Theirs 代表其他人修改...
這樣當(dāng)git進(jìn)行合并的時(shí)候,git就知道是其他人修改了,本地沒(méi)有更改,git就會(huì)自動(dòng)把最終結(jié)果變成如下:

如果換成下面的這樣,就需要人工解決了:

上面就是git merge 最基本的原理 “三路合并”。
深入原理分析
下面面的合并就是我們常見(jiàn)的分支graph,結(jié)合具體分析:

上面①~⑨代表一個(gè)個(gè)修改集合(commit)每個(gè)commit都有一個(gè)唯一7位SHA-1唯一表示。
①,②,④,⑦修改集串聯(lián)起來(lái)就是一個(gè)鏈,此時(shí)用master指向這個(gè)集合就代表master分支,分支本質(zhì)是一個(gè)快照,其實(shí)類比C中指針
同樣dev分支也是由一個(gè)個(gè)commit組成
現(xiàn)在在dev分支上由于各種原因要運(yùn)行g(shù)it merge master需要把master分支的更新合并到dev分支上,本質(zhì)上就是合并修改集 ⑦(Mine) 和 ⑧(Theirs) ,此時(shí)我們要 利用DAG(有向無(wú)環(huán)圖)相關(guān)算法找到我們公共的祖先 ②(Base)然后進(jìn)行三方合并,最后合并生成 ⑨
git merge-base –all commit_id1(Yours/Theirs) commit_id2(Yours/Theirs) 就能找出公共祖先的commitId(Base)
圖雖然復(fù)雜 但是核心原理是不變的,下面我們看 另外一個(gè)稍微高級(jí)一點(diǎn)的核心原理”遞歸三路合并” 也是我們很常見(jiàn)看到 git merge 輸出的 recursive strategy
遞歸三路合并原理
下圖中我們?nèi)绻喜?⑦(source) -> ⑥(destination):
簡(jiǎn)短描述下 如何會(huì)出現(xiàn)上面的圖:
- 在master分支上新建文件foo.c ,寫(xiě)入數(shù)據(jù)”A”到文件里面
- 新建分支task2 git checkout -b task2 0,0 代表commit Id
- 新建并提交commit ① 和 ③
- 切換分支到master,新建并提交commit ②
- 新建并修改foo.c文件中數(shù)據(jù)為”B”,并提交commit ④
- merge commit ③ git merge task2,生成commit ⑥
- 新建分支task1 git chekcout -b ④
- 在task1 merge ③ git merge task2 生成commit ⑤
- 新建commit ⑦,并修改foo.c文件內(nèi)容為”C”
- 切換分支到master上,并準(zhǔn)備merge task1 分支(merge ⑦-> ⑥)
從上面我們DAG圖可以知道公共祖先有③和④,那到底選擇哪個(gè)呢,我們分別來(lái)看:

如果選擇④作為公共祖先 根據(jù)最基本的三路合并,可以看到最終結(jié)果⑧ 將得到 /foo.c=C

最終期待的結(jié)果是什么?
- 我們?cè)贛aster上也是所有分支的起點(diǎn)定義了 /foo.c = A,在task2 分支上并沒(méi)有進(jìn)行任何修改。
- 最初修改 /foo.c = B 是在master 分支上,修改集④ 上修改為 /foo.c = B
- 第一次通過(guò) ③,④ 合并生成 ⑥, 最終使得Master分支上 ⑥ /foo.c = B
- 第二次通過(guò) ③,④ 又合并生成 ⑤, 最終使得task1分支上 ⑤ /foo.c = B
- 在task1分支上不希望 /foo.c = B ,所以在task1上新建一個(gè)⑦ /foo.c = C
- 我們知道 foo.c = B 是在 master分支上 ④ 進(jìn)行修改的,其他的/foo.c = B 都是來(lái)自④這次修改。
- 我們能從圖上可以知道 ⑦ 的修改一定是在 ④ 之后的,并不是因?yàn)棰?> ④ 而是 ④ 是 ⑦ 的祖先節(jié)點(diǎn),所以我們知道最終的修改合并之后就應(yīng)該保留 /foo.c = C
所以 我們的最佳公共祖先應(yīng)該是4,最終結(jié)果應(yīng)該是 /foo.c = C
git 如何選擇公共祖先呢?
你可能會(huì)說(shuō)用 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個(gè)都是最佳公共祖先時(shí)候,這個(gè)時(shí)候git 會(huì)隨便輸出一個(gè)不確定公共祖先。
git 是這樣進(jìn)行合并的:
- git 既不是直接用③,也不是用④,而是將2個(gè)祖先進(jìn)行合并成一個(gè)虛擬的 X /foo.c = B, 因?yàn)棰?和 ④ 公共祖先是 〇/foo.c = A
- git 用 X 做為 base 合并 ⑥ 和 ⑦ 結(jié)果就是 /foo.c = C

合并策略(git merge)
當(dāng)項(xiàng)目中包含多條功能分支時(shí),有時(shí)就需要使用 git merge 命令,指定將某個(gè)分支的提交合并到當(dāng)前分支。Git 中有兩個(gè)合并策略:fast-forward 和 no-fast-forward。
fast-forward
fast-forward(--ff) 意為快進(jìn)式合并,如果當(dāng)前分支在合并分支前,沒(méi)有做過(guò)額外提交。那么合并分支的過(guò)程不會(huì)產(chǎn)生的新的提交記錄,而是直接將分支上的提交添加進(jìn)來(lái),這稱為 fast-forward 合并。

當(dāng)merge ② 和 ⑥時(shí)候 由于②是公共祖先,所以進(jìn)行Fast-Forward 合并,直接指向⑥ 不用生成一個(gè)新的⑧進(jìn)行merge了。
no-fast-forward
no-fast-forward(--no-ff)意為非快進(jìn)式合并,fast-forward的場(chǎng)景很少遇到,基本是:在當(dāng)前分支分離出子分支后(比如分支dev),后續(xù)會(huì)有其他分支合并進(jìn)來(lái)的修改,而分離出的dev分支也做了修改。這個(gè)時(shí)候再使用git merge,就會(huì)觸發(fā) no-fast-forward 策略了。
在 no-fast-forward 策略下,Git 會(huì)在當(dāng)前分支(active branch)額外創(chuàng)建一個(gè)新的 合并提交(merging commit)。這條提交記錄既指向當(dāng)前分支,又指向合并分支。
看懂下面這個(gè)例子你就明白了:

現(xiàn)在 f 提交是我們正在合并的提交
如果現(xiàn)在找 e 和 d 的共同祖先,你會(huì)發(fā)現(xiàn)并不唯一,b 和 c 都是。那么此時(shí)怎么合并呢?
git 會(huì)首先將 b 和 c 合并成一個(gè)虛擬的提交 x,這個(gè) x 當(dāng)作 e 和 d 的共同祖先。
而要合并 b 和 c,也需要進(jìn)行同樣的操作,即找到一個(gè)共同的祖先 a。
我們這里的 a、b、c 只是個(gè)比較簡(jiǎn)單的例子,實(shí)際上提交樹(shù)往往更加復(fù)雜,這就需要不斷重復(fù)以上操作以便找到一個(gè)真實(shí)存在的共同祖先,而這個(gè)操作是遞歸的。這便是“遞歸三路合并”的含義。
這是 git 合并時(shí)默認(rèn)采用的策略。