管理原始碼分支的模式
現代原始碼控制系統提供了強大的工具,可以輕鬆地在原始碼中建立分支。但最終這些分支必須合併回來,許多團隊花費了過多的時間來應對他們糾結的分支叢林。有幾種模式可以讓團隊有效地使用分支,集中在整合多個開發人員的工作和組織通往產品發布的途徑。總體主題是分支應該頻繁整合,並專注於一個健康的可以輕鬆部署到產品中的主線。
2020 年 5 月 28 日
原始碼對任何軟體開發團隊來說都是至關重要的資產,數十年來,已經開發出一組原始碼管理工具來維護程式碼的狀態。這些工具允許追蹤變更,因此我們可以重新建立軟體的先前版本,並查看它隨著時間的推移如何發展。這些工具對於協調由多個程式設計師組成的團隊(他們都在處理共同的程式碼庫)也至關重要。透過記錄每個開發人員所做的變更,這些系統可以同時追蹤許多工作線,並協助開發人員找出如何將這些工作線合併在一起。
將開發工作區分為會分開和合併的工作線,是軟體開發團隊工作流程的核心,而且已經演變出多種模式來幫助我們掌控所有這些活動。就像大多數軟體模式一樣,其中很少有模式是所有團隊都應遵循的黃金標準。軟體開發工作流程非常依賴於背景,特別是團隊的社會結構和團隊遵循的其他實務。
我在這篇文章中的任務是討論這些模式,而且我在單一文章的背景下進行討論,其中我描述模式,但穿插模式說明與敘述部分,以更好地說明背景和它們之間的相互關係。為了讓區別它們更容易,我已使用「✣」符號來識別模式部分。
基本模式
在思考這些模式時,我發現發展兩個主要類別很有用。一組著重於整合,也就是多個開發人員如何將其工作結合為一個有條理的整體。另一組著重於生產路徑,使用分支來協助管理從整合程式碼庫到在生產中執行的產品的路線。一些模式支撐著這兩者,而且我現在將處理這些模式作為基本模式。這會留下一些既非基本模式,也不適合於這兩個主要類別的模式,因此我會將它們留到最後。
原始碼分支
建立一個副本並記錄對該副本的所有變更。
如果多個人處理相同的程式碼庫,他們很快就會無法處理相同的檔案。如果我想執行編譯,而我的同事正在輸入表達式,那麼編譯將會失敗。我們必須互相大喊:「我在編譯,不要變更任何東西」。即使只有兩個人,這也很難持續下去,對於一個較大的團隊來說,這將是不可理解的。
最簡單的答案是讓每個開發人員取得程式碼庫的副本。現在我們可以輕鬆處理我們自己的功能,但會出現一個新問題:當我們完成時,我們如何將兩個副本合併回一起?
原始碼控制系統讓這個程序變得容易許多。關鍵在於它會記錄每個變更為提交,並變更到每個分支。這不僅確保沒有人忘記他們對 utils.java
所做的微小變更,記錄變更也讓合併變得更容易,特別是在多個人變更同一個檔案時。
這讓我想到我將在本文中使用的分支定義。我定義一個 分支 為對程式庫的特定提交序列。一個分支的 頭部 或 提示 是該序列中最新的提交。

那是名詞,但也有動詞「分支」。我的意思是建立一個新分支,我們也可以將其視為將原始分支拆分為兩個。當一個分支的提交套用至另一個分支時,分支就會合併。

我對「分支」所使用的定義與我觀察到大多數開發人員談論它們的方式相符。但原始碼控制系統傾向於以更特定的方式使用「分支」。
我可以透過一個現代開發團隊的常見情況來說明這一點,該團隊將其原始碼保存在共用的 git 儲存庫中。一位開發人員 Scarlett 需要做一些變更,因此她複製了該 git 儲存庫並檢出主分支。她在她的主分支中做了一些變更並提交回去。同時,另一位開發人員,我們稱她為 Violet,將儲存庫複製到她的桌面上並檢出主分支。Scarlett 和 Violet 是在同一個分支上工作還是不同的分支?她們都在「主分支」上工作,但他們的提交彼此獨立,並且當她們將變更推回共用儲存庫時,需要合併。如果 Scarlett 決定不確定她所做的變更,她標記最後一次提交並將她的主分支重設為 origin/master(她從共用儲存庫複製的最後一次提交)會發生什麼事?

根據我先前給出的分支定義,Scarlett 和 Violet 在不同的分支上工作,彼此分開,並且與共用儲存庫上的主分支分開。當 Scarlett 使用標記擱置她的工作時,根據我的定義,它仍然是一個分支(她可能也會將其視為一個分支),但在 git 的術語中,它是一行標記的程式碼。
使用像 git 這樣的分散式版本控制系統,這表示當我們進一步複製一個儲存庫時,我們也會取得額外的分支。如果 Scarlett 複製她的本機儲存庫以放在她的筆電上以便她搭火車回家,她建立了第三個主分支。在 GitHub 中分岔也會產生相同的效果 - 每個分岔的儲存庫都有自己的一組額外分支。
當我們遇到不同的版本控制系統時,這種術語混淆會變得更糟,因為它們都有自己對分支構成的定義。Mercurial 中的分支與 git 中的分支非常不同,後者更接近 Mercurial 的書籤。Mercurial 也可以用未命名頭部進行分岔,而且 Mercurial 的人常常透過複製儲存庫來分岔。
所有這些術語混淆導致有些人避免使用這個術語。一個更通用的術語在此處很有用,那就是程式碼行。我定義一個 程式碼行 為程式庫版本的一個特定序列。它可以結束於標記,成為一個分支,或遺失在 git 的 reflog 中。你會注意到我的分支和程式碼行定義之間有很大的相似性。程式碼行在許多方面都是更有用的術語,我確實使用它,但它在實務上並未廣泛使用。因此,對於本文,除非我處於 git(或其他工具)術語的特定背景下,否則我將交替使用分支和程式碼行。
此定義的後果是,無論您使用哪個版本控制系統,只要開發人員在自己的機器上進行本地變更,他們至少會在其工作副本上有一個個人程式碼行。如果我複製專案的 git 儲存庫、檢出主程式,並更新一些檔案,這是一個新的程式碼行,即使我尚未提交任何內容。同樣地,如果我建立一個版本控制儲存庫主幹的工作副本,即使沒有涉及版本控制分支,該工作副本也是其自己的程式碼行。
何時使用
一個老笑話說,如果你從高樓墜落,墜落不會傷害你,但著陸會。因此,對於原始程式碼:分支很容易,合併比較困難。
在提交時記錄每個變更的原始程式碼控制系統確實讓合併過程變得更容易,但它們並未使其變得簡單。如果 Scarlett 和 Violet 都變更變數名稱,但變更為不同的名稱,則會發生原始程式碼管理系統無法在沒有人工干預的情況下解決的衝突。更尷尬的是,這種文字衝突至少是原始程式碼控制系統可以發現並提醒人類注意的事情。但衝突通常出現在文字合併沒有問題,但系統仍然無法運作的情況。想像一下,Scarlett 變更函數名稱,而 Violet 在她的分支中新增一些呼叫此函數使用其舊名稱的程式碼。這就是我所謂的語意衝突。當發生此類衝突時,系統可能會無法建置,或者可能會建置但執行時失敗。
這個問題對於使用並行或分散式運算的人來說並不陌生。我們有一些共用狀態(程式碼庫),開發人員並行進行更新。我們需要透過將變更序列化為一些共識更新來以某種方式將這些變更組合起來。我們的任務因系統執行和正確運作意味著該共用狀態有非常複雜的有效性準則而變得更加複雜。沒有辦法建立一個確定性演算法來尋找共識。人類需要找到共識,而該共識可能涉及混合不同更新的選擇部分。通常只有在有原始更新來解決衝突時才能達成共識。
我從「如果沒有分支會怎樣」開始。每個人都會編輯即時程式碼,半生不熟的變更會破壞系統,人們會互相踩踏。因此,我們給予個人凍結時間的錯覺,他們是唯一變更系統的人,而這些變更可以在完全成熟之前等待,再冒系統風險。但這是一個錯覺,最終必須付出代價。誰來付?什麼時候?多少?這就是這些模式討論的內容:支付代價的替代方案。
-- Kent Beck
因此,本文的其餘部分將說明各種模式,這些模式支援令人愉快的隔離,以及在您墜落時穿過您頭髮的風,但將與堅硬地面不可避免接觸的後果降到最低。
主線
作為產品當前狀態的單一共用分支
主線是一個特殊代碼行,我們認為它是團隊代碼的當前狀態。每當我想開始一項新工作時,我都會從主線將代碼拉到我的本地儲存庫中開始工作。每當我想與團隊其他成員分享我的工作時,我都會使用我的工作更新該主線,理想情況下使用我稍後將討論的主線整合模式。
不同的團隊使用不同的名稱來稱呼這個特殊分支,通常受到所使用的版本控制系統的慣例鼓勵。git 使用者通常會稱它為「master」,subversion 使用者通常稱它為「trunk」。
我必須在此強調,主線是一個單一共用代碼行。當人們在 git 中談論「master」時,他們可能指的是好幾種不同的東西,因為每個儲存庫複製都有自己的本地 master。通常,此類團隊有一個中央儲存庫 - 一個共用儲存庫,作為專案的單一記錄點,並且是大多數複製的來源。從頭開始一項新工作表示複製該中央儲存庫。如果我已經有一個複製,我從中央儲存庫拉取 master 開始,因此它與主線保持最新狀態。在這種情況下,主線是中央儲存庫中的 master 分支。
在我處理我的功能時,我有一個自己的個人開發分支,它可能是我的本地 master,或者我可能會建立一個獨立的本地分支。如果我處理這項工作一段時間,我可以透過定期拉取主線的變更並將它們合併到我的個人開發分支中,以保持與主線變更的最新狀態。
類似地,如果我想建立一個新版本的產品以供發布,我可以從當前主線開始。如果我需要修正錯誤以使產品穩定到足以發布,我可以使用發布分支。
何時使用
我記得在 2000 年代初期去與客戶的建置工程師交談。他的工作是組建團隊正在處理的產品建置。他會寄電子郵件給團隊的每位成員,而他們會透過傳送來自其已準備好整合的程式碼庫的各種檔案來回覆。然後他會將那些檔案複製到他的整合樹中,並嘗試編譯程式碼庫。他通常需要花費幾個星期建立一個可以編譯並準備好進行某種形式測試的建置。
相比之下,透過主線,任何人都可以快速從主線的頂端開始建立產品的最新建置。此外,主線不僅讓查看程式碼庫的狀態變得更容易,它也是我稍後將探討的許多其他模式的基礎。
主線的一種替代方案是發布列車。
健康分支
在每次提交時,執行自動化檢查,通常是建置和執行測試,以確保分支上沒有缺陷
由於主線具有這種共用、已核准的狀態,因此讓它保持在穩定狀態非常重要。同樣在 2000 年代初期,我記得與另一個組織的團隊交談,該組織以每天建置其每個產品而聞名。這在當時被認為是一種相當先進的做法,而這個組織也因此受到讚揚。這些文章中未提及的是,這些每日建置並非總是成功。事實上,發現有些團隊的每日建置已好幾個月沒有編譯並不罕見。
為了解決這個問題,我們可以努力保持分支的健康狀態,也就是說,它可以成功建置,而且軟體幾乎沒有錯誤,甚至沒有錯誤。為了確保這一點,我發現撰寫自測試程式碼至關重要。這種開發實務表示,當我們撰寫生產程式碼時,我們也會撰寫一套全面的自動化測試,以便我們可以確信,如果這些測試通過,則程式碼不包含任何錯誤。如果我們這樣做,則我們可以透過在每次提交時執行建置來保持分支的健康狀態,此建置包括執行此測試套件。如果系統無法編譯,或測試失敗,那麼我們的首要任務是在我們對該分支執行任何其他操作之前修復它們。這通常表示我們「凍結」分支,除了讓它再次變為健康的修復之外,不允許提交任何內容。
在提供足夠的健康信心方面,測試的程度存在著緊張關係。許多更徹底的測試需要花費大量時間來執行,這會延遲關於提交是否健康的回饋。團隊透過在部署管線上將測試分為多個階段來處理這個問題。這些測試的第一個階段應該執行得很快,通常不超過十分鐘,但仍然相當全面。我將這樣的套件稱為提交套件(儘管它通常稱為「單元測試」,因為提交套件通常主要是單元測試)。
理想情況下,應該在每次提交時執行所有範圍的測試。但是,如果測試很慢,例如需要讓伺服器浸泡幾個小時的效能測試,那就不切實際了。現今團隊通常可以建置可以在每次提交時執行的提交套件,並盡可能頻繁地執行部署管線的後續階段。
程式碼沒有錯誤執行還不足以表示程式碼良好。為了維持穩定的交付速度,我們需要保持程式碼的內部品質。一種流行的做法是使用整合前檢閱,儘管我們將看到,還有其他替代方案。
何時使用
每個團隊都應該為其開發工作流程中每個分支的健康狀態制定明確的標準。保持主線健康具有極大的價值。如果主線健康,則開發人員只需提取目前的基線,就可以開始一項新的工作,而不會陷入阻礙其工作的缺陷中。我們經常聽到人們花費數天時間嘗試修復或變通他們提取的程式碼中的錯誤,然後才能開始一項新的工作。
健康的幹線也讓生產路徑更順暢。隨時都可以從幹線的開頭建立新的生產候選。最好的團隊發現他們需要做的工作很少就能穩定這樣的程式碼庫,通常能夠直接從幹線釋出到生產環境。
擁有健康的幹線至關重要的是 自測程式碼,其提交套件可以在幾分鐘內執行。建立此功能可能是一項重大投資,但一旦我們能在幾分鐘內確保我的提交沒有損壞任何東西,這將完全改變我們的整個開發流程。我們可以更快地進行變更,自信地 重構 我們的程式碼,讓它易於使用,並大幅縮短從所需功能到在生產環境中執行程式碼的週期時間。
對於個人開發分支,保持它們健康是很明智的,因為這樣可以啟用 差異偵錯。但這種渴望與頻繁提交以檢查點您的當前狀態背道而馳。即使編譯失敗,我也可能會建立檢查點,如果我即將嘗試不同的路徑。我解決這種緊張局勢的方法是在完成我的即時工作後,壓縮掉任何不健康的提交。這樣一來,只有健康的提交才會在我的分支上保留幾個小時以上。
如果我保持我的個人分支健康,這也讓提交到幹線變得更容易 - 我知道與 幹線整合 一起出現的任何錯誤純粹是整合問題,而不是僅限於我的程式碼庫內的錯誤。這將讓找出並修復它們變得更快更容易。
整合模式
分支是關於管理隔離和整合的交互作用。讓所有人都一直處理單一的共用程式碼庫,這行不通,因為如果您正在輸入變數名稱的過程中,我就無法編譯程式。因此,至少在某種程度上,我們需要一個私人工作空間的概念,我可以處理一段時間。現代的原始碼控制工具讓分支和監控這些分支的變更變得容易。然而,在某個時間點,我們需要整合。思考分支策略實際上就是決定如何以及何時整合。
主線整合
開發人員透過從主線拉取、合併,以及(如果沒有問題)推回主線來整合自己的工作
主線明確定義團隊軟體的目前狀態。使用主線最大的好處之一是它簡化了整合。沒有主線,就是上面我所描述的與團隊中每個人協調的複雜任務。然而,有了主線,每個開發人員都可以自行整合。
我將逐步說明這項作業的運作方式。一位開發人員(我稱她為 Scarlett)透過將主線複製到自己的儲存庫中來開始一些工作。使用 git,如果她尚未複製中央儲存庫,她會複製它並簽出 master 分支。如果她已經複製,她會從主線拉取到本地的 master。然後,她可以在本地工作,將提交記錄提交到本地的 master。

