一個成功的 Git 分支模型(適用於商業應用開發)
在這篇文章中,我將推廣一下大約一年前我介紹過的一些項目(公私皆有)中使用的開發模型,它們的結果都非常成功。有段時間我非常想寫出來分享一下,但是我至今才抽出時間來。我不會言及任何項目細節,僅討論分支策略和發布管理。

為何使用 git?
關於 Git 和集中式源碼版本控制系統的優缺點對比討論, 見 此 web。這裡有很多精彩激烈的論戰。作為一名開發者,現在我更偏好使用 Git 。Git 真的改變了開發者關於合併和分支的認知。我來自傳統的 CVS/Subversion 世界,合併/分支是件恐怖的事情 (「小心合併衝突,它們會反噬你!」),而且大家偶爾才會做這件事。
但是使用 Git,這些操作就顯得輕而易舉,它們會成為你 日常 工作流的核心部分。例如,在 CVS/Subversion 手冊中,分支和合併直到最後章節才首次出現(僅供高級用戶參考),但是在 每個 Git 手冊中,第三章就覆蓋到了(基本上)。
因為具有簡單和可重複的基因,分支和合併不再是什麼值得擔心的問題了。版本控制工具應該專註於分支/合併,而非其他事情。
關於工具的了解已經夠了,那麼我們就開始進入開發模型了。這個我要介紹的開發模型,不過是每個團隊成員進入軟體開發流程之前必須遵循的規範。
去中心化和中心化
基於一個「真正」中心庫的這種分支模型可以讓我們很好的協同工作。注意這個庫僅僅是被認為一個中心庫(因為 GIT 在技術角度並沒有中心庫這一說法)。我們將此倉庫命名為 origin,因為這名字被 Git 用戶熟知。

所有開發者都從 origin 庫拉取或向其推送代碼。在中心化的推送-拉取關係之外,開發者也可以從其他的開發者庫拉取代碼更改。例如,為了開發一個大型功能,有多位開發者組成一個開發小組,在功能完成之前,開發過程不必推送至 origin 庫。在上面的圖中就有 Alice 和 Bob, Alice 和 David,還有 Clair 和 David 之間組成的小組。
技術層面,Alice 要在本地庫加一個遠程 Git 分支並命名為 bob,指向 Bob 的倉庫。其他人也一樣。
主分支

git 的核心在開發模式上受到了現有模式的極大啟發,中心倉庫在整個生命周期保持了兩個主要的分支:
masterdevelop
每個 Git 用戶都對在 origin 的 master 分支很熟悉。 跟 master 分支並行的是另一個稱為 develop 的分支。
我們稱 origin/master 為主分支,這個分支源碼的 HEAD 一直指向 可用於生產環境 的狀態。
我們稱 origin/develop 為主分支,這個分支源碼的 HEAD 總是反映下一個版本的最新開發狀況。有些人稱這個分支為 "整合分支" 。所有的每日自動構建都是從這兒構建的。
當 develop 分支上的源代碼達到一個穩定點並準備發布時, 所有的更改都應該以某種方式合併回 master 分支, 然後使用發行版本進行標註。 接下來將從細節上討論這是如何完成的。
因此, 每次將變更合併回 master分支時, 這是一個 根據定義 的新產品發布。 我們趨向於對此非常嚴格,因此理論上來講, 我們可以在每次提交到 master 分支時, 使用一個 Git 鉤子腳本來自動完成構建和發行我們的軟體到生產伺服器。
輔助分支
在討論完 master 分支和 develop 分支後,將要討論的多樣化的輔助分支,支持成員間並行開發, 輕鬆跟蹤功能開發、生產版本發布、還能快速修復生產環境中產生的 Bug 。和主分支不同的是,輔助分支只有有限的生命周期,通常在完成使命後會被刪除。
可能用到的輔助分支分類有:
- 功能分支
- 發布分支
- 修復 Bug 分支
每個分支都有特殊的用途。這些分支的來源分支和他們要合併回的分支都有嚴格的定義。在後面我們再具體討論。
技術上,輔助分支都沒有特殊含義。我們就是根據具體使用功能對輔助分支進行分類。輔助分支和普通的 Git 分支沒有區別。
功能分支
功能分支可能源自於:
develop 分支
功能分支必須合併回:
develop 分支
功能分支命名慣例:
任何名字都可以,但不能包含 master, develop, release-*, 或者 hotfix-* 。
功能分支(或稱為特性分支)是被用來開發新功能的,這些新功能是要即將上線或更長時間後發布的。功能分支創建後開始開發時,之後將要合併的時間點是不知道的。功能分支的精髓是伴隨開發過程一直存在,但是肯定會被合併回 develop (在下一個預期的發布版本中清晰的添加新功能 ) 或被丟棄 (萬一實驗不盡如人意)。
功能分支通常存在開發人員的倉庫中,不會出現在 origin 倉庫。
創建一個功能分支
當我們開始寫一個新的功能時,請從 develop 分支中切換出來
$ git checkout -b myfeature developSwitched to a new branch "myfeature"
在開發中加入已經完成的功能
完成的功能可能被合併進入 develop 分支中,以確保他們會被添加到即將發布的版本中去
$ git checkout developSwitched 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 標記將會在分支合併的時候在創建一個新的提交對象,即使這次合併可以使用 fast-forwark方法進行提交。這就可以避免丟失功能分支的歷史信息並且可以把所有的功能在疊加在一起提交上去,請看圖片對比:

