引言#
版本控制系統(Version Control Systems)不僅用於維護應用程式的完整變更歷史(包括原始碼、文件、資料庫定義、建置腳本、測試等),還扮演著另一個關鍵角色:讓團隊能夠在維護單一權威程式碼庫的同時,各自在應用程式的不同部分協同工作。
當團隊規模超過少數幾位開發者,多人同時在同一版本控制庫中全職工作便開始困難——彼此不小心破壞對方的功能。本章旨在探討團隊如何有效地運用版本控制。
本章涵蓋三個主要面向:
- 分支與合併(Branching and Merging)——版本控制中最具爭議性的議題
- 分散式版本控制系統(DVCS) 與 串流式版本控制系統(Stream-based VCS)——避免傳統工具問題的現代典範
- 分支模式(Branching Patterns)——實務中使用或避免分支的各種策略
部署管線(Deployment Pipeline)是將程式碼從 check-in 送到 production 的受控方式,但它只是大型軟體系統三個自由度之一。本章與上一章分別處理另外兩個維度:分支(Branches)與依賴(Dependencies)。
建立分支的三個正當理由#
- 發佈分支(Release Branch):為新版本建立分支,讓開發者能繼續開發新功能而不影響穩定的公開發佈版。Bug 在發佈分支中修復後再套用到主線,發佈分支永不合併回主線
- 實驗分支(Spike Branch):為探索新功能或重構而建立的分支,用完即棄,永不合併
- 短期分支(Short-lived Branch):極少數情況下,需要對程式碼庫進行大規模變更,且無法透過上一章描述的方法完成時使用,唯一目的是將程式碼庫帶到可以用漸進方式或 branch by abstraction 繼續變更的狀態
版本控制簡史#
CVS#
CVS(Concurrent Versions System) 是最早廣泛使用的免費版本控制系統,其最重要的創新是預設不鎖定檔案(“Concurrent” 即此意)。
CVS 的主要問題:
- 分支需要複製整個 repository,大型庫耗時且佔空間
- 合併時產生大量假衝突(phantom conflicts),且不自動合併新增檔案
- 標記(tagging)需要觸碰 repository 中的每個檔案
- Check-in 不是原子操作(atomic),中斷會導致 repository 處於中間狀態
- 檔案重新命名不是一等操作,會失去修訂歷史
- 對二進位檔案管理效率低落
Subversion(SVN)#
SVN 被設計為「更好的 CVS」,修正了 CVS 的許多問題:
- 版本化單位改為 revision(一組對檔案和目錄的變更集合),而非個別檔案
- 每次 commit 原子性地套用所有變更
- 分支和標記透過 copy-on-write 實作,幾乎是常數時間操作
- 支援 externals(掛載遠端 repository)和目錄、檔案屬性的版本化
- 本地保留最後 checkout 版本的副本,許多操作可離線執行
SVN 的限制:
- 只能在線上時才能 commit
.svn目錄追蹤的本地狀態可能導致混亂- 客戶端操作非原子性,中斷可能產生不一致狀態
- Revision 編號僅在單一 repository 內唯一,不具全域唯一性
商業版本控制系統#
作者推薦的商業 VCS:
| 工具 | 特色 |
|---|---|
| Perforce | 卓越的效能、可擴展性和工具支援 |
| AccuRev | 提供類似 ClearCase 的串流式開發能力,但沒有沉重的管理成本 |
| BitKeeper | 第一個真正的分散式版本控制系統 |
作者強烈建議避免使用 ClearCase、StarTeam 和 PVCS。任何仍在使用 Visual SourceSafe 的團隊應立即遷移,因為 VSS 在太多情況下會損毀其資料庫。
關閉悲觀鎖定(Switch Off Pessimistic Locking)#
如果版本控制系統支援樂觀鎖定(Optimistic Locking),務必使用。
- 悲觀鎖定(Pessimistic Locking):編輯檔案前必須取得排他鎖,看似能防止合併衝突,實際上嚴重降低開發效率
- 迫使團隊按元件分配行為以避免等待
- 打斷創意流程——開發者常臨時需要修改未預期的檔案
- 幾乎無法進行大範圍的重構
- 樂觀鎖定:假設大多數時候不同人不會同時修改相同內容,允許所有人自由存取,以行為單位追蹤變更並自動合併
- 合併衝突確實會發生,但大多數在數秒內即可解決
- 只有在不夠頻繁 commit 時才會花更長時間
悲觀鎖定唯一合理的使用情境是二進位檔案(如圖片、文件),因為無法有意義地合併。Subversion 允許對特定檔案按需鎖定,並可設定
svn:needs-lock屬性來強制悲觀鎖定。
分支與合併(Branching and Merging)#
分支的類型#
分支可依不同維度建立:
| 分支維度 | 適用範圍 |
|---|---|
| 實體分支(Physical) | 針對檔案、元件、子系統 |
| 功能分支(Functional) | 針對功能、邏輯變更、Bug 修復、增強、補丁、發佈、產品 |
| 環境分支(Environmental) | 針對建置與執行時期平台的各層面 |
| 組織分支(Organizational) | 針對活動/任務、子專案、角色、團隊 |
| 流程分支(Procedural) | 針對各種政策、流程、狀態 |
跨多個維度同時建立分支是可以的——前提是分支之間不需要互動。但通常情況下,我們最終需要透過**合併(Merging)**將某個分支的變更複製到另一個分支。
分支前請確保已將所有必要的東西放入版本控制——包括測試案例、組態、資料庫腳本等。分支意味著整個程式碼庫會在每個分支中獨立演化。
合併的挑戰#
合併的真正問題在於兩個分支中發生了衝突的變更:
- 字面衝突(Literal Conflicts):版本控制系統能偵測到的重疊變更
- 語意衝突(Semantic Conflicts):版本控制系統無法捕捉的意圖差異——例如一人重新命名了類別,另一人新增了對該類別的引用,合併表面上成功,但編譯或執行時期會失敗
分支間隔越久、參與人數越多,合併就越痛苦。減輕痛苦的方式:
- 早期分支(Early Branching):每個功能一個分支——但會產生更多分支要追蹤,只是延遲痛苦
- 延遲分支(Deferred Branching):每次發佈一個分支,並頻繁合併以降低每次合併的痛苦

