git 分支運作模式之我見

tag: merge / patch / 上游 / 下游 / 順流 / 逆流

特性: merge strategy

git 的 merge 有多種不同的 strategy 模式,可透過


git merge [-s ] [-X ]

比較常用的有


git merge -s ours # 以我方 branch 為主


git merge -s recursive -X ours # 如遇衝突以我方 branch 為主
git merge -s recursive -X theirs # 如遇衝突以對方 branch 為主

其中, 最重要的是預設模式為


git merge -s recursive

特性: merge base

git 在作 merge 時,除了參考兩方 branch 的 HEAD ,還會參考兩個 branch 的共同 ancestor 。

View post on imgur.com

以上圖為例,R 是 A,B 分支共同的 ancestor ,也是 A3, B2 的 merge base。

要如何找出 merge base 呢? 除了手算外,也可以下指令找出 merge base:


git merge-base [branch A] [branch B]

特性: git 下不會影響 merge base 的 merge 方式
git 還有其他不會影響 merge base 的 merge 方式,如下:

* git cherry-pick
* git merge –squash
* 以 patches 形式匯入

特性: merge 的累積性

當我們持續跟隨 merge 某個分支時,會發現新一次的 merge 會參考前一次 merge 的資訊,而只處理前一次 merge 完之後的新變更。也就是說,之前已經處理的 merge conflict ,不會再重複出現,也不需要重複處理。這個聰明特性讓 git merge 變得很好用。其示意圖大致如下:

View post on imgur.com

上圖中,黃色記號的點,是兩個 branch 在 merge 時,所納入計算的 merge base。我們可以注意到,merge base 會依新的 merge 成果,往前累積紀錄點。

另一個值得注意的地方是,雖然是 B 分支 merge A 分支,但 merge base 是在 A 分支上,而上圖的 merge 過程,merge base 都是保持在 A 分支上。

特性: 交叉 merge 的變化

當 git merge 的方向改變時, merge base 也會隨之變化。示例圖如下:

View post on imgur.com

上圖中,黃色記號所註記的 merge base 點,隨著 B merge A 切換成 A merge B 時,merge base 也跟著從 A 分支切換到 B 分支上。而這個變化,正是藏在細節裡的魔鬼,因為這個切換,使先前累積一致的紀錄點中斷了。在中斷之後,再一次回到 B merge A 時,會發現先前已經處理過的舊 conflict 又再一次重複出現。

也就是說,交叉 merge 會破壞掉原先 merge 的累積紀錄,不管是 A 分支或是 B 分支,都會失去 merge 的累積性。

在交叉 merge 的特性下,只會有兩個結果,一個是兩分支漸趨一致,最後合而為一,或是保持獨立分支,但一直不斷的處理先前舊的 merge conflict。

特性: merge base 俱有傳遞性
舉例
當 B merge A ,而 C 又 merge B 時,C 的 merge base 會分別紀錄在 A, B 分支上。接著,當 A merge C 時,merge base 切換紀錄在 C 分支上。則當下一次 C 再次 merge B 之時,會發現之前累積在 A, B 分支上的 merge base 紀錄中斷了,而切換到 C 分支上。

觀察: 順流發佈與逆流整合
在分支的使用過程中,我觀察到兩種類型的使用情境。

其一,順流發佈。
以 Linux Kernel 為例,主要的版本會在最上游的 kernel mainline 發佈,然後各個 Linux distribution 主分支再 merge mainline 新發佈的程式碼。而再更下游的 distribution 分支,會再 merge 上述的主分支的程式。

這個過程主要是從功能開發的上游往下游遞延,跟產品製造從生產、經銷、商店的發佈流程類似。

這個模式在 git branch 模式中,很容易實現,只要以 fork 的方式從上游衍生下游分支,並持續單向 merge 即可。

其二,逆流整合
商品出現問題時,回饋的方向則跟上述的流程是相反的。舉例來說,消費者可能先反應給商店,商店反應給經銷,經銷再回饋給製造的上游廠商,或是消費者也有管道可直接反應給經銷商、製造商。不論經過幾層,這個方向跟順流發佈是相反的。

程式也是一樣,可能有 bug 或是新功能,是散佈在下游的分支們之間。為了讓這些成果能夠發佈到更多地方使用,我們會嘗試將這些成果整合到上游的分支,以利下一波發佈時能一併分佈到其他更多下游分支。

一般來說,我們在 git 下實行的方式,大概是從主要版本中成立一個獨立開發分支,然後用這個分支去一一 merge 那些想要整合的下游分支。

順流發佈與逆流整合的衝突
我在嘗試建立分支之間的發佈與整合流程時,遭遇了大挫敗。情況大概是,我在處理完近幾個 merge conflict ,建立順流發佈的分支架構後,卻在一次逆流整合的小小 merge 之後,先前已經處理過的舊 conflict 又重新冒了出來。

為什麼?

這問題我卡了很久,後來研究發現,其實原因就是上述提及的兩個:
* 交叉 merge 的變化
* merge base 俱有傳遞性

也就是說,我在順流發佈與逆流整合的分支流程中,構成了迴路,使得整個 merge base 的累積性紀錄被破壞了。

重構 git 分支結構的兩個原則
要解決上述的問題,唯一的方法就是避免產生 merge base 的迴路。根據上述的種種特性與觀察,我想出了兩個原則來克服這個問題:

1. 在 git 分支結構中,區別出上游與下游的順序關係
2. 逆流方向必須使用不影響 merge base 的 merge 方式(ex: patches/cherry-pick/merge –squash/…)

重新審視 git workflow

以最常被引用的 nvie 的經典文章 A successful Git branching model 為例:

View post on imgur.com

從上圖的開發流程中,大略可分類為 develop + feature branches => release branches => master + hotfixes,可劃分為 upstream , midstream, downstream。其中,標上星號的是我認為是逆流方向的 merge的部份。

以 tag=1.0 這個節點為例,在 merge 產生 tag=1.0 時,參與 merge 計算的有,tag=0.2 節點跟綠色的 release branch 節點,還有作為 merge base 參考點的 hotfixed 紅色節點。但如果 hotfixes 往 develop 分支的 merge 是斷開的話,那麼 merge base 參考點,就會是 tag=0.1 節點了。也就是說圖中的 hotfixes 分支往 develop 分支的 merge ,破壞了原先累積性的 workflow 設計。雖然此示例圖比較簡單,看起來影響不是很明顯,但在更複雜的案例中,很可能就因某次的逆流 merge 而導致整體 workflow 衝突中斷。

這也是為什麼,這麼多人依這一篇文章建立 git workflow 運作一段時間之後,卻遇到一些難纏的 merge conflict 的問題的原因了。會出現奇怪的 conflict 其實不是你的錯,而這個 git workflow 本身就漏了這個細節中的魔鬼。

延伸思考

仔細觀察 Linux Kernel 的 workflow,你會發現 upstream 沒有在收 Pull Request 的,而是以 patch / squash merge 的方式在整合的。

為什麼 upstream 不收 Pull Request 呢? 又 Pull Request 適用在那些 workflow ,又不適合那些 workflow呢? 這些問題,從上述提出的觀點下,都有了解釋與答案了。

Leave a Reply

Your email address will not be published. Required fields are marked *