在後一種情況中,從 Git 歷史中是無法查看到是哪幾個提交對象在一起實現了一個功能 -——您必須手動讀取所有日誌消息。還原整個功能(即一組完整的提交)在後一種情況下是真的讓人很頭疼的事情,但是如果使用使用 --no-ff 標誌,就可以很容易完成這個任務的。
當然啦,它雖然創作更多的空的提交對象,但是增益卻遠遠大於成本。
發布分支
該分支可能源自於:
develop 分支
必須合併到:
develop 和 master 分支
分支命名習慣:
release-*
發布分支(Release branches) 支持新產品發布的準備。 它們允許在最後一刻追求細節。此外,它們允許小錯誤修復以及為發布準備元數據(版本號,構建日期等)。通過在發布分支上做的這些工作, develop 分支被清除以接收下一個大版本的功能特性。
從 develop 分支檢出一個新發布分支的重要時刻就是當開發(基本上)反映了新版預期狀態的時候。 至少,在那時,所有以『即將構建的發布版』( release-to-be-built )為目標的功能特性必須合併回 develop 分支。 針對未來版本的所有功能則可能不會 —— 它們必須等到發布分支檢出以後才可以這麼做。
正是在發布分支的開始,即將發布的版本才會被分配一個版本號 —— 一個前所未有的版本號。直到那一刻,develop 分支才反映了『下一版』的變更,但在發布分支開始前,對於『下一版』最終會是 0.3 版還是 1.0 版仍然是不明確的。該決定是在發布分支開始時進行的,並且由項目關於版本號碰撞的規則來執行。
創建一個發布分支
發布分支源於 develop 分支。舉個栗子,假設我們當前發布的產品版本為 1.1.5 ,並且即將發布一個新的大版本。 develop 分支已經為『下一版』做好了準備,我們決定把版本號改成 1.2(而不是 1.1.6 或者 2.0)。那麼,我們要做的只是檢出發布分支並給它一個可以反映版本號的名字:
$ git checkout -b release-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 是一個虛構的腳本文件,用以改變工作副本中的一些文件來反映新版本。(當然也可以手動啦,重點是 那些 改變的文件)然後,碰撞後的版本號就被提交了。
直到確定會推出發布版的這段時間裡,新分支都會一直存在。在此期間,bug 修復可能會應用到這個分支上(而不是 develop 分支)。 嚴禁在此分支添加大的新功能特性。 這些分支必須合併回 develop 分支,然後,等待下一個大版本的到來。
完成並發布你的分支
當你真的準備好要發布分支的時候,還需要執行一些別的操作。首先,發行版必須合併進 master 分支中(一定確保每次提交到 master 分支的都是最新的版本)。接下來,請一定標記對 master 分支的更新記錄,用於以後查看該版本時進行參考。最後, 發布新分支所做的更改需要重新合併為 develop 分支,以確保以後的版本也修復了這些錯誤。
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-*
熱修復(hotfix)分支和發布(release)分支很像,因為它們都意味著即將有新的生產版本發布,儘管不是意料之中的。它們產生的原因是現有的生產版本出現了不受歡迎的情況,從而必須立即起作用。當生產版本中的一個嚴重的 bug 必須馬上被修復,熱修復分支可能從 master 分支上用於標記生產版本的對應標籤上創建分支。
其本質是當有人準備對生產版本進行快速修復時,團隊成員(在 develop 分支)可以繼續工作。
創建修復 bug 分支
修復 bug 分支創建於 master 分支。 例如,1.2版本是當前生產環境的版本並且有 bug 。但是 develop 分支上的修改還不夠穩定。這時我們可以創建一個修復 bug 分支來解決這個問題:
$ 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 ,一次提交或多次分開提交。
$ git commit -m "Fixed severe production problem"[hotfix-1.2.1 abbe5d6] Fixed severe production problem5 files changed, 32 insertions(+), 17 deletions(-)
完成一個修復 bug 分支
當完成一個修復 bug 分支之後,bug 分支需要合併到 master 和 develop 分支上,以保證在下一版本中也包含該 bug 修復。 這與完成發布分支完全相似。
首先,更新 master 並對這次發布打上 tag 。
$ 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 分支里包含 bug 修復分支的改動:
$ git checkout developSwitched to branch develop$ git merge --no-ff hotfix-1.2.1Merge made by recursive.(Summary of changes)
對於上面的規則,有一點是例外的: 當發布分支已經存在時, bug 修復分支的改動應該合併到該發布分支,而不是 develop 分支。當發布分支完成的時候, 把 bug 修復分支反向合併到發布分支中,最終也會導致 bug 修復被合併到 develop 分支中去。(如果 develop 分支中的工作馬上就要這個 bug 修復的改動並且不能等待發布分支完成,那麼現在你也可以安全地將 bug 修複合併到 develop 分支中去。)
$ git branch -d hotfix-1.2.1Deleted branch hotfix-1.2.1 (was abbe5d6).
小結
雖然這種分支模式目前看來已經不是什麼新鮮事了,但這篇文章開頭的 「大圖」已經證明,這種模式對我們的項目實實在在的是非常有用。它形成了一個優雅並且更加容易理解的模型,並且能加強團隊中成員們對於分支和其釋放過程的理解。
這裡提供給大家上面大圖更加清晰的 PDF 版本,建議把它列印出來掛在牆上膜拜並且以便開發過程中快速查看。
更新: 如果有人需要這張圖片: 這裡附上下載文件 gitflow-model.src.key
Git 分支模型.pdf 下載
更多現代化 PHP 知識,請前往 Laravel / PHP 知識社區
推薦閱讀:
※搭建外匯平台的流程是什麼?
※如何優雅的打造DevOps團隊
※解讀ThoughtWorks技術雷達的正確姿勢
※排列組合KwCombinatorics
※數據標準化


