天天看點

一個成功的Git分支模型 . 為什麼是git? 去中心卻集中(Decentralized but centralized) 主分支(main branches) 輔助分支(Supporting branches) 小結

  • 能力所限,本文的翻譯多處都很不道地,如果哪些地方難于了解,還煩請檢視原文。—— Dbzhang800 20110921

在本文中,我向大家介紹的是在大約一年前我為自己的項目(包括工作和私人項目)引入的且已被證明非常成功的一個開發模型(development model)。這段時間我一直想寫點關于它的東西,但在此之前,我卻從未能抽出充足的時間來完成這件事。我不會談論項目的任何細節,隻涉及分支政策(branching strategy)和釋出管理(release management)。

一個成功的Git分支模型 . 為什麼是git? 去中心卻集中(Decentralized but centralized) 主分支(main branches) 輔助分支(Supporting branches) 小結

它的焦點是Git,我們所有源碼的版本管理工具。

為什麼是git?

要找一個對Git(與集中式的源碼控制系統比較)的優點和缺點的完整且透徹的讨論,可參閱這個網頁。那兒的火藥味很濃。作為一名開發人員,我喜歡Git勝過喜歡現有的其他所有工具。Git真正改變了開發人員對合并和分支和認識。在經典的CVS/SVN的世界中,合并/分支一直被認為有點吓人("提防合并沖突,它會咬你!"),且很少被使用的東西(每隔一段時間才可能用一次)。

但對于Git來說,這些操作是非常低廉和簡單的,而且它們被認為是日常工作​​流程中的核心操作之一,真的是如此。舉例來說,在CVS/SVN的書籍中,分支和合并在後面章節(面向進階使用者)才會被讨論,而在每一個git書中,它在第3章(基礎部分)已被介紹。

簡單和高頻率使用的結果就是,分支和合并不再是讓人恐懼的事情。版本控制工具比其它工具更被寄希望于輔助分支/合并操作。

對工具的介紹夠多了,讓我們向開發模式進軍吧。我在這兒要展示的模式,本質上不過是,為了使軟體開發過程可管理,每一個團隊成員要遵守的一套流程。

去中心卻集中(Decentralized but centralized)

我們使用的且與本分支模型配合很好的軟體倉庫的配置,需要有一個中心“真”倉庫。注意,它隻是被考慮成中心倉庫(因為Git是一個DVCS,在技術層面上并不存在一個像中心倉庫這樣的東西)。我們用origin指代這個倉庫,因為所有的Git使用者都熟悉這個名字。

一個成功的Git分支模型 . 為什麼是git? 去中心卻集中(Decentralized but centralized) 主分支(main branches) 輔助分支(Supporting branches) 小結

每個開發者拉取(pull)并推送(push)到origin。但除了這種集中式的推送拉取關系,每個開發者也可能會從其他的開發者處拉取代碼的變更,進而形成一個子團隊。例如,當與兩個或更多的開發者協同工作于一個大的新特性時,在将工作代碼推送到持久的origin之前,這可能很有用。在上圖中,Alice和Bob,Alice和David,以及Clair和David,分别構成了子團隊。

從技術上講,這隻不過意味着Alice定義了一個名為bob的Git的remote,它指向了Bob的軟體倉庫。反之亦然。

主分支(main branches)

本質上,該開發模型是從現有的模型中獲得靈感而産生的。中心倉庫中維護着兩個永生不滅的主分支:

  • master
  • develop
一個成功的Git分支模型 . 為什麼是git? 去中心卻集中(Decentralized but centralized) 主分支(main branches) 輔助分支(Supporting branches) 小結

每一個Git使用者應該都熟悉origin中的master分支。與master平行,還存在另一個被稱為develop的分支。

我們将origin/master考慮成這樣的一個主分支,其源碼的HEAD始終代表了産品就緒的狀态。

我們将origin/develop考慮成這樣的一個主分支,其源碼的HEAD總是代表已經并入了最新的開發變更的狀态,這些變更将用于下次的釋出。有人喜歡将其稱之為“內建分支(integration branch)”。這也是自動的nightly build的源碼來源。

