這纔是真正的Git——分支合併

「合併前文件還在的,合併後就不見了」、「我遇到Git合併的bug了」 是兩句常常聽到的話,但真的是Git的bug麼?或許只是你的預期不對。本文經過講解三向合併和Git的合併策略,step by step介紹Git是怎麼作一個合併的,讓你們對Git的合併結果有一個準確的預期,而且避免發生合併事故。html

這是一個系列的文章,計劃包括三篇:git

故事時間

在開始正文以前,先來聽一下這個故事。算法

以下圖,小明從節點A拉了一條dev分支出來,在節點B中新增了一個文件http.js,而且合併到master分支,合併節點爲E。這個時候發現會引發線上bug,趕忙撤回這個合併,新增一個revert節點E'。過了幾天小明繼續在dev分支上面開發新增了一個文件main.js,並在這個文件中import了http.js裏面的邏輯,在dev分支上面一切運行正常。可當他將此時的dev分支合併到master時候卻發現,http.js文件不見了,致使main.js裏面的邏輯運行報錯了。但此次合併並無任何衝突。他又得從新作了一下revert,而且迷茫的懷疑是Git的bug。測試

兩句常常聽到的話:3d

—— 」合併前文件還在的,合併後就不見了「code

—— 」我遇到Git的bug了「cdn

相信不少同窗或多或少在不熟悉Git合併策略的時候都會發生過相似上面的事情,明明在合併前文件還在的,爲何合併後文件就不在了麼?一度還懷疑是Git的bug。這篇文章的目的就是想跟你們講清楚Git是怎麼去合併分支的,以及一些底層的基礎概念,從而避免發生如故事中的問題,並對Git的合併結果有一個準確的預期。視頻

如何合併兩個文件

在看怎麼合併兩個分支以前,咱們先來看一下怎麼合併兩個文件,由於兩個文件的合併是兩個分支合併的基礎。htm

你們應該都據說過「三向合併」這個詞,不知道你們有沒有思考過爲何兩個文件的合併須要三向合併,只有二向是否能夠自動完成合並。以下圖blog

很明顯答案是不能,如上圖的例子,Git無法肯定這一行代碼是我修改的,仍是對方修改的,或者以前就沒有這行代碼,是咱們倆同時新增的。此時Git沒辦法幫咱們作自動合併。

因此咱們須要三向合併,所謂三向合併,就是找到兩個文件的一個合併base,以下圖,這樣子Git就能夠很清楚的知道說,對方修改了這一行代碼,而咱們沒有修改,自動幫咱們合併這兩個文件爲 Print("hello")。

接下來咱們瞭解一下什麼是衝突?衝突簡單的來講就是三向合併中的三方都互不相同,即參考合併base,咱們的分支和別人的分支都對同個地方作了修改。

Git 的合併策略

瞭解完怎麼合併兩個文件以後,咱們來看一個使用 git merge 來作分支合併。如上圖,將master分支合併到feature分支上,會新增一個commit節點來記錄此次合併。

Git會有不少合併策略,其中常見的是Fast-forward、Recursive 、Ours、Theirs、Octopus。下面分別介紹不一樣合併策略的原理以及應用場景。默認Git會幫你自動挑選合適的合併策略,若是你須要強制指定,使用git merge -s <策略名字>

瞭解Git合併策略的原理可讓你對Git的合併結果有一個準確的預期。

Fast-forward

Fast-forward是最簡單的一種合併策略,如上圖中將some feature分支合併進master分支,Git只須要將master分支的指向移動到最後一個commit節點上。

Fast-forward是Git在合併兩個沒有分叉的分支時的默認行爲,若是不想要這種表現,想明確記錄下每次的合併,可使用git merge --no-ff

Recursive

Recursive是Git分支合併策略中最重要也是最經常使用的策略,是Git在合併兩個有分叉的分支時的默認行爲。其算法能夠簡單描述爲:遞歸尋找路徑最短的惟一共同祖先節點,而後以其爲base節點進行遞歸三向合併。提及來有點繞,下面經過例子來解釋。

以下圖這種簡單的狀況,圓圈裏面的英文字母爲當前commit的文件內容,當咱們要合併中間兩個節點的時候,找到他們的共同祖先節點(左邊第一個),接着進行三向合併獲得結果爲B。(由於合併的base是「A」,下圖靠下的分支沒有修改內容仍爲「A」,下圖靠上的分支修改爲了「B」,因此合併結果爲「B」)。

但現實狀況老是複雜得多,會出現歷史記錄鏈互相交叉等狀況,以下圖

當Git在尋找路徑最短的共同祖先節點的時候,能夠找到兩個節點的,若是Git選用下圖這一個節點,那麼Git將沒法自動的合併。由於根據三向合併,這裏是是有衝突的,須要手動解決。(base爲「A「,合併的兩個分支內容爲」C「和」B「)

而若是Git選用的是下圖這個節點做爲合併的base時,根據三向合併,Git就能夠直接自動合併得出結果「C」。(base爲「B「,合併的兩個分支內容爲」C「和」B「)

做爲人類,在這個例子裏面咱們很天然的就能夠看出來合併的結果應該是「C」(以下圖,節點四、5都已是「B」了,節點6修改爲「C」,因此合併的預期爲「C」)

那怎麼保證Git可以找到正確的合併base節點,儘量的減小衝突呢?答案就是,Git在尋找路徑最短的共同祖先節點時,若是知足條件的祖先節點不惟一,那麼Git會繼續遞歸往下尋找直至惟一。仍是以剛剛這個例子圖解。