Figure 14.1: A typical example of poorly controlled branching
分支、串流與持續整合的張力#
分支與持續整合之間存在根本性的張力:如果團隊成員在不同分支上工作,他們就不是在持續整合。 讓持續整合得以實現的最重要實踐是:每個人每天至少 check in 到主線一次。
在典型的失控分支場景中(如圖 14.1),分支長期處於無法部署的狀態,團隊花大量資源追蹤分支、決定何時合併、執行合併,最後還需要將程式碼庫帶到可部署狀態——而這正是持續整合本該解決的問題。

Figure 14.2: Release branching strategy
作者強烈推薦的分支策略(圖 14.2):
- 僅在發佈時建立長期分支
- 新工作始終 commit 到 trunk
- 合併僅發生在修復需要從發佈分支合併到主線,或關鍵 bug 從主線合併到發佈分支時
- 程式碼始終處於可發佈狀態,分支更少,合併工作大幅減少
絕不應該使用長期、不頻繁合併的分支作為管理大型專案複雜度的首選方式。這只會在部署或發佈時累積大量問題。整合過程將變成高風險、不可預測的活動,耗費大量時間與金錢。
對於中大型團隊,正確的解決方案是將應用程式拆分為元件,並確保元件之間鬆耦合。在主線上持續漸進式合併所產生的壓力,會促使軟體設計變得更好。
分散式版本控制系統(Distributed Version Control Systems)#
DVCS 是什麼?#
DVCS 的基本設計原則是每個使用者在自己的電腦上保有一個完整的、一等的 repository,不需要特權的「master」repository(雖然大多數團隊會約定指定一個以進行持續整合)。
DVCS 的主要特性:
- 幾秒內即可開始使用——安裝後直接 commit 到本地 repository
- 可從其他使用者拉取更新,無需經過中央 repository
- 可選擇性地推送更新給特定使用者群組
- 支援 cherry-picking:透過使用者網路傳播修補程式
- 可離線 check in 程式碼
- 可將未完成功能頻繁 commit 到本地 repository 作為檢查點,不影響他人
- 可在推送前本地修改、重排、打包 commits(即 rebasing)
- 本地 repository 可輕鬆嘗試新想法,無需在中央 repository 建立分支
- 中央 repository 負載降低,可擴展性更佳
- 多份完整 repository 副本提供更高的容錯能力