當develop分支中的源碼達到一個穩定點且準備釋出時,所有的變更應當被合并回master分支,然後将打上版本号标簽。至于具體是怎麼實作的,接下來将進一步讨論。

是以,按(我們的)定義,每當變更被合并回master,都将導緻一個新産品釋出。我們傾向于采用非常嚴格的政策,是以理論上,我們可以使用一個Git的腳本鈎子,當master上有一個送出(commit)時,可以自動建構并将軟體轉到産品伺服器。

輔助分支(Supporting branches)

除了主分支master和develop,我們的開發模型使用了各種各樣的輔助分支來——幫助團隊成員間的并行開發,減輕特性的追蹤的痛苦,準備産品的釋出以及快速修複釋出産品中的缺陷。不同于主分支,這些分支的生存周期總是有限的,因為它們最終會被移除。

我們可能用到的不同的分支類型:

  • 特性分支(Feature branches)
  • 釋出分支(Release branches)
  • 快速修複分支(Hotfix branches)

這些分支中的每一個都有着特定的目的,也必須遵守嚴格的規則:哪些它們所起源于的分支,也是它們必須要合并到的目标分支。我們将很快将介紹一遍它們。

從技術角度看,這些分支絕沒有任何“特殊性”。分支類型是依據我們的使用方式劃分的。毫無疑問,它們仍是普通的Git分支。

特性分支(Feature branches)

可能起源于分支:develop

必須合并回分支:develop

分支命名慣例:除 master、develop、release-* 或 hotfix-* 外的任何名字

一個成功的Git分支模型 . 為什麼是git? 去中心卻集中(Decentralized but centralized) 主分支(main branches) 輔助分支(Supporting branches) 小結

Feature分支(有時被稱為topic分支),用于為即将到來的或遠期的版本開發新功能。當開始開發一個新的功能時,此時包含該功能的目标可能尚未很好定義。Feature分支的本質是,在新功能開發期間始終存在;但是它最終要合并回develop分支(将添加到下次釋出的版本中)或者廢棄(在實驗讓人沮喪的情況下)。

典型情況下,feature分支隻存在于開發者的倉庫中,而不在origin中出現。

建立feature分支

當開始工作于一個新功能時,從develop分支建立新的分支:

$ git checkout -b myfeature develop
Switched to a new branch "myfeature"      

将完成的feature并入develop

完成的特性将會合并回develop分支,并終會被加入将到來的release中:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop      

标記--no-ff使得,即使在可以fast-forward的條件下,合并操作也總是生成一個commit對象。這可以避免feature分支中曆史資訊的丢失,并可使feature分支中的所有commit保持仍在一塊。比較:

一個成功的Git分支模型 . 為什麼是git? 去中心卻集中(Decentralized but centralized) 主分支(main branches) 輔助分支(Supporting branches) 小結

在後一種情況下,從Git的曆史中是不可能看出,哪些commit對象共同實作了一個特性——你必須手動閱讀所有的日志資訊。而要移除整個特性(即,一組commit),在後一種情況下真真切切地讓人頭痛;而若使用了--no-ff标記,則很容易做到。

當然,這會建立一些空的commit對象,但收益比這點浪費要大得多。

不幸的是,我尚未找到使得git merge中預設行為啟用--no-ff的方法,但它真的很需要如此。

釋出分支(Release branches)

可能起源于分支:develop

必須合并回分支:develop 和 master

分支命名慣例:release-*

Release分支用于做新産品釋出前的準備。它使得在最後一刻可以進行細枝末節的完善。更進一步講,它允許小bug的修複以及釋出所需中繼資料(版本号,建構日期等)的準備。通過在release分支做這些工作,develop分支可以幹淨地接收為下一個大發行版準備的功能。

從develop建立release分支的關鍵時機是,develop分支(幾乎)可以反映新版本就緒狀态的時刻。至少是将要建構的發行版所有的feature分支都已經被合并到了develop分支的時刻。所有的針對未來版本開發的feature可能不會被并入——它們必須等到該release分支建立以後。

正是在release分支開始時,即将釋出的産品才被配置設定一個版本号——而不是先前。直到這一刻,develop分支才開始反映“下一版”的變化,但是在下一個release分支開始前,下一版最終将變成0.3還是1.0都是不明确的。這種決定是在release分支開始時,通過項目的版本号規則實施的。

