標籤:

一個成功的Git分支模型

本文譯自 http://nvie.com/posts/a-successful-git-branching-model/ (by Vincent Driessen)

本文中我會展示一種開發模型,一年前該模型就已經被我用在所有的項目中(包括工作中的項目和私有項目),結果是非常成功的。我早就想為此寫點東西,可直到現在才有時間。本文不會講述任何項目的細節,只會涉及到分支策略和發布管理。

本文使用Git作為所有源碼的版本控制工具。

為什麼是Git?

要全面了解Git與其它集中式版本控制系統相比的優劣,可以參考這個頁面。這方面的爭論可謂是硝煙瀰漫。作為一個開發者,所有這些工具中我最鍾情於Git。Git的的確確改變了人們考慮合併及分支的方式。在我之前所處的經典CVS/Subversion世界中,合併/分支總是被認為是有點可怕的事情(「小心合併衝突,丫會噁心到你」),因此你只應偶爾幹這種事情。

但有了Git,這類事情就變得非常簡單,分支及合併甚至被認為是你日常版本控制操作的核心之一。例如,在CVS/Subversion的書中,分支及合併往往在後面的章節才被介紹(針對高級用戶),但在每一本Git的書中,該內容已經在前3章中介紹(基礎)。

簡單及易重複性帶來的好處就是,分支及合併變得不再可怕。版本控制工具本該幫助我們方便的進行和分支及合併操作。

簡單介紹下工具後,讓我們來看開發模型。我講介紹的模型本質上只是一組步驟,每個團隊成員都必須遵循這些步驟以形成一個可靠管理的軟體開發過程。

去中心化但仍保持中心化

在這個分支模型中我們使用的,且被證實工作得很好的倉庫配置,其核心是一個中心「真理」倉庫。注意只有該倉庫才被認為是中心庫(由於Git是DVCS [分散式版本控制系統],在技術層面沒有中心庫這一東西)。之後我們用origin指代該倉庫,因為大多數Git用戶都熟悉這個名稱。

每個開發者都對origin做push和pull操作。不過除了這種中心化的push-pull關係外,每個開發者還可以從其他開發者或者小組處pull變更。例如,可能兩個或更多的開發者一起開發一個大的特性,在往origin永久性的push工作代碼之前,他們之間可以執行一些去中心化的操作。在上圖中,分別有Alice和Bob、Alice和David、Clair和David這些小組。

從技術上來說,這僅僅是Alice定義一個Git remote,名字為bob,指向Bob的倉庫,反過來也一樣。

主要分支