Figure 14.3: Lines of development in a DCVS repository
在 DVCS 中,每個本地 repository 實際上就是一個獨立分支,不存在「主線」的概念(如圖 14.3)。
DVCS 簡史#
Linux 核心長期在無版本控制下開發。2002 年 Linus Torvalds 採用了 BitKeeper——第一個廣泛使用的 DVCS(建立在 SCCS 之上)。後來多個開源 DVCS 專案興起,主要包括:
| 工具 | 說明 |
|---|---|
| Git | 由 Linus Torvalds 創建,用於維護 Linux 核心 |
| Mercurial | 由 Mozilla Foundation、OpenSolaris、OpenJDK 使用 |
| Bazaar | 由 Ubuntu 使用 |
DVCS 在企業環境中的顧慮#
企業對 DVCS 的三個主要疑慮:
- 任何複製本地 repository 的人都擁有完整歷史
- 稽核與工作流程更難掌控——使用者可以互相傳送變更,甚至修改本地歷史
- Git 實際上允許修改歷史,這在受監管環境中可能是紅線
在實務中,這些顧慮不構成障礙。一旦指定中央 repository,集中式版本控制的所有特性都可用。使用 DVCS 繞過中央 repository 推送變更通常比直接 push 更麻煩,況且沒有將變更推送到中央 repository,持續整合系統就無法觸發建置。
DVCS 的使用工作流程#
以 Mercurial 為例,DVCS 工作流程如下:
hg pull— 從遠端 repository 取得最新更新hg co— 更新本地工作副本- 撰寫程式碼
hg ci— 將變更儲存到本地 repositoryhg pull— 取得任何新的遠端更新hg merge— 合併,更新本地工作副本但不 check in- 本地執行 commit build
hg ci— 將合併 check in 到本地 repositoryhg push— 將更新推送到遠端 repository

Figure 14.4: DVCS workflow (diagram by Chris Turner)
合併比 Subversion 更安全,因為步驟 4 的額外 check-in 確保即使合併失敗,也能回到合併前的狀態。可重複步驟 1-8 多次後再執行步驟 9,甚至使用 rebasing 將所有變更合併為一個 commit。
在將變更從本地 repository commit 到中央 repository 之前,你的變更並未被整合。為了讓持續整合生效,你必須至少每天一次將變更推送到中央 repository,DVCS 的某些好處若被濫用會損害 CI 的效果。
串流式版本控制系統(Stream-Based Version Control Systems)#
串流的概念#
串流式版本控制系統(如 ClearCase、AccuRev)試圖透過讓變更集合可同時套用到多個分支來緩解合併問題。與傳統分支不同,串流具有**繼承(Inheritance)**的關鍵特性:對某個串流的變更會被其所有後代串流繼承。
實際應用場景:
- 跨版本 Bug 修復:將修復提升(promote)到所有需要該修復的分支的共同祖先串流,後代串流會自動繼承
- 更新第三方程式庫:check in 到所有需要更新的串流的祖先串流,所有繼承串流都會取得更新

Figure 14.5: Stream-based development
串流開發模型#
開發者在自己的工作空間中開發,完成後將變更提升到團隊串流進行持續整合,測試通過的功能再提升到發佈串流。這讓中大型團隊能同時處理多項功能而不互相影響,測試人員和專案經理也能 cherry-pick 需要的功能。
串流模型的常見問題:
- 不同團隊以不同方式修改共享程式碼導致的複雜合併
- 新功能依賴其他尚未提升的功能的依賴管理問題
- 整合和回歸測試在發佈串流上因新組態而中斷的整合問題
ClearCase 與從原始碼重建的反模式:串流模型中的提升是在原始碼層級而非二進位層級。每次提升到更高的串流都需要重新 checkout 原始碼並重建二進位檔。這違反了一個關鍵原則——你發佈的二進位檔應該與通過部署管線測試的二進位檔相同。從發佈串流重建可能因編譯器或依賴版本差異而引入 bug。
串流式系統與持續整合#
串流式開發的根本問題是:頻繁提升時(每天多次),簡單的方案同樣有效;不頻繁提升時,在發佈時會遇到所有持續整合本該解決的問題。
從 CI 角度看,向祖先串流提升 bugfix 會觸發所有後代串流的新建置,可能迅速耗盡建置系統的容量。應對策略是僅在直接關聯的串流發生變更時觸發建置,而非在祖先串流變更時觸發。
分支模式(Branching Patterns)#
在主線上開發(Develop on Mainline)#
這是作者最推薦的模式,也是唯一能實現真正持續整合的方式。
- 開發者幾乎總是 check in 到主線,分支極少使用
- 面對複雜變更時,將其規劃為一系列小型、漸進式的步驟,保持測試通過而不破壞現有功能
- 分支僅在不需要合併回主線的場景下建立(如發佈分支、spike 分支)
優點:
- 確保所有程式碼持續整合
- 開發者立即取得彼此的變更
- 避免專案結尾的「合併地獄」和「整合地獄」
如何應對「不是每次 check-in 都可發佈」的疑慮? 答案是:良好的元件化架構、漸進式開發和功能隱藏(Feature Hiding)。這需要在架構與開發上更加謹慎,但避免了不可預測的整合階段所帶來的巨大成本。
在主線上開發不代表要建立一個巨大的單體 repository。對中大型團隊而言,將應用程式拆分為鬆耦合元件是正解。部署管線的目標正是讓主線上的頻繁 check-in 帶來的暫時不穩定不影響穩固的發佈。