在她工作時,她的同事 Violet 將一些變更推送到主線。由於她在自己的程式碼行中工作,Scarlett 在處理自己的任務時可以忽略這些變更。

在某個時間點,她會到達想要整合的點。這的第一部分是將主線的目前狀態擷取到本地的 master 分支,這會拉取 Violet 的變更。由於她在本地 master 上工作,提交記錄會在 origin/master 上顯示為一個獨立的程式碼行。

現在她需要將自己的變更與 Violet 的變更結合。有些團隊喜歡透過合併來執行此操作,其他團隊則透過重新設定基礎來執行。一般來說,人們在談論將分支合併在一起時會使用「合併」這個字,無論他們實際上使用的是 git 合併還是重新設定基礎操作。我將遵循這個用法,因此,除非我實際上在討論合併和重新設定基礎之間的差異,否則請將「合併」視為可以使用任一種方式實作的邏輯任務。
關於是否使用香草合併、使用或避免快速轉送合併,或使用重新設定基礎,還有另一場討論。這不在本文的討論範圍內,不過如果有人寄給我足夠的 Tripel Karmeliet,我可能會針對這個問題撰寫一篇文章。畢竟,quid-pro-quos 這些天很流行。
如果 Scarlett 很幸運,與 Violet 的程式碼合併會很順利,否則她會遇到一些衝突需要處理。這些可能是文字衝突,大部分的版本控制系統都能自動處理。但語意衝突難處理得多,這時 自我測試程式碼 就很方便。(由於衝突會產生大量的工作,而且總是會帶來許多工作的風險,因此我用一大塊黃色標示它們。)

此時,Scarlett 需要驗證合併的程式碼是否符合主線的健康標準(假設主線是 健康分支)。這通常表示要建立程式碼並執行主線提交套件的任何測試。即使是順利合併,她也需要執行此步驟,因為即使是順利合併也可能隱藏語意衝突。提交套件中的任何失敗都應純粹是合併造成的,因為兩個合併父代都應該是綠色的。知道這點應有助於她追蹤問題,因為她可以查看差異找出線索。
透過這個建置和測試,她已成功將主線拉入她的程式碼線,但 - 這點很重要且常被忽略 - 她尚未完成與主線的整合。若要完成整合,她必須將她的變更推送到主線。除非她這麼做,否則團隊中的其他人將會與她的變更隔離 - 基本上沒有整合。整合既是拉也是推 - 只有在 Scarlett 推送後,她的工作才會與專案的其他部分整合。

現今許多團隊要求在將提交新增到主線前進行程式碼檢閱步驟 - 我稱此模式為 整合前檢閱,稍後會討論。
偶爾會有人在 Scarlett 推送前與主線整合。這種情況下,她必須再次拉取並合併。通常這只是一個偶發問題,而且可以在沒有進一步協調的情況下解決。我曾看過建置時間很長的團隊使用整合接力棒,這樣只有持有接力棒的開發人員才能整合。但隨著建置時間的縮短,近年來我鮮少聽聞此做法。
何時使用
顧名思義,如果我們也在產品上使用主線,我才可以使用主線整合。
使用主線整合的一種替代方案是僅從主線拉取,將這些變更合併到個人開發分支。這可能很有用 - 拉取至少可以提醒 Scarlett 其他人員已整合的變更,並偵測她的工作與主線之間的衝突。但在 Scarlett 推送之前,Violet 無法偵測到她正在處理的內容與 Scarlett 的變更之間的任何衝突。
當人們使用「整合」一詞時,他們常常會錯過這個重點。當人們僅僅拉取時,常會聽到有人說他們正在將主線整合到自己的分支。我已學會對此保持警覺,並進一步探究以查看他們的意思是僅拉取還是適當的主線整合。兩者的後果大不相同,因此不要混淆這些術語非常重要。
另一種替代方案是當 Scarlett 正在進行一些尚未準備好與團隊其他成員完全整合的工作,但它與 Violet 重疊,而且她想要與她分享時。在這種情況下,他們可以開啟 協作分支。
功能分支
將功能的所有工作放在它自己的分支上,在功能完成時整合到主線。
使用功能分支時,開發人員在開始處理功能時會開啟一個分支,持續處理該功能直到完成,然後與主線整合。
例如,我們來追蹤 Scarlett。她會挑選將當地銷售稅的收集新增到他們網站的功能。她從產品的目前穩定版本開始,她會將主線拉取到她的本地儲存庫,然後從目前主線的頂端建立一個新分支。她處理該功能需要多長時間就花多長時間,對該本地分支進行一系列提交。

她可能會將該分支推送到專案儲存庫,以便其他人可以查看她的變更。
在她處理的同時,其他提交會登陸到主線。因此,她可能會不時從主線拉取,以便她可以判斷那裡的任何變更是否可能影響她的功能。

請注意,這不是我上面所述的整合,因為她沒有推回主線。在這個時間點,只有她可以看到她的工作,其他人無法看到。
有些團隊希望確保所有程式碼,無論是否已整合,都保留在中央儲存庫中。在這種情況下,Scarlett 會將她的功能分支推送到中央儲存庫。這也會讓其他團隊成員看到她在處理什麼,即使尚未整合到其他人的工作中。
當她完成功能處理後,她會執行 主線整合,將功能納入產品中。

