章節 ▾ 第二版

3.1 Git 分支 - 分支概覽

幾乎所有的版本控制系統都支援某種形式的分支。分支意味著你可以脫離主開發線,繼續進行開發而不影響主線。在許多版本控制工具中,這是一個相對昂貴的過程,通常需要你建立一個原始碼目錄的全新副本,這對於大型專案來說可能非常耗時。

有些人將 Git 的分支模型稱為其“殺手級特性”,它無疑使 Git 在版本控制社群中脫穎而出。為什麼它如此特別?Git 分支的實現方式非常輕量級,使得分支操作幾乎瞬時完成,而且在分支之間切換也通常一樣快。與其他許多版本控制系統不同,Git 鼓勵經常進行分支和合並的工作流程,甚至一天內進行多次。理解並掌握這個特性會給你一個強大而獨特的工具,並可能徹底改變你的開發方式。

分支概覽

為了真正理解 Git 的分支工作方式,我們需要退一步,審視 Git 是如何儲存其資料的。

正如你可能還記得 Git 是什麼? 中提到的,Git 儲存資料的方式不是一系列變更集或差異,而是一系列快照

當你提交時,Git 會儲存一個提交物件,其中包含指向你暫存內容快照的指標。該物件還包含作者姓名和電子郵件地址、你輸入的提交資訊,以及指向該提交之前一個或多個提交的指標(其父提交):對於初始提交為零個父提交,對於普通提交為一個父提交,而對於合併兩個或多個分支產生的提交則有多個父提交。

為了視覺化這一點,我們假設你有一個包含三個檔案的目錄,並將它們全部暫存然後提交。暫存檔案會計算每個檔案的校驗和(我們在 Git 是什麼? 中提到的 SHA-1 雜湊),將該檔案的版本儲存在 Git 倉庫中(Git 將它們稱為blob),並將該校驗和新增到暫存區。

$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'

當你透過執行 git commit 來建立提交時,Git 會校驗每個子目錄(在本例中,只有根專案目錄),並將它們作為樹物件儲存在 Git 倉庫中。然後,Git 會建立一個提交物件,該物件包含元資料以及指向根專案樹的指標,以便在需要時重新建立該快照。

你的 Git 倉庫現在包含五個物件:三個blob(每個代表三個檔案之一的內容)、一個tree(列出目錄內容並指定哪些檔名對應哪些 blob),以及一個commit(指向該根樹幷包含所有提交元資料)。

A commit and its tree
圖 9. 一個提交及其樹

如果你進行一些更改並再次提交,下一個提交會儲存一個指向其緊前面提交的指標。

Commits and their parents
圖 10. 提交及其父提交

Git 中的分支只是一個指向這些提交之一的、可移動的輕量級指標。Git 中的預設分支名稱是 master。當你開始進行提交時,會有一個指向你最新提交的 master 分支。每次提交時,master 分支指標會自動向前移動。

注意

Git 中的“master”分支並非特殊分支。它與其他任何分支完全相同。幾乎每個倉庫都有一個的原因是 git init 命令預設建立它,而且大多數人懶得去更改它。

A branch and its commit history
圖 11. 一個分支及其提交歷史

建立新分支

當你建立新分支時會發生什麼?嗯,這樣做會建立一個新的指標供你移動。假設你想建立一個名為 testing 的新分支。你可以透過 git branch 命令來實現

$ git branch testing

這會建立一個指向你當前所在提交的新指標。

Two branches pointing into the same series of commits
圖 12. 兩個分支指向同一系列提交

Git 如何知道你當前在哪個分支上?它會維護一個特殊的指標,稱為 HEAD。請注意,這與你在其他版本控制系統(如 Subversion 或 CVS)中可能習慣的 HEAD 概念大不相同。在 Git 中,它是指向你當前所在本地分支的指標。在這種情況下,你仍然在 master 分支上。git branch 命令僅僅建立了一個新分支,並沒有切換到該分支。

HEAD pointing to a branch
圖 13. HEAD 指向一個分支

你可以透過執行簡單的 git log 命令來輕鬆檢視這一點,該命令會顯示分支指標指向何處。此選項稱為 --decorate

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit

你可以看到 mastertesting 分支都指向 f30ab 提交。

切換分支

要切換到現有分支,請執行 git checkout 命令。讓我們切換到新的 testing 分支

$ git checkout testing

這會將 HEAD 移動到指向 testing 分支。

HEAD points to the current branch
圖 14. HEAD 指向當前分支

這有什麼意義呢?嗯,讓我們再進行一次提交

$ vim test.rb
$ git commit -a -m 'Make a change'
The HEAD branch moves forward when a commit is made
圖 15. 提交時 HEAD 分支向前移動

這很有趣,因為現在你的 testing 分支已經向前移動了,而你的 master 分支仍然指向你在執行 git checkout 切換分支時所在的提交。讓我們切換回 master 分支

$ git checkout master
注意
git log 不會總是顯示所有分支

如果你現在執行 git log,你可能會想知道你剛剛建立的“testing”分支去哪兒了,因為它不會出現在輸出中。

分支並沒有消失;Git 只是不知道你對該分支感興趣,它試圖顯示它認為你感興趣的內容。換句話說,預設情況下,git log 只會顯示你已檢出的分支下方的提交歷史。

要顯示所需分支的提交歷史,你必須顯式指定它:git log testing。要顯示所有分支,請將 --all 新增到你的 git log 命令中。

HEAD moves when you checkout
圖 16. 檢出時 HEAD 移動

該命令執行了兩項操作。它將 HEAD 指標移回指向 master 分支,並將工作目錄中的檔案恢復到 master 指向的快照。這也意味著你從現在開始所做的更改將與舊版本專案分道揚鑣。它基本上是回滾了你在 testing 分支中所做的工作,以便你可以朝不同的方向前進。

注意
切換分支會更改工作目錄中的檔案

重要的是要注意,當你切換 Git 中的分支時,工作目錄中的檔案會發生變化。如果你切換到較舊的分支,你的工作目錄將恢復到上一次在該分支提交時的樣子。如果 Git 無法乾淨地執行此操作,它將不允許你切換。

讓我們進行一些更改並再次提交

$ vim test.rb
$ git commit -a -m 'Make other changes'

現在你的專案歷史已經分叉(請參見 分叉歷史)。你建立了一個分支並切換到它,在該分支上進行了一些工作,然後切換回你的主分支並進行了其他工作。這些更改都隔離在不同的分支中:你可以在分支之間來回切換,並在準備好時將它們合併。所有這些都透過簡單的 branchcheckoutcommit 命令完成。

Divergent history
圖 17. 分叉歷史

你也可以透過 git log 命令輕鬆地看到這一點。如果你執行 git log --oneline --decorate --graph --all,它將打印出你的提交歷史,顯示你的分支指標在哪裡以及你的歷史如何分叉。

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Make other changes
| * 87ab2 (testing) Make a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 Initial commit of my project

由於 Git 中的分支實際上是一個包含其指向的提交的 40 位 SHA-1 校驗和的簡單檔案,因此建立和銷燬分支的成本很低。建立一個新分支就像向一個檔案寫入 41 位元組(40 個字元和一個換行符)一樣快速簡單。

這與大多數舊版本控制工具的分支方式形成了鮮明對比,它們通常涉及將專案的所有檔案複製到第二個目錄中。根據專案大小,這可能需要幾秒鐘甚至幾分鐘,而在 Git 中,這個過程總是瞬時的。此外,由於我們在提交時記錄了父提交,因此查詢合適的合併基礎進行合併會自動完成,並且通常非常容易。這些特性有助於鼓勵開發人員頻繁建立和使用分支。

讓我們看看為什麼你應該這樣做。

注意
同時建立新分支並切換到該分支

通常情況下,你會建立一個新分支並立即想切換到該新分支——這可以透過一個操作完成,即使用 git checkout -b <newbranchname>

注意

從 Git 版本 2.23 開始,你可以使用 git switch 代替 git checkout

  • 切換到現有分支:git switch testing-branch

  • 建立新分支並切換到它:git switch -c new-branch-c 標誌表示 create(建立),你也可以使用完整標誌:--create

  • 返回到之前檢出的分支:git switch -