Git詳解之三 Git分支(第一部分)

Git 分支

幾乎每一種版本控制系統都以某種形式支持分支。使用分支意味着你能夠從開發主線上分離開來,而後在不影響主線的同時繼續工做。在不少版本控制系統中,這是個昂貴的過程,經常須要建立一個源代碼目錄的完整副本,對大型項目來講會花費很長時間。html

有人把 Git 的分支模型稱爲「必殺技特性」,而正是由於它,將 Git 從版本控制系統家族裏區分出來。Git 有何特別之處呢?Git 的分支可謂是難以置信的輕量級,它的新建操做幾乎能夠在瞬間完成,而且在不一樣分支間切換起來也差很少同樣快。和許多其餘版本控制系統不一樣,Git 鼓勵在工做流程中頻繁使用分支與合併,哪怕一天以內進行許屢次都沒有關係。理解分支的概念並熟練運用後,你纔會意識到爲何 Git 是一個如此強大而獨特的工具,並今後真正改變你的開發方式。python

3.1  何謂分支

爲了理解 Git 分支的實現方式,咱們須要回顧一下 Git 是如何儲存數據的。或許你還記得第一章的內容,Git 保存的不是文件差別或者變化量,而只是一系列文件快照。git

在 Git 中提交時,會保存一個提交(commit)對象,該對象包含一個指向暫存內容快照的指針,包含本次提交的做者等相關附屬信息,包含零個或多個指向該提交對 象的父對象指針:首次提交是沒有直接祖先的,普通提交有一個祖先,由兩個或多個分支合併產生的提交則有多個祖先。github

爲直觀起見,咱們假設在工做目錄中有三個文件,準備將它們暫存後提交。暫存操做會對每個文件計算校驗和(即第一章中提到的 SHA-1 哈希字串),而後把當前版本的文件快照保存到 Git 倉庫中(Git 使用 blob 類型的對象存儲這些快照),並將校驗和加入暫存區域:vim

$ 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 所示:數據結構

圖 3-1. 單個提交對象在倉庫中的數據結構工具

做些修改後再次提交,那麼此次的提交對象會包含一個指向上次提交對象的指針(譯註:即下圖中的 parent 對象)。兩次提交後,倉庫歷史會變成圖 3-2 的樣子:測試

圖 3-2. 多個提交對象之間的連接關係網站

如今來談分支。Git 中的分支,其實本質上僅僅是個指向 commit 對象的可變指針。Git 會使用 master 做爲分支的默認名字。在若干次提交後,你其實已經有了一個指向最後一次提交對象的 master 分支,它在每次提交的時候都會自動向前移動。

圖 3-3. 分支其實就是從某個提交對象往回看的歷史

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

$ git branch testing

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

圖 3-4. 多個分支指向提交數據的歷史

那麼,Git 是如何知道你當前在哪一個分支上工做的呢?其實答案也很簡單,它保存着一個名爲 HEAD 的特別指針。請注意它和你熟知的許多其餘版本控制系統(好比 Subversion 或 CVS)裏的 HEAD 概念大不相同。在 Git 中,它是一個指向你正在工做中的本地分支的指針(譯註:將 HEAD 想象爲當前分支的別名。)。運行git branch 命令,僅僅是創建了一個新的分支,但不會自動切換到這個分支中去,因此在這個例子中,咱們依然還在 master 分支裏工做(參考圖 3-5)。

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

要切換到其餘分支,能夠執行 git checkout 命令。咱們如今轉換到新建的 testing 分支:

$ git checkout testing

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

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

這樣的實現方式會給咱們帶來什麼好處呢?好吧,如今不妨再提交一次:

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

圖 3-7 展現了提交後的結果。

圖 3-7. 每次提交後 HEAD 隨着分支一塊兒向前移動

很是有趣,如今 testing 分支向前移動了一格,而 master 分支仍然指向原先 git checkout 時所在的 commit 對象。如今咱們回到 master 分支看看:

$ git checkout master

圖 3-8 顯示告終果。

圖 3-7. 每次提交後 HEAD 隨着分支一塊兒向前移動

很是有趣,如今 testing 分支向前移動了一格,而 master 分支仍然指向原先 git checkout 時所在的 commit 對象。如今咱們回到 master 分支看看:

$ git checkout master

圖 3-8 顯示告終果。


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

這條命令作了兩件事。它把 HEAD 指針移回到 master 分支,並把工做目錄中的文件換成了 master 分支所指向的快照內容。也就是說,如今開始所作的改動,將始於本項目中一個較老的版本。它的主要做用是將 testing 分支裏做出的修改暫時取消,這樣你就能夠向另外一個方向進行開發。

咱們做些修改後再次提交:

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

如今咱們的項目提交歷史產生了分叉(如圖 3-9 所示),由於剛纔咱們建立了一個分支,轉換到其中進行了一些工做,而後又回到原來的主分支進行了另一些工做。這些改變分別孤立在不一樣的分支裏:咱們能夠 在不一樣分支裏反覆切換,並在時機成熟時把它們合併到一塊兒。而全部這些工做,僅僅須要branch 和 checkout 這兩條命令就能夠完成。

圖 3-9. 不一樣流向的分支歷史

因爲 Git 中的分支實際上僅是一個包含所指對象校驗和(40 個字符長度 SHA-1 字串)的文件,因此建立和銷燬一個分支就變得很是廉價。說白了,新建一個分支就是向一個文件寫入 41 個字節(外加一個換行符)那麼簡單,固然也就很快了。

這和大多數版本控制系統造成了鮮明對比,它們管理分支大多采起備份全部項目文件到特定目錄的方式,因此根據項目文件數量和大小不一樣,可能花費的時間 也會有至關大的差異,快則幾秒,慢則數分鐘。而 Git 的實現與項目複雜度無關,它永遠能夠在幾毫秒的時間內完成分支的建立和切換。同時,由於每次提交時都記錄了祖先信息(譯註:即parent 對象),未來要合併分支時,尋找恰當的合併基礎(譯註:即共同祖先)的工做其實已經天然而然地擺在那裏了,因此實現起來很是容易。Git 鼓勵開發者頻繁使用分支,正是由於有着這些特性做保障。

接下來看看,咱們爲何應該頻繁使用分支。

3.2  分支的新建與合併

如今讓咱們來看一個簡單的分支與合併的例子,實際工做中大致也會用到這樣的工做流程:

1. 開發某個網站。 2. 爲實現某個新的需求,建立一個分支。 3. 在這個分支上開展工做。

假設此時,你忽然接到一個電話說有個很嚴重的問題須要緊急修補,那麼能夠按照下面的方式處理:

1. 返回到原先已經發布到生產服務器上的分支。 2. 爲此次緊急修補創建一個新分支,並在其中修復問題。 3. 經過測試後,回到生產服務器所在的分支,將修補分支合併進來,而後再推送到生產服務器上。 4. 切換到以前實現新需求的分支,繼續工做。

分支的新建與切換

首先,咱們假設你正在項目中愉快地工做,而且已經提交了幾回更新(見圖 3-10)。

圖 3-10. 一個簡短的提交歷史

如今,你決定要修補問題追蹤系統上的 #53 問題。順帶說明下,Git 並不一樣任何特定的問題追蹤系統打交道。這裏爲了說明要解決的問題,才把新建的分支取名爲 iss53。要新建並切換到該分支,運行git checkout 並加上 -b 參數:

$ git checkout -b iss53 Switched to a new branch "iss53"

這至關於執行下面這兩條命令:

$ git branch iss53 
$ git checkout iss53

圖 3-11 示意該命令的執行結果。


圖 3-11. 建立了一個新分支的指針

接着你開始嘗試修復問題,在提交了若干次更新後,iss53 分支的指針也會隨着向前推動,由於它就是當前分支(換句話說,當前的 HEAD 指針正指向 iss53,見圖 3-12):

$ vim index.html 
$ git commit -a -m 'added a new footer [issue 53]'

圖 3-12. iss53 分支隨工做進展向前推動

如今你就接到了那個網站問題的緊急電話,須要立刻修補。有了 Git ,咱們就不須要同時發佈這個補丁和 iss53 裏做出的修改,也不須要在建立和發佈該補丁到服務器以前花費大力氣來複原這些修改。惟一須要的僅僅是切換回master 分支。