如果 Scarlett 同時處理多項功能,她會為每一項功能開啟一個獨立的分支。
何時使用
功能分支是當今產業中常見的模式。要討論何時使用它,我需要介紹它的主要替代方案 - 持續整合。但首先我需要討論整合頻率的角色。
整合頻率
我們執行整合的頻率對團隊的運作方式有顯著的影響力。DevOps 狀態報告 的研究指出,菁英開發團隊的整合頻率明顯高於表現不佳的團隊 - 這項觀察符合我的經驗和我許多產業同儕的經驗。我將透過考慮 Scarlett 和 Violet 主演的兩個整合頻率範例來說明這如何發揮作用。
低頻整合
我將從低頻情況開始。在此,我們的兩位英雄透過將主線複製到他們的分支中來開始一段工作,然後進行一些他們還不想推出的本機提交。

當他們工作時,其他人將提交發佈到主線。(我無法快速想出另一個顏色的名字 - 也許是 grayham?)

這個團隊透過保持一個健康的 branch,並在每次提交後從主線拉取來工作。由於主線沒有變更,因此 Scarlett 在她的前兩次提交中沒有任何要拉取的內容,但現在需要拉取 M1。

我已用黃色方塊標示合併。這個合併將提交 S1..3 與 M1 合併。很快地,Violet 也需要執行相同的動作。

在這個時間點,兩位開發人員都已更新到主線,但他們尚未整合,因為他們彼此孤立。Scarlett 不知道 Violet 在 V1..3 中所做的任何變更。
Scarlett 再做幾次本機提交後,便準備進行主線整合。由於她先前已拉取 M1,因此這對她來說是一次輕鬆的推送。

然而,Violet 的練習較為複雜。當她進行主線整合時,她現在必須整合 S1..5 與 V1..6。

我根據所涉及的提交數量科學地計算了合併的大小。但即使你忽略我臉頰上的舌狀突起,你也能理解 Violet 的合併最有可能困難重重。
高頻整合
在前面的範例中,我們的兩位色彩繽紛的開發人員在幾次本機提交後進行整合。讓我們看看如果他們在每次本機提交後進行主線整合,會發生什麼事。
第一個變更在 Violet 的第一次提交中顯而易見,因為她立即進行整合。由於主線並未變更,因此這是一個簡單的推送。

Scarlett 的第一次提交也有主線整合,但由於 Violet 先進行,她需要進行合併。但由於她只將 V1 與 S1 進行合併,因此合併很小。

Scarlett 的下一次整合是一個簡單的推送,這表示 Violet 的下一次提交也需要與 Scarlett 的最新兩次提交進行合併。然而,這仍然是一個相當小的合併,Violet 的一次和 Scarlett 的兩次。

當對主線的外部推送出現時,它會以 Scarlett 和 Violet 整合的通常節奏被接收。

雖然這與之前發生的事情類似,但整合較小。這次 Scarlett 只需要將 S3 與 M1 進行整合,因為 S1 和 S2 已在主線上。這表示 Grayham 在推送 M1 之前必須整合主線上已有的內容 (S1..2、V1..2)。
開發人員繼續進行其餘的工作,在每次提交中進行整合。

比較整合頻率
讓我們再次檢視兩個整體畫面
低頻率

高頻率

這裡有兩個非常明顯的差異。首先,高頻率整合,顧名思義,有更多的整合 - 在這個玩具範例中,數量是兩倍。但更重要的是,這些整合比低頻率情況下的整合小很多。較小的整合意味著較少的工作,因為較少的程式碼變更可能會導致衝突。但更重要的是,它也降低了風險。大型合併的問題並非在於它們所涉及的工作,而是這些工作的不可確定性。大多數時候,即使大型合併也能順利進行,但偶爾它們會非常、非常糟糕。偶爾的痛苦最終會比規律的痛苦更糟。如果我將每次整合多花十分鐘與五十次中有一次花 6 小時來修復整合進行比較 - 我比較喜歡哪一個?如果我只看工作量,那麼 1 比 50 會更好,因為它是 6 小時而不是 8 小時 20 分鐘。但這種不確定性讓 1 比 50 的情況感覺更糟,一種導致整合恐懼的不確定性。
讓我們從另一個角度來看這些頻率之間的差異。如果 Scarlett 和 Violet 在他們的第一個提交中發生衝突會怎樣?他們什麼時候會發現衝突已經發生?在低頻率的情況下,他們在 Violet 的最後一次合併之前不會發現它,因為這是 S1 和 V1 第一次被放在一起。但在高頻率的情況下,它們在 Scarlett 的第一次合併中就被檢測到了。
低頻率

高頻率

頻繁整合會增加合併的頻率,但會降低它們的複雜性和風險。頻繁整合也會讓團隊更快速地發現衝突。當然,這兩件事是相關的。惡劣的合併通常是團隊工作中潛在衝突的結果,只有在整合發生時才會浮出水面。
也許 Violet 正在查看帳單計算,並發現它包括評估稅,而作者假設了特定的稅收機制。她的功能需要不同的稅務處理,因此直接的方法是從帳單計算中刪除稅,然後稍後作為一個單獨的功能來執行。帳單計算只在幾個地方被呼叫,因此很容易使用
自我測試程式碼是我們的救星。如果我們有強大的測試套件,將它用於健康分支的一部分,將會發現衝突,因此錯誤進入生產的機會遠遠降低。但即使有強大的測試套件作為主線的守門人,大型整合也會讓生活更困難。我們必須整合的程式碼越多,找到錯誤就越困難。我們也更有可能遇到多個會互相干擾的錯誤,這些錯誤特別難以理解。我們不僅可以透過較小的提交來減少需要檢視的內容,我們還可以透過差異偵錯來幫助縮小引發問題的變更範圍。
許多人沒有意識到,原始碼控制系統是一種溝通工具。它允許 Scarlett 查看團隊中其他人在做什麼。透過頻繁整合,她不僅可以在發生衝突時立即收到警示,她還可以更了解每個人在做什麼,以及程式碼庫如何演進。我們不再像獨立作業的個人,而更像是一個團隊共同合作。
增加整合頻率是縮小功能大小的一個重要原因,但還有其他優點。功能越小,建置就越快,進入生產環境就越快,開始提供其價值就越快。此外,較小的功能會縮短回饋時間,讓團隊在更了解其客戶後,做出更好的功能決策。
持續整合
開發人員在擁有可以分享的健康提交後,就會立即進行主線整合,通常不到一天的工作量
一旦團隊體驗過高頻率整合既更有效率又更不緊張,自然會問「我們可以多久進行一次?」功能分支暗示變更集大小的下限 - 你不能小於一個有凝聚力的功能。
持續整合套用不同的整合觸發器 - 只要您在功能上取得進展,且您的分支仍然正常,您便會整合。這並不要求功能必須完成,只要程式碼庫有值得變更的內容即可。經驗法則為 "所有人每天都提交至主線",或更精確地說:您在本地儲存庫中不應有超過一天的工作未整合。實際上,大多數持續整合的實務人員會每天整合多次,樂於整合一小時或更少的工作成果。
使用持續整合的開發人員需要習慣在功能部分建置時,達到頻繁整合點的概念。他們需要考慮如何在不於執行系統中公開部分建置功能的情況下執行此操作。這通常很簡單:如果我正在實作依賴於優惠券代碼的折扣演算法,而該代碼尚未在有效清單中,那麼即使在生產環境中,我的代碼也不會被呼叫。類似地,如果我正在新增一個功能,詢問保險索賠人是否為吸菸者,我可以建置並測試代碼背後的邏輯,並透過將提出問題的使用者介面保留到建置功能的最後一天,確保它不會用於生產環境中。最後,透過連接 Keystone Interface 來隱藏部分建置的功能通常是一種有效的方法。
如果沒有辦法輕鬆隱藏部分功能,我們可以使用 功能旗標。除了隱藏部分建置的功能外,此類旗標還允許將功能選擇性地揭露給部分使用者 - 通常有助於緩慢推出新功能。
整合部分建置的功能特別讓那些擔心主線中有錯誤程式碼的人感到憂心。因此,那些使用持續整合的人也需要自測程式碼,這樣才能確信在主線中有部分建置的功能並不會增加錯誤發生的機率。透過這種方法,開發人員在撰寫部分建置功能時,會撰寫測試,並將功能程式碼和測試一起提交到主線中(或許使用測試驅動開發)。
就本地儲存庫而言,大多數使用持續整合的人不會費心使用獨立的本地分支來作業。通常直接提交到本地主控端,並在完成時執行主線整合即可。不過,如果開發人員偏好,也可以開啟功能分支並在那裡作業,並定期整合回本地主控端和主線中。功能分支和持續整合之間的差異不在於是否有功能分支,而在於開發人員與主線整合的時間點。
何時使用
持續整合是功能分支的替代方案。兩者之間的取捨足夠複雜,值得在本文中專門探討,現在正是時候來處理這個問題了。
比較功能分支和持續整合
功能分支似乎是目前業界最常見的分支策略,但有一群實務工作者主張持續整合通常是較好的方法。持續整合提供的關鍵優勢是它支援較高(通常高出許多)的整合頻率。
整合頻率的差異取決於團隊能夠縮小其功能的程度。如果團隊的所有功能都能在一天內完成,那麼他們就能執行功能分支和持續整合。但大多數團隊的功能長度都比這長,而且功能長度越大,這兩種模式之間的差異就越大。
正如我已經指出的,整合頻率越高,整合的複雜度就越低,對整合的恐懼也越少。這通常很難溝通。如果你生活在每隔幾週或幾個月才整合一次的世界中,那麼整合很可能會是一項非常艱鉅的活動。很難相信這是一件可以每天執行多次的事情。但整合就是其中一項頻率降低難度的事情。這是一個反直覺的概念:「如果它很痛,就更常做」。但整合的規模越小,它們就不太可能變成痛苦和絕望的史詩級合併。對於功能分支而言,這表示功能應該更小:以天為單位,而不是以週為單位(更不用說以月為單位了)。
持續整合讓團隊能夠獲得高頻率整合的優點,同時將功能長度與整合頻率脫鉤。如果團隊偏好一或兩週的功能長度,持續整合讓他們能夠在獲得最高整合頻率的所有優點的同時,執行此操作。合併的規模較小,需要處理的工作較少。更重要的是,正如我在上面所解釋的,更頻繁地執行合併可以降低發生嚴重合併的風險,這既可以消除由此產生的不愉快驚喜,也可以降低整體合併恐懼。如果程式碼中出現衝突,高頻率整合可以快速發現這些衝突,在這些衝突導致那些令人不快的整合問題之前。這些優點非常強大,以至於有些團隊的功能只需要幾天就能完成,但他們仍然執行持續整合。
持續整合的明顯缺點是它缺乏將重大整合收束到主線的機制。這不僅是一個錯失的慶祝機會,如果團隊不擅長保持分支的健康,這也會構成風險。將某個功能的所有提交記錄整合在一起,也能讓團隊在稍後決定是否將某個功能納入即將推出的版本。雖然功能標記允許從使用者的角度啟用或停用功能,但該功能的程式碼仍會存在於產品中。對此的疑慮通常被誇大了,畢竟程式碼並不會增加任何重量,但這表示想要進行持續整合的團隊必須制定嚴謹的測試方案,才能確信主線即使每天整合多次也能保持健康。有些團隊發現很難想像這種技能,但其他人則發現這既有可能,又能帶來解放感。這個先決條件表示,功能分支更適合於不會強制執行健康分支,而且需要發行分支在發行前穩定程式碼的團隊。
雖然合併的大小和不確定性是功能分支最明顯的問題,但它最大的問題可能是會阻礙重構。重構在定期進行且摩擦力低時最有效。重構會產生衝突,如果沒有快速發現並解決這些衝突,合併就會變得困難重重。因此,重構最適合高頻率的整合,所以它成為 極限編程 的一部分也就不足為奇了,而極限編程也將持續整合列為原始做法之一。功能分支還會阻礙開發人員進行未視為正在建置功能一部分的變更,這會破壞重構穩定改善程式碼庫的能力。
我們發現,在合併到主幹之前,分支或分岔的生命週期很短(少於一天),而且總共活躍的分支少於三個,這些都是持續交付的重要面向,而且所有這些都會提升效能。每天將程式碼合併到主幹或主版本也是如此。
-- 2016 DevOps 報告
當我看到軟體開發實務的科學研究時,我通常會因為它們的方法論有嚴重的問題而無法信服。其中一個例外是 DevOps 報告,它制定了一個軟體交付效能指標,將此指標與組織效能的更廣泛衡量標準相關聯,而組織效能又與投資報酬率和獲利能力等商業指標相關聯。他們在 2016 年首次評估持續整合,發現它有助於提升軟體開發效能,而這項發現也在後續的每次調查中都得到重複驗證。
使用持續整合並不會消除保持功能小型的其他優點。頻繁地釋放小型功能可提供快速的回饋循環,這對於改善產品來說可以發揮奇效。許多使用持續整合的團隊也努力建立產品的精簡切片,並盡可能頻繁地釋放新功能。
功能分支
- 功能中的所有程式碼都可以作為一個單元來評估品質
- 功能程式碼僅在功能完成時才新增到產品中
- 合併次數較少
持續整合
- 支援比功能長度更高的整合頻率
- 減少尋找衝突的時間
- 較小的合併
- 鼓勵重構
- 需要承諾維護健康的支線(因此需要自我測試程式碼)
- 科學證據表明它有助於提高軟體交付效能
功能分支和開源
許多人將功能分支的普及歸因於 GitHub 和源自開源開發的拉取請求模型。有鑑於此,了解開源工作與許多商業軟體開發之間存在截然不同的背景是值得的。開源專案以許多不同的方式架構,但常見的架構是單人或小組擔任維護者,負責大部分的程式設計。維護者與一群較大的程式設計師合作,這些程式設計師是貢獻者。維護者通常不認識貢獻者,因此不知道他們貢獻的程式碼品質。維護者對於貢獻者實際投入工作的時間也幾乎沒有把握,更不用說他們完成工作的效率如何了。
在此脈絡中,功能分支很有道理。如果有人要新增一個功能,無論大小,而我不知道它什麼時候(或是否)會完成,那麼在我整合之前等它完成是有道理的。能夠檢閱程式碼也很重要,以確保它通過我的程式碼庫所具備的任何品質標準。
但是許多商業軟體團隊的工作脈絡非常不同。有一個全職團隊,所有成員都對軟體承諾大量的時間,通常是全職。專案負責人很了解這些成員(除了他們剛開始的時候),並且可以對程式碼品質和交付能力有可靠的預期。由於他們是受薪員工,因此負責人對投入專案的時間以及編碼標準和團隊習慣等事項有更大的控制權。
在這個非常不同的脈絡中,很明顯,此類商業團隊的分支策略不必與在開源世界中運作的分支策略相同。持續整合對於偶爾對開源工作做出貢獻的人來說幾乎不可能,但對於商業工作來說卻是一個現實的替代方案。團隊不應假設對開源環境有效的方法自動適用於他們不同的脈絡。
整合前審查
在提交到主線之前,每一個提交都會經過同儕審查。
長期以來,程式碼審查一直被鼓勵作為一種提高程式碼品質、改善模組化、可讀性,以及移除缺陷的方法。儘管如此,商業組織通常發現很難將其納入軟體開發工作流程中。然而,開源世界廣泛採用了這樣的想法:在接受專案的貢獻之前應先審查這些貢獻,並且這種方法近年來已廣泛傳播到開發組織中,特別是在矽谷。這種工作流程特別適合 GitHub 的拉取請求機制。
這種工作流程從 Scarlett 完成她希望整合的一塊工作時開始。當她執行 主線整合 時(假設她的團隊實行這種做法),一旦她成功建置,但在她推送到主線之前,她會將她的提交發送出去進行審查。團隊中的另一位成員,例如 Violet,然後對提交進行程式碼審查。如果她對提交有任何問題,她會提出一些評論,然後 Scarlett 和 Violet 會來回討論,直到 Scarlett 和 Violet 都滿意為止。只有在他們完成後,才會將提交置於主線。
預先整合檢視在開放原始碼中越來越受歡迎,它非常符合承諾維護者和偶爾貢獻者的組織模式。它們允許維護者密切關注任何貢獻。它們也與功能分支很好地結合,因為完成的功能標示了一個明確的點來進行這樣的程式碼檢視。如果您不確定貢獻者是否會完成某項功能,為什麼要檢視他們的部分工作?最好等到功能完成。這種做法也在大型網路公司中廣泛傳播,Google 和 Facebook 都建立了特殊工具來幫助順利完成這項工作。
培養及時預先整合檢視的紀律非常重要。如果開發人員完成了一些工作,並在幾天內繼續進行其他工作,那麼當檢視意見回來時,這項工作就不再是他們最優先考慮的事項。對於已完成的功能來說,這令人沮喪,但對於部分完成的功能來說,這更糟,因為在檢視得到確認之前,可能很難取得進一步進展。原則上,可以對預先整合檢視進行持續整合,而且實際上也是可行的 - Google 遵循這種方法。但儘管這是可能的,但它很難,而且相對罕見。預先整合檢視和功能分支是更常見的組合。
何時使用
將 OSS 和私人軟體開發團隊的需求混為一談,就像當前軟體開發儀式的原罪
儘管預先整合檢視在過去十年中已成為一種流行的做法,但它也有缺點和替代方案。即使做得很好,預先整合檢視總會在整合過程中引入一些延遲,從而導致整合頻率降低。結對程式設計提供了一個持續的程式碼檢視過程,其反饋週期比等待程式碼檢視要快。(就像持續整合和重構一樣,它是極限程式設計的原始做法之一)。
許多使用預整合檢閱的團隊並未迅速執行。他們能提供的寶貴回饋往往太遲而無法發揮作用。在那個時間點,會出現一個尷尬的選擇,是要進行大量返工,還是接受可能可行但會損害程式碼庫品質的內容。
程式碼檢閱並不侷限於程式碼進入主線之前。許多技術領導者發現,在提交後檢閱程式碼很有用,當他們看到疑慮時,可以追上開發人員。重構文化在此很有價值。如果執行得當,這會建立一個社群,讓團隊中的每個人定期檢閱程式碼庫並修正他們看到的問題,我將此做法稱為精煉程式碼檢閱
預整合檢閱的取捨主要取決於團隊的社會結構。正如我已經提到的,開放原始碼專案通常具有少數受信任的維護者和許多不受信任的貢獻者的結構。商業團隊通常都是全職的,但可能具有類似的結構。專案負責人(例如維護者)信任一小群(可能單一)維護者,並對團隊其他成員貢獻的程式碼保持警覺。團隊成員可能同時被分配到多個專案,這讓他們更像是開放原始碼貢獻者。如果存在這樣的社會結構,那麼預整合檢閱和功能分支就非常有意義。但是,信任度較高的團隊通常會發現其他機制可以在不增加整合流程摩擦的情況下,維持程式碼品質。
因此,雖然預整合檢閱可能是一種有價值的做法,但它絕不是建立健全程式碼庫的必要途徑,特別是如果你希望培養一個平衡良好的團隊,而且不依賴其最初的領導者。
整合摩擦
拉取請求會增加開銷以應對低信任的情況,例如允許你不認識的人為你的專案提供貢獻。
對你自己的團隊中的開發人員施加拉取請求,就像讓你的家人通過機場安全檢查站才能進入你的家。
-- Kief Morris
預整合檢閱的問題之一,是它通常會讓整合變得更麻煩。這是整合摩擦的一個例子,整合摩擦是指讓整合花時間或費力進行的活動。整合摩擦越多,開發人員就越傾向於降低整合頻率。想像一個(功能失調的)組織堅持要求所有提交到主線都需要填寫一份需要半小時才能填完的表單。這種制度會讓人們不願意頻繁整合。無論你對功能分支和持續整合的態度如何,審查任何會增加這種摩擦的事情都是有價值的。除非它明顯增加了價值,否則應移除任何此類摩擦。
手動流程是摩擦的一個常見來源,特別是如果它涉及與獨立組織的協調。這種摩擦通常可以透過使用自動化流程、改善開發人員教育(以消除需求)以及將步驟推遲到部署管道或生產中的 QA的後續步驟來減少。你可以在持續整合和持續交付的資料中找到更多消除這種摩擦的想法。這種摩擦也會出現在生產路徑中,具有相同的困難和處理方式。
讓人們不願意考慮持續整合的事情之一是,如果他們只在整合摩擦程度很高的環境中工作。如果整合需要一個小時,那麼顯然一天做幾次是荒謬的。加入一個整合不是大事的團隊,有人可以在幾分鐘內完成,感覺就像一個不同的世界。我懷疑關於功能分支和持續整合的優點的許多爭論之所以混亂,是因為人們沒有體驗過這兩個世界,因此無法完全理解這兩個觀點。
文化因素會影響整合摩擦,特別是團隊成員之間的信任。如果我是一個團隊領導,我不相信我的同事能做好工作,那麼我可能會想要防止損壞程式碼庫的提交。自然而然地,這是預整合檢閱的驅動力之一。但是,如果我在一個我相信同事判斷力的團隊中,我可能會更願意接受提交後檢閱,或者完全取消檢閱,並依賴定期精進檢閱來清理任何問題。我在這種環境中的收穫是消除了預提交檢閱帶來的摩擦,從而鼓勵更高的整合頻率。在功能分支與持續整合的爭論中,團隊信任通常是最重要的因素。
一種保留預整合檢閱(在需要時)但鼓勵減少摩擦途徑的有趣方法是 Rouan Wilsenach 的發布/展示/詢問。這將變更分類為發布(整合到主線)、展示(整合到主線,但開啟拉取請求以溝通和討論變更)或詢問(開啟拉取請求進行預整合檢閱)。
模組化的重要性
大多數關心軟體架構的人都會強調模組化對一個良好系統的重要性。如果我必須對一個模組化不佳的系統進行小幅度變更,我必須了解幾乎所有部分,因為即使是微小的變更都可能影響程式碼庫的許多部分。然而,如果模組化良好,我只需要了解一個或兩個模組中的程式碼、少數幾個介面,就可以忽略其餘部分。這種減少理解工作量的能力,就是為什麼隨著系統的成長,值得在模組化上投入大量心力的原因。
模組化也會影響整合。如果一個系統有良好的模組,那麼大多數時候 Scarlett 和 Violet 會在程式碼庫中分開的部分工作,而他們的變更不會造成衝突。良好的模組化也會增強 Keystone Interface 和 Branch By Abstraction 等技術,以避免需要分支提供的隔離。通常團隊被迫使用原始碼分支,因為缺乏模組化讓他們沒有其他選擇。
功能分支是一個模組化架構的替代方案,它不是建構具備在執行時/部署時輕鬆交換功能的能力的系統,而是將自己與透過手動合併提供此機制的原始碼控制結合在一起。
-- Dan Bodart
支援是雙向的。儘管有許多嘗試,但在我們開始編寫程式之前,要建構一個良好的模組化架構仍然極為困難。為了實現模組化,我們需要在系統成長時持續觀察它,並朝向更模組化的方向發展。重構是實現此目標的關鍵,而重構需要高頻率的整合。因此,模組化和快速整合在一個健康的程式碼庫中相互支援。
這一切都說明了模組化雖然難以實現,但值得努力。這項工作涉及良好的開發實務、學習設計模式,以及從程式碼庫中獲得的經驗。不應僅以可以理解的遺忘慾望來關閉混亂的合併,而應詢問為什麼合併會混亂。這些答案通常會是一個重要的線索,說明如何改善模組化,進而改善程式碼庫的健康狀況,並因此提升團隊的生產力。
關於整合模式的個人想法
我作為一名寫作者的目標不是說服你遵循特定的路徑,而是告知你應該考慮的因素,因為你決定要遵循哪條路徑。儘管如此,我還是會在此加入我對前面提到的模式的偏好。
總體而言,我更喜歡在使用 持續整合 的團隊中工作。我承認背景是關鍵,而且在許多情況下,持續整合並非最佳選項,但我的反應是努力改變那個背景。我之所以有這樣的偏好,是因為我希望處於一個每個人都可以輕鬆重構程式碼庫、改善其模組化、保持其健康狀態的環境中,所有這些都是為了讓我們能夠快速回應不斷變化的業務需求。
現今我比較像一位作家,而非開發人員,但我仍選擇在 Thoughtworks 工作,這是一家充滿支持這種工作方式的人的公司。這是因為我相信這種 極限編程 風格是我們開發軟體最有效率的方法之一,而我想進一步觀察團隊發展這種方法,以提升我們專業的效率。
從主線到產品發布的途徑
主線是一個活躍的分支,定期發布新的和修改的程式碼。保持其健康非常重要,這樣當人們開始新的工作時,他們將從一個穩定的基礎開始。如果它足夠健康,您還可以將程式碼直接從主線發布到生產環境。

