Git 分支的最佳實踐


Git 分支的最佳實踐

譯自:A successful Git branching model » nvie.com

https://nvie.com/posts/a-successful-git-branching-model/

本文將展示我一年前在自己的項目中成功運用的開發模型。我一直打算把這些東西寫出來,但總是沒有抽出時間,現在終於寫好了。這裡介紹的不是任何項目的細節,而是有關分支的策略以及對發佈的管理。

Git 分支的最佳實踐

image.png

在我的演示中,所有的操作都是通過 git 完成的。

為什麼選擇 git ?

為了了斷 git 和中心源代碼控制系統的比較和爭論,請移步這裡看看 鏈接1 鏈接2。作為一個開發者,我喜歡 git 超過其它任何現有的工具。Git 真正改變了開發者對於合併和分支的認識。在傳統的 CVS/SVN 裡,合併/分支總是有點令人害怕的(“注意合併衝突,它們會搞死你的”)。

但是 git 中的這些操作是如此的簡單有效,它們真正作為你每天工作流程的一部分。比如,在 CVS/SVN 的書籍裡,分支和合並總是最後一個章節的討論重點(對於高級用戶),而在每一本 git 的書裡 鏈接1 鏈接2 鏈接3,這些內容已經被包含在第三章(基礎)裡了。

因為它的簡單直接和重複性,分支和合並不再令人害怕。版本控制工具比其它任何東西都支持分支/合併。

有關工具就介紹到這裡,我們現在進入開發模型這個正題。我要展現的模型本質上無外乎是一個流程的集合,每個團隊成員都有必要遵守這些流程,來達到管理軟件開發流程的目的。

分散但也集中

我們的分支模型中使用良好的代碼庫的設置方式,是圍繞一個真實的中心代碼庫的。注意,這裡的代碼庫僅僅被看做是一箇中心代碼庫(因為 git 是 DVCS,即分散版本控制系統,從技術層面看,是沒有所謂的中心代碼庫的)。我們習慣於把這個中心代碼庫命名為 origin,這同時也是所有 git 用戶的習慣。

Git 分支的最佳實踐

每一位開發者都向 origin 這個中心結點 pull 和 push。但是除此之外,每一位開發者也可以向其它結點 pull 改變形成子團隊。比如,對於兩個以上開發者同時開發一項大的新特性來說,為了不必過早向 origin 推送開發進度,這就非常有用。在上面的這個例子中,Alice 和 Bob、Alice 和 David、Clair 和 David 都是這樣的子團隊。

從技術角度,這無非意味著 Alice 定義一個名為 Bob 的 git remote,指向 Bob 的代碼庫,反之亦然。

主分支

Git 分支的最佳實踐


該開發模型的核心基本和現有的模型是一樣的。中心代碼庫永遠維持著兩個主要的分支:

  • master
  • develop

在 origin 上的 master 分支和每個 git 用戶的保持一致。而和 master 分支並行的另一個分支叫做 develop。

我們認為 origin/master 是其 HEAD 源代碼總是代表了生產環境準備就緒的狀態的主分支。

我們認為 origin/develop 是其 HEAD 源代碼總是代表了最後一次交付的可以趕上下一次發佈的狀態的主分支。有人也把它叫做“集成分支”。該源代碼還被作為了 nightly build 自動化任務的來源。

每當 develop 分支到達一個穩定的階段,可以對外發布時,所有的改變都會被合併到 master 分支,並打一個發佈版本的 tag。具體操作方法我們稍後討論。

因此,每次改動被合併到 master 的時候,這就是一個真正的新的發佈產品。我們建議對此進行嚴格的控制,因此理論上我們可以為每次 master 分支的提交都掛一個鉤子腳本,向生產環境自動化構建併發布我們的軟件。

支持型分支

