Git
Chapters ▾ 2nd Edition

3.1 使用 Git 分支 - 簡述分支

幾乎每一種版本控制系統(Version Control System,以下簡稱 VCS)都支援某種形式的分支(branch)功能, 使用分支意味著你可以從開發主線上分離開來,然後在不影響主線的情況下繼續工作; 在很多 VCS 中,這是個昂貴的過程,常常需要對原始程式碼目錄建立一個完整的副本,對大型專案來說會花費很長時間。

有人把 Git 的分支模型視為它的「殺手級功能」,正是因為它而讓 Git 在 VCS 社群中顯得與眾不同。 它有何特別之處呢? Git 的分支簡直是難以置信的羽量級,新建分支的操作幾乎可以在瞬間完成,並且一般來說切換不同分支也很快; 跟其它的 VCS 不一樣的地方是 Git 鼓勵在工作流程中頻繁地使用分支與合併(merge),即使一天之內進行許多次都沒問題。 理解並掌握這個特性後,它會給你一個強大而獨特的工具,從此完全地改變你的開發方式。

簡述分支

為了理解 Git 分支(branch)的使用方式,我們需要回顧一下 Git 是如何保存資料的。

或許你還記得 開始 的內容,Git 保存的不是變更集或者差異內容,而是一系列快照。

當你製造一個提交(commit)時,Git 會儲存一個提交物件,該物件內容包含一個指標,用來代表已預存的快照內容; 這個物件內容還包含「作者名字和電子郵件」、「你輸入的訊息內容」、「指向前一個提交的指標(該提交的親代提交)」:沒有親代(parent)提交表示它是初始的第一個提交,一般情況下只有一個親代提交,超過一個親代提交表示它是從二個以上的分支合併而來的。

為了具體說明,讓我們假設你有一個目錄包含了三個檔案,你預存(stage)並提交了它們; 檔案預存操作會對每一個檔案內容(譯註:請注意,只有檔案「內容」)計算雜湊值(即 開始 中提到的 SHA-1 雜湊值),然後把那個檔案內容版本保存到 Git 版本庫中(Git 把它們視為 blob 類型的物件),再將這個雜湊值寫入預存區(staging area):

$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'

當使用 git commit 建立一個提交時,Git 會先計算每一個子目錄(本例中則只有專案根目錄)的雜湊值,然後在 Git 版本庫中將這些目錄記錄為樹(tree)物件; 之後 Git 建立提交物件,它除了包含相關提交資訊以外,還包含著指向專案根目錄的樹物件指標,如此它就可以在需要的時候重建此次快照內容。

你的 Git 版本庫現在有五個物件:三個 blob 物件用來儲存檔案內容、一個樹物件用來列出目錄的內容並紀錄各個檔案所對應的 blob 物件、一個提交用來記錄根目錄的樹物件和其他提交資訊。

單個提交在版本庫中的資料結構。
圖表 9. 單個提交在版本庫中的資料結構

如果你做一些修改並再次提交,這次的提交會再包含一個指向上次提交的指標(譯注:即下圖中的 parent 欄位)。

提交和它們的親代提交。
圖表 10. 提交和它們的親代提交

Git 分支其實只是一個指向某提交的可移動輕量級指標, Git 預設分支名稱是 master, 隨著不斷地製作提交,master 分支會為你一直指向最後一個提交, 它在每次提交的時候都會自動向前移動。

筆記

「master」在 Git 中並不是一個特殊的分支, 它和其它分支並無分別, 之所以幾乎每個版本庫裡都會有這個分支的原因是 git init 命令的預設行為會產生它,而大部分的人就這麼直接使用它。

分支及其提交歷史。
圖表 11. 分支及其提交歷史

建立一個新的分支

建立一個新分支會發生什麼事呢? 答案很簡單,建立一個新的、可移動的指標; 比如新建一個 testing 分支, 可以使用 git branch 命令:

$ git branch testing

這會在目前提交上新建一個指標。