將主線保持在始終可發布狀態的哲學是 持續交付 的核心原則。為此,必須具備決心和技能,才能將主線維護為 健康分支,通常使用 部署管道 來支援所需的密集測試。
以這種方式工作的團隊通常可以透過在每個已發布版本上使用標籤來追蹤其發布。但沒有使用持續交付的團隊需要另一種方法。
發布分支
只接受提交以穩定產品版本並準備發布的分支。
典型的發布分支將從當前主線複製,但不允許向其中新增任何新功能。主要開發團隊會繼續將這些功能新增到主線,這些功能將在未來的版本中獲得採用。負責發布的開發人員只專注於移除任何阻止發布準備就緒的缺陷。對這些缺陷的任何修復都會在發布分支上建立,並合併到主線。一旦沒有更多需要處理的錯誤,該分支就準備好進行生產發布。

儘管發布分支上的修復工作範圍(希望)小於新功能程式碼,但隨著時間的推移,將它們合併回主線會變得越來越困難。分支不可避免地會分歧,因此隨著更多提交修改主線,將發布分支合併到主線會變得更加困難。
以這種方式將提交套用到發布分支的一個問題是,很容易忽略將它們複製到主線,特別是因為分歧而變得更加困難。由此產生的回歸非常令人尷尬。因此,有些人 贊成在主線上建立提交,並且只在它們在那裡工作後才將它們挑選到發布分支。

櫻桃採摘是指將一個提交從一個分支複製到另一個分支,但這些分支並未合併。也就是說,只複製一個提交,而不是從分支點以來的先前提交。在此範例中,如果我要將 F1 合併到發行分支,則這將包含 M4 和 M5。但櫻桃採摘只會採用 F1。櫻桃採摘可能無法乾淨地套用於發行分支,因為它可能依賴於 M4 和 M5 中所做的變更。
在主線上撰寫發行修正的缺點是許多團隊發現這樣做比較困難,而且在主線上以一種方式修正,然後在發行發生前必須在發行分支上重新處理,這令人沮喪。當有時間壓力要推出發行時,尤其如此。
一次只在製作環境中有一個版本的團隊只需要一個發行分支,但有些產品會在製作環境中存在許多發行。在客戶套件上執行的軟體只有在客戶希望時才會升級。許多客戶不願意升級,除非他們有令人信服的新功能,因為他們曾被失敗的升級所困擾。然而,這些客戶仍然需要錯誤修正,特別是如果涉及安全性問題。在這種情況下,開發團隊會為仍在使用的每個發行版本開啟發行分支,並根據需要套用修正。

隨著開發的進行,將修正套用於較舊的發行版本會變得越來越困難,但這通常是經營成本。只能透過鼓勵客戶頻繁升級到最新版本來減輕這種情況。保持產品穩定對此至關重要,一旦被困擾,客戶就不願意再次進行不必要的升級。
(我聽過發行分支的其他術語包括:「發行準備分支」、「穩定分支」、「候選分支」和「強化分支」。但「發行分支」似乎是最常見的。)
何時使用
當團隊無法讓主線保持在健康狀態時,發行分支是一個有價值的工具。它允許團隊的一部分專注於必要的錯誤修正,以便為製作環境做好準備。測試人員可以從此分支的頂端拉取最穩定的最新候選版本。每個人都可以看到已對產品進行了哪些穩定化處理。
儘管發行分支有價值,但大多數最佳團隊不會將此模式用於單一製作產品,因為他們不需要。如果主線保持足夠健康,則可以將任何提交到主線直接發布。在這種情況下,發行應標記有公開可見的版本和建置編號。
您可能已注意到我在前一段中加入了笨拙的形容詞「單一製作」。這是因為當團隊需要管理製作環境中的多個版本時,此模式變得至關重要。
當發行過程中存在重大摩擦時,發行分支也可能很方便 - 例如,必須核准所有製作發行的發行委員會。正如 Chris Oldwood 所說「在這些情況下,發行分支更像是一個隔離區,而企業齒輪緩慢轉動」。一般來說,應盡可能從發行過程中移除此類摩擦,就像我們需要移除整合摩擦一樣。然而,在某些情況下,例如行動應用程式商店,這可能是不可行的。在許多情況下,標籤在大部分時間都已足夠,而且只有在來源需要進行一些必要的變更時才會開啟分支。
成熟分支
一個分支,其頭部標記了程式碼庫成熟度層級的最新版本。
團隊通常想知道來源的最新版本是什麼,這項事實可能會因為具有不同成熟度層級的程式碼庫而變得複雜。QA 工程師可能想查看產品的最新暫存版本,偵錯生產故障的人員則想查看最新的生產版本。
成熟度分支提供一種執行此追蹤的方法。其想法是,一旦程式碼庫的版本達到某個準備就緒的層級,就會將其複製到特定分支。
考慮生產的成熟度分支。當我們準備好生產版本時,我們會開啟一個版本分支來穩定產品。一旦準備就緒,我們就會將其複製到長期執行的生產分支。我認為這是複製而非合併,因為我們希望生產程式碼與在上游分支上測試的程式碼完全相同。

成熟度分支的優點之一是,它清楚地顯示了在版本工作流程中達到該階段的每個程式碼版本。因此,在上方的範例中,我們只想要在生產分支上有一個單一提交,結合提交 M1-3 和 F1-2。有一些 SCM 技巧可以達成此目的,但無論如何,這會失去與主線上的細微提交的連結。這些提交應記錄在提交訊息中,以協助人員在稍後追蹤它們。
成熟度分支通常以開發流程中適當的階段命名。因此,有「生產分支」、「暫存分支」和「QA 分支」等術語。偶爾我會聽到有人將生產成熟度分支稱為「版本分支」。
何時使用
原始碼控制系統支援協作和追蹤程式碼庫的歷史記錄。使用成熟度分支可讓人員透過顯示版本工作流程中特定階段的版本歷史記錄,取得一些重要的資訊。
我可以透過查看相關分支的頭部,找出最新版本,例如目前執行的生產程式碼。如果出現我確定之前不存在的錯誤,我可以查看分支上的先前版本,並查看生產中的特定程式碼庫變更。
自動化可以與特定分支的變更結合在一起 - 例如,自動化程序可以在每次對生產分支進行提交時,將版本部署到生產環境。
使用成熟度分支的替代方法是套用標記方案。一旦版本準備好進行 QA,就可以標記為此版本 - 通常會包含建置編號。因此,當建置 762 準備好進行 QA 時,可以標記為「qa-762」,準備好進行生產時,則標記為「prod-762」。然後,我們可以透過在程式碼儲存庫中搜尋與我們的標記方案相符的標籤,取得歷史記錄。自動化也可以根據標籤指派為基礎。
因此,成熟度分支可以為工作流程增加一些便利性,但許多組織發現標記可以完美地運作。因此,我將其視為那些沒有強大優點或成本的模式之一。然而,通常需要使用原始碼管理系統進行此類追蹤,表示團隊的部署管線工具不足。
變異:長生命週期發布分支
我可以將此視為發布分支模式的變體,它將其與候選發布的成熟度分支結合在一起。當我們希望進行發布時,我們將主線複製到此發布分支中。與每個發布分支一樣,提交只會提交到發布分支以提高穩定性。這些修正也會合併到主線中。當發生這種情況時,我們標記一個發布,並且當我們想要進行另一次發布時,可以再次複製主線。

提交可以像在成熟度分支中更典型的那樣複製,或合併。如果合併,我們必須小心,確保發布分支的頭部與主線的頭部完全匹配。執行此操作的一種方法是在合併之前還原已應用於主線的所有修正。有些團隊在合併後也會壓縮提交,以確保每個提交都代表一個完整的候選發布。(發現這很棘手的人有充分的理由更喜歡為每個發布剪切一個新分支。)
此方法僅適用於一次在生產中只有一個發布的產品。
團隊喜歡這種方法的一個原因是,它確保發布分支的頭部始終指向下一個候選發布,而不是必須找出最新發布分支的頭部。然而,至少在 git 中,我們通過在團隊剪切新的發布分支時使用硬重置移動的「發布」分支名稱來實現相同的目的,在舊的發布分支上留下標籤。
環境分支
通過應用原始碼提交來配置產品在新的環境中運行。
軟體通常需要在不同的環境中運行,例如開發人員的工作站、生產伺服器,以及各種測試和分段環境。通常在這些不同的環境中運行需要一些組態變更,例如用於存取資料庫的 URL、訊息系統的位置,以及關鍵資源的 URL。
環境分支是一個分支,其中包含提交,這些提交應用於原始碼,以重新組態產品在不同的環境中運行。我們可能在主線上執行版本 2.4,現在希望在我們的分段伺服器上執行它。我們通過從版本 2.4 開始剪切一個新分支來執行此操作,應用適當的環境變更,重新建置產品,並將其部署到分段環境。

變更通常是手動應用的,儘管如果負責的人員對 git 感到自在,他們可能會從較早的分支中挑選變更。
環境分支模式通常與 成熟度分支 結合使用。長期的 QA 成熟度分支可能包括 QA 環境的組態調整。然後,合併到此分支將選取組態變更。類似地,長期的發布分支可能包括這些組態變更。
何時使用
環境分支是一種有吸引力的方法。它允許我們以任何我們需要的方式調整應用程式,以便為新環境做好準備。我們可以將這些變更保留在一個 diff 中,該 diff 可以挑選到產品的未來版本中。然而,這是 反模式 的經典範例 - 在你開始時看起來很有吸引力,但很快就會導致一個充滿痛苦、龍和冠狀病毒的世界。
環境變更時潛藏的危險在於,當我們將應用程式從一個環境移到另一個環境時,應用程式的行為會有所改變。如果我們無法在開發人員的工作站上執行生產環境中的版本並進行除錯,則會讓問題的修復難度大幅提升。我們可能會引入只會在特定環境中出現的錯誤,而最危險的環境莫過於生產環境。由於存在此一危險,我們希望盡可能確保在生產環境中執行的程式碼與在其他地方執行的程式碼相同。
環境分支的問題在於其極高的彈性,而這正是其吸引人的原因。由於我們可以在這些差異中變更程式碼的任何面向,因此我們可以輕鬆地引入組態修補程式,而這些修補程式會導致行為差異和隨之而來的錯誤。
因此,許多組織明智地堅持一項鐵律,即一旦編譯執行檔,該執行檔就必須是所有環境中執行的執行檔。如果需要變更組態,則必須透過明確的組態檔案或環境變數等機制將其隔離。如此一來,它們可以最小化為在執行期間不會變更的常數設定,讓錯誤滋生的空間變小。
對於直接執行其原始碼的軟體(例如 JavaScript、Python、Ruby),可執行檔和組態之間的簡單分界線很容易變得模糊不清,但原則相同。將任何環境變更保持在最小範圍,且不要使用原始碼分支來套用它們。一般經驗法則為,您應該能夠查看產品的任何版本並在任何環境中執行它,因此任何純粹因為部署環境不同而變更的內容都不應該在原始碼控制中。有一種說法是將預設參數組合儲存在原始碼控制中,但應用程式的每個版本都應該能夠根據動態因素(例如環境變數)需要在這些不同的組態之間切換。
環境分支是使用原始碼分支作為陽春模組化架構的一個範例。如果應用程式需要在不同的環境中執行,則在不同環境之間切換的能力必須是其設計中的一等公民。環境分支可以用作缺乏此一設計的應用程式的權宜機制,但隨後應該優先將其移除,並採用永續的替代方案。
熱修補分支
用於擷取工作以修復緊急生產缺陷的分支。
如果在生產環境中出現嚴重的錯誤,則需要盡快修復。處理此錯誤的工作優先順序將高於團隊執行的任何其他工作,且其他工作不應採取任何措施來減緩此熱修補程式的處理進度。
熱修補程式的工作需要在原始碼控制中進行,以便團隊能夠適當地記錄並協作處理。他們可以透過在最新發布版本中開啟分支,並在該分支上套用熱修補程式的任何變更來執行此操作。

一旦修復套用至生產環境,且每個人都有機會好好睡一晚,則可以將熱修補程式套用至主線,以確保不會在下一版本中發生回歸。如果已為下一版本開啟發布分支,則熱修補程式也需要套用至該分支。如果發布之間的時間很長,則熱修補程式很可能會建立在已變更的程式碼之上,因此合併會比較困難。在這種情況下,揭露錯誤的良好測試真的很有幫助。
如果團隊使用發布分支,則可以在發布分支上執行熱修補程式的工作,並在完成時建立新的發布。這基本上會將舊的發布分支轉換為熱修補程式分支。

與發布分支一樣,也可以在主線上進行熱修補程式,並將其挑選至發布分支。但這種情況較不常見,因為熱修補程式通常是在強大的時間壓力下進行的。
如果團隊進行持續傳遞,則可以直接從主線發布熱修補程式。他們仍然可以使用熱修補程式分支,但他們會從最新的提交開始,而不是從最後發布的提交開始。

我將新版本標記為 2.2.1,因為如果團隊以這種方式工作,M4 和 M5 很可能不會公開新功能。如果公開,則熱修補程式很可能會摺疊到 2.3 版本中。當然,這說明了透過持續傳遞,熱修補程式不需要迴避正常的發布流程。如果團隊有足夠靈敏的發布流程,則可以像正常情況一樣處理熱修補程式,而這正是持續傳遞思維模式的一項重大優點。
一個適合持續交付團隊的特殊處理方式,就是禁止在熱修補完成前提交任何主線。這符合「沒有人比修復主線有更重要的任務」的口號,事實上,這適用於主線發現的任何缺陷,即使尚未發佈到生產環境中。(所以我認為這並非真正的特殊處理。)
何時使用
熱修補通常在壓力很大的時候進行,而當一個團隊壓力最大的時候,最有可能犯錯。在這種情況下,比平常更重要的是使用原始碼控制,並比看起來合理的情況下更頻繁地提交。將這項工作保留在分支上,讓所有人都知道正在採取什麼措施來解決問題。唯一的例外是可直接應用於主線的簡單修復。
這裡更有趣的問題是決定要修復哪個熱門錯誤,以及哪些可以留給正常的開發工作流程。團隊發佈的頻率越高,就能將生產錯誤修復留給常規的開發節奏。在大多數情況下,決策將主要取決於錯誤的業務影響,以及它如何與團隊的發佈頻率相符。
發布列車
在設定的時間間隔內發佈,例如火車按照定期時間表發車。開發人員在完成其功能時選擇要搭乘哪班火車。
使用發佈列車的團隊將設定定期的發佈節奏,例如每兩週或每六個月。根據列車時間表的隱喻,設定團隊將為每個發佈版本建立發佈分支的日期。人們決定他們希望功能搭乘哪班火車,並針對該列車設定其工作目標,在列車載入時將其提交放入適當的分支。一旦火車開出,該分支就是發佈分支,並且只接受修復。
使用每月列車的團隊將根據 2 月的發佈版本,為 3 月開始一個分支。隨著月份的進行,他們將新增功能。在設定的日期,可能是該月的第三個星期三,列車開出,凍結該分支的功能。他們為 4 月列車開始一個新分支,並新增功能到該分支。同時,一些開發人員穩定 3 月列車,並在準備好時將其發佈到生產環境。應用於 3 月列車的任何修復都會挑選到 4 月列車。

發行列車通常與功能分支一起使用。當 Scarlett 感覺她即將完成她的功能時,她將決定要搭乘哪一班列車。如果她認為她可以在 3 月發行時完成,她會整合到 3 月列車中,但如果不行,她會等到下一班列車並在那裡整合。
有些團隊在列車出發前幾天會進行軟凍結(這是硬凍結)。一旦發行列車處於軟凍結狀態,開發人員就不應將工作推送到該列車上,除非他們確信其功能穩定且已準備好發布。在軟凍結後新增並顯示錯誤的功能將會還原(從列車中移除),而不是在列車上修復。
現今,當人們聽到「發行列車」時,他們通常會聽到 SAFe 中的敏捷發行列車概念。SAFe 的敏捷發行列車是一種團隊組織結構,指的是一個大型團隊團隊,共享一個共同的發行列車時程。雖然它使用發行列車模式,但它與我這裡描述的並不相同。
何時使用
發行列車模式的核心概念是發行流程的規律性。如果您事先知道發行列車應在何時出發,您可以規劃您的功能以完成該列車。如果您認為您無法在 3 月列車中完成您的功能,那麼您知道您將搭乘下一班列車。
當發行過程中存在重大摩擦時,發行列車特別有用。一個外部測試小組需要花費數週時間來驗證發行,或者一個發行委員會需要在有新產品版本之前達成共識。如果是這樣,通常更明智的做法是嘗試消除發行摩擦並允許更頻繁的發行。當然,在某些情況下,這可能是幾乎不可能的,例如行動裝置上應用程式商店使用的驗證流程。調整發行列車以符合此類發行摩擦,然後可以充分利用情況。
發行列車機制有助於將所有人的注意力集中在哪些功能應在何時出現,從而有助於預測功能何時會完成。
這種方法的一個明顯缺點是,在列車期間早期完成的功能將坐在列車上看書,等待出發。如果這些功能很重要,那就表示產品錯失了數週或數月的重大功能。
發行列車可能是改善團隊發行流程的一個有價值的階段。如果一個團隊難以進行穩定的發行,那麼直接跳到持續交付可能會跳得太遠。選擇一個合適的發行列車期間,一個困難但可行的期間,可能是第一步。隨著團隊技能的提升,他們可以增加列車的頻率,最終隨著能力的增長而放棄它們以進行持續交付
變異:載入未來列車
功能列車的基本範例是,當前一班列車出發時,一班新列車會抵達月台接載功能。但另一種方法是同時讓多班列車接受功能。如果 Scarlett 認為她的功能無法在 3 月列車中完成,她仍然可以將她大部分完成的功能推送到 4 月列車,並在列車出發前推動進一步的提交以完成它。

在定期間隔,我們從三月列車拉到四月列車。有些團隊偏好在三月列車出發時才這樣做,因此他們只要進行一次合併,但我們這些知道小規模合併會呈指數級容易的人會偏好盡快拉取每個三月提交。
載入未來列車允許正在開發四月功能的開發人員在不干擾三月列車工作的情況下進行協作。它的缺點是,如果四月的人員進行與三月工作衝突的變更,三月工作人員不會收到回饋,因此會讓未來的合併更為複雜。
與主線的常規發布比較
發布列車的主要好處之一是定期將發布內容發布到製作環境。但為新的開發新增多個分支會增加複雜性。如果我們的目標是定期發布,我們可以使用主線同樣順利地達成此目標。決定發布時程,然後從主線的頂端在該時程上切斷一個發布分支。

如果有 發布就緒主線,就不需要發布分支。透過此類定期發布,開發人員仍然有選項可以透過在定期發布日期前不推送到主線,將幾乎完成的功能保留到下一次發布。透過 持續整合,如果人員希望功能等到下一次預定發布,他們可以隨時延後放置關鍵石或保持功能旗標關閉。
發布就緒主線
讓主線保持足夠健康,以便主線的負責人可以隨時直接將其放入製作環境
當我開始撰寫此區段時,我評論說,如果你讓 主線 成為 健康分支,並且讓健康檢查足夠高,那麼你可以在任何時候直接從主線發布,並使用標籤記錄發布。