不過在此以前,留心你的暫存區或者工做目錄裏,那些尚未提交的修改,它會和你即將檢出的分支產生衝突從而阻止 Git 爲你切換分支。切換分支的時候最好保持一個清潔的工做區域。稍後會介紹幾個繞過這種問題的辦法(分別叫作 stashing 和 commit amending)。目前已經提交了全部的修改,因此接下來能夠正常轉換到master 分支:

$ git checkout master Switched to branch "master"

此時工做目錄中的內容和你在解決問題 #53 以前如出一轍,你能夠集中精力進行緊急修補。這一點值得牢記:Git 會把工做目錄的內容恢復爲檢出某分支時它所指向的那個提交對象的快照。它會自動添加、刪除和修改文件以確保目錄的內容和你當時提交時徹底同樣。

接下來,你得進行緊急修補。咱們建立一個緊急修補分支 hotfix 來開展工做,直到搞定(見圖 3-13):

$ git checkout -b 'hotfix' Switched to a new branch "hotfix" 
$ vim index.html 
$ git commit -a -m 'fixed the broken email addre

圖 3-13. hotfix 分支是從 master 分支所在點分化出來的

有必要做些測試,確保修補是成功的,而後回到 master 分支並把它合併進來,而後發佈到生產服務器。用 git merge命令來進行合併:

$ git checkout master 
$ git merge hotfix 
Updating f42c576..3a0874c Fast forward README | 1 - 1 files changed, 0 insertions(+), 1 deletions(-)

請注意,合併時出現了「Fast forward」的提示。因爲當前 master 分支所在的提交對象是要併入的 hotfix 分支的直接上游,Git 只需把master 分支指針直接右移。換句話說,若是順着一個分支走下去能夠到達另外一個分支的話,那麼 Git 在合併二者時,只會簡單地把指針右移,由於這種單線的歷史分支不存在任何須要解決的分歧,因此這種合併過程能夠稱爲快進(Fast forward)。

如今最新的修改已經在當前 master 分支所指向的提交對象中了,能夠部署到生產服務器上去了(見圖 3-14)。


在那個超級重要的修補發佈之後,你想要回到被打擾以前的工做。因爲當前 hotfix 分支和 master 都指向相同的提交對象,因此hotfix 已經完成了歷史使命,能夠刪掉了。使用 git branch 的 -d 選項執行刪除操做:

$ git branch -d hotfix 
Deleted branch hotfix (3a0874c).

如今回到以前未完成的 #53 問題修復分支上繼續工做(圖 3-15):

$ git checkout iss53 Switched to branch "iss53" $ vim index.html 
$ git commit -a -m 'finished the new footer [issue 53]' [iss

圖 3-15. iss53 分支能夠不受影響繼續推動。

不用擔憂以前 hotfix 分支的修改內容還沒有包含到 iss53 中來。若是確實須要歸入這次修補,能夠用git merge master 把 master 分支合併到 iss53;或者等 iss53 完成以後,再將iss53 分支中的更新併入master。

分支的合併

在問題 #53 相關的工做完成以後,能夠合併回 master 分支。實際操做同前面合併 hotfix 分支差很少,只需回到master 分支,運行 git merge 命令指定要合併進來的分支:

$ git checkout master 
$ git merge iss53 Merge made by recursive. README | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)

請注意,此次合併操做的底層實現,並不一樣於以前 hotfix 的併入方式。由於此次你的開發歷史是從更早的地方開始分叉的。因爲當前 master 分支所指向的提交對象(C4)並非 iss53 分支的直接祖先,Git 不得不進行一些額外處理。就此例而言,Git 會用兩個分支的末端(C4 和 C5)以及它們的共同祖先(C2)進行一次簡單的三方合併計算。圖 3-16 用紅框標出了 Git 用於合併的三個提交對象:

圖 3-16. Git 爲分支合併自動識別出最佳的同源合併點。

此次,Git 沒有簡單地把分支指針右移,而是對三方合併後的結果從新作一個新的快照,並自動建立一個指向它的提交對象(C6)(見圖 3-17)。這個提交對象比較特殊,它有兩個祖先(C4 和 C5)。