此開發模型的核心主要受現有的模型啟發。中心倉庫包含了兩個主要分支,這兩個分支的壽命是無限的:

  • master
  • develop
  • 每個Git用於都應該熟悉origin上的master分支。與master分支平行存在的,是另外一個名為develop的分支。

    我們認為origin/develop分支上的HEAD源碼反映了開發過程中最新的提交變更。有人會稱之為「集成分支」。該分支是自動化每日構建的代碼源。

    當develop分支上的源碼到達一個穩定的狀態時,就可以發布版本。所有develop上的變更都應該以某種方式合併回master分支,並且使用發布版本號打上標籤。稍後我們會討論具體操作細節。

    因此,每次有變化被合併到master分支時,根據定義這就是一次新的產品版本發布。我們趨向於嚴格遵守該規範,所以理論上來說,每次master有提交時,我們都可以使用一個Git鉤子(hook)腳本來自動構建並部署軟體至產品環境伺服器。

    支持性分支

    緊接著主要分支master和develop,我們的開發模型使用多種支持性分支來幫助團隊成員間實現並行開發、追蹤產品特性、準備產品版本發布、以及快速修復產品問題。與主要分支不同的是,這些分支的壽命是有限的,它們最終都會被刪除。

    我們會用到的分支有這幾類:

  • 特性分支(feature branch)
  • 發布分支(release branch)
  • 熱補丁分支(hotfix branch)
  • 上述每種分支都有特定的用途,它們各自關於源自什麼分支、合併回什麼分支,都有嚴格的規定。稍後我們逐個進行介紹。

    從技術角度來說,這些分支一點都不「特殊」。分支按照我們對其的使用方式進行分類。技術角度它們都一樣是平常的Git分支。

    特性分支

    可能的分支來源:develop必須合併回:develop分支命令約定:任何除master, develop, release-*, 或 hotfix-*以外的名稱

    特性分支(有時也被稱作topic分支)是用來為下一發布版本開發新特性。當開始開發一個特性的時候,該特性會成為哪個發布版本的一部分,往往還不知道。特性分支的重點是,只要特性還在開發,該分支就會一直存在,不過它最終會被合併回develop分支(將該特性加入到發布版本中),或者被丟棄(如果試驗的結果令人失望)。

    特性分支往往只存在於開發者的倉庫中,而不會出現在origin。

    創建一個特性分支

    開始開發新特性的時候,從develop分支創建特性分支。

    $ git checkout -b myfeature developSwitch to a new branch 「myfeature」

    合併完成的特性回develop

    完成的特性應該被合併回develop分支以將特性加入到下一個發布版本中:

    $ git checkout developSwitch to branch 『develop』$ git merge –no-ff myfeatureUpdating ea1b82a..05e9557(Summary of changes)$ git branch -d myfeatureDeleted branch myfeature (was 05e9557).$ git push origin develop

    上述代碼中的–no-ff標記會使合併永遠創建一個新的commit對象,即使該合併能以fast-forward的方式進行。這麼做可以避免丟失特性分支存在的歷史信息,同時也能清晰的展現一組commit一起構成一個特性。比較下面的圖:

    在第2張圖中,已經無法一眼從Git歷史中看到哪些commit對象構成了一個特性——你需要閱讀日誌以獲得該信息。在這種情況下,回退(revert)整個特性(一組commit)就會比較麻煩,而如果使用了–no-diff就會簡單很多。

    是的,這麼做會造成一些(空的)commit對象,但這麼做是利大於弊的。

    可惜的是,我沒能找到方法讓–no-diff成為默認的git merge行為參數,但其實應該這麼做。

    發布分支

    可能的分支來源:develop必須合併回:develop和master分支命名約定:release-*

    發布分支為準備新的產品版本發布做支持。它允許你在最後時刻檢查所有的細節。此外,它還允許你修復小bug以及準備版本發布的元數據(例如版本號,構建日期等等)。在發布分支做這些事情之後,develop分支就會顯得比較乾淨,也方便為下一大版本發布接受特性。

    從develop分支創建發布分支的時間通常是develop分支(差不多)能反映新版本所期望狀態的時候。至少說,這是時候版本發布所計劃的特性都已經合併回了develop分支。而未來其它版本發布計劃的特性則不應該合併,它們必須等到當前的版本分支創建好之後才能合併。

    正是在發布分支創建的時候,對應的版本發布才獲得一個版本號——不能更早。在該時刻之前,develop分支反映的是「下一版本」的相關變更,但不知道這「下一版本」到底會成為0.3還是1.0,直到發布分支被創建。版本號是在發布分支創建時,基於項目版本號規則確定的。

    創建一個發布分支

    發布分支從develop分支創建。例如,假設1.1.5是當前的產品版本,同時我們即將發布下個版本。develop分支的狀態已經是準備好「下一版本」發布了,我們也決定下個版本是1.2(而不是1.1.6或者2.0)。因此我們創建發布分支,並且為其賦予一個能體現新版本號的名稱:

    $ git checkout -b releases-1.2 developSwitched to a new branch 「release-1.2」$ ./bump-version.sh 1.2Files modified successfully. version bumped to 1.2.$ git commit -a -m 「Bumped version number to 1.2」[release-1.2 74d9424] Bumped version number to 1.21 files changed. 1 insertions(+). 1 deletions(-)

    創建新分支並轉到該分支之後,我們設定版本號。這裡的bump-version.sh是一個虛構的shell腳本,它修改一些本地工作區的文件以體現新的版本號。(當然這也可以手動完成——這裡只是說要有一些文件變更)接著,提交新版本號。

    新的發布分支可能存在一段時間,直到該版本明確對外交付。這段時間內,該分支上可能會有一些bug的修復(而不是在develop分支上)。在該分支上添加新特性是嚴格禁止的。新特性必須合併到develop分支,然後等待下一個版本發布。

    結束一個發布分支

    當發布分支達到一個可以正式發布的狀態時,我們就需要執行一些操作。首先,將發布分支合併至master(記住,我們之前定義master分支上的每一個commit都對應一個新版本)。接著,master分支上的commit必須被打上標籤(tag),以方便將來尋找歷史版本。最後,發布分支上的變更需要合併回develop,這樣將來的版本也能包含相關的bug修復。

    前兩步在Git中的操作:

    $ git checkout masterSwitched to branch 『master』$ git merge –no-ff release-1.2Merge made by recursive.(Summary of changes)$ git tag -a 1.2

    現在版本發布完成了,而且為未來的查閱提供了標籤。

    提醒:你可能同時也會想要用 -s 或者 -u <key> 來對標籤進行簽名。

    為了能保留髮布分支上的變更,我們還需要將分支合併回develop。在Git中:

    $ git checkout developSwitched to branch 『develop』$ git merge –no-ff release-1.2Merge made by recursive.(Summary of changes)

    這一操作可能會導致合併衝突(可能性還很大,因為我們改變了版本號)。如果發現,則修復之並提交。

    現在工作才算真正完成了,最後一步是刪除發布分支,因為我們已不再需要它:

    $ git branch -d release-1.2Deleted branch release-1.2 (was ff452fe).

    熱補丁分支

    可能的分支來源:master必須合併回:develop和master分支命名約定:hotfix-*

    熱補丁分支和發布分支十分類似,它的目的也是發布一個新的產品版本,儘管是不在計劃中的版本發布。當產品版本發現未預期的問題的時候,就需要理解著手處理,這個時候就要用到熱補丁分支。當產品版本的重大bug需要立即解決的時候,我們從對應版本的標籤創建出一個熱補丁分支。

    使用熱補丁分支的主要作用是(develop分支上的)團隊成員可以繼續工作,而另外的人可以在熱補丁分支上進行快速的產品bug修復。

    創建一個熱補丁分支

    熱補丁分支從master分支創建。例如,假設1.2是當前正在被使用的產品版本,由於一個嚴重的bug,產品引起了很多問題。同時,develop分支還處於不穩定狀態,無法發布新的版本。這時我們可以創建一個熱補丁分支,並在該分支上修復問題:

    $ git checkout -b hotfix-1.2.1 masterSwitched to a new branch 「hotfix-1.2.1″$ ./bump-version.sh 1.2.1Files modified successfully, version bumped to 1.2.1.$ git commit -a -m 「Bumped version number to 1.2.1″[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.11 files changed, 1 insertions(+), 1 deletions(-)

    不要忘了在創建熱補丁分之後設定一個新的版本號!

    然後,修復bug並使用一個或者多個單獨的commit提交。

    $ git commit -m 「Fixed severe production problem」[hotfix-1.2.1 abbe5d6] Fixed severe production problem5 files changed, 32 insertions(+), 17 deletions(-)

    結束一個熱補丁分支

    修復完成後,熱補丁分支需要合併回master,但同時它還需要被合併回develop,這樣相關的修復代碼才會同時被包含在下個版本中。這與我們完成發布分支很類似。

    首先,更新master分支並打上標籤。

    $ git checkout masterSwitched to branch 『master』$ git merge –no-ff hotfix-1.2.1Merge made by recursive.(Summary of changes)$ git tag -a 1.2.1

    提醒:你可能同時也會想要用 -s 或者 -u <key> 來對標籤進行簽名。

    接著,將修復代碼合併到develop:

    $ git checkout developSwitched to branch 『develop』$ git merge –no-ff hotfix-1.2.1Merge made by recursive.(Summary of changes)

    這裡還有個例外情況,如果這個時候有發布分支存在,熱補丁分支的變更則應該合併至發布分支,而不是develop。將熱補丁合併到發布分支,也意味著當發布分支結束的時候,變更最終會被合併到develop。(如果develop上的開發工作急需熱補丁並無法等待發布分支完成,這時你也已經可以安全地將熱補丁合併到develop分支。)

    最後,刪除臨時的熱補丁分支:

    $ git branch -d hotfix-1.2.1Deleted branch hotfix-1.2.1 (was abbe5d6).

    小結

    雖然這個分支模型中沒有什麼特別新鮮的東西,但本文起始處的「全景圖」事實上在我們的項目中起到了非常大的作用。它幫助建立了優雅的,易理解的概念模型,使得團隊成員能夠快速建立並理解一個公用的分支和發布過程。

    我同時也提供了一個該圖對應的高質量PDF版本。你可以列印出來並掛在牆上,隨時參考。


    原創文章,轉載請註明出處, 本文地址: http://www.juvenxu.com/2010/11/28/a-successful-git-branching-model/

    Related Posts

  • 另一種持續集成策略——延遲提交
  • 讓Maven正確處理javac警告
  • 沒有銀彈,但你也需要子彈——讀《測試驅動開發的藝術》
  • 團隊之美
  • 變化是如何產生的——有關結對編程
  • 推薦閱讀:

    「呂文藝八字模型理論」有什麼用途?(本原法師 呂文藝)
    我所認識的「模型」與這個專欄的初心
    OPC統一構架 | 工業百條
    hobby boss DDG-67科爾號模型小測評
    模型第一次上色大概需要買些什麼材料?

    TAG:成功 | 模型 |