我花了很多時間描述與這個簡單機制不同的模式,因此我認為現在是時候強調這個機制了,因為如果團隊可以做到,這是一個絕佳的選擇。
儘管提交到主線的每個提交都可以發佈,並不表示應該發佈。這是 持續交付 和 持續部署 之間的微妙區別。使用持續部署的團隊會發佈主線接受的每個變更,但對於持續交付,儘管每個變更都可以發佈,但是否發佈是一項業務決策。(因此,持續部署是持續交付的一個子集。)我們可以將持續交付視為隨時發佈的選項,我們是否行使該選項的決定取決於更廣泛的問題。
何時使用
結合 持續整合 作為 持續交付 的一部分,發佈就緒主線是高績效團隊的常見特徵。鑑於此,以及我對持續交付的眾所周知熱情,您可能會期望我說發佈就緒主線始終優於我在本節中描述的替代方案。
然而,模式都是關於背景的。在一個背景下非常好的模式在另一個背景下可能是一個陷阱。發佈就緒主線的有效性取決於團隊的 整合頻率。如果團隊使用 功能分支,並且通常每月只整合一次新功能,那麼團隊很可能會陷入困境,而堅持發佈就緒主線可能會阻礙他們的改進。困境在於他們無法對不斷變化的產品需求做出回應,因為從構想到生產的週期時間太長。由於每個功能都很龐大,因此他們還可能進行複雜的合併和驗證,從而導致許多衝突。這些衝突可能出現在整合時,或者當他們從主線拉取到其功能分支時,會持續消耗開發人員的精力。這種阻力會阻礙重構,從而降低模組化,進一步加劇問題。
擺脫這個陷阱的關鍵是增加整合頻率,但在許多情況下,在維護發佈就緒主線的同時很難實現這一點。在這種情況下,通常最好放棄發佈就緒主線,鼓勵更頻繁的整合,並使用 發佈分支 來穩定主線以進行生產。當然,隨著時間的推移,我們希望通過改進部署管道來消除對發佈分支的需求。
在高頻整合的背景下,準備好發佈的主線具有明顯的簡潔優勢。無需費心處理我已描述的各種分支的複雜性。甚至熱修復程式也可以應用於主線,然後應用於生產,這使得它們不再特別到足以獲得一個名稱。
此外,保持主線準備好發佈會鼓勵一種有價值的紀律。它將生產準備放在開發人員的首位,確保問題不會逐漸滲入系統,無論是錯誤還是減緩產品週期時間的流程問題。持續交付的完整紀律——開發人員每天多次整合到主線中而不會中斷它——對許多人來說似乎非常困難。然而,一旦實現並成為一種習慣,團隊就會發現它顯著減輕了壓力,而且相對容易跟上。這就是為什麼它是 Agile Fluency® 模型的交付區域的關鍵要素。
其他分支模式
本文的主要重點是討論圍繞團隊整合和生產路徑的模式。但還有一些其他模式我想提一下。
實驗分支
收集對程式碼庫的實驗性工作,預計不會直接合併到產品中。
實驗性分支是開發人員想要嘗試一些想法的地方,但不要指望他們的變更可以簡單地整合回主線。我可能會發現一個新函式庫,我認為它可以很好地替換我們正在使用的函式庫。為了幫助做出是否切換的決定,我啟動一個分支,並嘗試使用它編寫或重寫系統相關部分。練習的目標不是將程式碼貢獻到程式碼庫,而是瞭解新工具在我特定環境中的適用性。我可能會自己這樣做,或與一些同事一起處理。
類似地,我有一個新的功能要實作,並且可以看到幾種方法來處理它。我花幾天時間在每個替代方案上,以幫助我決定選擇哪一個。
這裡的關鍵點是,預期實驗性分支上的程式碼將被放棄,而不是合併到主線中。這不是絕對的——如果我碰巧喜歡結果,並且程式碼可以輕鬆整合,那麼我不會忽視這個機會——但我不期望會這樣。我可能會放鬆一些通常的習慣,減少測試,一些隨意的程式碼重複,而不是嘗試乾淨地重構它。我預計,如果我喜歡這個實驗,我將從頭開始將這個想法應用到生產程式碼中,使用實驗性分支作為提醒和指南,但不會使用任何提交。
一旦我完成對實驗性分支的工作,在 git 中,我通常會新增一個標籤並移除分支。該標籤會保留程式碼行,以防我稍後想重新檢查它——我使用一個慣例,例如以「exp」開頭標籤名稱,以明確其性質。
何時使用
每當我想嘗試某事,但不確定我是否會最終使用它時,實驗性分支就很有用。這樣我就可以隨心所欲地做任何事情,無論多麼古怪,但我有信心可以輕鬆地把它放在一邊。
有時我會認為自己正在進行例行工作,但後來才發現自己所做的其實是一項實驗。如果發生這種情況,我可以開啟一個新的實驗分支,並將我的主要工作分支重設為最後一個穩定的提交。
未來分支
單一分支用於處理過於複雜而無法使用其他方法處理的變更。
這是一種罕見的模式,但偶爾會在人們使用持續整合時出現。有時,團隊需要對程式碼庫進行非常複雜的變更,而用於整合進行中工作的通常技術並不適用。在這種情況下,團隊會執行看起來很像功能分支的動作,他們會切斷未來分支,並且僅從主線拉取,直到最後才進行主線整合。
未來分支和功能分支之間最大的不同在於,只有一個未來分支。因此,在未來分支上工作的成員永遠不會偏離主線太遠,而且沒有其他需要處理的分歧分支。
未來分支上可能會有數位開發人員進行工作,在這種情況下,他們會對未來分支執行持續整合。在執行整合時,他們會先將主線拉取到未來分支,然後再整合他們的變更。這會減慢整合過程,但這是使用未來分支的代價。
何時使用
我必須強調,這是一種罕見的模式。我懷疑大多數執行持續整合的團隊永遠不需要使用它。我曾看過它用於系統架構中特別複雜的變更。一般來說,這是一種最後的手段,只有在我們無法找出如何使用類似 抽象分支 的方法時才使用。
未來分支仍應盡可能保持簡短,因為它們會在團隊中建立一個分區,而就像任何分散式系統中的分區一樣,我們需要將它們保持在絕對的最小值。
協作分支
為開發人員建立的分支,讓他們可以在沒有正式整合的情況下與團隊的其他成員分享工作。
當團隊使用 主線 時,大多數的協作都是透過主線進行。只有在 主線整合 發生時,團隊的其他成員才會看到開發人員正在做什麼。
有時開發人員想要在整合之前分享他們的成果。開啟一個協作分支可以讓他們臨時執行此動作。這個分支可以推送到團隊的中央儲存庫,協作者可以從他們的個人儲存庫直接拉取和推送,或者可以設定一個短暫的儲存庫來處理協作工作。
協作分支通常是暫時的,一旦工作整合到主線中就會關閉。
何時使用
隨著整合頻率降低,協作分支會變得越來越有用。如果團隊成員需要協調對多個人重要的程式碼區域進行某些變更,則長期的功能分支通常需要非正式協作。但是,使用持續整合的團隊可能永遠不需要開啟協作分支,因為他們只有短暫的時間讓彼此看不到自己的工作。唯一的例外是實驗分支,根據定義,它永遠不會被整合。如果多個人共同進行實驗,他們需要讓實驗分支也成為協作分支。
團隊整合分支
允許子團隊在與主線整合之前相互整合。
較大的專案可能有多個團隊在單一邏輯程式碼庫上作業。團隊整合分支允許團隊成員相互整合,而無需使用主線與專案的所有成員整合。
實際上,團隊將團隊整合分支視為團隊內的主線,並與其整合,就像他們整合整個專案主線一樣。除了這些整合之外,團隊還進行一項獨立的工作,以與專案主線整合。
何時使用
使用團隊整合分支的明顯驅動力是程式碼庫由如此多的開發人員積極開發,以至於將它們拆分為獨立的團隊是有道理的。但我們應該對這個假設保持警惕,因為我遇到過許多團隊,這些團隊似乎太龐大,無法全部脫離單一主線作業,但仍能全部管理相同的工作。(我已經收到過多達一百名開發人員的報告。)
團隊整合分支的一個更重要的驅動力是所需整合頻率的差異。如果專案整體預期團隊進行長達數週的功能分支,但子團隊偏好持續整合,則團隊可以設定團隊整合分支,對其進行持續整合,並在完成後將他們正在處理的功能與主線整合。
如果整體專案用於健康分支的標準與子團隊的健康標準之間存在差異,則會產生類似的影響。如果較大的專案無法將主線維持在足夠高的穩定程度,則子團隊可能會選擇在更嚴格的健康層級上作業。同樣地,如果子團隊難以讓其提交對受控良好的主線來說足夠健康,他們可能會選擇使用團隊整合分支,並使用自己的發布分支在進入主線之前穩定程式碼。這不是我通常會偏好的情況,但在特別緊張的情況下可能是必要的。
我們也可以將團隊整合分支視為協作分支的更結構化形式,它基於正式的專案組織,而非臨時協作。
查看一些分支政策
在本文中,我已針對模式討論分支。我這樣做是因為我不想提倡單一分支方法,而是要說明人們執行此操作的常見方式,並反思它們在軟體開發中發現的各種不同脈絡中的權衡。
多年來,已說明許多分支方法。在我嘗試了解它們如何運作以及何時最佳使用時,我已透過心中形成一半的模式對它們進行評估。現在我終於開發並寫下這些模式,我認為檢視其中一些政策並了解我如何根據模式思考它們會很有用。
Git-flow
Git-flow 已成為我遇到的最常見分支政策之一。它是由 Vincent Driessen 在 2010 年編寫的,當時 git 正在流行。在 git 之前,分支通常被視為進階主題。Git 讓分支更具吸引力,部分原因是改進的工具(例如,更好地處理檔案移動),但也是因為複製儲存庫本質上是一個分支,在推回至中央儲存庫時需要對合併問題進行類似的思考。
Git-Flow 使用 主線(稱之為「開發」)在單一「原始」儲存庫中。它使用 功能分支 來協調多位開發人員。鼓勵開發人員使用其個人儲存庫作為 協作分支,以與從事類似工作的其他開發人員進行協調。
git 的傳統核心分支名稱為「master」,在 git-flow 中,master 用作生產 成熟度分支。Git-Flow 使用 發布分支,以便工作從「開發」透過發布分支傳遞到「master」。修正程式透過 修正程式分支 進行組織。
Git-Flow 沒有說明功能分支的長度,因此也沒有預期的整合頻率。它也沒有說明主線是否應該是 健康分支,如果是,需要什麼程度的健康。發布分支的存在表示它不是 可發布主線。
正如 Driessen 今年在附錄中指出的,git-flow 是為多個版本在生產環境中發布的專案而設計的,例如安裝在客戶網站上的軟體。當然,擁有多個實時版本是使用發布分支的主要觸發因素之一。然而,許多使用者在單一生產網路應用程式的背景下採用 git-flow - 在這種情況下,這種分支結構很容易變得比必要的更複雜。
儘管 git-flow 非常流行,因為許多人表示他們使用它,但我們常發現聲稱使用 git-flow 的人實際上正在做一些完全不同的事情。通常,他們的實際做法更接近 GitHub Flow。
GitHub Flow
儘管 Git-flow 真正流行起來,但其分支結構對網路應用程式來說不必要的複雜性促使許多替代方案出現。隨著 GitHub 的普及,其開發人員使用的分支政策成為一個眾所周知的政策 - 稱為 GitHub Flow,這一點並不令人意外。Scott Chacon 對此做出了最佳說明
由於名稱為 GitHub Flow,因此毫不意外它已知是基於 git-flow,並對其做出反應。這兩者之間的本質區別在於不同的產品類型,這意味著不同的背景,因此有不同的模式。Git-Flow 假設產品在生產環境中有幾個版本。GitHub Flow 假設生產環境中有一個版本,並頻繁整合到可發布主線中。在這種背景下,發布分支是不需要的。生產問題的修復方式與一般功能相同,因此不需要Hotfix 分支,因為 hotfix 分支通常表示偏離正常流程。移除這些分支會大幅簡化分支結構,使其成為主線和功能分支。
GitHub Flow 將其主線稱為「master」。開發人員使用功能分支。他們定期將其功能分支推送到中央儲存庫,以支援可見性,但在功能完成之前不會與主線整合。Chacon 指出功能分支可以是單一行程式碼,也可以執行數週。無論哪種情況,此流程都旨在以相同的方式運作。由於是 GitHub,因此拉取請求機制是主線整合的一部分,並使用整合前檢閱。
Git-flow 和 GitHub Flow 經常讓人混淆,因此與這些事情一樣,深入探究名稱的背後意義才能真正了解正在發生的事情。兩者的共同主題是使用主線和功能分支。
基於主幹的開發
正如我之前寫的,我大多數時候聽到「主幹驅動開發」是持續整合的同義詞。但將主幹驅動開發視為 git-flow 和 GitHub Flow 的分支政策替代方案也是合理的。Paul Hammant 撰寫了一個 深入探討的網站 來解釋這種方法。Paul 是我在 Thoughtworks 的長期同事,他曾有過帶著他信賴的 +4 砍刀涉足客戶僵化的分支結構的輝煌紀錄。
主幹驅動開發專注於在 主線(稱為「主幹」,這是「主線」的常見同義詞)上完成所有工作,從而避免任何種類的長期分支。較小的團隊使用 主線整合 直接提交到主線,較大的團隊可以使用短暫的 功能分支,其中「短暫」意味著不超過幾天 - 在實務上可能等同於 持續整合。團隊可以使用 發布分支(稱為「發布分支」)或 發布就緒主線(「從主幹發布」)。
最終想法和建議
從最早的程式開始,人們發現,如果他們想要一個與現有程式略有不同的程式,那麼複製一份原始碼並根據需要調整它是很容易的。有了所有原始碼,我就能夠做出我想要的任何變更。但透過這個動作,我讓我的副本更難接受原始碼中的新功能和錯誤修正。隨著時間的推移,這可能會變得不可能,就像許多企業在他們的早期 COBOL 程式中發現的那樣,並在今天遭受大量客製化 ERP 套件的困擾。即使沒有使用這個名稱,任何時候我們複製原始碼並修改它,我們都在進行 原始碼分支,即使沒有涉及版本控制系統也是如此。
正如我在這篇長文中開頭所說:分支很容易,合併比較困難。分支是一種強大的技術,但它讓我想到了 goto 語句、全域變數和並發鎖。功能強大、易於使用,但更容易過度使用,它們往往會成為對不謹慎和缺乏經驗的人的陷阱。原始碼控制系統可以透過仔細追蹤變更來幫助控制分支,但最終它們只能作為問題的見證者。
我不是那種說分支是邪惡的人。有日常問題,例如多個開發人員貢獻到單一程式碼庫,明智地使用分支是必要的。但我們應該始終對此保持警惕,並記住 Paracelsus 的觀察:有益的藥物和毒藥之間的區別在於劑量。
因此,我對分支的第一個提示是:每當你考慮使用分支時,找出你將如何合併。任何時候你使用任何技術,你都在與替代方案進行權衡。在不了解技術的所有成本的情況下,你無法做出明智的權衡決策,而對於分支來說,當你合併時,吹笛者會向你索取她的費用。
因此,下一個準則:確保你了解分支的替代方案,它們通常更為優異。記住Bodart 定律,是否有方法透過改善你的模組化來解決你的問題?你能改善你的部署管線嗎?標籤夠用嗎?你的流程需要做哪些變更才能讓這個分支不再必要?這個分支很可能確實是目前最好的路線,但它是一個警訊,提醒你有一個更深層的問題應該在未來幾個月內解決。通常,消除對分支的需求是一件好事。
記住LeRoy 的插圖:分支在沒有整合的情況下執行時,會呈指數型分歧。因此,請考慮你整合分支的頻率。目標是將你的整合頻率提高一倍。(這顯然有一個限制,但除非你處於持續整合的區域,否則你不會接近這個限制。)整合的頻率會受到阻礙,但這些阻礙通常正是需要大量使用炸藥才能改善你的開發流程的阻礙。
由於合併是分支中最困難的部分,因此請注意是什麼讓合併變得困難。有時這是流程問題,有時是架構的失敗。無論是什麼,都不要屈服於斯德哥爾摩症候群。任何合併問題,尤其是導致危機的問題,都是改善團隊效率的指標。請記住,只有在你從中學習時,錯誤才有價值。
我這裡描述的模式概述了我和我同事在旅程中遇到的常見分支配置。透過命名、說明,最重要的是,說明它們何時有用,我希望這能幫助你評估何時使用它們。請記住,與任何模式一樣,它們很少是普遍的好或壞,它們對你的價值取決於你所處的環境。當你遇到分支政策(無論是像 git-flow 或基幹開發等眾所周知的政策,還是開發組織中自產的政策)時,我希望了解其中的模式將有助於你決定它們是否適合你的情況,以及哪些其他模式可以融入其中。
致謝
Badri Janakiraman、Brad Appleton、Dave Farley、James Shore、Kent Beck、Kevin Yeung、Marcos Brizeno、Paul Hammant、Pete Hodgson 和 Tim Cochran 閱讀了本文的草稿,並提供意見回饋以改善本文。
Peter Becker 提醒我指出,分叉也是一種分支形式。我從 Steve Berczuk 的軟體組態管理模式中取了「主線」這個名稱。
延伸閱讀
有很多關於分支的書面資料,我無法對所有資料進行嚴肅的調查。但我確實想強調 Steve Berczuk 的書:軟體組態管理模式。Steve 的著作,以及他的貢獻者 Brad Appleton 的著作,對我思考原始碼管理的方式產生了持久的影響。
重大修訂
2021 年 1 月 4 日:新增拉取請求側欄
2021 年 1 月 2 日:將「已審查提交」模式重新命名為「整合前審查」,我認為這個名稱更為清楚
2020 年 5 月 28 日:發布最終章節
2020 年 5 月 27 日:發布查看一些分支政策
2020 年 5 月 21 日:發布協作分支和團隊整合分支
2020 年 5 月 20 日:起草最終想法
2020 年 5 月 19 日:發布未來分支
2020 年 5 月 18 日:發布實驗分支
2020 年 5 月 14 日:發布已發布準備就緒的主線
2020 年 5 月 13 日:起草有關分支政策的部分
2020 年 5 月 13 日:發布發布列車
2020 年 5 月 12 日:發布熱修復分支
2020 年 5 月 11 日:起草已發布準備就緒的主線
2020 年 5 月 11 日:發布環境分支
2020 年 5 月 7 日:發布成熟度分支
2020 年 5 月 6 日:發布發布分支
2020 年 5 月 5 日:發布整合摩擦、模組化的重要性,以及我對整合模式的個人想法
2020 年 5 月 4 日:發布已審查提交
2020 年 4 月 30 日:發布持續整合和功能分支的比較。
2020 年 4 月 29 日:發布持續整合
2020 年 4 月 28 日:草稿:新增模組化部分
2020 年 4 月 28 日:發布整合頻率
2020 年 4 月 27 日:草稿:將生產分支概括為成熟度分支
2020 年 4 月 27 日:發布功能分支
2020 年 4 月 23 日:發布主線整合
2020 年 4 月 22 日:發布健康分支
2020 年 4 月 21 日:發布主線。
2020 年 4 月 20 日:發布第一部分:原始碼分支。
2020 年 4 月 5 日:第五稿:處理發布模式的審查意見,撰寫發布列車,修改原始碼分支。
2020 年 3 月 30 日:第四稿:處理基礎和整合部分的大部分審查意見。將原始碼分支設為模式。
2020 年 3 月 12 日:第三稿:將模式改寫為特殊部分
2020 年 3 月 5 日:第二稿:將文字重新整理為整合模式和生產路徑。新增發布分支和熱修復的插圖,並重新撰寫文字以符合插圖
2020 年 2 月 24 日:第一稿:與審查人員分享
2020 年 1 月 28 日:開始撰寫