二個分支都指向同一系列的提交歷史。
圖表 12. 二個分支都指向同一系列的提交歷史

Git 如何知道你目前在哪個分支上工作的呢? 其實它保存了一個名為 HEAD 的特別指標; 請注意它和你可能慣用的其他 VCSs 裡的 HEAD 概念大不相同,比如 Subversion 或 CVS; 在 Git 中,它就是一個指向你正在工作中的本地分支的指標(譯注:HEAD 等於「目前的」), 所以在這個例子中,你仍然在 master 分支上工作; 執行 git branch 命令,只是「建立」一個新的分支——它並不會切換到這個分支。

HEAD 指向一個分支。
圖表 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 Fixed bug #1328 - stack overflow under certain conditions
98ca9 The initial commit of my project

你可以看到「master」和「testing」分支就顯示在 f30ab 提交旁邊。

在分支之間切換

要切換到一個已經存在的分支,你可以執行 git checkout 命令, 讓我們切換到新的 testing 分支:

$ git checkout testing

這會移動 HEAD 並指向 testing 分支。

被 HEAD 指向的分支是目前分支。
圖表 14. 被 HEAD 指向的分支是目前分支

這樣做有什麼意義呢? 好吧!讓我們再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'
當再次提交時,被 HEAD 指向的分支會往前走。
圖表 15. 當再次提交時,被 HEAD 指向的分支會往前走

非常有趣,現在 testing 分支向前移動了,而 master 分支仍然指向當初在執行 git checkout 時所在的提交, 讓我們切回 master 分支看看:

$ git checkout master
當你檢出時,HEAD 會移動。
圖表 16. 當你檢出時,HEAD 會移動

這條命令做了兩件事, 它把 HEAD 指標移回去並指向 master 分支,然後把工作目錄中的檔案換成 master 分支所指向的快照內容; 也就是說,現在開始所做的改動,將基於專案中較舊的版本,然後與其它提交歷史分離開來; 它實際上是取消你在 testing 分支裡所做的修改,這樣你就可以往不同方向前進。

筆記
切換分支會修改工作目錄裡的檔案

重要的是要注意:當你在 Git 中切換分支時,工作目錄內的檔案將會被修改; 如果切換到舊分支,你的工作目錄會回復到看起來就像當初你最後一次在這個分支提交時的樣子。 如果 Git 無法很乾淨地切換過去,它就不會讓你切換過去。

讓我們做一些修改並再次提交:

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

現在你的專案歷史開始分離了(詳見 分離的歷史); 你建立並切換到新分支,在上面進行了一些工作,然後切換回到主分支進行了另外一些工作, 雙方的改變分別隔離在不同的分支裡:你可以在不同分支裡反覆切換,並在時機成熟時把它們合併到一起; 而所有這些工作只需要簡單的 branchcheckoutcommit 命令。

分離的歷史。
圖表 17. 分離的歷史

你一樣可以從 git log 中輕鬆地看出這件事, 執行 git log --oneline --decorate --graph --all,它會印出你的提交歷史,顯示你的分支指標在哪裡,以及歷史如何被分離開來。

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

由於 Git 分支實際上只是一個檔案,該檔案內容是這個分支指向的提交的雜湊值(40 個字元長度的 SHA-1 字串),所以建立和銷毀一個分支就變得非常廉價; 新建一個分支就是向一個檔寫入 41 個位元組(40 個字元外加一個換行符號)那樣地簡單和快速。

這樣的分支功能和大多數舊 VCS 的分支功能形成了鮮明的對比,有些分支功能甚至需要複製專案中全部的檔案到另一個資料夾, 而根據專案檔案數量和大小的不同,可能花費的時間快則幾秒,慢則數分鐘;而在 Git 中幾乎都在瞬間完成。 還有,因為每次提交時都記錄了親代資訊,將來要合併分支時,它通常會幫我們自動並輕鬆地找到適當的合併基礎; 這樣子的特性在無形間鼓勵了開發者頻繁地建立和使用分支。

讓我們來瞧一瞧為什麼你應該要這麼做。