Figure 14.6: Design and adoption of a consistent merge strategy
為發佈建立分支(Branch for Release)#
這是唯一永遠可接受的分支情境。
- 在發佈前建立分支,測試和驗證在分支上進行,新開發在主線繼續
- 取代了邪惡的「程式碼凍結(Code Freeze)」做法
- 發佈分支的規則:
- 功能始終在主線上開發
- 分支在功能完整、準備開始新功能時建立
- 分支上只 commit 關鍵缺陷修復,並立即合併到主線
- 後續發佈分支始終從主線建立,而非從現有發佈分支
- 當發佈頻率達到每週一次左右時,發佈分支不再有必要——直接推出新版本比在分支上修補更簡單
避免從發佈分支再建立子分支,這會產生「階梯結構」,難以辨別各版本間的共同程式碼。
按功能分支(Branch by Feature)#
此模式旨在讓大型團隊同時開發多個功能,同時保持主線可發佈。每個 story 或功能在獨立分支上開發,只有通過測試者驗收後才合併到主線。
成功的必要前提:
- 主線的變更必須每天合併到每個分支
- 分支必須短期存在(理想數天,最多不超過一個迭代)
- 活躍分支數量必須限制在進行中的 story 數量內
- 重構必須立即合併以最小化合併衝突
- 技術主管負責審查所有合併,有權拒絕可能破壞 trunk 的修補
Branch by feature 本質上與持續整合對立。即使在每個分支上做 CI,也不是真正的整合——你沒有整合你的分支。作者曾見過即使是小型、經驗豐富的敏捷團隊也搞砸這個模式。建議始終從「在主線上開發」模式開始。
在開源專案中此模式可以非常有效(GitHub 的 fork/pull 模型),因為有少數經驗豐富的 committer 把關,且發佈日期相對彈性。商業專案中則需要:模組化且良好分解的程式碼庫、小型團隊加上經驗豐富的 leader、全團隊承諾頻繁整合到主線。
按團隊分支(Branch by Team)#
此模式嘗試解決大型團隊同時處理多個工作流的問題,同時維持可隨時發佈的主線。

Figure 14.7: Branch by team
工作流程:
- 建立小團隊,各自在自己的分支上工作
- 功能/story 完成後,穩定分支並合併到 trunk
- Trunk 的任何變更每天合併到每個分支
- 每次 check-in 在分支上執行單元測試和驗收測試
- 每次分支合併到 trunk 時,執行所有測試(含整合測試)
優點與風險:
- 比 branch by feature 分支更少,整合更頻繁
- 但因為整個團隊 check in 到同一分支,分支發散更快,合併可能更複雜
- 工作單位是整個分支而非單一變更——無法只合併單一修復到主線
- 使用 DVCS 的 cherry-picking 和 rebasing 能力可緩解部分問題
結合使用 DVCS 時,此模式從「不推薦」變為「在特定條件下可推薦」。Linux 核心開發團隊正是使用此模式的變體,為作業系統不同部分(如排程器、網路堆疊)維護獨立 repository。
總結#
版本控制系統的演化與圍繞它們的組態管理實踐,是軟體產業歷史的重要一環。作者在三種版本控制典範之間進行了比較:
| 典範 | 評價 |
|---|---|
| 集中式模型(Centralized) | 標準但受限 |
| 分散式模型(Distributed) | 將持續帶來巨大正面影響 |
| 串流式模型(Stream-based) | 強大但複雜 |
持續整合與分支之間存在根本性的張力。 每次做出分支的決定,都在某種程度上妥協 CI。分支的代價體現在增加的風險中,唯一降低風險的方式是確保任何活躍分支每天或更頻繁地合併回主線。
作者唯一無保留推薦的分支理由是:為發佈而分支、為 spike 而分支,以及在極端情況下(沒有其他合理方式讓應用程式達到可透過其他方法繼續變更的狀態時)建立短期分支。其他所有分支模式都伴隨著需要仔細權衡的成本與風險。