我們的開發模型裡,緊接著 master 和 develop 主分支的,是多種多樣的支持型分支。它們的目的是幫助團隊成員並行處理每次追蹤特性、準備發佈、快速修復線上問題等開發任務。和之前的主分支不同,這些分支的生命週期都是有限的,它們最終都會被刪除掉。

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

  • feature 分支
  • release 分支
  • hotfix 分支

每一種分支都有一個特別的目的,並且有嚴格的規則,諸如哪些分支是它們的起始分支、哪些分支必須是它們合併的目標等。我們快速把它們過一遍。

這些“特殊”的分支在技術上是沒有任何特殊的。分支的類型取決於我們如何運用它們。它們完完全全都是普通而又平凡的 git 分支。

feature 分支

Git 分支的最佳實踐

  • 可能派發自:develop
  • 必須合併回:develop
  • 分支命名規範:除了 master 、develop 、release-* 或 hotfix-* 的任何名字

Feature 分支(有時也被稱作 topic 分支)用來開發包括即將發佈或遠期發佈的新的特性。當我們開始開發一個特性的時候,發佈合併的目標可能還不太確定。Feature 分支的生命週期會和新特性的開發週期保持同步,但是最終會合並回 develop (恩,下次發佈的時候把這個新特性帶上)或被拋棄(真是一次杯具的嘗試啊)。

Feature 分支通常僅存在於開發者的代碼庫中,並不出現在 origin 裡。

創建一個 feature 分支

當開始一個新特性的時候,從 develop 分支派發出一個分支

<code>$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
/<code>

把完成的特性合併回 develop

完成的特性可以合併回 develop

分支並趕上下一次發佈:

<code>$ git checkout develop
Switched to a new 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
/<code>

-no-ff 標記使得合併操作總是產生一次新的提交,哪怕合併操作可以快速完成。這個標記避免將 feature 分支和團隊協作的所有提交的歷史信息混在主分支的其它提交之後。比較一下:


Git 分支的最佳實踐


在右邊的例子裡,我們不可能從 git 的歷史記錄中看出來哪些提交實現了這一特性——你可能不得不查看每一筆提交日誌。恢復一個完整的特性(比如通過一組提交)在右邊變成了一個頭疼事情,而如果使用了 --no-ff 之後,就變得簡單了。

是的,這會創造一些沒有必要的(空的)提交記錄,但是得到的是大量的好處。

不幸的是,我還沒有找到一個在 git merge 時默認就把 --no-ff 標記打上的辦法,但這很重要。

release 分支

  • 可能派發自:develop
  • 必須合併回:develop 和 master
  • 分支命名規範:release-*

Release 分支用來支持新的生產環境發佈的準備工作。允許在最後階段產生提交點(dotting i's)和交匯點(crossing t's)。而且允許小幅度的問題修復以及準備發佈時的meta數據(比如版本號、發佈日期等)。在 release 分支做了上述這些工作之後,develop 分支會被“翻篇兒”,開始接收下一次發佈的新特性。

我們選擇(幾近)完成所有預期的開發的時候,作為從 develop 派發出 release 分支的時機。最起碼所有準備構建發佈的功能都已經及時合併到了 develop 分支。而往後才會發佈的功能則不應該合併到 develop 分支——他們必須等到 release 分支派發出去之後再做合併。

在一個 release 分支的開始,我們就賦予其一個明確的版本號。直到該分支創建之前,develop 分支上的描述都是“下一次”release 的改動,但這個“下一次”release 其實也沒說清楚是 0.3 release 還是 1.0 release。而在一個 release 分支的開始時這一點就會確定。這將成為有關項目版本號晉升的一個守則。

創建一個 release 分支

Release 分支派發自 develop 分支。比如,我們當前的生產環境發佈的版本是 1.1.5,馬上有一個 release 要發佈了。develop 分支已經為“下一次”release 做好了準備,並且我們已經決定把新的版本號定為 1.2 (而不是 1.1.6 或 2.0)。所以我們派發一個 release 分支並以新的版本號為其命名:

<code>$ 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(-)
/<code>

