Version Control and Branch Management#
本章由 Titus Winters 撰寫,探討版本控制(Version Control)在軟體工程中的核心地位,以及分支管理(Branch Management)策略對組織效率的深遠影響。作者特別倡導 trunk-based development(主幹開發),並以 Google 大規模實踐經驗說明「One Version Rule」的重要性。
什麼是版本控制?#
版本控制系統(Version Control System, VCS)是追蹤檔案隨時間變動的系統。VCS 維護一組受管理檔案的 metadata,檔案與 metadata 的副本統稱為 repository(簡稱 repo)。
VCS 的核心功能包括:
- 協作協調:允許多位開發者同時在相同檔案集上工作
- 原子性提交(Atomicity):確保一次邏輯變更涉及的多個檔案被視為單一單元
- 衝突偵測:在 commit 時,若檔案自上次 sync 後已被修改,則拒絕提交
為什麼需要版本控制?#
假設沒有 VCS,小型分散式團隊可能透過來回傳遞檔案副本協作。這在非同步編輯時勉強可行(例如不同時區的人),但只要有機會讓人無法確認哪個版本最新,問題就會迅速浮現——最終演變成 Presentation v5 - final - redlines - Josh's version v2 的混亂局面。沒有單一公認的 Source of Truth,協作將充滿摩擦與錯誤。
即便引入共享儲存空間,仍需帶外協調(out-of-band coordination)來避免覆蓋彼此的工作。直接在共享儲存空間工作意味著任何無法持續保持 build 正常的開發任務,都會阻礙團隊中的每個人。缺乏檔案鎖定與合併追蹤,最終必然導致碰撞與工作遺失。
若將檔案鎖定編碼到軟體中,我們已開始重新發明早期 VCS(如 RCS)。當你意識到檔案級鎖定粒度太粗,開始需要行級追蹤時——我們確實在重新發明版本控制。既然如此,不如直接使用現成工具。
版本控制的重要性#
軟體工程是「程式設計 + 時間」的整合。VCS 可被概念化為標準檔案系統的延伸:
- 檔案系統:
filename → contents - VCS:
(filename, time, branch) → contents
版本控制讓「時間」成為操作中的顯式考量——這在程式設計任務中或許不必要,但在軟體工程任務中至關重要。
對 VCS 的殘餘猶豫幾乎直接源於混淆「程式設計」與「軟體工程」。我們教授程式設計、訓練程式設計師、基於程式設計問題面試。新進員工可能很少接觸過由多人或超過幾週維護的程式碼。在這種經驗背景下,版本控制看似一個外星解方——為有時不明顯的好處增加大量複雜性。
版本控制的多重價值#
- 規模擴展:允許團隊與組織的規模化成長,協調多位開發者與多個時間點的工作
- 法規與稽核:提供每一行程式碼變更的正式紀錄,滿足日益增長的稽核需求
- 來源追蹤:混合內部開發與第三方來源時,追蹤每行程式碼的 provenance
- 反思時刻:commit 的儀式觸發自省——上次 commit 後完成了什麼?程式碼狀態是否令人滿意?
幾乎所有有經驗的軟體工程師,對於超過一兩天的專案,即使是單人專案,也會本能地使用版本控制。這說明版本控制在價值(包含風險降低)與額外開銷之間的取捨非常明確。
集中式 VCS 與分散式 VCS#
集中式 VCS(Centralized VCS)#
集中式 VCS 的模型是單一中央 repository(通常存放在組織的共享計算資源上)。開發者可在本地工作站 checkout 檔案,但與版本控制狀態互動的操作(新增、同步、更新等)需與中央伺服器通訊。所有開發者的 commit 都提交至該中央 repository。
歷史演進:
- 1970-1980 年代(RCS 等):基於檔案鎖定,一次只有一人可編輯。小修改或小團隊時運作良好,但鎖定一旦被爭搶,就有規模化的內在問題
- 1990-2000 年代初(CVS、Subversion):避免排他鎖定,改為追蹤 sync 狀態,只要你的基礎版本包含 repository 中所有變更即可 commit。CVS 允許多人同時 checkout 同一檔案。Subversion 進一步提供真正的 commit 原子性、版本追蹤,以及對 rename、symbolic link 等操作的更好支援
分散式 VCS(Distributed VCS, DVCS)#
DVCS(如 Git、Mercurial)的核心差異在於:哪些副本算作 repository?
- 任何 clone/fork 都是完整的 repository,可以本地 commit
- 「中心性」純粹是概念上的、策略上的,而非技術本質
- DVCS 允許更好的離線操作,在開源世界獲得特別強大的採用
當今主流的版本控制系統是 Git(DVCS)。在沒有特殊需求的情況下,使用 Git 是合理的預設選擇——與多數人使用相同工具本身就有價值。
Google 與 DVCS 的關係#
Google 的主要 repository 基於大規模自建的集中式 VCS。嘗試轉向 Git 等標準工具時,受到以下因素阻礙:
- 程式碼庫與使用者基數的龐大規模(50,000+ 工程師)
- Hyrum’s Law 效應——許多系統依賴於單調遞增版本號而非 commit hash
- Repository 本身約 86 TB 的資料量
DVCS 模型需傳輸大量歷史與 metadata,隨組織規模擴大,開發者實際操作的檔案比例越來越小,傳輸幾乎全是浪費。在 Google 的工作流程中,程式碼庫的中心化與雲端儲存對規模化而言至關重要。多數檔案唯一需要在本地的場景是建置,但分散式(且可重現的)建置系統在這項任務上似乎也有更好的擴展性(參見第 18 章)。
Source of Truth#
集中式 VCS 將 Source of Truth 烘焙進系統設計:trunk 上最近的 commit 就是當前版本。而 DVCS 沒有這種內建概念,因此需要更明確的策略與規範。
實務上,管理良好的 DVCS 專案會宣告某個特定 repository 的特定分支為 Source of Truth(例如 GitHub 上的主要 repository)。集中化與 Source of Truth 回歸 DVCS 世界並非偶然。
情境:缺乏明確的 Source of Truth#
若團隊避免定義特定 branch + repository 為最終 Source of Truth:
- 從隊友的 repository pull 後,無法清楚判斷哪些變更已存在
- 確保發行版包含所有近期開發功能,缺乏可擴展的機制
- 新開發者加入時,無法取得已知良好的程式碼副本
若關注隨團隊成長需要次線性(sublinear)人力投入的系統,定義一個 repository(一個分支)為最終 Source of Truth 至關重要。
Source of Truth 的相對性#
Source of Truth 具有相對性。對同一個專案,不同組織的 Source of Truth 可能不同。例如,Google 工程師、RedHat 工程師和 Linus Torvalds 對 Linux Kernel patch 可能各有不同的 Source of Truth。
DVCS 在組織層級式且對外不可見時運作良好——RedHat 工程師可以 commit 到本地的 Source of Truth repository,變更定期向上游推送,而 Linus 有完全不同的 Source of Truth 概念。只要沒有選擇或不確定性(變更該推送到何處),便能避免大量混亂的擴展問題。
值得注意的是,版本控制中沒有比分支使用與管理更多策略與慣例的議題。「trunk」在 VCS 中只是技術預設,組織可以在此基礎上選擇不同策略。技術只是其中一部分,幾乎總有同等份量的策略與使用慣例疊加其上。
版本控制 vs. 依賴管理#
版本控制策略主要管理自有程式碼,粒度通常較細。依賴管理(Dependency Management,參見第 21 章)則聚焦於其他組織管理的專案,粒度較粗,且無法完全控制。
分支管理#
進行中的工作等同於分支#
組織在討論分支管理策略時,應認知到每一份進行中的工作都等同於一個分支——無論是 DVCS 的本地 staging commit,還是集中式 VCS 的未提交本地修改。

