天天看點

git分支介紹.1 Git 分支 - 何謂分支

.1 Git 分支 - 何謂分支

何謂分支

為了了解 Git 分支的實作方式,我們需要回顧一下 Git 是如何儲存資料的。或許你還記得第一章的内容,Git 儲存的不是檔案差異或者變化量,而隻是一系列檔案快照。

在 Git 中送出時,會儲存一個送出(commit)對象,該對象包含一個指向暫存内容快照的指針,包含本次送出的作者等相關附屬資訊,包含零個或多個指向該送出對象的父對象指針:首次送出是沒有直接祖先的,普通送出有一個祖先,由兩個或多個分支合并産生的送出則有多個祖先。

為直覺起見,我們假設在工作目錄中有三個檔案,準備将它們暫存後送出。暫存操作會對每一個檔案計算校驗和(即第一章中提到的 SHA-1 哈希字串),然後把目前版本的檔案快照儲存到 Git 倉庫中(Git 使用 blob 類型的對象存儲這些快照),并将校驗和加入暫存區域:

$ git add README test.rb LICENSE

$ git commit -m 'initial commit of my project'

當使用 git commit 建立一個送出對象前,Git 會先計算每一個子目錄(本例中就是項目根目錄)的校驗和,然後在 Git 倉庫中将這些目錄儲存為樹(tree)對象。之後 Git 建立的送出對象,除了包含相關送出資訊以外,還包含着指向這個樹對象(項目根目錄)的指針,如此它就可以在将來需要的時候,重制此次快照的内容了。

現在,Git 倉庫中有五個對象:三個表示檔案快照内容的 blob 對象;一個記錄着目錄樹内容及其中各個檔案對應 blob 對象索引的 tree 對象;以及一個包含指向 tree 對象(根目錄)的索引和其他送出資訊中繼資料的 commit 對象。概念上來說,倉庫中的各個對象儲存的資料和互相關系看起來如圖 3-1 所示:

git分支介紹.1 Git 分支 - 何謂分支

圖 3-1. 單個送出對象在倉庫中的資料結構

作些修改後再次送出,那麼這次的送出對象會包含一個指向上次送出對象的指針(譯注:即下圖中的 parent 對象)。兩次送出後,倉庫曆史會變成圖 3-2 的樣子:

git分支介紹.1 Git 分支 - 何謂分支

圖 3-2. 多個送出對象之間的連結關系

現在來談分支。Git 中的分支,其實本質上僅僅是個指向 commit 對象的可變指針。Git 會使用 master 作為分支的預設名字。在若幹次送出後,你其實已經有了一個指向最後一次送出對象的 master 分支,它在每次送出的時候都會自動向前移動。

git分支介紹.1 Git 分支 - 何謂分支

圖 3-3. 分支其實就是從某個送出對象往回看的曆史

那麼,Git 又是如何建立一個新的分支的呢?答案很簡單,建立一個新的分支指針。比如建立一個 testing 分支,可以使用 git branch 指令:

$ git branch testing

這會在目前 commit 對象上建立一個分支指針(見圖 3-4)。

git分支介紹.1 Git 分支 - 何謂分支

圖 3-4. 多個分支指向送出資料的曆史

那麼,Git 是如何知道你目前在哪個分支上工作的呢?其實答案也很簡單,它儲存着一個名為 HEAD 的特别指針。請注意它和你熟知的許多其他版本控制系統(比如 Subversion 或 CVS)裡的 HEAD 概念大不相同。在 Git 中,它是一個指向你正在工作中的本地分支的指針(譯注:将 HEAD 想象為目前分支的别名。)。運作 git branch 指令,僅僅是建立了一個新的分支,但不會自動切換到這個分支中去,是以在這個例子中,我們依然還在 master 分支裡工作(參考圖 3-5)。

git分支介紹.1 Git 分支 - 何謂分支

圖 3-5. HEAD 指向目前所在的分支

要切換到其他分支,可以執行 git checkout 指令。我們現在轉換到建立的 testing 分支:

$ git checkout testing

這樣 HEAD 就指向了 testing 分支(見圖3-6)。

git分支介紹.1 Git 分支 - 何謂分支

圖 3-6. HEAD 在你轉換分支時指向新的分支

這樣的實作方式會給我們帶來什麼好處呢?好吧,現在不妨再送出一次:

$ vim test.rb

$ git commit -a -m 'made a change'

圖 3-7 展示了送出後的結果。

git分支介紹.1 Git 分支 - 何謂分支

圖 3-7. 每次送出後 HEAD 随着分支一起向前移動

非常有趣,現在 testing 分支向前移動了一格,而 master 分支仍然指向原先 git checkout 時所在的 commit 對象。現在我們回到 master 分支看看:

$ git checkout master

圖 3-8 顯示了結果。

git分支介紹.1 Git 分支 - 何謂分支

圖 3-8. HEAD 在一次 checkout 之後移動到了另一個分支

這條指令做了兩件事。它把 HEAD 指針移回到 master 分支,并把工作目錄中的檔案換成了 master 分支所指向的快照内容。也就是說,現在開始所做的改動,将始于本項目中一個較老的版本。它的主要作用是将 testing 分支裡作出的修改暫時取消,這樣你就可以向另一個方向進行開發。

我們作些修改後再次送出:

$ vim test.rb

$ git commit -a -m 'made other changes'

現在我們的項目送出曆史産生了分叉(如圖 3-9 所示),因為剛才我們建立了一個分支,轉換到其中進行了一些工作,然後又回到原來的主分支進行了另外一些工作。這些改變分别孤立在不同的分支裡:我們可以在不同分支裡反複切換,并在時機成熟時把它們合并到一起。而所有這些工作,僅僅需要 branch 和 checkout 這兩條指令就可以完成。

git分支介紹.1 Git 分支 - 何謂分支

圖 3-9. 不同流向的分支曆史

由于 Git 中的分支實際上僅是一個包含所指對象校驗和(40 個字元長度 SHA-1 字串)的檔案,是以建立和銷毀一個分支就變得非常廉價。說白了,建立一個分支就是向一個檔案寫入 41 個位元組(外加一個換行符)那麼簡單,當然也就很快了。

這和大多數版本控制系統形成了鮮明對比,它們管理分支大多采取備份所有項目檔案到特定目錄的方式,是以根據項目檔案數量和大小不同,可能花費的時間也會有相當大的差别,快則幾秒,慢則數分鐘。而 Git 的實作與項目複雜度無關,它永遠可以在幾毫秒的時間内完成分支的建立和切換。同時,因為每次送出時都記錄了祖先資訊(譯注:即 parent 對象),将來要合并分支時,尋找恰當的合并基礎(譯注:即共同祖先)的工作其實已經自然而然地擺在那裡了,是以實作起來非常容易。Git 鼓勵開發者頻繁使用分支,正是因為有着這些特性作保障。

接下來看看,我們為什麼應該頻繁使用分支。