值得一提的是 Git 能夠本身裁決哪一個共同祖先纔是最佳合併基礎;這和 CVS 或 Subversion(1.5 之後的版本)不一樣,它們須要開發者手工指定合併基礎。因此此特性讓 Git 的合併操做比其餘系統都要簡單很多。

圖 3-17. Git 自動建立了一個包含了合併結果的提交對象。

既然以前的工做成果已經合併到 master 了,那麼 iss53 也就沒用了。你能夠就此刪除它,並在問題追蹤系統裏關閉該問題。

既然以前的工做成果已經合併到 master 了,那麼 iss53 也就沒用了。你能夠就此刪除它,並在問題追蹤系統裏關閉該問題。

$ git branch -d iss53

遇到衝突時的分支合併

有時候合併操做並不會如此順利。若是在不一樣的分支中都修改了同一個文件的同一部分,Git 就沒法乾淨地把二者合到一塊兒(譯註:邏輯上說,這種問題只能由人來裁決。)。若是你在解決問題 #53 的過程當中修改了hotfix 中修改的部分,將獲得相似下面的結果:

$ git merge iss53 
Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result.

Git 做了合併,但沒有提交,它會停下來等你解決衝突。要看看哪些文件在合併時發生衝突,能夠用 git status 查閱:

[master*]$ git status
 index.html: needs merge # On branch master # Changed but not updated: # (use "git add ..." to update what will be committed) # (use "git checkout -- ..." to discard changes in working directory) # #	unmerged: index.html #

任何包含未解決衝突的文件都會以未合併(unmerged)的狀態列出。Git 會在有衝突的文件里加入標準的衝突解決標記,能夠經過它們來手工定位並解決這些衝突。能夠看到此文件包含相似下面這樣的部分:

<<<<<<< HEAD:index.htmlcontact : email.support@github.com
=======please contact us at support@github.com
>>>>>>> iss53:index.html

能夠看到 ======= 隔開的上半部分,是 HEAD(即 master 分支,在運行merge 命令時所切換到的分支)中的內容,下半部分是在 iss53 分支中的內容。解決衝突的辦法無非是兩者選其一或者由你親自整合到一塊兒。好比你能夠經過把這段內容替換爲下面這樣來解決:

please contact us at email.support@github.com

這個解決方案各採納了兩個分支中的一部份內容,並且我還刪除了 <<<<<<<,======= 和 >>>>>>> 這些行。在解決了全部文件裏的全部衝突後,運行 git add 將把它們標記爲已解決狀態(譯註:實際上就是來一次快照保存到暫存區域。)。由於一旦暫存,就表示衝突已經解決。若是你想用一個有圖形界面的工具來解決這些問題,不妨運行git mergetool,它會調用一個可視化的合併工具並引導你解決全部衝突:

$ git mergetool 
merge tool 
candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff Merging the files: index.html Normal merge conflict for 'index.html': {local}: modified {remote}: modified Hit return to start merge resolution tool (opendiff):

若是不想用默認的合併工具(Git 爲我默認選擇了 opendiff,由於我在 Mac 上運行了該命令),你能夠在上方」merge tool candidates」裏找到可用的合併工具列表,輸入你想用的工具名。咱們將在第七章討論怎樣改變環境中的默認值。

退出合併工具之後,Git 會詢問你合併是否成功。若是回答是,它會爲你把相關文件暫存起來,以代表狀態爲已解決。

再運行一次 git status 來確認全部衝突都已解決:

$ git status 
# On branch master # Changes to be committed: # (use "git reset HEAD ..." to unstage) # #	modified: index.html #

若是以爲滿意了,而且確認全部衝突都已解決,也就是進入了暫存區,就能夠用 git commit 來完成此次合併提交。提交的記錄差很少是這樣:

Merge branch 'iss53' Conflicts: index.html # # It looks like you may be committing a MERGE. # If this is not correct, please remove the file # .git/MERGE_HEAD # and try again. #

若是想給未來看此次合併的人一些方便,能夠修改該信息,提供更多合併細節。好比你都做了哪些改動,以及這麼作的緣由。有時候裁決衝突的理由並不直接或明顯,有必要略加註解。

相關文章
相關標籤/搜索