以下圖所示,咱們想要合併節點5和節點6,Git找到路徑最短的祖先節點2和3。

由於共同祖先節點不惟一,因此Git遞歸以節點2和節點3爲咱們要合併的節點,尋找他們的路徑最短的共同祖先,找到惟一的節點1。

接着Git以節點1爲base,對節點2和節點3作三向合併,獲得一個臨時節點,根據三向合併的結果,這個節點的內容爲「B」。

再以這個臨時節點爲base,對節點5和節點6作三向合併,獲得合併節點7,根據三向合併的結果,節點7的內容爲「C」

至此Git完成遞歸合併,自動合併節點5和節點6,結果爲「C」,沒有衝突。

Recursive策略已經被大量的場景證實它是一個儘可能減小衝突的合併策略,咱們能夠看到有趣的一點是,對於兩個合併分支的中間節點(如上圖節點4,5),只參與了base的計算,而最終真正被三向合併拿來作合併的節點,只包括末端以及base節點。

須要注意Git只是使用這些策略儘可能的去幫你減小衝突,若是衝突不可避免,那Git就會提示衝突,須要手工解決。(也就是真正意義上的衝突)。

Ours & Theirs

Ours和Theirs這兩種合併策略也是比較簡單的,簡單來講就是保留雙方的歷史記錄,但徹底忽略掉這一方的文件變動。以下圖在master分支裏面執行git merge -s ours dev,會產生藍色的這一個合併節點,其內容跟其上一個節點(master分支方向上的)徹底同樣,即master分支合併先後項目文件沒有任何變更。

而若是使用theirs則徹底相反,徹底拋棄掉當前分支的文件內容,直接採用對方分支的文件內容。

這兩種策略的一個使用場景是好比如今要實現同一功能,你同時嘗試了兩個方案,分別在分支是dev1和dev2上,最後通過測試你選用了dev2這個方案。但你不想丟棄dev1的這樣一個嘗試,但願把它合入主幹方便後期查看,這個時候你就能夠在dev2分支中執行git merge -s ours dev1

Octopus

這種合併策略比較神奇,通常來講咱們的合併節點都只有兩個parent(即合併兩條分支),而這種合併策略能夠作兩個以上分支的合併,這也是git merge兩個以上分支時的默認行爲。好比在dev1分支上執行git merge dev2 dev3

他的一個使用場景是在測試環境或預發佈環境,你須要將多個開發分支修改的內容合併在一塊兒,若是不用這個策略,你每次只能合併一個分支,這樣就會致使大量的合併節點產生。而使用Octopus這種合併策略就能夠用一個合併節點將他們所有合併進來。

Git rebase

git rebase 也是一種常常被用來作合併的方法,其與git merge的最大區別是,他會更改變動歷史對應的commit節點。

以下圖,當在feature分支中執行rebase master時,Git會以master分支對應的commit節點爲起點,新增兩個全新的commit代替feature分支中的commit節點。其緣由是新的commit指向的parent變了,因此對應的SHA1值也會改變,因此沒辦法複用原feature分支中的commit。(這句話的理解須要這篇文章的基礎知識)

對於合併時候要使用git merge仍是git rebase的爭論,我我的的見解是沒有銀彈,根據團隊和項目習慣選擇就能夠。git rebase能夠給咱們帶來清晰的歷史記錄,git merge能夠保留真實的提交時間等信息,而且不容易出問題,處理衝突也比較方便。惟一有一點須要注意的是,不要對已經處於遠端的多人共用分支作rebase操做。

我我的的一個習慣是:對於本地的分支或者肯定只有一我的使用的遠端分支用rebase,其他狀況用merge。

rebase還有一個很是好用的東西叫interactive模式,使用方法是git rebase -i。能夠實現壓縮幾個commit,修改commit信息,拋棄某個commit等功能。好比說我要壓縮下圖260a12a五、956e1d18,將他們與9dae0027合併爲一個commit,我只需將260a12a五、956e1d18前面的pick改爲「s」,而後保存就能夠了。

限於篇幅,git rebase -i 還有不少實用的功能暫不展開,感興趣的同窗能夠本身研究一下。

總結

如今咱們再來看一下文章開頭的例子,咱們就能夠理解爲何最後一次merge會致使http.js文件不見了。根據Git的合併策略,在合併兩個有分叉的分支(上圖中的D、E‘)時,Git 默認會選擇Recursive策略。找到D和E’的最短路徑共同祖先節點B,以B爲base,對D,E‘作三向合併。B中有http.js,D中有http.js和main.js,E’中什麼都沒有。根據三向合併,B、D中都有http.js且沒有變動,E‘刪除了http.js,因此合併結果就是沒有http.js,沒有衝突,因此http.js文件不見了。

這個例子理解原理以後解決方法有不少,這裏簡單帶過兩個方法:1. revert節點E'以後,此時的dev分支要拋棄刪除掉,從新從E'節點拉出分支繼續工做,而不是在原dev分支上繼續開發節點D;2. 在節點D合併回E’節點時,先revert一下E‘節點生成E’‘(即revert的revert),再將節點D合併進來。

Git有不少種分支合併策略,本文介紹了Fast-forward、Recursive、Ours/Theirs、Octopus合併策略以及三向合併。掌握這些合併策略以及他們的使用場景可讓你避免發生一些合併問題,並對合並結果有一個準確的預期。

但願這篇文章對你們有用,感興趣的同窗能夠逛一逛個人博客 www.lzane.com 或看看個人其餘文章。

參考

相關文章
相關標籤/搜索