1. 程式人生 > >git Merge 原理演算法文章標題

git Merge 原理演算法文章標題

最近碰到一系列問題,正好求知所問深入學習了下git 內部原理,東西比較多,先從git merge 說起,因為merge是所有版本控制系統中最最核心之一,本文通過討論是2個commit 之間的合併 類似git merge C1 C2 ,更多的 git merge C1 C2 C3 ..Cn-1,Cn 合併也是一樣的,他們主要是先將Cn 和 Cn-1先合併然後從後往前在遞迴合併所有。

merge 常見誤區

git merge 是用時間先後決定merge結果的,後面會覆蓋前面的?

答 :git 是分散式的檔案版本控制系統,在分散式環境中時間是不可靠的,git是靠三路合併演算法進行合併的。

git merge 只要兩行不相同就一定會報衝突,叫人工解決?

答:git 儘管兩行內容不一樣,git 會進行取捨,當git無法進行取捨的時候才會進行人工解決衝突

merge 基礎

我們知道git 合併檔案是以行為單位進行一行一行進行合併的,但是有些時候並不是兩行內容不一樣git就會報衝突,因為smart git 會幫我們自動幫我們進行取捨,分析出那個結果才是我們所期望的,如果smart git 都無法進行取捨時候才會報衝突,這個時候才需要我們進行人工干預。那git 是如何幫我們進行Smart 操作的呢?

二路合併

二路合併演算法就是講兩個檔案進行逐行對別,如果行內容不同就報衝突。

Mine 代表你本地修改
Theirs 代表其他人修改
假設對於同一個檔案,出現你和其他人一起修改,此時如果git來進行合併,git就懵逼了,因為Git既不敢得罪你(Mine),也不能得罪他們(Theirs) 無理無據,git只能讓你自己搞了,但是這種情況太多了而且也沒有必要…

三路合併

三路合併就是先找出一個基準,然後以基準為Base 進行合併,如果2個檔案相對基準(base)都發生了改變 那git 就報衝突,然後讓你人工決斷。否則,git將取相對於基準(base)變化的那個為最終結果。

Base 代表上一個版本,即公共祖先
Mine 代表你本地修改
Theirs 代表其他人修改
這樣當git進行合併的時候,git就知道是其他人修改了,本地沒有更改,git就會自動把最終結果變成如下,這個結構也是大多merge 工具的常見佈局,比如IDEA

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

上面就是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)

現實總是殘酷的,其中中分支的Graph並不像這樣簡單有序,看下面。

image.png

image.png

圖雖然複雜 但是核心原理是不變的,下面我們看 另外一個稍微高階一點的核心原理”遞迴三路合併” 也是我們很常見看到 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 是這樣進行合併的:

image.png

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了。