建立釋出分支

Release分支是從develop分支中建立而來。例如,假定1.1.5是目前産品的版本,而我們即将有一個大的發行版。Develop分支對于“下一版”已經就緒,而且我們已經确定這将是版本1.2(而不是1.1.6或2.0)。是以,我們建立release分支并賦予它一個反映新版本号的名字:

$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files 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.2
1 files changed, 1 insertions(+), 1 deletions(-)      

建立一個新的分支并切換進來以後,我們修改版本号。這兒的 bump-version.sh 是一個虛構的腳本,被用來修改工作區中的一些檔案以反映出新版本。(當然,這也可以是手動修改的)。而後,修改後的版本号被送出。

在釋出最終完成之前,這個新的分支可能一直存在。在這期間,修複的bug會應用到該分支(而不是develop分支)。在這兒添加大的新特性是嚴格被禁止的。它們必須被合并到develop,并去等待下一個大的發行版本。

完成釋出分支

當release分支的狀态已經符合正式發行的要求時,一些動作需要被實施。首先,release分支被合并進master(回想一下,按定義,master上的每一個送出都導緻一個新的版本)。其次,master上的該次送出必須被打上标簽,以友善以後引用這個曆史版本。最後,release分支中的所有變更都需要合并回develop,使得未來版本也包含這些bug更新檔。

在Git中,前兩步操作:

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

release現在已經完成,且打上标簽以備未來引用。

注: 你可能也想使用 -s 或 -u <key> 标記來私密簽名。

要保持我們在release分支所做的變更,我們需要将其合并會develop。在Git中:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)      

這步操作可能引起合并沖突(很可能,因為我們已經改變了版本号)。如果是這樣,修複并送出。

現在,我們真的完成了,且該release分支将被移除,因為我們不再需要它了:

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

快速修複分支(Hotfix branches)

可能起源于分支:master

必須合并回分支:develop 和 master

分支命名慣例:hotfix-*

一個成功的Git分支模型 . 為什麼是git? 去中心卻集中(Decentralized but centralized) 主分支(main branches) 輔助分支(Supporting branches) 小結

Hotfix分支非常像release分支,因為它也是為新産品(雖然是計劃外的)釋出做準備的。它們起源于對目前已釋出産品中不理想狀态的立即響應的需求。當産品中出現一個緊急bug需要被立即解決時,一個hotfix分支可以從master分支中标記該産品版本的标簽開始建立。

其實質是,當一個人在準備産品的快速修複時,團隊其他成員在develop分支的工作能夠繼續進行。

建立hotfix分支

Hotfix分支從master分支中建立。例如,假定1.2是目前已經釋出的産品,卻由于嚴重的bug導緻了問題。但是develop分支中的變更尚不穩定。我們可能選擇建立hotfix分支并開始修複這個問題:

$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files 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.1
1 files changed, 1 insertions(+), 1 deletions(-)      

建立分支以後不用忘記修改版本号!

然後,修複bug并将代碼以一個或多個commit的形式送出。

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

結束hotfix分支

當完成時,對bug的修複需要合并進master分支,也需要合并回develop分支,以確定更新檔代碼也被包含在下一個版本中。這與release分支結束時的情況是完全類似的。

首先,更新master并對該次釋出打上标簽。

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

注: 你可能也想使用 -s 或 -u <key> 标記來私密簽名。

接着,使develop中也包含該修複代碼:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)      

此處規則的一個例外是,當release分支已經存在,hotfix的變更需要合并進這個release分支,而不是develop。合并進release分支的更新檔代碼最終也會在release分支完成時被合并進develop。(如果develop中的工作急需這個更新檔且等不及release最終完成,你也可以安全地将更新檔合并進develop。)

最後,移除這個臨時分支:

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

小結

雖然這個分支模型沒有什麼令人震驚的新東西,本文一開始給出的“大圖檔”在我們的項目中被證明了非常有用。它形成了一個優雅的很容易了解的模式,并強化團隊成員對分支和釋出過程的共享的了解。

這裡提供一個該圖檔的高品質PDF版本。列印它然後挂在牆上以供随時快速參考。

繼續閱讀