創建好並切換到新的分支之後,我們完成對版本號的晉升。這裡的 bump-version.sh 是一個虛構的用來改變代碼庫中某些文件以反映新版本的 shell 腳本。(當然你也可以手動完成這些改變——重點是有些文件發生了改變)然後,晉升了的版本號會被提交。

這個新的分支會存在一段時間,直到它確實發佈出去了為止。期間可能會有 bug 修復(這比在 develop 做更合理)。但我們嚴格禁止在此開發龐大的新特性,它們應該合併到 develop 分支,並放入下次發佈。

完成一個 release 分支

當 release 分支真正發佈成功之後,還有些事情需要收尾。首先,release 分支會被合併到 master (別忘了,master 上的每一次提交都代表一個真正的新的發佈);然後,為 master 上的這次提交打一個 tag,以便作為版本歷史的重要參考;最後,還要把 release 分支產生的改動合併回 develop,以便後續的發佈同樣包含對這些 bug 的修復。

前兩部在 git 下是這樣操作的:

<code>$ 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
/<code>

現在發佈工作已經完成了,同時 tag 也打好了,用在未來做參考。

補充:你也可以通過 -s 或 -u 標記打 tag。

為了保留 release 分支裡的改動記錄,我們需要把這些改動合併回 develop。git 操作如下:

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

這一步有可能導致衝突的發生(只是有理論上的可能性,因為我們已經改變了版本號),一旦發現,解決衝突然後提交就好了。

現在我們真正完成了一個 release 分支,該把它刪掉了,因為它的使命已經完成了:

<code>$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).
/<code>

hotfix 分支

Git 分支的最佳實踐

  • 可能派發自:master
  • 必須合併回:develop 和 master
  • 分支命名規範:hotfix-*

Hotfix 分支和 release 分支非常類似,因為他們都意味著會產生一個新的生產環境的發佈,儘管 hotfix 分支不是先前就計劃好的。他們在實時的生產環境版本出現意外需要快速響應時,從 master 分支相應的 tag 被派發。

我們這樣做的根本原因,是為了讓團隊其中一個人來快速修復生產環境的問題,其他成員可以按工作計劃繼續工作下去而不受太大影響。

創建一個 hotfix 分支

Hotfix 分支創建自 master 分支。例如,假設 1.2 版本是目前的生產環境且出現了一個嚴重的 bug,但是目前的 develop 並不足夠穩定。那麼我們可以派發出一個 hotfix 分支來開始我們的修復工作:

<code>$ 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(-)
/<code>

別忘了在派發出分支之後晉升版本號!

然後,修復 bug,提交改動。通過一個或多個提交都可以。

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

完成一個 hotfix 分支

當我們完成之後,對 bug 的修復需要合併回 master,同時也需要合併回 develop,以保證接下來的發佈也都已經解決了這個 bug。這和 release 分支的完成方式是完全一樣的。

首先,更新 master 併為本次發佈打一個 tag:

<code>$ 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
/<code>

補充:你也可以通過 -s 或 -u 標記打 tag。

然後,把已修復的 bug 合併到 develop:

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

/<code>

這個規矩的一個額外之處是:如果此時已經存在了一個 release 分支,那麼 hotfix 的改變需要合併到這個 release 分支,而不是 develop 分支。因為把對 bug 的修復合併回 release 分支之後,release 分支最終還是會合並回 develop 分支的。(如果在 develop 分支中立刻需要對這個 bug 的修復,且等不及 release 分支合併回來,則你還是可以直接合並回 develop 分支的,這是絕對沒問題的)

最後,刪掉這個臨時的分支:

<code>$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
/<code>

摘要其實這個分支模型裡沒有什麼新奇的東西。文章開頭的那張大圖對我們的項目來說非常有用。它非常易於團隊成員理解這個優雅有效的模型,並在團隊內部達成共識。這裡還有一份那張大圖的 高清PDF版本,你可以把它當做手冊放在手邊快速瀏覽。


分享到:


相關文章: