git merge是怎樣斷定衝突的?

在解決git merge的衝突時,有時我總忍不住吐槽git實在太不智能了,明明僅僅是往代碼裏面插入幾行,沒想到合併就失敗了,只能手工去一個個確認。真不知道git的合併衝突是怎麼斷定的。git

在一次解決了涉及幾十個文件的合併衝突後(整整花了我一個晚上和一個早上的時間!),我終於下定決心,去看一下git merge代碼裏面衝突斷定的具體實現。正所謂冤有頭債有主,至少下次遇到一樣的問題時就能夠知道本身栽在誰的手裏了。因而就有了這樣一篇文章,講講git merge內部的衝突斷定機制。github

recursive three-way merge和ancestor

git的源碼
先用merge做關鍵字搜索,看看涉及的相關代碼。
找了一段時間,找到了git merge的時候,比較待合併文件的函數入口:ll_merge。另外還有一份文檔,它也指出ll_merge正是合併實現的入口。api

從函數簽名能夠看到,mmfile_t應該就表明了待合併的文件。有趣的是,這裏待合併的文件並非兩份,而是三份。svg

int ll_merge(mmbuffer_t *result_buf,
         const char *path,
         mmfile_t *ancestor, const char *ancestor_label,
         mmfile_t *ours, const char *our_label,
         mmfile_t *theirs, const char *their_label,
         const struct ll_merge_options *opts)

看過git help merge的讀者應該知道,ours表示當前分支,theirs表示待合併分支。看得出來,這個函數就是把某個文件在不一樣分支上的版本合併在一塊兒。那麼ancestor又是位於哪一個分支呢?倒過來從調用方開始閱讀代碼,能夠看出大致的流程是這樣的,git merge會找出三個commit,而後對每一個待合併的文件調用ll_merge,生成最終的合併結果。按註釋的說法,ancestor是後面兩個commit(ourstheirs)的公共祖先(ancestor)。另外前面提到的文檔也說明,git合併的時候使用的是recursive three-way merge函數

three-way merge

關於recursive three-way merge, wikipedia上有個相關的介紹#Recursive_three-way_merge)。就是在合併的時候,將ours,theirs和ancestor三個版本的文件進行比較,獲取ours和ancestor的diff,以及theirs和ancestor的diff,這樣作可以發現兩個不一樣的分支到底作了哪些改動。畢竟後面git須要斷定衝突的內容,若是沒有原第一版本的信息,只是簡單地比較兩個文件,是作不到的。gitlab

鑑於個人目標是發掘git斷定衝突的機制,因此沒有去看git裏面查找ancestor的實現。不過只需肉眼在圖形化界面裏瞅上一眼,就能夠找到ancestor commit。(好比在gitlab的network界面中,回溯兩個分支的commit線,一直到岔路口)ui

有一點須要注意的是,revert一個commit不會改變它的ancestor。所謂的revert,只是在當前commit的上面添加了新的undo commit,並無改變「岔路口」的位置。不要想固然地認爲,revert以後ancestor就變成上一個commit的ancestor了。尤爲是在revert merge commit的時候,老是容易忘掉這個事實。假如你revert了一個merge commit,在從新merge的時候,git所參照的ancestor將不是merge以前的ancestor,而是revert以後的ancestor。因而就掉到坑裏去了。建議全部讀者都看一下git官方對於revert merge commit潛在後果的說法:https://github.com/git/git/blob/master/Documentation/howto/revert-a-faulty-merge.txt
結論是,若是一個merge commit引入的bug容易修復,請不要輕易revert一個merge commit。spa

剖析xdiff

ll_merge往下追,能夠看到後面出了一條旁路:ll_binary_merge。這個函數專門處理bin類型文件的合併。它的實現簡單粗暴,若是你沒有指定合併策略(theris或ours),直接報Cannot merge binary files錯誤。看來在git看來,二進制文件並無diff的價值。code

主路徑從ll_xdl_mergexdl_merge,進到一個叫xdiff的庫中。終於找到git merge的具體實現了。three

平心而論,xdiff的代碼風格十分糟糕,不只註釋太少,並且結構體成員變量竟然使用相似i一、i2這樣的命名,看得我頭昏腦脹、心煩意燥。

吐槽結束,先講下xdl_merge的流程。xdl_merge作了下面四件事:

  1. xdl_do_diff完成two-way diff(ours和ancestor,theirs和ancestor),生成修改記錄,存儲到xdfenv_t中。

  2. xdl_change_compact壓縮相鄰的修改記錄,再用xdl_build_script創建xdchange_t鏈表,記錄雙方修改。xdchange_t主要包括了修改的起始行號和修改範圍。

  3. 這時候分三種狀況,其中兩種是隻有一方有修改(只有ours或theirs一條鏈表),直接退出。最後一種是雙方都有修改,須要合併修改記錄。因爲修改記錄是按行號有序排列的,因此直接合並兩個鏈表。修改記錄若是沒有重疊部分,按前後順序標記爲我方修改/他方修改。若是發生了重疊,就表示發生了衝突。以後會從新過一遍兩個待合併鏈表,對於那些標記爲衝突的部分,比較它們是否相等的,若是是,標記爲雙方修改。

  4. xdl_fill_merge_buffer輸出合併結果。若是有衝突,調用fill_conflict_hunk輸出衝突狀況。若是沒有衝突(標記爲我方修改/他方修改/雙方修改),則合併ancestor的原內容和修改記錄,按標記的類型取修改後的內容,並輸出。

輸出衝突狀況的代碼位於fill_conflict_hunk中。它的實現很簡單,畢竟此時咱們已經有了雙方修改的內容,如今只須要同時輸出衝突內容,供用戶取捨。(這即是那次花了一個晚上和一個早上改掉的衝突的源頭,兇手就是你,哼)。

輸出格式恐怕你們都很熟悉。該函數會先打印若干個<,個數由DEFAULT_CONFLICT_MARKER_SIZE決定,也便是7個。而後是ours分支名。接着輸出我方的修改,而後輸出若干個=。最後是他方的修改,以及若干個>。這個就是折磨人的合併衝突了:

<<<<<<< HEAD
3
=======
2
>>>>>>> branch1

總結

git merge的衝突斷定機制以下:先尋找兩個commit的公共祖先,比較同一個文件分別在ours和theirs下對於公共祖先的差別,而後合併這兩組差別。若是雙方同時修改了一處地方且修改內容不一樣,就斷定爲合併衝突,依次輸出雙方修改的內容。

相關文章
相關標籤/搜索