自從 Git 出現之後,分支管理就深入人心。但是隨著我們團隊在合併 master 分支時,開始優先採用 squash merge,事情還是有了變化。我也開始採用另一種不同於傳統開發模式的分支合併方法。在此我簡單撰文闡述一下。
這個模式還是爭議很大的,文末我也列舉了很多不適用的情況,還請讀者不吝提出質疑。
為什麼使用 squash merge?
首先,我們可能需要解釋一下,為什麼我們採用 squash merge 而不是傳統的 merge 合併程式碼。直接原因很簡單:為了保持 master 分支的純淨和簡潔。Master 分支所應該表現的,就是我們因應各種明確的需求、最佳化、bug 修復所進行的程式碼變化。隨著 master 版本的逐步前進,我們可以窺探到程式碼生長的過程。
在團隊開發中,一般來說大家遵從的 Git 使用方法是這樣子的:
- master 分支作為釋出分支,原則上發不到生產環境的程式碼都應該基於 master 分支
- 每個人管理至少一個開發分支,開發分支與具體的需求繫結;新的需求開新的分支;開發分支開發完成後,合併到 master 分支
但是,作為程式碼分支,其過程可簡可繁,針對 master 分支,我們重點關注的是為了完成一個需求,程式碼做了哪些變更,但是在開發者開發過程中,這個分支不可能只有一個提交點,特別是發現了 bug 之後,肯定也會調整邏輯再 commit 一次。對於這些很快就發現並在上線之前就修復掉的 commits,我們並不需要它出現在 master 分支中。實際上我們的解決方法是:
- 控制 master 分支許可權,所有分支均需要透過 MR / PR 才允許合併
- 透過 MR / PR 後,預設使用 Squash Merge 模式進行合併
Squash 合併模式,又稱為 “壓縮合並”,會將分支的所有提交點(commit)合併成一個,然後再合併到 master 分支上。當然,這種模式也是有前提的:
- 每一個 MR 儘可能原子,就是為了完成一件事情而提交一個 MR,不夾帶與 commit 描述無關的私貨
- Git 系統支援將 MR / PR 單進行關聯,這樣一來即便 master 分支損失了一些細節,依然可以開啟具體的 MR / PR 單來獲取資訊。
Develop / test 分支的管理
上面我只講了 master 和個人需求分支的管理方式。但是在團隊開發中,往往有很多開發者、很多需求,大家一起共用一個開發 / 測試環境,為了能夠共用,那麼各位開發者往往會再建立一個 develop
分支,大家把自己的開發進度,如果自測透過,那麼就合併到 develop
分支上,這樣以確保同一個環境都能夠包含多人都包含的 feature。
然而,這種方法,其實也遇到了我們推崇 squash merge 時所要解決的同樣的問題:我們是不是需要關注那麼多的過程資訊?顯然,即便未合併到 master 分支,那麼對於一個共用分支來說,也是不需要的。很大程度上,所謂的 develop
分支僅僅是一個用來包容所有已開發 / 測試程式碼的路徑而已,網上推程式碼的開發者們壓根就不關注其變更歷史。為此,在第五人的開發過程中,我提出了另一種分支模式,我稱為 rebase & squash
模式。
這個模式的具體操作方式,我們以下面的例子,對比傳統模式來說明一下吧:
Rebase & Squash 模式的操作模式
建立和開發分支
首先,我們手頭有一個 master 分支。因為我們採取了 squash merge 的模式,因此這個分支非常純淨,就是一條單純的曲線
假設張三和李四分別需要開發一個分支,那麼他們自然就是從最新的 master 分支中 checkout 新的 feature 分支,進行開發
合併分支
與此同時,還有其他同學也在開發分支,並且合併到了 master,因此大家的分支都在往後生長。
到了某個時間節點,張三扯著嗓子吼一聲:“開發環境我用一下噢!”這個時候李四也說:“我也要用,幫我發一下”。張三看到 master 分支已經發生變化了,於是張三從 master 分支 checkout 了一個 develop 分支,並且把自己和李四的程式碼分支都合併到分支上。
在 rebase & squash
模式下也是一樣的,但不同的是,傳統模式下,develop
分支建立之後,會一直存在於遠端,繼續合併;而 rebase & squash
模式下,這個分支只會臨時地存在於遠端,當完成了流水線的編譯、釋出之後,就將分支從遠端刪除。我們將這種臨時分支表上虛線框以示區別。
公共開發分支演化
張三合併了分支,釋出到環境上除錯。哎,發現有 bug,這就需要修改程式碼了。OK,張三在自己的 feature 分支上改了程式碼,然後再合併到 develop
分支:
在 rebase & squash
模式下,之前的臨時分支已經刪除了,那麼只是重複一下上一次的操作,再建立臨時 develop
分支,用完即棄就行了。
這個時候,兩種模式下 develop
分支的複雜度差異已經可以看出端倪了。
引入新開發分支
這個時候,王五也加入來參與開發了,ta 自然是建立了一個新的分支。等到王五開發完畢,也準備用到統一環境的時候,王五也喊了一聲:“開發環境有誰在用嗎?”張三李四說:“合併到 develop 分支再發。”王五看了一下 develop
分支,因為李四的開發,develop
分支相比上一個小節,又往前了一個 commit;此外,還因為 master 分支往前也走了一步, 所以 master 分支的變動也被某位同學合併到了 develop 分支,以確保 develop 分支不能落後於線上版本。
不過這並不影響王五的操作方案,於是王五把自己的分支往 develop
一合,再提交了一個。
在 rebase & squash
模式下,還是老三樣,直接從 master 拉出臨時分支,然後王五扯嗓子喊一聲:“開發環境誰在用?我要合哪些分支?”這個之後張三李四說:“我們建了一個共享文件,你就按照文件上的分支合就行。”於是王五把自己的分支和張三李四的分支都合併、編譯、釋出,然後刪除臨時分支。
Rebase
在傳統模式下,個人的分支,一旦被別人 merge 了,或者是 merge 到別的分支了,那麼這個個人分支就不能亂動,不能輕易進行壓縮、rebase 等破壞分支鏈一致性的操作,否則進行了修改之後的分支,會被視為新的分支。比如張三修了 bug 之後,覺得自己的分支太複雜了,好多叫做 “dev”、“bugfix” 的 commit message,哎合併一下算了。但是合併了之後,重新 merge 到 develop
分支的時候,張三發現多出來一條旁路了:
在 rebase & squash
模式下,個人的分支,被視為個人在工作層面的 “私有財產”,這裡的 “私有” 指的是,分支的所有權屬於個人,個人可以對這個分支作任何操作,如果覺得分支過於難看的話,完全可以自作主張進行合併,也可以為了跟上 master 分支的進度,進行 rebase 操作,這在這個模式下是極為常見的操作:
MR / PR
我們假設,張三完成了開發、測試,並且提交了 MR / PR。透過了之後,透過系統成功壓縮合併到了 master 分支。那麼在傳統模式下,儘管張三刪除了遠端名為 feature/zhangsan
分支,但由於 Git 的分支特點,張三原來的那個開發分支的支架,依然存在於 develop
分支上:
那麼基於 develop
應該不落後於 master 分支的原則, 需要再執行一次合併:
在 rebase & squash
模式下,事情就沒那麼複雜了,張三的分支合併到了 master,其他的分支你們隨意,能保持最新的話,就 rebase,不急的話也沒問題。張三的開發分支,也不會在其他的分支中留下痕跡。
新一輪的開發
這個時候,李四也要進行除錯了,我們對比一下兩種模式下,分支的變化:
傳統模式:
對於一個成熟的開發團隊而言,開發分支是動態的,不停滾動前進的。如果採用傳統模式,很難遇到某一個時間點,develop 分支上的 MR 都合併入 master,從而可以刪除並開啟全新的分支。因此,develop
分支的混亂交叉,會在遠端長時間地存在,並且實質上,這些分支的歷史資訊,一點意義都沒有。
rebase & squash
模式:
採用這種模式,我們將關注的重點聚焦於各特性分支,而不是分支的合併上。分支也能夠一直保持最新和整潔。
衝突處理
有經驗的同學估計很快就能發現一個關鍵問題點:如果分支發生衝突了怎麼辦?
傳統模式下,分支的衝突處理可以輕易地透過 merge 來解決,但是在 rebase & squash
模式下,rebase 天生就帶來重複解決衝突的副作用。
首先最理想的情況是,衝突點儘快合入 master 分支,然後相關的分支重新 rebase master。但實際操作中,衝突點可能無法快速解決,這個時候,這個模式也是有解決方法的。
引入衝突解決分支
我們回到張三李四的場景。我們假設發生了衝突:
首先,解決衝突的時候,衝突的當事雙方必然需要進行協商,然後選定解決衝突的方案,這即便是傳統的分支模式也不例外。此時,我們應該找到衝突點,然後基於衝突點,執行 merge 並解決衝突,生成一個基準分支
然後,將這個基準分支,基於 master 進行 rebase 和 squash 操作,合併為一個提交點(或者想要保留之前的 commit,其實也行):
然後,相關當事人基於這個新的基準分支,將自己的分支進行 rebase 操作:
迴歸正途
有了基準分支之後,當事分支將自己的基準分支改為這個新的基準分支。基準分支也可以隨時跟著 rebase master:
基準分支發生變化,特性分支也可以選擇跟隨,也可以選擇不跟隨,其實影響不大。
而如果引入了新的開發者王五的時候,王五並沒有什麼衝突的壓力,ta 依然可以按照原本的模式,正常從 master 分支拉出來幹活,並且合併別人的分支:
基準分支的刪除
基準分支是為了解決衝突而產生的,不希望長期存在。當由基準分支派生出來的任意一個特性分支透過了 MR / PR,並且壓縮合併入 master 的時候,我們就可以考慮基準分支的刪除操作了。這個刪除可以由剛剛合入程式碼的開發同學來負責。我們先看看分支合併以後的狀態(灰色分支表示不存在了):
這個時候,另一個特性分支,只需要 rebase master,就可以迴歸了,畢竟衝突點早就合入 master 了,世界重歸寧靜。
適應症
在食品領域,拋開劑量談毒性就是耍流氓;在軟體工程,拋開場景談應用也是耍流氓。我提的這種模式,並不是要革傳統分支模式的命,在統一 squash merge 模式的大背景下,兩種模式最終的走向都是保持主幹分支的簡潔可讀,同時解決團隊開發的問題。
就我個人的實踐經驗來說,rebase & squash
模式比較適合以下場景:
- 專案團隊規模比較小,針對同一個模組的開發者基本不超過三四人
- 程式碼衝突不是常態,平均一個星期需要解決的衝突在一兩次以下
- 框架成熟,大部份需求開發規模相對較小;或者是即便規模大,但基本上也都是一到兩個同學負責這一需求
- 需求多,同時迭代速度快,經常大量需求同時轉測
- 專案需求相互之間關聯度低,或者是主要是前後依賴關係,很少有交叉依賴
反過來,我們也可以列舉出不適合這種分支模式的情況:
- 專案團隊規模比較大,針對同一個木虧的開發者經常達到十幾甚至幾十人——這種大團隊自然有大團隊的開發模式,並且有專職的專案工程師。這種情況下,我們應該遵從專案工程師的管理模式進行開發
- 經常需要解決程式碼衝突
- 專案處於早期開發模式,這個時候框架、技術方案之類的都不成熟,那麼程式碼的變更雖然混亂且頻繁,但有時候為了便於覆盤,我們有必要進行比較完善的保留
- 需求數量相對比較少,但每一個需求的規模都比較大,每次變更都會涉及大量程式碼的測試、提交、合併,並且非常重視版本
- 需求關係複雜,互相耦合的需求同時開發是常態
不過其實,我覺得兩種模式並不是你死我活的關係。即便是針對傳統模式或者是其他的分支管理模式,本文提出的這種模式也是可以在子模組的小團隊中採用的。比如說,小團隊開發階段採用 rebase & squash,但最終合併到大專案的關鍵分支中則採用專案模式。
寫給 scrum master 們
雖然我提出了這種模式,不過我個人是從未強制其他人 follow。原因嘛其實也挺明顯,且不說這個模式解決衝突時的麻煩,據我瞭解大部份開發者並不特別在意保持與 master 分支的更新,而主要關注自己開發中的分支。
這個模式的個人喜好特徵非常明顯。我個人就一直使用這種混合模式對自己的程式碼進行管理,同時參照團隊模式進行合併。其實本質上,就是如何選取基準分支的問題——master
分支也可以是相對的,在不同的場景下,我們開發中可以視另一個分支為我們的基準分支,那麼 rebase 其實也就是另一種 squash merge 而已。
另外一種場景是推薦給 scrum master 同學,特別是需要清晰掌握各開發分支狀態的 scrum master。這種模式可以非常方便 scrum master 清晰掌握專案中各分支的進展。但是在這種模式下,scrum master 也應該承擔起分支合併衝突處理以及基準分支的管理職責。
Anyway,這是我個人在開發過程中摸索出的一種新玩法,分享出來,是請開發者批判性地學習這種模式啦。
本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
原作者: amc,原文釋出於騰訊雲開發者社群,也是本人的部落格。歡迎轉載,但請註明出處。
原文標題:《一種邪道的 Git 整潔之法——rebase & squash》
釋出日期:2024-11-25
原文連結:https://cloud.tencent.com/developer/article/2470889。