Martin Fowler
現代的原始碼控制系統提供了強大的工具,可以非常輕鬆地在原始碼上建立分支。但最終分支還是要合併在一起,許多團隊不得不花相當多的時間去處理相互糾纏的分支。這裡有幾種模式讓團隊可以有效地使用分支,專注於整合多個開發人員的工作並組織產品釋出的路線。最重要的一點,分支應該頻繁整合,盡力保持一個無需過多幹預就可部署生產的健康主線。
對任何軟體開發團隊來說,原始碼都是重要的資產。幾十年來,已有一系列原始碼管理工具被開發出來,用於維護程式碼。這些工具可以跟蹤變更,因此我們可以恢復軟體的歷史版本並檢視它的演進過程。這些工具還是開發團隊的協作中心,團隊中的所有程式設計師都在一個公共的程式碼庫上工作。透過記錄每位開發人員所做的更改,這些系統可以一次跟蹤多行工作內容,並幫助開發人員解決如何把這些內容合併到一起。
將開發活動劃分為分解和合並的工作流,是軟體開發團隊工作流程的核心,並且已演化出多種模式幫助我們處理所有這些活動。像大多數軟體模式一樣,幾乎沒有哪種模式是所有團隊都應遵循的黃金法則。軟體開發工作流程依賴於具體環境,特別是團隊的社會結構和團隊遵循的其他實踐。本文將詳述這些模式,並在模式描述中夾雜可以更好地說明模式背景和相互關係的敘事部分。為便於區分,模式描述的章節將附以圖示“✣”。
在思考這些程式碼分支模式時,我發現它們可以分為兩大類:一類模式著眼於整合,即多個開發人員如何將他們的工作成果組合成一個連貫的整體。另一類則著眼於生產路徑,即使用分支幫助管理從整合程式碼庫到生產環境執行產品的路徑。一些模式為這兩大類模式提供支撐,我將它們歸類為基本模式,在本節中講述。還有一些模式既不基本也不適合于歸類到整合和生產路徑這兩大類模式,我把它們留到最後來講。1.1 源分支✣
建立一個副本並記錄對該副本的所有更改。
如果幾個人在同一程式碼基礎上工作,那麼很快他們就無法在相同檔案上工作。如果我想執行一個編譯,而我的同事還正在敲入一個表示式,那麼編譯將失敗。我們不得不相互呼喊:“我正在編譯,什麼都不要更改!”即使團隊只有兩個人,這也難以維持正常工作;如果是更大的團隊,這種混亂場景會更加令人難以想象。
對此場景案例的簡單解決辦法是讓每個開發人員都獲取一個程式碼庫的副本,然後我們就可以輕鬆地進行自己負責的功能開發。但是又會出現一個新問題:開發完成後,如何將兩個副本再次合併在一起?原始碼控制系統使此過程更加容易。關鍵在於它會將每個分支上所有的更改都記錄為提交。這不僅可以確保沒有人忘記他們對 utils.java 所做的微小更改,而且記錄更改使執行合併更加容易,尤其是當幾個人更改了同一檔案時。這就引出了本文中使用的分支(branch)的定義。我將分支定義為對程式碼庫的特定提交序列。分支的 head 或 tip 指向該序列中的最新提交。分支是個名詞,但也有動詞“建立分支”的意思。這裡我的意思是建立一個新分支,我們也可以將其視為將原始分支分為兩個分支。當來自一個分支的提交被應用到另一分支時,即為分支合併。我用於“分支”的定義與我觀察大多數開發人員談論它們的方式相對應。但是原始碼控制系統更傾向於以特定的方式使用“分支”。以一種常見情況來說明這一點,一個現代開發團隊,該團隊將其原始碼儲存在共享的 git 倉庫中。一名開發人員 Scarlett (以猩紅色表示) 需要進行一些更改,因此她克隆了 git 倉庫並檢出了 master 分支。她做了幾處更改,然後重新提交給她的 master 分支。同時,另一個開發人員,Violet (以紫色表示) 將倉庫克隆到自己桌面上,並簽出 master 分支。那麼 Scarlett 和 Violet 是在同一個分支上工作還是分別在另一個分支上工作?答案是:他們都在 “master” 上工作。但是他們的提交彼此獨立,並且當他們將更改推回到共享倉庫時都需要合併。如果 Scarlett 不確定自己所做的更改,會發生什麼情況,因此她標記了最後的提交,並將她的 master 分支重置回 origin/master(她克隆共享倉庫時的最後一次提交)。根據我前文給出的分支定義,Scarlett 和 Violet 分別在單獨的分支上工作,這兩個分支彼此分開,並且與共享倉庫上的 master 分支隔離。當 Scarlett 放棄帶有標籤的分支開發時,根據定義,它仍然是一個分支(並且她很可能將其視為分支),但是在 git 看來,這是一個帶標籤的程式碼行。使用 git 這樣的分散式版本控制系統,這意味著每當我們進一步克隆倉庫時,就會獲得其他分支。如果 Scarlett 在回家的火車上克隆了自己的本地倉庫到膝上型電腦上,那麼她將建立第 3 個 master 分支。在 GitHub 中派生也會產生相同的效果 —— 每個派生的倉庫都有自己額外的分支集。當我們遇到不同的版本控制系統時,這種術語的混亂會變得更糟,因為它們對分支的構成都有自己的定義。Mercurial 中的分支與 git 中的分支完全不同,後者更接近 Mercurial 的書籤。Mercurial 也可以用未命名的 head 建立分支,使用 Mercurial 的人們經常透過克隆倉庫來建立分支。所有這些術語上的混亂導致一些人避免使用該術語。在這裡更通用的術語是程式碼線(CodeLine)。我將程式碼線定義為程式碼庫的一系列特定版本。它可以以標籤結尾,或是一個分支,又或者淹沒在 git 的 reflog 中。你會注意到我對分支和程式碼線的定義是如此相似。程式碼線在許多方面都是更有用的術語,我確實使用過,但是在實踐中並未廣泛使用。因此,對於本文而言,除非我處於 git(或其他工具)術語的特定上下文中,否則我將交替使用分支和程式碼線。此定義的結果是,無論你使用的是哪種版本控制系統,一旦有開發人員在進行本地更改後,每個開發人員在本地的工作副本中都至少具有一條個人程式碼線。如果我克隆一個專案的 git 庫,檢出 master 分支並更新一些檔案 —— 這就是一條新的程式碼線,即使我還沒有提交任何內容。同樣,如果我從 subversion 庫的主幹建了自己的工作副本,即使不涉及任何 subversion 分支,該工作副本也是獨立的程式碼線。一個老話說,如果你從高樓上摔下來,墜落不會傷害到你,但是著陸會。對原始碼來說也是一樣的道理:建立分支容易,但合併困難。記錄提交中所有更改的原始碼控制系統確實讓合併過程更加容易,但並沒有使合併過程不再重要。如果 Scarlett 和 Violet 都將變數的名稱更改為不同的名稱,則存在衝突,如果沒有人工干預,源管理系統將無法自行處理。為了凸顯這種文字衝突的尷尬,原始碼控制系統至少還可以發現並提醒人們看一下。但是在文字合併沒有問題的地方也經常會出現衝突,系統仍然無法正常工作。想象一下,Scarlett 更改了函式的名稱,而 Violet 向其分支新增了一些程式碼,以其舊名稱呼叫該函式。這就是我所說的語義衝突。當發生此類衝突時,系統可能無法構建,也可能會構建成功但在執行時失敗。Jonny LeRoy 喜歡指出人們(包括我)繪製分支圖的這個瑕疵。任何有平行計算或分散式計算工作經驗的人都熟悉的問題是:當多個開發人員同時更新時,程式碼倉會處於某個共享狀態。我們需要透過將這些更新序列化為某個共識更新的方式,把這些開發人員的更新結合起來 。事實上,使系統正確執行和執行意味著該共識狀態的有效性標準非常複雜,這使我們的任務也變得更加複雜。無法建立確定性演算法來找到共識。人們需要尋求共識,並且共識可能涉及混合不同更新的選擇部分。通常,只有透過原始更新解決衝突才能達成共識。我說:“如果沒有分支該怎麼辦”。每個人都將實時編輯程式碼,考慮不周的更改會使系統崩潰,人們會互相踩踏。因此,我們給個人一種時間凍結的錯覺,認為他們是唯一更改系統的人,這些變更可以等到他們對系統風險考慮充分後才變更。但這是一種錯覺,最終代價還是該來的會來。誰買單?什麼時候?代價是多少?這些模式正在討論的就是:選擇如何支付代價。—— Kent Beck
因此,在下文中我將列出各種模式,這些模式支援友好的隔離,就像當你從高處落下時,風穿過髮絲,同時又把不可避免的與堅硬地面的碰撞後果降到最低。1.2 主線 ✣
單一、共享、代表產品當前狀態的分支。
主線(mainline)是一個特殊的程式碼線,代表團隊程式碼的當前狀態。當我想開始一項新工作,我會從主線中拉取程式碼到我的本地版本庫,在本地版本庫上工作。當我要與團隊的其他成員分享我的工作成果時,我會用我的工作成果更新主線,理想狀態下將應用後面要討論的主線整合模式。
不同的團隊使用不同的名稱稱呼這一特殊分支,通常會受使用的版本控制系統慣例的影響。Git 使用者通常稱之為 “master”, subversion 使用者通常稱之它為 “主幹”。在這裡必須強調,主線是一個單一的、共享的程式碼線。當人們在 git 中談論 “master” 時,他們可能在說幾件不同的事情,因為每個程式碼庫的克隆都有自己的本地 master。通常,團隊會有一個中央倉庫 —— 一個作為專案單一記錄點的共享倉庫,並且是大多數克隆的起源。從頭開始一項新工作意味著克隆該中央倉庫。如果已經有了一個克隆,我會首先從中央倉庫拉取 master 分支,以保持與主線同步。在這種情況下,主線就是中央倉庫的 master 分支。當我在開發自己的功能時,我在使用自己的開發分支,這個分支可以是我本地版本庫的 master 分支,也可以是其他本地分支。如果需要在自己的開發分支上工作較長時間,我可以每隔一段時間拉取主線的更改,並把這些更改合併到我自己的開發分支上,以獲取主線上最新的更改。同樣,如果我想建立產品釋出的新版本,我可以從當前主線開始。如果我需要修復錯誤,以釋出足夠穩定的產品,我可以使用某一發布分支。我記得在 21 世紀初常和一個客戶端構建工程師討論。他的工作是整合團隊正在開發的產品。他會給團隊的每個成員發一封電子郵件,團隊成員則會發回各自程式碼庫中等待整合的各種不同檔案。這位構建工程師就把這些檔案複製到他的整合樹中,並嘗試編譯程式碼庫。建立一個能夠編譯,並可供某種形式進行測試的構建,通常需要耗費這位構建工程師幾周的時間。相比之下,透過主線,任何人都可以從主線的一部分快速開始產品最新的構建。更重要的是,主線不僅僅使得觀察程式碼庫狀態更容易,它還是許多其他模式的基礎,這些模式將後文中描述。在每次提交時執行自動檢查,以確保分支沒有缺陷,自動檢查通常包括構建和執行測試。
由於主線具有共享的並且是已被認可的狀態,因此保持主線處於穩定狀態非常重要。還是在 21 世紀初,我記得曾和某一組織的一個開發團隊一起討論,這個組織因對所有產品執行每日構建而廣為人知。在當時,每日構建被認為是相當先進的做法,這個組織也因此而獲得讚譽。在這些讚揚的文章中沒有提到的是,那些每日構建並不總是成功的。實際上,一些團隊的日常構建連續數月都無法編譯成功,這在當年並不罕見。為了解決這個問題,我們可以努力去保持一個分支是健康的——也就是這個分支是可以成功構建並且執行時幾乎沒有 bug 的。為了確保這一點,我發現編寫自測程式碼是至關重要的。這種開發實踐是指我們在編寫生產程式碼時,還要編寫一套全面的自動化測試,讓我們可以確信,如果這些測試透過,那麼這些程式碼就不會有 bug。如果我們這樣做,就可以透過每次提交執行一個構建來保持分支健康,這個構建過程也包括執行這套測試。如果系統無法編譯,或者測試失敗,那麼我們的第一要務就是在我們對該分支進行任何其他操作之前就先對其進行修復。通常這意味著我們“凍結”了這個分支——除為了修復以使其恢復正常的提交之外,不會允許在這個分支進行任何提交。為了給保持分支健康提供足夠的信心,在測試的程度上存在一定矛盾。許多更徹底的測試需要大量的時間去執行,這就會延遲對提交是否正常的反饋。一些團隊透過將測試分散到部署流水線的多個階段來解決這個問題。這些測試的第一個階段應執行快速,一般不超過十分鐘,但仍應相當全面。我將這樣的測試集稱為提交套件 (不過它通常會被稱為“單元測試”,因為這樣的提交套件中的測試大多數是單元測試)。理想情況下,應在每次提交時執行全方位的測試。但是,如果測試執行很慢,例如需要佔用伺服器幾個小時的效能測試,那就有點不切實際。如今,團隊通常會構建一個提交套件,在每次提交時執行,而對部署流水線後續的階段,會盡可能頻繁地執行。程式碼執行沒有錯誤並不足以說明就是好的程式碼。為了保持穩定的交付節奏,我們需要保持足夠高的程式碼內建質量。一種流行的方法是使用提交稽核(Reviewed Commits),然而我們也要看到還有其他選擇。每個團隊都應當在他們的開發工作流程中明確每個分支的健康狀況標準。保持主線健康有無比重要的價值。如果主線是健康的,那麼開發人員只要從當前的主線拉取程式碼就可以開始新的工作,而不會糾結於那些可能會妨礙他們工作的缺陷。我們經常聽說有人在開始新的工作前要花幾天時間去嘗試修復或繞過他們拉取程式碼中的問題。健康的主線也可以簡化生產路徑。可以隨時從主線的最新版本構建新的生產候選物件。最好的團隊發現他們幾乎不需要做任何工作來穩定這樣的程式碼庫,這些程式碼庫通常能夠直接從主線釋出到生產環境。主線健康的關鍵是自測程式碼,以及一個可在幾分鐘內執行完成的提交套件。建設這樣的能力會是很有意義的投入,一旦我們可以在幾分鐘之內確保我的提交不會搞砸任何東西,那將徹底改變我們的整個開發過程。我們可以更快地進行更改,自信地重構我們的程式碼讓它更好用,並大大減少從期望功能到生產中執行程式碼的交付週期。保持個人開發分支的健康是明智的做法,因為這樣可以啟用差異除錯。但是,這種期望和頻繁提交當前狀態為檢查點是背道而馳的。如果我要嘗試一個不同的路徑,那麼即使編譯失敗可能也會去建立一個檢查點。解決這種矛盾的方法是,一旦完成我最近的工作,就去除所有不健康的提交。這樣,只有健康的提交會在我的分支上保留超過幾個小時。如果我保持個人分支的健康,這也能使提交到主線變得更加容易——我會知道任何在主線整合(Mainline Integration)中突然出現的錯誤都純粹是由於整合問題引起的,而不單單是我程式碼庫中的錯誤。這將使查詢和修復錯誤變得更快也更容易。
分支開發涉及到在管理分離和合並時的相互影響。由於所有人始終使用同一套共享程式碼庫,如果你正在輸入變數名,我這邊就無法編譯程式,這是行不通的。因此,至少在某種程度上,我們需要有一個私有工作區的概念,讓我可以暫時在這個私有工作區裡工作。現代的原始碼控制工具使得建立分支和監視這些分支的變更變得很容易。然而,在某些時候,我們還需要合併分支。考慮分支開發策略實際上就是決定我們合併分支的方式和時機。開發人員透過從主線中拉取、合併,以及(在健康的情況下)推回主線來整合他們的工作。
主線清晰定義了團隊軟體當前的狀態。使用主線的最大好處之一是簡化了整合。如果沒有主線,這就是我前面描述的要與團隊中每個人進行協調的複雜任務。然而,有了主線,每個開發人員都可以自己整合。我將透過一個例子來說明它的工作原理。有一個名為 Scarlett 的開發人員,透過將主線克隆到自己的倉庫中開始某項工作。在 git 中,如果她還沒有中央倉庫的克隆,她將會克隆中央倉庫,檢出 master 分支。如果她已經有了中央倉庫的克隆,她將拉取主線到她的本地 master 分支。然後,她就可以在本地工作,在她的本地 master 分支上進行提交。當她工作的時候,她的同事 Violet 把一些變更推送到了主線上。由於 Scarlett 是在自己的程式碼線上工作,所以當她在做自己的事情時,可以忽略這些變化。在某個時間點,Scarlett 達到了可以整合的程度。第一步,是將當前的主線狀態提取(fetch)到本地主分支中,這將拉取到 Violet 的變更。當她在本地分支工作時,提交將在 origin/master (本地主分支名)上作為一個單獨的程式碼線顯示。現在她需要把她的變更和 Violet 的變更合併起來。有些團隊喜歡透過 merge(合併)來做到這一點,而另一些團隊則喜歡透過 rebase(變基)來實現。通常,人們在談到將分支融合在一起時,無論是實際使用 git merge 還是 rebase 操作,都會使用“merge(合併)”一詞。我將遵循這種用法,因此,除非我實際上正在討論合併和變基之間的區別,否則請考慮將“merge(合併)”作為可以以兩者中任意一個方法實現的邏輯操作。關於是使用普通的合併,還是使用或避免 fast-forward 快速合併,或者是使用 rebase ,另外還有一些其他的討論。這超出了本文的範圍,但是如果人們寄給我足夠多的 Tripel Karmeliet(卡美里特啤酒)的話,我可能會寫一篇關於這個問題的文章,畢竟如今比較流行“投桃報李”嘛。
如果 Scarlett 幸運的話,合併 Violet 的程式碼將是一個清晰的過程,否則,她將會遇到一些衝突。這些可能是文字衝突,大部分原始碼控制系統可以自動處理這些衝突。但是語義衝突更難處理,這就是有“自測程式碼”的方便之處。(由於衝突會產生很多的工作量,而且總是會引入許多工作中的風險,所以我用一塊醒目的黃色來標記它們。)此時,Scarlett 需要驗證合併的程式碼滿足主線的健康標準 (假設主線是一個健康分支)。這通常意味著構建程式碼並執行構成主線提交套件的所有測試。即使這是一個乾淨的合併,她也需要做這些工作,因為儘管是一個乾淨的合併也可能隱藏語義衝突。提交套件中的任何故障都應該完全歸因於這次合併,因為用於合併的兩個父版本都應該是綠色的(譯者注:即沒有故障,在套件中測試透過顯示為綠色)。知道這一點將有助於她追蹤問題,因為她可以檢視差異以尋找線索。透過這個構建和測試,她已經成功地把主線拉到了她的程式碼線,但是——還有一件既重要又常常被人忽略的事——她還沒有完成與主線的整合。要完成整合,她必須將所做的更改推入主線。如果她不這麼做,團隊中的其他人都將與她的變更隔離開來——本質上沒有整合。整合既是拉取也是推送——只有在 Scarlett 把更改推入主線之後,她的工作內容才與專案中的其餘部分整合。現在許多團隊在將程式碼提交新增到主線之前,需要一個程式碼評審的步驟——我稱之為“提交評審”模式,後面會進行討論。
有時候,在 Scarlett 進行推送前,其他人會和主線整合。在這種情況下,她必須再次拉取和合並分支。通常,這只是一個偶然的事件,在不需要任何進一步協調的情況下就可以被解決。我見過長時間構建的團隊使用整合接力棒,這樣只有持有接力棒的開發人員才能整合。但是近年來,隨著構建時間的縮短,我還沒有聽到太多這樣的情況。顧名思義,只有當我們在產品上使用主線時,我才能使用主線整合。使用主線整合的一個替代方法是從主線拉取這些變更,合併到個人開發分支中。這可能是有用的——至少拉取時可以讓 Scarlett 意識到其他人已經整合了變更,並發現她的工作和主線之間的衝突。但是,在 Scarlett 推送上傳之前, Violet 將無法發現她的工作內容與 Scarlett 的變更之間有任何衝突。當人們使用“integrate(整合)”這個詞時,他們往往忽略了一個要點。經常聽到有人說,他們正在整合主線到他們的分支,而實際上他們只是在從主線拉取。我已經學會了對此保持警惕,並進一步確認,看看它們是指拉取還是真正的主線整合。兩者的結果是有很大差異的,所以不要混淆術語是很重要的。另一種選擇是,當 Scarlett 在做的一些工作還沒有準備好與團隊其他成員的工作完全整合,但和 Violet 的有重疊之處,並想和她一起共享。在這種情況下,他們可以開啟一個協作分支。為某個功能特性建立獨立的分支,在該分支上完成與該特性相關的所有工作,在功能特性完成後整合到主線中。
按照特性分支開發這種模式,當開發人員要開始開發某個功能特性時,他們會開啟一個分支,並持續在這個分支上工作直到功能特性完成,然後再與主線整合。例如,讓我們來看下 Scarlett。她領取的是一個給他們的網站中增加本地營業稅集合的功能。她從產品最新的穩定版本開始,從主線拉取到她的本地倉庫,然後從當前主線的頂端建立一個新的分支。不管多久,她會為完成這個功能,在這個本地分支上進行一系列提交。她可能會將該分支推送到專案倉庫,以便其他人可以檢視她的更改。當她在工作時,主線上也會有其他提交。因此,她可能要不時地從主線拉取版本,以便獲知是否有任何改變可能會影響她正在開發的功能。請注意,這不是我們上文說過的整合,因為她沒有推送回主線。在這個點上,只有她在看自己的工作內容,其他人則沒有。一些團隊希望確保所有程式碼都儲存在中央倉庫中,無論這些程式碼是否已被整合。在這種情況下,Scarlett 會將她的特性分支推送到中央倉庫中。這將允許其他團隊成員檢視她正在進行的工作,即使該工作尚未整合到其他人的工作中。
當她完成了這個功能特性的開發後,她將執行主線整合,將這個功能特性整合到產品中。如果 Scarlett 同時進行多個功能特性的工作,那她將為每個特性開啟一個獨立的分支。特性分支開發是如今業界一種流行的模式。要討論何時使用它,我需要介紹它的主要替代方案——持續整合。但是首先我要談談整合頻率的作用。我們進行整合的頻率對團隊的運作有著顯著的影響。《DevOps現狀調查報告》的研究表明,精英開發團隊的整合頻率要比績效低下的團隊高得多 —— 這一觀察結果符合我和眾多業界同行的經驗。我將透過由 Scarlett 和 Violet 為主角的兩個整合頻率的案例來說明這一點。我先從低頻整合的示例開始。在這裡,我們的兩個主人公從克隆主線到各自的本地分支展開工作,然後各自執行了幾個還不願推送的本地提交。當他們工作時,另外有人向主線進行了一個提交。(我不能很快想出另一個人名,那是一種顏色,就叫Grayham?)這個團隊透過保持一個健康分支,並在每次提交後拉取主線程式碼進行團隊協作。Scarlett 的前兩個提交沒有任何新程式碼可拉取,因為當時主線沒有變化,但現在她需要拉取標記為 M1 的程式碼。我用黃色框標記了此次合併。這次是將 S1 到 S3 與 M1 合併。很快,Violet 需要做同樣的事情。這時,兩個開發人員的原生程式碼都已跟上主線的變化,但由於他們的原生程式碼彼此隔離,所以他們尚未彼此整合。Scarlett 不知道 Violet 在 V1 到 V3 的更改。Scarlett 進行了更多的本地提交,準備好了進行主線整合。對她來說,這是一個輕鬆的推送,因為她較早拉取了 M1。而 Violet 的操作則更為複雜。當她進行主線整合時,她現在需要整合 S1..5 與 V1..6。我已經根據涉及的提交個數科學地計算了合併工作量的大小。然而,即使你沒注意到上圖的那些舌狀凸起,你也會意識到 Violet 的合併很有可能比較困難。在前面的示例中,我們兩個多彩的開發人員是在進行了幾個本地提交之後整合的。讓我們看看如果他們在每個本地提交之後進行主線整合會發生什麼。當 Violet 在第一個本地提交後就立即整合到主線時,第一個變更是顯而易見的。由於主線沒有任何更改,因此這就是一個簡單的推送。Scarlett 的第一個提交也需要主線整合,但是由於 Violet 先進行了整合,因此 Scarlett 需要做一次合併。但是由於她只需合併 V1 與 S1,所以合併的工作量很小。Scarlett 的下一個整合是一個簡單的推送,這意味著 Violet 的下一個提交也將需要與 Scarlett 的最近兩個提交合並。但這仍然是一個很小的合併,僅僅是 Violet 的一個提交和 Scarlett 的兩個提交的合併。當有外部的提交推送到主線時,它會按照 Scarlett 和 Violet 正常的整合節奏被提取過來。儘管它與以前發生的情況相似,但整合難度較小。Scarlett 這次只需要將 S3 與 M1 整合在一起,因為 S1 和 S2 已經在主線上了。這意味著 Grayham 在推 M1 之前就必須整合主線上已經存在的內容(S1..2,V1..2)。開發人員繼續進行剩餘的工作,並在每次提交時進行整合。首先,顧名思義,高頻整合意味著做更多的整合——在這個小例子中,後者整合次數是前者的兩倍。但更重要的是,這些整合比低頻例子中的整合要小得多。較小的整合意味著更少的工作量,因為可能引起衝突的程式碼更改會更少。但是比減少工作量更重要的是,它也降低了風險。大規模合併的問題與其認為是處理合併產生的工作量,還不如說是這裡面的不確定性。多數情況下,大規模的合併也會很順利,但有時候,大規模合併會非常非常糟糕。偶爾的痛苦最後會比常態化的痛苦更糟。如果比較兩種情況,一種是每次整合需要額外花費 10 分鐘,另一種是有 1/50 的機率需要花費 6 小時做一次整合修復——我更喜歡哪個?如果僅看花費工作量,那麼 1/50 看起來更好,因為它是 6 小時而不是 8 小時 20 分鐘。但是不確定性使 1/50 的案例變得更加糟糕,這種不確定性會導致整合恐懼。當團隊獲得一些糟糕的合併體驗時,他們往往會更謹慎地進行整合。這很容易變成一種正反饋迴路——像許多正反饋迴路一樣,有著非常消極的後果。(譯者注:正反饋迴路也叫自增強迴路,是一種疊加增強的過程)最明顯的結果是,團隊進行整合的頻率降低了,這會導致更嚴重的合併衝突,而合併衝突會導致更低的整合頻率……從而陷入惡性迴圈。一個更加不易察覺的問題是,團隊會停止執行那些他們認為會使整合變得更加困難的事情。尤其是,這會讓他們抗拒重構。但是減少重構會導致程式碼庫變得越來越不健康,難以理解和修改,從而降低了團隊的功能特性交付速度。由於完成功能特性所需的時間更長,因此進一步增加了整合頻率(譯者注:原文可能有誤,這裡應該是降低整合頻率),從而使這種正反饋環路變得更不堪一擊。這個問題有個反直覺的答案——“如果一件事令人痛苦……那就更頻繁地去做它”
讓我們從另一個角度來看這些頻率之間的差異。如果 Scarlett 和 Violet 在第一次提交時發生衝突,會發生什麼?他們將在何時發現出現了衝突?在低頻的例子中,直到 Violet 最後一次合併,他們才發現衝突,因為那是 S1 和 V1 第一次放到一起。但是在高頻的例子中,在 Scarlett 的第一次合併中就會發現它們。頻繁的整合會增加合併的頻率,但可以降低合併的複雜性和風險。頻繁的整合還可以提醒團隊更快地解決衝突。當然,這兩件事是聯絡在一起的。糟糕的合併通常是團隊工作中隱藏著衝突的結果,只有在進行整合時才浮現出來。比如 Violet 正在看賬單計費功能,並且看到程式碼的編寫者有按一種特定的稅收制度評估稅額。而她的功能特性需要用不同的方式處理稅額,因此最直接的方法是將稅額從賬單的計算中剔除,一會兒再把它作為獨立的功能進行開發。計費功能僅在少數的幾個地方被呼叫,因此使用“ 搬移語句到呼叫者”(譯者注:《重構:改善既有程式碼的設計》8.4 )進行重構很容易——這讓程式在未來的演進中更為合理。然而,Scarlett 不知道 Violet 正在做這件事,她按賬單函式處理稅款的假定實現她的功能特性。自測程式碼是我們的救命稻草。如果我們有一個強大的測試套件,把它作為健康分支的一部分使用,將可以發現那些衝突,從而讓問題進入生產環境的可能性大大降低。但是,即使有強大的測試套件充當了主線的看門人,大規模整合依然令人頭疼。我們需要整合的程式碼越多,發現問題的難度就越大。我們也會有更大的機率遇到各種各樣妨礙執行且難以理解的問題。除了透過較小的提交來降低影響,我們還可以使用“差異除錯”來幫助定位哪一次變更導致問題。很多人沒有意識到的是,原始碼控制系統其實是一種交流工具。它使 Scarlett 可以看到團隊中其他人在做什麼。透過頻繁的整合,她不僅會在出現衝突時立即得到警告,而且她還能更瞭解每個人都在幹什麼,以及程式碼庫是如何演進的。我們不是一個人在衝鋒,而是和團隊在一起工作。增加整合頻率是縮減功能特性大小的重要原因,同時這還有其他優點。功能越小,構建速度越快,投生速度越快,價值交付的啟動也就越迅速。此外,較小的功能特性減少了反饋時間,使團隊可以在更加了解客戶後做出更好的功能決策。
2.4 持續整合 ✣
一旦有可共享的健康提交,開發人員就進行主線整合,這樣的工作量通常是不到一天。
一旦團隊在體驗到高頻整合既高效又輕鬆後,很自然地就會問“我們的整合頻率能有多快?”。特性分支意味著變更集粒度的下限 —— 不可能有比內聚的特性更小的粒度。持續整合為整合提供了一種不同的觸發方式——只要在特性功能開發上取得了大的進展,並且分支仍然健康,就可以整合。我們不指望功能特性已經完整實現,只要對程式碼庫有足夠的修改就行。經驗法則是“每個人每天都要提交到主線”,或者更確切地說,“原生程式碼庫中永遠不要存放超過 1 天未經整合的程式碼”。實際上,大多數持續整合的踐行者每天會多次整合,他們樂於整合 1 小時或更少的工作。要了解更多關於如何有效持續整合的詳細資訊,請檢視我的詳細文章。欲瞭解更多細節,請查閱 Paul Duvall, Steve Matyas 和 Andrew Glover 的著作。Paul Hammant 維護了一個名為trunkbaseddevelopment.com的網站,其中有很多持續整合的技術。
使用持續整合的開發人員需要習慣整合半成品達成頻繁整合的想法。他們還要考慮如何在執行的系統中不暴露半成品來做到這一點。通常這並不複雜——如果我正在實現一個依賴優惠碼的折扣演算法,而這個優惠碼還不在有效列表中,那麼我的程式碼就不會被呼叫,即使已經是生產版本。同樣,如果我新增了一個功能,詢問保險索賠人是否是吸菸者,我可以構建和測試程式碼背後的邏輯,並透過將詢問問題的使用者介面留到構建這個特性的最後一天再做,來確保它不會在生產中被使用。透過最後連線介面對映(Keystone Interface)來隱藏半成品通常是一種有效的技術。如果沒法輕鬆地隱藏掉半成品,我們可以使用特性開關。除了隱藏半成品之外,特性開關還可以有選擇地向其中一部分使用者顯示某一功能特性——這通常便於逐步推出一個新的功能特性。整合半成品尤其會引起那些擔心主線中有錯誤程式碼者的憂心。因此,使用持續整合需要自測程式碼,這樣就有信心把半成品合併到主線,而不會增加出現錯誤的機率。這種方法要求開發人員在編寫功能程式碼時,為半成品編寫測試,並將功能程式碼和測試一起提交到主線中 (或許可以用測試驅動開發)。就原生程式碼庫而言,大多數使用持續整合的人不會想要在單獨的本地分支上工作。通常是直接在本地 master 分支上提交,工作完成後進行主線整合。然而,如果開發人員喜歡的話,開一個特性分支並在上面工作,每隔一段時間就整合回本地 master 分支和主線,那也相當不錯。特性分支開發與持續整合之間的區別,不在於是否有特徵分支,而是在於開發人員何時與主線整合。持續整合是特性分支開發的另一種選擇。兩者之間的權衡值得在本文中用單獨的章節描述,下面將對這兩者進行對比。在 ThoughtWorks 於 2000 年開始使用持續整合時,我們編寫了 CruiseControl, 這是一個守護程式,每當有程式碼提交到主線後,就會自動構建軟體產品。從那時起,許多這樣的工具 (如 Jenkins、TeamCity、Travis CI、Circle CI、Bamboo 等等) 被開發出來。但是大多數使用這些工具的組織都是在提交時自動構建特性分支——這雖然有用,但也意味著這些組織並非真正在實踐持續整合。(還不如叫它們持續構建工具。)因為這樣語義擴散,有些人開始使用 “主幹開發” 一詞來代替“持續整合”。(部分人確實對這兩個術語進行了細微的區分,但是沒有一致的用法)。雖然在語言方面我通常是描述派,但我更喜歡使用 “持續整合”。一部分原因是我不認為試圖不斷提出新術語是對抗語義擴散的可行方法。然而,或許更主要的原因是,我認為改變術語將粗暴地抹殺早期的極限程式設計先驅者們的貢獻,尤其是 Kent Beck 的,他在 20 世紀 90 年代創造並明確定義了持續整合的實踐。
目前,特性分支看起來是業界最常見的分支策略,但是一些實踐者強烈認為持續整合是一種更好的方法。持續整合的主要優勢是支援更高的整合頻率,而且通常是高很多的整合頻率。整合頻率的差異取決於團隊能夠把功能拆分到多小。如果團隊拆分的所有功能特性都可以在一天之內完成,那麼他們既可以實行特性分支開發,也可實行持續整合。但是大多數團隊的特性持續時間都比這更長——特性持續的時間越長,這兩種模式之間的差異就越大。正如我已經指出的那樣,更高的整合頻率可以減少複雜的整合,並減少對整合的恐懼。這通常是一件很難溝通的事情。如果你生活在每隔幾周或幾個月進行整合的世界中,那麼整合很可能是一項令人焦慮的活動。很難相信一天可以進行很多次整合。但整合是可透過加快頻率降低難度的事情之一。這是一種違反直覺的想法——“如果一件事令人痛苦——那就更頻繁地去做它”。整合的規模越小,整合就越不可能變成充滿痛苦和絕望的史詩般的合併。對於特性分支開發,高頻整合鼓勵更小的特性規模:幾天而不是幾周(幾個月根本行不通)。持續整合使團隊可以從高頻整合中受益,同時將特性規模與整合頻率解耦。如果團隊更喜歡一兩個星期的特性粒度,持續整合支援這樣的粒度拆分,同時仍讓團隊獲得最高整合頻率的所有好處。合併規模越小,所需的工作越少。更重要的是,正如我在上文中所解釋的,更頻繁地進行合併可以減少出現極為糟糕的合併的風險,這既消除了這種合併帶來的驚嚇,也減少了合併的整體恐懼感。如果程式碼中出現衝突,則高頻整合會在導致這些討厭的整合問題之前迅速發現它們。持續整合可為團隊帶來極強的效益,以至於有的團隊,有些功能只需幾天完成,還依舊在做持續整合。持續整合的明顯缺點是,缺乏向主線進行最重要的整合的封閉。如果一個團隊不善於保持健康的分支,這不僅是一個對失敗的慶祝(譯者注:慶祝失敗是為了改進),更是一個風險。將一個功能特性的所有提交聚在一起,還可以在後期決定是否在即將釋出的版本中包含一個特性。雖然功能開關允許從使用者角度開啟或關閉功能,但該功能的程式碼仍在產品中。對這一點的擔憂通常會被過分誇大,畢竟程式碼不會太重要,但這確實意味著想要實行持續整合的團隊必須開發一組強大的測試集,以便他們可以確信主線能保持健康,即使每天進行多次整合。有些團隊覺得這種技能是難以想象的,但另一些團隊則認為這不僅是可能的而且遊刃有餘。此先決條件確實意味著,特性分支開發這種方式更適合那些不強制保持健康分支、並且需要用釋出分支在釋出之前穩定程式碼的團隊。雖然合併的規模大小和不確定性是特性分支開發最明顯的問題,但最大的問題可能是特性分支開發遏制重構。定期進行且幾乎沒有衝突的重構最為有效。重構會引入衝突,如果這些衝突沒有被發現並迅速解決,合併就會變得困難重重。因此,重構在高頻整合中效果最好,所以重構作為極限程式設計的一部分流行起來也不足為奇,而且持續整合也是極限程式設計最初的實踐之一。特性分支開發也不鼓勵開發人員做當前特性外的更改,這會破壞團隊的重構能力,影響程式碼庫穩定性的提升。當我遇到有關軟體開發實踐的科學研究時,由於他們的方法學存在嚴重問題,通常我並不買賬。但 《DevOps 現狀調查報告》是一個例外,該報告揭露了軟體交付效能的度量指標,並將其與更廣泛的組織績效度量相關聯,而組織績效又與投資回報率和盈利能力等業務度量指標相關。在2016年,他們首先評估了持續整合,發現它有助於提高軟體開發效能,此後的每項調查中都重影印證了這一發現。我們發現在合併到主幹之前,具有極短生命週期(少於一天)的分支或派生,並且總共少於三個活動分支,是持續交付的重要特徵,並且所有這些都有助於提高績效。每天將程式碼合併到主幹或 master 中也是如此。——《 2016 年 DevOps 現狀調查報告》
使用持續整合並不會消除保持功能粒度小的其他優勢。頻繁釋出小的功能特性可提供快速的反饋週期,從而為改進產品創造奇蹟。許多使用持續整合的團隊還在努力構建產品的分層,並儘可能頻繁地釋出新功能。許多人將特性分支開發的流行歸因於github 和起源於開源開發的拉取請求模型。有鑑於此,有必要了解一下開源工作與許多商業軟體開發之間截然不同的環境。開源專案的結構有許多不同的方式,但是一個常見的結構是一個人或一小群人作為開源專案的維護者,承擔大部分程式設計工作。維護者與更多的開發貢獻者一起工作。維護者通常不瞭解貢獻者,因此對他們貢獻的程式碼的質量一無所知。維護者還不確定貢獻者將在開源專案中實際投入多少時間,更不用說他們的工作成效。在這種情況下,特性分支開發非常有意義。如果有人要新增一個或大或小的功能,而我不知道這項功能什麼時候(或者是否)會被完成,那麼對我來說,等到它完成後再整合是有意義的。另外,更為重要的是要能夠稽核程式碼,以確保它透過我為程式碼庫設定的任何質量門禁。但是許多商業軟體團隊的工作環境截然不同。有一個全職的團隊,他們全都為軟體開發投入大量時間,通常是全職的。專案負責人非常瞭解這些人(除了剛開始的時候),並且可以對程式碼質量和交付能力有可靠的預期。由於他們是帶薪僱員,專案負責人對專案投入的時間,編碼標準和團隊習慣也有更好的掌控。在這迥然不同的環境下,應該清楚地知道,此類商業團隊的分支策略不必與在開源世界運用的分支策略相同。持續整合幾乎不可能適合偶爾為開源工作做出貢獻的人,但是對於商業工作而言,這是一個現實的選擇方案。團隊不應假定那些在開源環境行得通的做法可以自動適應他們與之不同的工作環境。長期以來,程式碼審查一直被推薦用於提升程式碼質量,提高模組化和可讀性,以及消除缺陷。儘管如此,商業機構往往發現很難把程式碼審查融入到軟體開發工作流程中。然而,開源世界廣泛採用了這樣的信念:在專案貢獻被接受納入專案主線之前,應先對其進行評審,並且這種方式近年來在開發組織中廣泛傳播,尤其是在矽谷。這樣的工作流程特別適合 GitHub 的拉取請求機制。類似這樣的流程會在 Scarlett 完成希望被整合的工作內容時開始。一旦她成功完成構建,就要進行主線整合(如果她的團隊有這樣的慣例),但是在推送到主線前,她要先傳送她的提交進行評審。團隊的其他成員,例如 Violet,接著對這個提交進行程式碼稽核。如果她認為提交有問題,會反饋一些意見,然後會有一些反覆,直到 Scarlett 和 Violet 都滿意為止。提交只有在透過評審後才會被納入主線。對提交評審(Reviewed Commits)在開源中越來越受歡迎,它非常適合由提交維護者和臨時貢獻者組成這樣模式的組織。對提交評審使得維護人員可以密切關注任何一個貢獻,也非常適合特性分支開發,因為一個完成的特性清晰地標記出需要程式碼評審的節點。如果您不確定貢獻者是否完成了功能,為什麼還要評審他們的半成品?最好還是等功能完成時再做。這種做法在更大的網際網路公司中也廣泛傳播,Google 和 Facebook 都開發有專用工具支援平滑開展對提交評審。約定及時對提交評審的行為準則非常重要。如果開發人員完成了某項工作,並花了幾天時間進行其他工作,那麼當他們收到返回的評審意見時,他們對被評審工作的印象已經不再清晰。如果被評審的提交是已經完成的功能,這會令人沮喪,但對於部分完成的功能,情況會嚴重得多,因為在確認評審透過之前,工作可能很難進一步開展。理論上,可以結合對提交評審來進行持續整合,而且實踐上也確實是有可能的—— Google 就遵循這個方法。但是,儘管可能,但很難執行,而且相對罕見。對提交評審和特性分支開發是更為常見的組合。將開源軟體和私有軟體開發團隊的需求混為一談就像是當前軟體開發儀式的原罪。
儘管在過去十年中,對提交評審已成為一種流行的做法,但仍有弊端和替代方案。即使做得很好,對提交評審也總是會在整合過程中引入一些延遲,從而導致了更低的整合頻率。結對程式設計提供了持續的程式碼稽核過程,帶來比等待程式碼評審更快的反饋週期。(就像持續整合和重構一樣,結對程式設計是極限程式設計最初的實踐之一)。許多使用對提交評審的團隊並沒有做到足夠迅速。他們能夠提供有價值的反饋往往因為來得太遲而不再有效。那時就會面臨一個令人尷尬的選擇,要麼大量返工,要麼接受能行得通但損害程式碼庫質量的工作。程式碼評審並不侷限於只在程式碼合入主線前進行。許多技術領導者發現在提交後評審程式碼會很有用,當他們發現問題時,就可以及時與開發人員聯絡。重構文化在這裡是非常有價值,做得好可以形成一種社群氛圍,團隊中的每個人都將定期評審程式碼庫中的程式碼並修復他們看到的問題。圍繞對提交評審的利弊權衡主要取決於團隊的社會結構。正如我已經提到的,開源專案通常具有一些受信任的維護者和許多不受信任的貢獻者的結構。商業團隊通常都是全職的,但結構可能相似。專案負責人(類似於一個維護者)信賴一小組(也可能是某個)維護者,並且對團隊其他成員貢獻的程式碼保持警惕。團隊成員可能同時分配到多個專案中,使他們更像開源貢獻者。如果存在這樣的社會結構,那麼對提交評審和特性分支開發將具有很大的意義。但是,團隊在互相具有較高信任度時,通常能找到機制來保持程式碼高質量,且不會增加整合過程的衝突。因此,儘管對提交評審可以是一種有價值的實踐,但並不是通向健康程式碼庫的必要途徑。如果你希望團隊平衡成長,而不過度依賴其最初的領導者時尤其如此。對提交評審的問題之一,是它往往讓整合變得更加麻煩。這是整合阻力(Integration Friction)的一個例子——這些活動讓整合耗時或費力。整合阻力越多,開發人員就越傾向於降低整合頻率。想象某個 (功能不健全的) 組織堅持認為所有對主線的提交都要填寫一份需要耗時半小時的表格。這樣的制度會阻礙人們頻繁整合。無論你對特性分支開發和持續整合的態度如何,審視任何增加這種衝突的東西都是有價值的。任何這樣的衝突都應該被移除,除非它有明顯的增值作用。拉取請求增加了額外的開銷以應對低信任度情景,例如,允許你不認識的人為你的專案做出貢獻。而把拉取請求強加給你自己團隊中的開發人員,就像讓你的家人透過機場安檢進入你家一樣。
手動過程是這裡常見的衝突源,尤其是當涉及與不同組織的協調時。這種摩擦通常可以透過使用自動化流程、加強開發人員培訓 (以消除需求) 以及將步驟推到部署流水線或生產中質量保證的後續步驟來減少。您可以在關於持續整合和持續交付的資料中找到更多消除這種衝突的方法。這種衝突也會在生產的路徑中出現,有著同樣的困難和處理方法。讓人們不願意考慮持續整合的原因之一是,設想他們只在整合阻力嚴重的環境中工作過。如果做一次整合需要一個小時,那麼一天做幾次整合顯然是荒謬的。而如果加入一個團隊,在那裡整合是一個分分鐘可以完成的小事,就會感覺像是一個完全不同的世界。關於特徵分支開發和持續整合優點的許多爭論是混亂複雜的,我懷疑就是因為人們沒有經歷過這兩個世界,因此不能完全理解這兩種觀點。文化因素影響整合阻力——尤其是團隊成員之間的信任。如果我是一個團隊的領導者,而我不信任我的同事會做得很好,那麼我很可能會想要阻止損害程式碼庫的提交。這自然也是對提交評審的驅動因素之一。但如果我在信任同事判斷的一個團隊裡,那麼我可能會更願意接受提交後的審查,或者完全砍掉審查,而去依靠集體重構來解決問題。在這種環境下,我的收穫是消除了提交前評審所帶來的摩擦,從而鼓勵了更高頻率的整合。團隊信任通常是特性分支與持續整合爭論的最重要因素。大多數關心軟體架構的人都會強調模組化對一個行為良好的系統的重要性。如果我面臨著對一個模組化程度較差的系統做一個小的改動,我必須理解幾乎所有的模組,因為即使是一個小的改動也會波及到程式碼庫的許多部分。然而,如果模組化程度好,我只需要理解一兩個模組的程式碼,再理解幾個模組的介面,就可以忽略其他的模組。這種能力能夠減少我需要在理解上花費的精力,這就是為什麼隨著系統的發展,值得在模組化上投入這麼多精力的原因。模組化也會影響整合。如果一個系統有很好的模組,那麼大多數時候Scarlett和Violet會在程式碼庫中分離良好的部分工作,他們的變化不會引起衝突。良好的模組化還可以增強介面對映(Keystone Interface)和抽象分支(Branch By Abstraction)等技術,以避免因分支而產生的對隔離的需求。通常團隊被迫使用源分支,是因為缺乏模組化使他們沒有其他選擇。特性分支是一種低階的模組化方法,它們要透過手動合併把自己結合到提供這種機制的原始碼控制系統裡,而不是構建一個能夠在執行/部署時輕鬆交換特性的系統。
支撐應該是雙向的。儘管做了很多嘗試,但在我們開始程式設計之前,建立一個好的模組化架構仍然是非常困難的。為了實現模組化,我們需要在系統成長的過程中不斷觀察系統,並使其趨向於更加模組化的方向。重構是實現這一目標的關鍵,而重構需要高頻率的整合。因此,模組化和快速整合在一個健康的程式碼庫中是相互支撐的。這都是說,模組化雖然很難實現,但值得努力。這種努力包括良好的開發實踐,學習設計模式,以及從程式碼庫的經驗中學習。混亂的合併不應該只是因為某個可以理解的願望來封閉和遺忘——而是要問為什麼合併是混亂的。這些答案往往會成為如何改進模組化的重要線索,改善程式碼庫的健康狀況,從而提高團隊的工作效率。作為作家,我的目的不是要說服您遵循特定的路線,而是要告訴你,當您決定選擇哪種道路時,應該考慮的因素。儘管如此,我還是在這裡表明我的觀點,我更喜歡前面提到的哪種模式。總的來說,我更喜歡在一個實行持續整合的團隊中工作。我認識到環境是關鍵因素,在很多情況下,持續整合並不是最好的選擇——但我的反應是努力改變這種環境。之所以我有這種偏好,是因為我想工作在這樣一種環境中,每個人都可以輕鬆地不斷重構程式碼庫、提升程式碼庫的模組化程度,保持程式碼庫的健康狀態——所有這些都是為了使我們能夠快速響應不斷變化的業務需求。如今,我更像是一個作家,而不是一個開發者,但我仍然選擇在 ThoughtWorks 工作,這家公司到處都是喜歡這種工作方式的人。這是因為我相信這種極限程式設計風格是我們開發軟體最有效的方式之一,我想觀察團隊進一步發展這種方式,以提高我們這個行業的效率。
主線是一個活躍的分支,定期會有新修改的程式碼加入。保持主線的健康很重要,這樣人們就能在一個穩定的基礎上開始新的開發工作。當它足夠健康時,你也可以直接從主線向生產環境釋出程式碼。這種使主線始終處於可釋出狀態的理念是持續交付的核心原則。要做到這一點,必須具備將主線維護為健康分支的決心和技術手段,通常使用部署流水線來支援所需的密集測試。以這種方式工作的團隊通常可以在每個釋出版本上用標籤來跟蹤他們的釋出。而不實施持續交付的團隊則需要用另一種方法。一個只接受特定提交的分支,這些提交因穩固產品的某個待發布版本而被接受。一個典型的釋出分支會從當前主線複製,但不允許向該分支新增任何新的功能特性。這些新的功能特性會由主力開發團隊繼續向主線新增,並被未來的某個釋出獲取。在釋出分支工作的開發人員則專注於消除那些影響該釋出生產就緒的缺陷。任何對這些缺陷的修復都將在釋出分支上建立併合併到主線。一旦不再有需要修復的缺陷,該分支就可被用於生產釋出。儘管在釋出分支上修復錯誤比開發新的功能特性程式碼工作量小(希望如此),但隨著時間的推移,將這些修復合併回主線將變得越來越難。分支不可避免地會偏離,隨著對主線的提交增多,將釋出分支合併到主線變得越來越難。以這種方式向釋出分支提交有一個問題,就是很容易忽略將這些提交複製到主線,特別是在因分支偏離造成合並越來越難的時候。由此產生的回退令人左右為難。因此,有些人傾向於在主線上建立提交,並且只有當這些提交在主線可以工作時,才會被揀選(cherry-pick)到釋出分支。揀選(cherry-pick)是指將某個提交從一個分支複製到另一個分支,但分支並沒有被合併。也就是說,僅複製一個提交,而不是複製自分支分叉點開始的多個先前的提交。在上圖中的示例中,如果我要將 F1 合併到釋出分支中,就會包括 M4 和 M5。但是透過揀選,可以做到只獲取 F1 的改動。揀選可能無法完全適用於釋出分支,因為它可能依賴於先前提交的更改,如本例中的 M4 和 M5。
在主線上編寫釋出修復的缺點是:許多團隊發現這樣做會更加困難,並且令人沮喪的是,在主線上以某種方式修復後,在釋出之前又不得不在釋出分支上重複同樣的操作。在有釋出進度壓力時,尤為如此。單一生產版本的團隊只需要一個釋出分支,但有些產品在生產使用中有多個釋出版本並存。在客戶的配套裝置上執行的軟體只有在客戶願意時才會被升級。許多客戶不願意升級,除非有必須的新功能,因為他們曾因升級失敗遭受過損失。然而,這些客戶仍然希望修復缺陷,特別涉及到安全問題的缺陷。在這種情況下,開發團隊會為每個仍在使用的釋出保留髮布分支,並根據需要對這些分支進行修復。隨著開發的進行,對舊版本進行修復變得越來越困難,但這通常是開展業務的代價,只能透過鼓勵客戶頻繁升級到最新版本來緩解。為此保持產品穩定至關重要,一旦遇到升級受挫,客戶可能會不願意再做不必要的升級。我聽過的其他釋出分支的術語有:“預釋出分支(release-preparation branch)”、“穩定分支(stabilization branch)”、“候選分支(candidate branch)”和“固化分支(hardening branch)”。但“釋出分支”似乎是最常見的。
當團隊無法保持主線處於健康狀態時,釋出分支是一個有價值的工具。它使團隊的部分成員能夠專注於那些必要的缺陷修復,這些缺陷修復是使釋出達到生產就緒狀態所必須的。測試人員可以從這個分支的頂端拉取最新、最穩定的待發布程式碼。每個人都可以看到為了穩定產品做了哪些改動。儘管釋出分支很有價值,但大多數最優秀的團隊並不會在單件生產(single-production)的產品上採用這種模式,因為他們不需要。如果主線保持足夠健康,那麼任何對主線的提交都可以直接釋出。在這種情況下,應該使用公開可見的版本構建編號( version and build number)標記發行版本。你可能已經注意到,我在上一段中用了一個笨拙的形容詞 “單件生產”。這是因為當團隊需要同時管理多個生產版本時,這種模式變得至關重要。當釋出過程存在顯著的阻力時,釋出分支也可能會很方便,例如,所有生產釋出都需經發布委員會批准。正如 Chris Oldwood 所說,“在這些情況下,釋出分支更像是一個等待公司齒輪緩慢轉動的隔離區”。一般來說,在釋出過程中應該儘可能地消除這種阻力,就像我們需要消除整合阻力一樣。然而,在某些情況下這並不可行,比如移動應用商店。在很多類似情況下,多數時候一個標籤就已足夠,只有當需要對程式碼進行一些關鍵的更改時,才會開啟分支。釋出分支也可以是環境分支(Environment Branch),這取決於使用該模式的關注點。還有一個變體,長期存在的釋出分支(long-lived release branch),我將在稍後介紹。一個分支,其最新引用就是程式碼庫某一級別成熟度的最新版本。
團隊通常希望知道原始碼的最新版本是什麼,但事實可能會因為程式碼庫的成熟度不同而變得複雜。一位質量保證工程師可能希望檢視產品最新的試執行版本,而生產故障的除錯人原則希望檢視最新的生產版本。成熟度分支提供了一種進行這種跟蹤的方式。一旦某個程式碼庫的版本達到一定程度的就緒狀態,它就會被複制到特定的分支中。考慮一個用於生產的成熟度分支。當我們要準備一次生產釋出時,我們會開啟一個釋出分支來穩定製品。一旦準備好,我們就把它複製到一個長期執行的生產分支上。我之所以認為這是複製而不是合併,是因為我們希望生產程式碼與在上游分支上測試的程式碼完全相同。成熟度分支的吸引力之一是,它清楚地顯示了在釋出工作流中達到該階段程式碼的每個版本。因此,在上面的示例中,我們只需要向生產分支進行一次組合了 M1-3 和 F1-2 的提交。這裡也有幾個原始碼控制管理的小技巧(SCM-jiggery-pokery)可以實現這一點,但無論如何,這都丟失了和主線上那些細粒度提交的聯絡。而這些提交應該被記錄在提交訊息中,以幫助人們事後追溯它們。成熟度分支通常以開發流程中的相應階段命名,比如像“生產分支(production branch)”、“試執行分支(staging branch)”和“測試分支(QA branch)”之類的術語。偶爾也聽說人們把生產成熟度分支稱為“釋出分支”。原始碼控制系統支援協作和跟蹤程式碼庫的歷史資訊。使用成熟度分支,人們可以透過顯示釋出工作流程中特定階段的版本歷史來獲取一些重要的資訊。透過檢視相關分支的最新引用,我就可以找到所需的最新版本,比如當前正在執行的生產程式碼。如果出現了一個我確定之前不存在的缺陷,我可以檢視該分支的歷史版本以及生產中特定程式碼庫更改。自動化可以與特定分支上的變更繫結——例如,每當生產分支有新的提交時,自動化流程就可以將版本部署到生產環境中。使用成熟度分支的一個替代方法是採用標籤方案。一旦一個版本為 QA 做好了準備,就可以像這樣對其進行標記——通常是以包含一個構建版本號的方式進行打標籤。因此,當 762 號構建為 QA 做好準備時,它可被打上“qa-762”的標籤;當該構建生產就緒時,它可被標記為“prod-762”。然後,我們可以透過搜尋程式碼庫查詢和我們標籤方案相匹配的標籤,獲得歷史記錄。自動化同樣可以基於標籤分配。因此,儘管成熟度分支可以為工作流程增加一些便利,但許多組織發現打標籤也可以工作得很順暢。所以我認為這是一種沒有很強收益或成本的模式。然而,如果需要使用原始碼管理系統來進行這樣的跟蹤,也標誌著該團隊部署流水線的工具並不完善。我可以把這看作是釋出分支模式的一個變種,它結合成熟度分支一起作為釋出的候選版本。當我們想要做一次釋出時,我們從主線複製到這個釋出分支。就像每一個釋出分支一樣,提交只在釋出分支上進行,以提高穩定性。這些修復也會被合併到主線中。當一個版本釋出時,我們會給它打上釋出的標籤,當想要釋出另一個版本時,我們可以再次複製主線。提交可以像成熟度分支中經典的做法那樣複製,或者合併進來。如果合併進來,我們必須注意讓釋出分支的最新引用和主線的最新引用完全匹配。一種方法是在合併前重置所有已應用於主線的修復。有些團隊也會在合併後限制提交,以確保每個提交都代表一個完整的釋出候選版本。(覺得這很棘手的人會有充分的理由傾向於為每個版本剪一個新的分支)。團隊喜歡這種方式的一個原因是,它可以確保釋出分支的最新引用始終指向下一個釋出候選版本,而不必翻出最近的釋出分支的最新版本。然而,至少在 git 中,我們透過在分支命名中包含 “release” 來實現同樣的效果,這個分支名在團隊切分一個新的釋出分支時,會透過強制重置而移動,並在舊的釋出分支上留下一個標籤。軟體通常需要在不同的環境中執行,例如開發人員的工作站、生產伺服器,以及可能的各種測試和試執行環境。通常,在這些不同的環境中執行需要更改一些配置,例如用於訪問資料庫的 URL、訊息系統的位置以及一些關鍵資源的 URL。環境分支是包含產品配置提交的分支,這些對原始碼修改的提交重新配置產品以在不同的環境中執行。環境分支是用於提交產品配置原始碼的分支,這些配置程式碼使產品在不同的環境中執行。我們可能有一個在主線上執行的 2.4 版,現在我們希望在試執行伺服器上執行。為此,我們從 2.4 版開始剪下一個新分支,適配環境引數,重建產品,然後將其部署到試執行環境。這些更改通常是手工進行,不過要是這些負責人員熟悉 git,他們也可能會從較早期的分支中揀選出這些更改。環境分支模式通常與成熟度分支結合使用。一個長期存在的 QA 成熟度分支可能包括針對 QA 環境的配置調整。合併到該分支中就會獲取這些配置變更。同樣,長期存在的釋出分支可能也會包含這些配置更改。環境分支是一種具有吸引力的方法。它允許我們可以按任何需要的方式調整應用程式,來為一個新環境做準備。我們可以將這些更改保留在一個差異檔案(diff)中,以便於可以被揀選到產品的未來版本中。但是,這是“反模式”的典型示例——開始時看起來很吸引人,但是很快就會走向一個充斥著痛苦、暴力與瘟疫的世界。任何環境的轉移會伴有潛在的風險,那就是當我們把應用程式從一個環境轉移到另一個環境時,它的行為是否發生了變化。如果我們不能把一個在生產環境中執行的版本拿到開發者的工作站上除錯,就會使問題的修復變得更加困難。我們會引入僅在某些環境中出現的缺陷,風險最高的就是生產環境。由於這種風險的存在,我們希望儘可能確保在生產環境中執行的程式碼與其他地方相同。環境分支的問題在於使其吸引人的極度靈活性。由於我們可以在這些差異檔案中更改程式碼的任何方面,因此我們可以輕鬆地引入配置補丁,這些補丁會導致行為變化以及隨之而來的缺陷。因此,許多組織明智地堅持一個鐵律,那就是一旦編譯了可執行檔案,在每個環境中執行的就必須是同一個可執行檔案。如果需要更改配置,則必須透過顯式配置檔案或環境變數之類的機制將其隔離。這樣,可以將它們最小化為簡單的常量設定,這些常量在執行期間不會更改,從而減少了缺陷滋生的餘地。對於直接執行原始碼的軟體(例如JavaScript,Python,Ruby)來說,可執行檔案和配置間的簡單劃分很容易變得非常模糊,但原理仍然適用。使任何環境變化保持最小,並且不要使用源分支來應用它們。一般的經驗法則是,您應該能夠簽出該產品的任何版本,並可以在任何環境中執行它,因此,純粹因不同的部署環境而發生的任何更改都不應置於原始碼管理中。在原始碼管理中儲存預設引數的組合是有道理的,但是應用程式的每個版本都應該能夠根據動態因素(例如環境變數)按需在這些不同的配置之間進行切換。環境分支是使用源分支作為低階的模組化架構示例。如果應用程式需要在不同的環境中執行,那麼在不同環境之間切換的能力就必須成為其設計的首要部分。對於缺乏這種設計的應用來說,環境分支可以作為一種偷工減料的機制,但隨後應該優先考慮用可持續的替代方案來移除它。一旦生產中出現嚴重缺陷,就需要儘快修復。處理此缺陷的工作將比團隊正在進行的任何其他工作具有更高的優先順序,其他任何影響熱修復工作進度的事都不應該做。熱修復工作需要在原始碼控制下完成,以便團隊可以準確記錄和協同工作。透過在最新發布的版本上開啟一個分支,並在該分支上進行熱修復的更改,可以實現這一點。一旦修復被應用到生產中,每個人都有機會睡個好覺,然後,該修補程式就可以應用到主線,以確保已被修復的缺陷不會在下一個版本重現。如果有針對下一版本的釋出分支,修補程式也需要應用到該釋出分支上。如果釋出版本之間的時間跨度大,那麼修補程式很可能需要在已經改動過的程式碼上進行,合併起來就會更麻煩。在這種情況下,那些可以暴露缺陷的優質測試會很有幫助。如果團隊正在使用釋出分支,那麼修復缺陷的工作可以在釋出分支上進行,並在修復完成後建立新的釋出分支。本質上,這將舊的釋出分支轉變成了熱修復分支。與釋出分支一樣,修復也可以在主線上進行,然後再揀選到釋出分支。但這種方式比較少見,因為熱修復通常是在很大的時間壓力下完成的。如果團隊實行持續交付,就可以直接從主線釋出修補程式。他們可能仍然使用熱修復分支,但是他們會從最新的提交開始,而不是從上次釋出的提交開始。在上圖中,我為新的釋出打的標籤是 2.2.1,因為如果團隊採用這種工作方式,M4 和 M5 很可能不會公開新功能。如果確實有新功能,那麼這個修復程式很可能就會被包含在 2.3 版的釋出中。當然,這說明,有了持續交付,修復缺陷不用避開正常的釋出過程。如果一個團隊的釋出過程響應足夠迅速,就可以像正常的釋出一樣處理修復程式——這是持續交付思維的一個重要好處。有一個適合持續交付團隊的特殊處理方式,在修復程式完成之前,禁止對主線進行任何提交。這符合“沒有人有比修復主線更重要的任務”這一準則——實際上,主線上發現的任何缺陷,包括尚未交付生產的缺陷,都是如此。(所以我想這並不是真正的特殊處理方式。)修復程式通常是在壓力很大的情況下完成的,當團隊處於最大壓力之下時,很可能會犯錯誤。在這種情況下,使用原始碼控制,並以比常理更頻繁地提交,比平常更有價值。把修復工作放在一個分支上,可以讓每個人都知道為了解決問題正在做的事。唯一的例外是可以直接應用到主線的簡單修復。這裡更令人感興趣的問題是區分什麼是需要緊急修復的缺陷,什麼是可以留在常規開發工作流程中處理的問題。團隊釋出的頻率越高,就越能將生產缺陷的修復留在正常的開發節奏中。在大多數情況下,如何決定將主要取決於缺陷對業務的影響,以及其與團隊釋出頻率的匹配程度。以固定的時間間隔內釋出,就像火車按確定的時刻表發車一樣。開發人員在完成功能開發後,選擇要趕哪趟火車。
使用釋出火車 (release train) 的團隊會設定一個定期的釋出節奏,例如每兩週或每六個月釋出一次。就像列車時刻表一樣,對於每一次釋出,安排好拉出對應釋出分支的時間。人們決定他們想要某一個功能趕哪列火車,然後以那列火車為工作目標,當火車裝車時將他們的提交放到相應的分支上。一旦火車出發,該分支就是釋出分支,將只接受修復。一個使用月度火車的團隊,會基於 2 月份的釋出,開啟一個 3 月份的分支。他們會隨著時間的推移增加新的功能。在一個設定的日期,或許是當月的第 3 個週三,火車出發——該分支的功能被凍結(feature-freezing)。他們為 4 月的火車開設一個新的分支,並在其中新增新的功能。與此同時,一些開發人員會穩定 3 月的火車,在其就緒後投入生產。任何對 3 月火車的修復都會被揀選到 4 月火車上。釋出火車通常與特性分支開發一起使用。當 Scarlett 意識到何時可以完成自己的功能時,她將決定去搭乘哪趟火車。如果她認為可以趕上 3 月的釋出,她會將該功能整合到 3 月的火車,但如果不能,她就會等下一趟,並向那裡整合。一些團隊在火車發車(即硬凍結)前幾天使用軟凍結。一旦釋出火車處於軟凍結狀態,開發人員就不應該再向該列火車推送工作,除非他們確信他們的功能是穩定的並且是釋出就緒的。任何在軟凍結後被發現有缺陷的功能將被回退(推下火車),而不是在火車上修復缺陷。如今,當人們聽到 “釋出火車” 時,往往聽到的是來自 SAFe 的敏捷釋出火車(Agile Release Train)。SAFe 的敏捷釋出火車是一種團隊組織結構,是指一個大規模團隊,這個大規模團隊中的小團隊共享一個通用釋出火車時刻表。雖然 SAFe 的敏捷釋出火車也使用釋出火車的模式,但和我在這裡描述的不是一回事。
釋出火車模式的一個核心概念是釋出過程的規律性。如果你事先知道釋出火車何時出發,你就可以計劃在這趟火車上要完成的功能。如果你認為不能在 3 月的列車中完成你的功能,你就會知道要趕下一趟。當釋出過程存在明顯阻力時,釋出火車特別有用。比如有一個外部測試組,需要幾周的時間來驗證一個釋出;或者有一個釋出委員會,需要在產品有新版之前達成一致。如果是這種情況,通常更為明智的做法是嘗試消除釋出阻力,並允許更頻繁的釋出。當然,或許在某些情況下,這幾乎是不可能的,例如移動裝置的應用商店驗證過程。配合這種釋出阻力對釋出火車進行調整,可能就可改善這種狀況。釋出火車的機制有助於將所有人的注意力集中在什麼功能應該何時出現上,從而有助於預測功能何時完成。這種方法的一個明顯缺點是,在火車早期完成的功能將坐在火車上一邊看書一邊等待發車(譯者注:這是個有趣的比喻,已經完成的功能被擱置了)。如果這些功能很重要,那就意味著該產品會有幾周或幾月的時間錯失一項重要功能。釋出火車可以成為團隊改進發布過程難能可貴的階段。如果團隊難以進行穩定的釋出,那麼直接跳到持續交付就可能是不切實際的。選擇一個恰當的釋出火車期限,一個困難但又合理的期限,可以是一個好的開始。隨著團隊技能的提高,他們可以增加發車頻率,最終隨著能力的增長,放棄釋出火車,轉而進行持續交付。功能火車的基本例子是在前一列火車出發的同時,有一列新的列車到達站臺接收功能。但還有另一種方法是讓多個列車同時接收功能。如果 Scarlett 認為她的功能無法在 3 月的火車出發前完成,她還可以將她基本完成的功能推送到 4 月的火車上,並在 4 月的火車出發前推送更多的提交直到功能完成。我們定期從 3 月的火車拉取改動到 4 月的火車。有些團隊更喜歡只在 3 月的火車出發時做這件事,這樣他們就只需要一次合併。但是我們當中那些知道小規模合併會倍加容易的人,更願意儘快把 3 月的提交拉到 4 月的火車上。承載未來的火車可以讓那些正在開發 4 月功能的開發人員在不會干擾 3 月火車工作的情況下進行協作。它的缺點是,如果開發 4 月功能的人員做出了與 3 月工作相沖突的更改,3 月的人員得不到任何反饋,從而會使未來的合併更加複雜。釋出列車的主要好處之一是有規律的生產釋出節奏。但為新開發設定多個分支會增加複雜性。如果我們的目標是定期釋出,我們同樣可以使用主線來實現。確定釋出計劃是什麼,然後按預定時間從主線頂端分一個釋出分支。如果有一個釋出就緒的主線(Release-Ready Mainline),就不再需要釋出分支。使用像這樣有規律的釋出,如果剛好在定期釋出日期之前,開發人員仍然可以選擇不向主線推送接近完成的功能從而將其延期到下一次釋出。在持續整合時,如果人們希望將某個功能推遲到下一期,他們總是可以延遲放置服務對映或保持功能特性開關處於關閉狀態。保持主線足夠的健康,以便於主線的最新引用總是可以直接投入生產
我在“從主線到生產釋出的路徑”這一章內容開始的時候曾說過,如果你使主線成為一個健康分支,並且讓健康檢查足夠先進,那麼你就可以如願地直接從主線釋出,並使用標籤記錄釋出版本。我已經花了很多時間來描述可替代這種簡單機制的模式,所以我認為是時候強調這一點了,因為,如果一個團隊能夠做到的話,它會是一個絕佳的選擇。僅僅因為主線的每一次提交都是可釋出的,並不意味著它就應該被髮布。這就是持續交付和持續部署之間的微妙區別。使用持續部署的團隊確實會釋出每一個被接受到主線的變更,但在用持續交付的情況下,雖然每一個變更都是可以釋出的,但是否釋出是一個業務決策。(因此,持續部署是持續交付的一個子集。) 我們可以認為持續交付給我們提供了隨時釋出的選擇權,我們是否行使這一選擇權則取決於更廣泛的議題。連同作為持續交付一部分的持續整合,主線釋出是高績效團隊的一個共同特徵。考慮到這一點,以及我對持續交付眾所周知的熱忱,你可能會期望我說,與我在本章中所描述的替代方案相比,主線釋出總是更優的選擇。然而,模式都是和環境相關的。在一種環境下出彩的模式,可能在另一個環境中卻是陷阱。主線釋出的有效性受團隊的整合頻率制約。如果團隊使用特性分支開發,通常每個月只整合一次新功能,那麼,團隊很可能處於一個糟糕的境地,並且對主線釋出的堅持可能是他們改進的障礙。這種糟糕處境就是他們無法響應不斷變化的產品需求,因為從想法到生產的週期太長。因為每個功能都很大,他們還可能會有複雜的合併和驗證,從而導致了很多衝突。這些可能會在整合時表現出來,或者在開發人員從主線拉到他們的特性分支時,對他們造成持續的消耗。這些累贅阻礙了重構,從而降低了模組化,使問題更加嚴重。擺脫這個陷阱的關鍵是增加整合頻率,但在許多情況下,可能很難在維持主線釋出的同時實現這點。在這種情況下,最好放棄主線釋出,鼓勵更頻繁的整合,並使用釋出分支來穩定生產的主線。當然,隨著時間的推移,我們希望透過改進部署流水線來消除對釋出分支的需求。在高頻整合環境中,主線釋出具有明顯的簡單性優勢。無需煩惱於我所描述的各種分支的複雜性。即使是熱修復也可以應用於主線,然後釋出到生產中,使它們不再特殊到值得關注。此外,保持主線釋出就緒可以推行一種寶貴的紀律。它使得生產準備工作在開發人員的意識中保持最高優先,確保問題不會逐漸蔓延到系統中,無論是以缺陷還是流程問題的形式,從而降低產品的週期時間。對於許多人來說,完整的持續交付原則“開發人員每日多次整合到主線而不破壞它”似乎非常困難。然而,一旦實現併成為一種習慣,團隊會發現它能顯著減輕壓力,並且相對容易跟上。這就是為什麼它是敏捷流暢度®模型(Agile Fluency® Model)中交付區域的關鍵元素。
本文的主旨是圍繞團隊整合的模式和生產的路徑進行討論。但還有一些其他的模式我也想提一下。把一個程式碼庫上試驗性的工作收集在一起,這些工作不希望直接合併到產品中。
試驗分支是開發人員希望嘗試一些想法的地方,但並不希望他們的變化會被簡單地整合回主線。我要是發現了一個新的庫,我認為它可以很好地替代我們已經在使用的庫。為了有助於決定是否要切換,我開啟一個分支,然後就嘗試用這個庫來編寫,或者重寫系統的相關部分。練習的目的不是為了向程式碼庫貢獻程式碼,而是為了瞭解一個新工具在我的特定環境中的適用性。我可以自己做這項工作,也可以和一些同事一起做。類似的,我有一個新的功能要實現,可以看到有幾種方法來處理它。我花了幾天的時間來研究每一種選擇,以幫助我決定選擇哪一種。這裡的關鍵點是期望試驗分支上的程式碼會被放棄,而不是被合併到主線。這並非絕對,“如果剛好我喜歡試驗的結果,而且程式碼可以很容易地整合,那麼我不會對這樣的機會視而不見”,可我不指望會是這樣的情況。我可能會放鬆一些慣常的習慣,比如減少測試、寫的一些亂糟糟重複的程式碼,而不是試圖整潔地重構進去。如果我喜歡這個試驗,我會從頭開始,把這個想法應用到生產程式碼中,用試驗分支作為提醒和指導,但不使用任何試驗分支上的提交。一旦我完成了一個試驗分支的工作,在git 中,我通常會新增一個標籤,然後刪除該分支。這個標籤可以保留程式碼基線,以備我以後重新檢視——我使用一個慣例,比如以 "exp "開頭的標籤名,以明確其性質。每當我想嘗試某件事情而又不確定最終是否會用到它時,試驗分支就很有用。這樣我就可以做任何我喜歡的事情,不管多麼癲狂,但我有信心可以輕鬆地把它放到一邊。有時我會以為自己在做常規的工作,但意識到我在做的其實是一個試驗。如果發生這種情況,我可以開啟一個新的試驗分支,並將我的主工作分支重置為最後的穩定提交。一個單獨的分支,用於處理因侵入性過強而難以用其它方法處理的變更。
這是一種罕見的分支模式,當人們使用持續整合時,才會偶爾會出現這種情況。有時,團隊需要對程式碼庫進行非常具有侵入性的修改,而整合在製品的通用技術並不適用此修改。在這種情況下,團隊所做的事情看起來很像特性分支:他們建了一個只從主線中提取的未來分支,直到最後才進行主線合併。未來分支與特性分支的最大區別在於,未來分支只有一個。因此,從事未來分支工作的人從不會偏離主線太遠,而且他們也沒有其他不同的分支需要處理。可能有幾個開發人員在未來分支工作,在這種情況下,他們會與未來分支做持續整合。在進行整合時,他們首先會從主線拉取程式碼到未來分支,然後再整合他們的更改。這將減緩整合程式,但這也正是使用未來分支的成本。我要強調,這是一種罕見的模式。我懷疑大多數進行持續整合的團隊永遠都不需要使用它。我曾見過它用於對系統中的架構進行特別有侵入性的變更。一般來說,這是最後的手段,只有當我們無法想出如何使用類似於抽象分支的方法時才使用。未來的分支仍然應該儘可能小,因為它們在團隊中建立了一個分割槽,並且就像在任何分散式系統中的分割槽一樣,我們需要將它們保持在一個絕對最小值的水平。一個為開發人員在沒有正式整合時和團隊其他成員共享工作而建立的分支。
當一個團隊使用主線時,那麼大多數協作都是透過主線進行的。只有在發生主線整合時,團隊的其他成員才會看到開發人員正在做什麼。有時,開發人員想在整合之前共享他們的工作。開啟一個分支供協作者使用,可以讓他們在臨時性的基礎上做到這一點。分支可以被推送到團隊的中心倉庫,協作者可以直接從他們的個人倉庫中拉取和推送,或者建立一個短期儲存的倉庫來處理協作工作。一個協作分支通常是臨時的,一旦工作整合到主線中就會被關閉。隨著整合頻率的降低,協作分支變得越來越有用。如果團隊成員需要配合修改對幾個人都很重要的一塊程式碼,長期存在的特性分支經常需要非正式的協作。然而,使用持續整合的團隊可能永遠不需要開啟協作分支,因為他們只有很短的時間彼此看不到他們的工作。主要的例外是一個試驗分支,從定義上講,它永遠不會被整合。如果幾個人一起做一個試驗,他們需要使試驗分支也是一個協作分支。大型專案可能有幾個團隊在一個邏輯上獨立的程式碼庫上執行。團隊整合分支允許團隊成員彼此整合,而不必使用主線與專案的所有成員整合。實際上,團隊將團隊整合分支視為團隊內部的主線,像和整個專案的主線整合一樣與之整合。除了這些整合之外,團隊還要執行一項單獨的工作去和專案主線整合。使用團隊整合分支顯而易見的驅動力是由於有太多開發人員活躍在程式碼庫上進行開發,這些人員多到有必要拆分成單獨的團隊。但我們也要對那種假設持謹慎態度,因為我遇到過許多團隊,他們看起來規模太大了,以至於無法全部在一條單獨的主線上工作,然而還是設法做到了(我曾收到過上百位開發人員這樣工作的報告。)團隊整合分支的一個更重要的驅動因素是在整合頻率期望上的差異。如果專案總體上期望團隊做長達幾周的特性分支,但是子團隊更喜歡持續整合,那麼團隊可以建立一個團隊整合分支,使用它進行持續整合,一旦他們正在做的特性完成後就把它整合到主線。如果整個專案對健康分支採用的標準與子團隊的健康標準之間存在差異,也會產生類似的效果。如果更大範圍的專案不能保持主線處於足夠高的穩定程度,子團隊可能會選擇在更嚴格的健康水平下執行。同理,如果子團隊很難讓自己的提交對於控制良好的主線來說足夠健康,他們可能會選擇使用團隊整合分支,在進入主線之前使用自己的釋出分支來穩定程式碼。這不是我通常贊成的情況,但在特別焦慮的情況下可能是必要的。我們還可以將團隊整合分支看作是一種更結構化的協作分支形式,它基於正式的專案組織而不是臨時協作。
在這篇文章中,我從模式的角度來談論分支。之所以這樣做,是因為我並不願鼓吹某種最佳分支方法,而是想闡述人們常用的方法,並在各種不同環境下的軟體開發中,反思這些方法的取捨。多年來,我們已經描述了很多分支方法。當我試圖瞭解它們是如何工作的,以及什麼時候使用它們最好時,我透過我腦海中半成形的模式來評估它們。現在我終於思考成熟並寫下了這些模式,我認為考慮這些策略中的一部分是有價值的,看看我是如何從模式角度來思考它們的。Git-flow 已成為我遇到的最常見的分支策略之一。Git-flow 是在 2010 年由 Vincent Driessen 寫的,當時 git 越來越受歡迎。在 git 出現之前,分支通常被視為一個超前的話題。Git 使分支變得更有吸引力,一方面是因為工具的改進(例如可以更好地處理檔案移動),另一方面是因為克隆一個倉庫本質上就是新建了一個分支,並且在推送回中心倉庫時,同樣需要考慮合併的問題。Git-Flow 在獨立的“origin”倉庫中使用主線(命名為“develop”)。它使用特性分支開發協調多個開發人員,鼓勵開發人員使用他們的私有倉庫作為協作分支,與從事類似工作的其他開發人員協調工作。Git 的核心分支傳統上稱之為“master”,在 git-flow 中,master 被用作生產成熟度分支。Git-Flow 使用釋出分支模式,透過釋出分支,"develop"上的工作被傳遞到 master 分支。補丁程式透過熱修復分支處理。Git-Flow 並沒有說明特性分支的存續時長,因此也沒有說明預期的整合頻率。它也沒有說明主線是否應該是一個健康分支,如果應該,主線應保持在哪個等級的健康水平。而釋出分支的存在,意味著它不是一個釋出就緒主線模式。正如 Driessen 在今年的補充說明中指出的那樣,git-flow 是為那些生產中有多個釋出版本的專案而設計的,例如安裝在客戶處的軟體。當然,擁有多個在用版本是使用釋出分支的主要動因之一。然而,許多使用者在開發單一生產版本的 web應用時選擇選擇了git-flow —— 此時,這樣的分支結構很容易帶來不必要的複雜度。儘管從某種意義上來說 git-flow 非常流行,因為很多人都聲稱他們在使用 git-flow。但經常會發現那些聲稱自己在使用 git-flow 的人實際上在做一些完全不同的事情。通常,他們的實際做法更接近於 GitHub Flow。雖然 Git-flow 確實很流行,但對 web 應用來說,其分支結構過於複雜,這催生了大量競品。隨著 GitHub 的流行,其開發人員使用的分支策略 GitHub Flow 成為眾所周知的策略,也就不足為奇。對 GitHub Flow 的最佳描述來自 Scott Chacon。有 GitHub Flow 這樣的名稱,毫無疑問,它是基於 git-flow,並有所變化的。兩者之間的本質區別在於產品的種類不同,這意味著不同的環境以及不同的模式。Git-Flow 假定一個產品有多個生產版本。GitHub Flow 假設一個產品只有一個生產版本,並以高頻次整合到釋出就緒主線上。在這種情況下,不需要釋出分支。生產問題的修復方式也與常規功能的開發方式相同,也就不需要熱修復分支,從某種意義上講,熱修復分支通常意味著與正常流程的偏差。去除這些分支帶來極大的簡化,變成了一個主線和多個特性分支的分支結構。GitHub Flow 將其主線稱為“master”。開發人員使用特性分支模式工作,他們定期將自己的特性分支推送到中心倉庫,以支援可見性,但直到功能特性完成,才會與主線整合。Chacon 表示特性分支可以是一行程式碼,也可以是需要持續數週的工作。無論哪種情況下,該過程均以相同的方式進行。使用 GitHub,拉取請求機制是主線整合的一部分,並應用對提交評審(Reviewed Commits)。Git-flow 和 GitHub Flow 經常會被混淆,因此像往常一樣,對這些東西的研究要比名稱更深入,才能真正瞭解是怎麼回事。兩者共同的主題思想是使用主線和特性分支。正如我在前面所寫的,我大多數時候聽到的“主幹驅動開發”是持續整合的代名詞。但是將主幹驅動開發視為 git-flow 和 GitHub Flow 的分支策略替代方案也是合理的。Paul Hammant 寫了一個深入詳盡的網站來解釋這種方法。Paul 是我在 ThoughtWorks 的一位老同事,他有一個可信賴的記錄,佩戴著他+4級可靠的砍刀跋涉過一個客戶僵化的分支結構。基於主幹的開發專注於在主線(也稱為“主幹”,這是“主線”的常見同義詞)上進行所有工作,從而避免任何形式的長期分支。較小的團隊使用主線整合模式直接向主線提交,較大的團隊可能會使用短期的特性分支,其中“短”意味著不超過幾天——這大概相當於實踐中的持續整合。團隊可能使用釋出分支(用於釋出的分支)或釋出就緒主線(從主幹釋出)。
從最早的程式設計開始,人們就發現,如果他們想要一個與現有程式有些不同的程式,很容易拿一份原始碼,複製後根據需要進行調整。有了所有的原始碼,我就可做出我想要的任何改變。但是這樣做,讓我的副本很難接受原始來源中的新功能和錯誤修復。隨著時間的流逝,這或許會變為不可能,就如同許多企業在其早期的 COBOL 程式中發現了這些問題,並在如今廣泛定製的 ERP 軟體包中遭受影響。無論是否使用源分支這個名字,只要我們複製程式碼並對其進行修改,我們就在應用這種模式,即使沒有用到任何版本控制系統。正如我在長篇文章開始時所說的那樣:分支很容易,合併卻比較困難。分支是一項強大的技術,但它使我想到了 goto 語句、全域性變數和併發鎖。功能強大,易於使用,但更容易過度使用,它們常常成為那些粗心和缺乏經驗者的陷阱。原始碼控制系統可以透過仔細跟蹤變更來幫助控制分支,但最終它們只能充當問題的見證者。我不是說分支是有害的。有些日常的問題,例如多個開發人員對單個程式碼庫做出貢獻,在這種情況下,明智地使用分支至關重要。但是我們應該始終保持警惕,並記住 Paracelsus(譯者注:瑞典科學家 Paracelsus 被認為是16世紀“毒理學之父”)所指出的,“劑量的不同區分藥物和毒物”。因此,我進行分支的第一個提示是:每當你考慮使用分支時,都要弄清楚你要如何合併。無論何時,你使用任何技術,都是在權衡其他選擇。在不瞭解某項技術所有成本的情況下,你無法做出明智的決策,對於分支,在你合併時,吹笛者會收取費用(譯者注:吹笛者收取費用隱喻付出成本)。因此,下一個準則是:確保你瞭解分支的替代方案,它們通常是更好的選擇。記住 Bodart 的法則(譯者注:參見本文整合模式篇“模組化的重要性”),有沒有辦法透過提高模組化來解決您的問題?你可以改善部署流水線嗎?是否一個標籤就足夠了?你對流程進行哪些更改會使該分支變得不必要?實際上,這個分支就目前來說很可能是最好的解決方案——但卻是一種壞味道,警醒著你在未來幾個月內,還有一個更深層的問題應當解決。擺脫對分支的需求通常是一件好事。請記住LeRoy的插圖:分支在沒有整合的情況下執行時會以指數方式偏離。因此,請考慮你整合分支的頻率。目標旨在使您的整合頻率提高一倍。(這裡顯然有一個限制,但是除非你處於持續整合的地帶,否則你不會接近它。)更頻繁的整合會有障礙,但是這些障礙往往正是需要給予過量炸藥才能改善你的開發過程的。由於合併是分支的難點,因此要注意是什麼導致了合併困難。有時是一個流程問題,有時是架構的缺點。無論是什麼,都不要屈服於斯德哥爾摩綜合症(Stockholm Syndrome)。任何合併問題,尤其是引起危機的問題,都是提高團隊效率的標誌。請記住,只有從錯誤中學習,錯誤才有價值。我在這裡描述的模式概述了我和同事們旅行中遇到的常見分支配置。透過命名它們,進行解釋,最重要的是,解釋它們何時有用,我希望可以幫助你評估何時使用它們。請記住,與任何模式一樣,它們很少有普遍的好壞之分——它們對你的價值取決於你所處的環境。當你遇到分支策略時(無論是像git-flow還是基於主幹開發這樣眾所周知的策略,還是在開發組織中自行開發的東西),我希望瞭解其中的模式能幫助你確定它們是否適合您的情況,以及加入哪些其它模式會有幫助。感謝巴德里·賈納基拉曼(Badri Janakiraman)、布拉德·阿普爾頓(Brad Appleton)、戴夫·法利(Dave Farley)、詹姆斯·肖爾(James Shore)、肯特·貝克(Kent Beck)、凱文·楊(Kevin Yeung)、馬科斯·布里澤諾(Marcos Brizeno)、保羅·哈曼特(Paul Hammant)、皮特·霍奇森(Pete Hodgson)和蒂姆·科克倫(Tim Cochran)閱讀了本文的草稿,並給了我有關如何改進它的反饋。彼得·貝克爾(Peter Becker)提醒我並指出,派生(forks)也是一種分支形式。我使用了 Steve Berczuk 的《軟體配置管理模式》中“主線”這個名字。關於分支有很多資料,我無法詳盡調查所有資料。但是,我確實想強調一下 Steve Berczuk 的書:《軟體配置管理模式》。Steve 的著作以及他的貢獻者布拉德·阿普爾頓(Brad Appleton)的著作,對我就原始碼管理的看法產生了深遠的影響。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日:開始撰寫
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31558019/viewspace-2721279/,如需轉載,請註明出處,否則將追究法律責任。