Figure 16.1: Two revision numbers in Perforce
例如 Perforce 中,每個 change 有兩個版本號:一個標示隱含的 branch point(建立時),一個標示 recommit 位置。
以重構任務「將 Widget 重新命名為 OldWidget」為例,根據分支管理策略,可能有不同解讀:
- 僅在 Source of Truth repository 的 trunk 分支上重新命名
- 在 Source of Truth repository 的所有分支上重新命名
- 在所有分支上重新命名,並找出所有引用 Widget 的未完成變更
Dev Branches(開發分支)#
Dev branch 是「已完成但未提交」與「新工作基於此進行」之間的折衷點。它試圖解決的問題(產品不穩定)是合理的。在缺乏一致性單元測試的年代(參見第 11 章),引入任何變更都有高風險導致系統其他部分的功能退化,將 trunk 視為特殊存在有其道理。Tech Lead 可能會說:「新的變更須通過完整測試才能提交到 trunk,團隊改用 feature-specific 開發分支。」
然而,Google 的經驗發現此問題透過以下方式能更好地解決:
- 更廣泛的測試
- 持續整合(CI,參見第 23 章)
- 徹底的 code review
反對 dev branches 的理由:
- 小合併比大合併容易——同一組 commit 終究要合併到 trunk
- 作者合併比批次合併容易——由撰寫變更的工程師合併,比將不相關變更批次合併更簡單
- 隔離回歸更容易——單一工程師的變更比大型 dev branch 的合併更容易定位問題
- 擴展風險——多個分支長期隔離開發時,協調合併操作成本顯著增加
組織如何陷入 Dev Branch 的陷阱#
- 團隊觀察到「合併長期開發分支降低了穩定性」
- 錯誤結論:「分支合併有風險」
- 而非正確解方:「改善測試」和「不要使用分支開發策略」
- 團隊開始基於其他進行中的分支開發新分支
- 組織規模擴大,開發分支數量增長
- 某位不幸的工程師成為 Build Master / Merge Coordinator
- 定期召開「每週合併策略會議」
替代方案:trunk-based development,大量依賴測試與 CI,保持 build 為綠色,在 runtime 停用未完成/未測試的功能。沒有「合併策略」會議,沒有大型/昂貴的合併。
Release Branches(發行分支)#
若產品發行週期超過數小時,建立代表實際發行版程式碼的 release branch 是合理的。關鍵缺陷可從 trunk cherry-pick 至 release branch。
Release branch 與 dev branch 的根本差異:
- Dev branch:預期合併回 trunk,可能被其他團隊進一步分支
- Release branch:預期最終被棄用
DORA 研究顯示,最高效能的技術組織幾乎不存在 release branch——已達成 Continuous Deployment(CD)的組織,能從 trunk 每天多次發布,因此傾向直接修復並重新部署,cherry-pick 和分支顯得多餘。當然,這對數位部署(如 web 服務和 app)的適用性高於需要推送實體產品給客戶的組織。
同一 DORA 研究也顯示 trunk-based development、無長期 dev branch 與良好技術成果之間有強正相關。核心觀點清晰:分支是生產力的拖累。在許多情況下,複雜的分支合併策略只是一種感知上的安全拐杖——試圖保持 trunk 穩定,但有其他更好的方式可以達成此目標。
Google 的版本控制實踐#
Google 絕大多數原始碼管理在一個 monorepo 中,由約 50,000 名工程師共享。除 Chromium 和 Android 等大型開源專案外,幾乎所有 Google 擁有的專案都在其中——包括 Search、Gmail、廣告產品、Google Cloud Platform 等面向公眾的產品,以及支援和開發這些產品所需的內部基礎設施。
Piper——Google 的 VCS#
- 自建的集中式 VCS,以分散式微服務架構運行於 Google 生產環境
- 儲存超過 80 TB 的內容與 metadata
- 每個工作日處理 60,000 至 70,000 次 commit(包含人工與半自動化流程)
- Binary artifact 很常見,因為完整 repository 不需要被傳輸,因此 binary artifact 的常見成本不太適用
- 建立新 client、新增檔案、commit 一個變更僅需約 15 秒——這種低延遲互動大幅簡化開發者體驗
精細的所有權機制#
由於 Piper 是自建產品,Google 能自訂並強制執行所選的原始碼控制策略。在 monorepo 中,檔案層級結構的每一層都有 OWNERS 檔案,列出有權核准該子樹內 commit 的工程師(除了上層目錄已列出的 OWNERS 之外)。
在多 repository 環境中,這可能透過獨立 repository 的檔案系統權限或 Git commit hook 實現。但透過控制 VCS,Google 能讓所有權與核准的概念更顯式,並在 commit 操作時由 VCS 強制執行。
這個模型具有靈活性——所有權僅是文字檔案,不綁定於實體 repository 分離,因此在團隊轉移或組織重組時能輕易更新。
One Version Rule#
在 Source of Truth 的基礎上,Google 的關鍵策略是 One Version:
對於 repository 中的每一個依賴,只能有一個版本可供選擇。
- 第三方套件:repository 中只能有單一版本
- 內部套件:不允許未重新命名的 fork——必須能安全地將原始版本與 fork 混合在同一專案中
情境:多個可用版本的危害#
假設某團隊發現通用基礎設施程式碼的 bug,選擇 fork 而非就地修復:
- 其他團隊開始依賴此 fork
- 若任何專案同時依賴原版與 fork,最好的情況是 build 失敗,最壞的情況是難以理解的 runtime bug
- 此 fork 為程式碼庫引入了著色/分區屬性——任何目標的 transitive dependency 必須恰好包含一份此 library
- 新增依賴變成可能需要執行整個程式碼庫所有測試的操作
Java 的 shading 技術可在處理函數時運作,但在處理可跨套件傳遞的型別時,理論與實務皆失敗。多版本方案的根本問題是:需要同一依賴的多個版本。
One-Version Rule 的核心原則#
開發者在選擇依賴元件版本時,絕不應有選擇的餘地。
對個別開發者而言,缺乏選擇可能像是任意的阻礙。但對組織而言,這是高效擴展的關鍵元件。一致性在組織的所有層級都具有深遠的重要性。
(幾乎)不使用長期開發分支#
One Version Rule 隱含的深層策略:開發分支應盡量短命。這與以下研究成果一致:
- Agile 流程
- DORA 對 trunk-based development 的研究
- The Phoenix Project 關於「減少進行中工作」的教訓
在 Google monorepo 中約 1,000 個團隊裡,只有極少數(幾乎可確定少於 10 個)擁有長期開發分支。這些通常源於非常特定的原因:
- 跨版本資料相容性:讀取者與寫入者需在長時間內對檔案格式達成共識
- API 相容性承諾:舊版微服務 client 需與新版 server 相容
Google 的大規模變更(Large-Scale Changes, LSCs;參見第 22 章)策略與工具,更加強調了 trunk-based development 的重要性:跨程式碼庫的廣泛/淺層變更,在僅修改 trunk 分支上所有 check-in 內容時,已經是龐大且乏味的工程。若還有無限數量的 dev branch 需要同時重構,將是極大的額外負擔。在 DVCS 模型中,甚至可能無法識別所有這些分支。
跨時間的依賴(dependency across time)比時間不變的程式碼(time-invariant code)成本高昂且複雜得多。Google 生產服務對外作出此類承諾的情況相對較少。此外,Google 有一項「build horizon」限制:每個生產 job 最多每六個月需重新建置與部署。
Release Branch 的使用#
Google 許多團隊使用 release branch,但 cherry-pick 有限。使用原則:
- 合理建立(例如月度發行、實體設備出貨)
- 保持 cherry-pick 最小化
- 不要計劃重新合併回 trunk
Monorepo#
Google 於 2016 年發表了關於 monorepo 方法的論文。Monorepo 的核心優勢:
- 遵守 One Version 變得極為自然——違反反而更困難
- 無需決定哪些版本是官方版本
- 建構工具能統一理解整體 build 狀態
- 工程師能看到其他人在做什麼,據此做出更好的決策
Monorepo 並非唯一正解#
開源社群以 manyrepo 方式運作良好。重要的不是堅持 monorepo,而是盡可能遵循 One-Version 原則。
選擇的考量因素:
- 規模:Git 在數百萬 commit 後常有效能問題,大型 binary artifact 使 clone 變慢
- 安全/法規:不同專案可能有不同的保密、法規、隱私需求
- Virtual Monorepo(VMR):Git submodules、Bazel external dependencies、CMake subprojects 等工具,能弱近似 monorepo 行為
若組織中每個專案有相同的保密、法律、隱私與安全需求,真正的 monorepo 是好選擇。否則,追求 monorepo 的功能性,但保留以不同方式實現的彈性。
版本控制的未來#
Google 並非唯一公開討論 monorepo 優勢的組織——Microsoft、Facebook、Netflix、Uber 也公開提到此方法,DORA 更廣泛發表相關研究。多數反對 monorepo 的論點聚焦於單一大型 repository 的技術限制。
若從上游 clone repository 是快速且低成本的,開發者更可能保持變更小巧且隔離。若 clone 或其他常見 VCS 操作耗費開發者數小時,組織自然會迴避依賴如此龐大的 repository。Google 透過提供大規模可擴展的 VCS 避免了這個陷阱。
另一個反對 monorepo 的論點是它不符合開源世界的開發方式。但 OSS 的許多實踐源於優先考慮自由、缺乏協調和缺乏計算資源。在組織邊界內,我們可以假設計算資源的可用性、協調的存在,以及某種程度的集中化權威。
較不常見但更合理的擔憂是:隨組織擴大,不太可能每段程式碼都受相同的法律、合規、保密和隱私要求約束。Manyrepo 的一個天生優勢是獨立 repository 明顯能有不同的授權開發者、可見性和權限。將此功能嵌入 monorepo 雖然可行,但意味著持續的客製化與維護成本。
作者預期未來 10 至 20 年的演進方向:
- VCS 將支援更大的 repository,同時提供更好的機制跨專案與組織邊界串聯
- Virtual Monorepo 將成為事實標準——某個組織(可能是現有的套件管理群組或 Linux 發行版)將催化此發展
- 版本號將被更普遍地認知為時間戳記,允許版本偏差(version skew)等同引入時間維度的複雜性
- Git 持續改進(shallow clones、sparse branches、更好的最佳化),「必須保持小型 repository」的重要性將降低
結論#
版本控制系統是技術帶來的協作挑戰與機會的自然延伸,歷史上與軟體工程的理解同步演進。
- 早期系統提供簡單的檔案級鎖定
- 隨軟體工程專案與團隊規模成長,VCS 的規模問題浮現,理解隨之演進
- 開發轉向 OSS 模型與分散式貢獻者後,VCS 也隨之去中心化
作者預期 VCS 技術將轉向假設持續網路連線,更聚焦於雲端儲存與建置,避免傳輸不必要的檔案和 artifact。即便允許分散式開發,某物仍必須被集中認定為 Source of Truth。
當前 DVCS 的去中心化是技術對產業需求(特別是開源社群)的合理回應。但 DVCS 的配置需要嚴格控制,並搭配對組織有意義的分支管理策略。分支的自由放任可能導致開發者與程式碼部署之間無上限的額外開銷。複雜技術不必以複雜方式使用:如同我們在 monorepo 和 trunk-based development 模型中看到的,保持分支策略簡潔通常帶來更好的工程成果。
Choice leads to costs——One-Version Rule 是本章最核心的建議:組織內的開發者不應在 commit 去處或依賴版本上有選擇餘地。雖然這對個別開發者可能令人困擾,但從整體來看,最終結果遠優於有選擇的情況。
TL;DRs#
- 任何規模超過「單人且永不更新的玩具專案」的軟體開發都應使用版本控制
- 「該依賴哪個版本?」的選擇存在內在的擴展問題
- One-Version Rule 對組織效率出人意料地重要——消除 commit 去處與依賴選擇能帶來顯著簡化
- 在某些語言中,可嘗試 shading、separate compilation 等技術手段迴避多版本問題,但這些都是純粹的浪費勞動——工程師只是在繞過技術債
- DORA 研究顯示 trunk-based development 是高效能開發組織的預測因子;長期開發分支不是好的預設計畫
- 使用任何合理的版本控制系統。若組織偏好獨立 repository,跨 repository 的依賴仍應為 unpinned / at head / trunk-based