鍍金池/ 教程/ Java/ Git 分支
起步
Git 分支
自定義 Git
Git 基礎(chǔ)
Git 工具
Git 與其他系統(tǒng)
服務(wù)器上的 Git
Git 內(nèi)部原理
分布式 Git

Git 分支

幾乎每一種版本控制系統(tǒng)都以某種形式支持分支。使用分支意味著你可以從開(kāi)發(fā)主線(xiàn)上分離開(kāi)來(lái),然后在不影響主線(xiàn)的同時(shí)繼續(xù)工作。在很多版本控制系統(tǒng)中,這是個(gè)昂貴的過(guò)程,常常需要?jiǎng)?chuàng)建一個(gè)源代碼目錄的完整副本,對(duì)大型項(xiàng)目來(lái)說(shuō)會(huì)花費(fèi)很長(zhǎng)時(shí)間。

有人把 Git 的分支模型稱(chēng)為“必殺技特性”,而正是因?yàn)樗?,?Git 從版本控制系統(tǒng)家族里區(qū)分出來(lái)。Git 有何特別之處呢?Git 的分支可謂是難以置信的輕量級(jí),它的新建操作幾乎可以在瞬間完成,并且在不同分支間切換起來(lái)也差不多一樣快。和許多其他版本控制系統(tǒng)不同,Git 鼓勵(lì)在工作流程中頻繁使用分支與合并,哪怕一天之內(nèi)進(jìn)行許多次都沒(méi)有關(guān)系。理解分支的概念并熟練運(yùn)用后,你才會(huì)意識(shí)到為什么 Git 是一個(gè)如此強(qiáng)大而獨(dú)特的工具,并從此真正改變你的開(kāi)發(fā)方式。

何謂分支

為了理解 Git 分支的實(shí)現(xiàn)方式,我們需要回顧一下 Git 是如何儲(chǔ)存數(shù)據(jù)的。或許你還記得第一章的內(nèi)容,Git 保存的不是文件差異或者變化量,而只是一系列文件快照。

在 Git 中提交時(shí),會(huì)保存一個(gè)提交(commit)對(duì)象,該對(duì)象包含一個(gè)指向暫存內(nèi)容快照的指針,包含本次提交的作者等相關(guān)附屬信息,包含零個(gè)或多個(gè)指向該提交對(duì)象的父對(duì)象指針:首次提交是沒(méi)有直接祖先的,普通提交有一個(gè)祖先,由兩個(gè)或多個(gè)分支合并產(chǎn)生的提交則有多個(gè)祖先。

為直觀(guān)起見(jiàn),我們假設(shè)在工作目錄中有三個(gè)文件,準(zhǔn)備將它們暫存后提交。暫存操作會(huì)對(duì)每一個(gè)文件計(jì)算校驗(yàn)和(即第一章中提到的 SHA-1 哈希字串),然后把當(dāng)前版本的文件快照保存到 Git 倉(cāng)庫(kù)中(Git 使用 blob 類(lèi)型的對(duì)象存儲(chǔ)這些快照),并將校驗(yàn)和加入暫存區(qū)域:

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

當(dāng)使用 git commit 新建一個(gè)提交對(duì)象前,Git 會(huì)先計(jì)算每一個(gè)子目錄(本例中就是項(xiàng)目根目錄)的校驗(yàn)和,然后在 Git 倉(cāng)庫(kù)中將這些目錄保存為樹(shù)(tree)對(duì)象。之后 Git 創(chuàng)建的提交對(duì)象,除了包含相關(guān)提交信息以外,還包含著指向這個(gè)樹(shù)對(duì)象(項(xiàng)目根目錄)的指針,如此它就可以在將來(lái)需要的時(shí)候,重現(xiàn)此次快照的內(nèi)容了。

現(xiàn)在,Git 倉(cāng)庫(kù)中有五個(gè)對(duì)象:三個(gè)表示文件快照內(nèi)容的 blob 對(duì)象;一個(gè)記錄著目錄樹(shù)內(nèi)容及其中各個(gè)文件對(duì)應(yīng) blob 對(duì)象索引的 tree 對(duì)象;以及一個(gè)包含指向 tree 對(duì)象(根目錄)的索引和其他提交信息元數(shù)據(jù)的 commit 對(duì)象。概念上來(lái)說(shuō),倉(cāng)庫(kù)中的各個(gè)對(duì)象保存的數(shù)據(jù)和相互關(guān)系看起來(lái)如圖 3-1 所示:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0301-tn.png" alt="" />

圖 3-1. 單個(gè)提交對(duì)象在倉(cāng)庫(kù)中的數(shù)據(jù)結(jié)構(gòu)

作些修改后再次提交,那么這次的提交對(duì)象會(huì)包含一個(gè)指向上次提交對(duì)象的指針(譯注:即下圖中的 parent 對(duì)象)。兩次提交后,倉(cāng)庫(kù)歷史會(huì)變成圖 3-2 的樣子:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0302-tn.png" alt="" />

圖 3-2. 多個(gè)提交對(duì)象之間的鏈接關(guān)系

現(xiàn)在來(lái)談分支。Git 中的分支,其實(shí)本質(zhì)上僅僅是個(gè)指向 commit 對(duì)象的可變指針。Git 會(huì)使用 master 作為分支的默認(rèn)名字。在若干次提交后,你其實(shí)已經(jīng)有了一個(gè)指向最后一次提交對(duì)象的 master 分支,它在每次提交的時(shí)候都會(huì)自動(dòng)向前移動(dòng)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0303-tn.png" alt="" />

圖 3-3. 分支其實(shí)就是從某個(gè)提交對(duì)象往回看的歷史

那么,Git 又是如何創(chuàng)建一個(gè)新的分支的呢?答案很簡(jiǎn)單,創(chuàng)建一個(gè)新的分支指針。比如新建一個(gè) testing 分支,可以使用 git branch 命令:

$ git branch testing

這會(huì)在當(dāng)前 commit 對(duì)象上新建一個(gè)分支指針(見(jiàn)圖 3-4)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0304-tn.png" alt="" />

圖 3-4. 多個(gè)分支指向提交數(shù)據(jù)的歷史

那么,Git 是如何知道你當(dāng)前在哪個(gè)分支上工作的呢?其實(shí)答案也很簡(jiǎn)單,它保存著一個(gè)名為 HEAD 的特別指針。請(qǐng)注意它和你熟知的許多其他版本控制系統(tǒng)(比如 Subversion 或 CVS)里的 HEAD 概念大不相同。在 Git 中,它是一個(gè)指向你正在工作中的本地分支的指針(譯注:將 HEAD 想象為當(dāng)前分支的別名。)。運(yùn)行 git branch 命令,僅僅是建立了一個(gè)新的分支,但不會(huì)自動(dòng)切換到這個(gè)分支中去,所以在這個(gè)例子中,我們依然還在 master 分支里工作(參考圖 3-5)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0305-tn.png" alt="" />

圖 3-5. HEAD 指向當(dāng)前所在的分支

要切換到其他分支,可以執(zhí)行 git checkout 命令。我們現(xiàn)在轉(zhuǎn)換到新建的 testing 分支:

$ git checkout testing

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

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0306-tn.png" alt="" />

圖 3-6. HEAD 在你轉(zhuǎn)換分支時(shí)指向新的分支

這樣的實(shí)現(xiàn)方式會(huì)給我們帶來(lái)什么好處呢?好吧,現(xiàn)在不妨再提交一次:

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

圖 3-7 展示了提交后的結(jié)果。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0307-tn.png" alt="" />

圖 3-7. 每次提交后 HEAD 隨著分支一起向前移動(dòng)

非常有趣,現(xiàn)在 testing 分支向前移動(dòng)了一格,而 master 分支仍然指向原先 git checkout 時(shí)所在的 commit 對(duì)象?,F(xiàn)在我們回到 master 分支看看:

$ git checkout master

圖 3-8 顯示了結(jié)果。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0308-tn.png" alt="" />

圖 3-8. HEAD 在一次 checkout 之后移動(dòng)到了另一個(gè)分支

這條命令做了兩件事。它把 HEAD 指針移回到 master 分支,并把工作目錄中的文件換成了 master 分支所指向的快照內(nèi)容。也就是說(shuō),現(xiàn)在開(kāi)始所做的改動(dòng),將始于本項(xiàng)目中一個(gè)較老的版本。它的主要作用是將 testing 分支里作出的修改暫時(shí)取消,這樣你就可以向另一個(gè)方向進(jìn)行開(kāi)發(fā)。

我們作些修改后再次提交:

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

現(xiàn)在我們的項(xiàng)目提交歷史產(chǎn)生了分叉(如圖 3-9 所示),因?yàn)閯偛盼覀儎?chuàng)建了一個(gè)分支,轉(zhuǎn)換到其中進(jìn)行了一些工作,然后又回到原來(lái)的主分支進(jìn)行了另外一些工作。這些改變分別孤立在不同的分支里:我們可以在不同分支里反復(fù)切換,并在時(shí)機(jī)成熟時(shí)把它們合并到一起。而所有這些工作,僅僅需要 branchcheckout 這兩條命令就可以完成。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0309-tn.png" alt="" />

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

由于 Git 中的分支實(shí)際上僅是一個(gè)包含所指對(duì)象校驗(yàn)和(40 個(gè)字符長(zhǎng)度 SHA-1 字串)的文件,所以創(chuàng)建和銷(xiāo)毀一個(gè)分支就變得非常廉價(jià)。說(shuō)白了,新建一個(gè)分支就是向一個(gè)文件寫(xiě)入 41 個(gè)字節(jié)(外加一個(gè)換行符)那么簡(jiǎn)單,當(dāng)然也就很快了。

這和大多數(shù)版本控制系統(tǒng)形成了鮮明對(duì)比,它們管理分支大多采取備份所有項(xiàng)目文件到特定目錄的方式,所以根據(jù)項(xiàng)目文件數(shù)量和大小不同,可能花費(fèi)的時(shí)間也會(huì)有相當(dāng)大的差別,快則幾秒,慢則數(shù)分鐘。而 Git 的實(shí)現(xiàn)與項(xiàng)目復(fù)雜度無(wú)關(guān),它永遠(yuǎn)可以在幾毫秒的時(shí)間內(nèi)完成分支的創(chuàng)建和切換。同時(shí),因?yàn)槊看翁峤粫r(shí)都記錄了祖先信息(譯注:即 parent 對(duì)象),將來(lái)要合并分支時(shí),尋找恰當(dāng)?shù)暮喜⒒A(chǔ)(譯注:即共同祖先)的工作其實(shí)已經(jīng)自然而然地?cái)[在那里了,所以實(shí)現(xiàn)起來(lái)非常容易。Git 鼓勵(lì)開(kāi)發(fā)者頻繁使用分支,正是因?yàn)橛兄@些特性作保障。

接下來(lái)看看,我們?yōu)槭裁磻?yīng)該頻繁使用分支。

分支的新建與合并

現(xiàn)在讓我們來(lái)看一個(gè)簡(jiǎn)單的分支與合并的例子,實(shí)際工作中大體也會(huì)用到這樣的工作流程:

  1. 開(kāi)發(fā)某個(gè)網(wǎng)站。
  2. 為實(shí)現(xiàn)某個(gè)新的需求,創(chuàng)建一個(gè)分支。
  3. 在這個(gè)分支上開(kāi)展工作。

假設(shè)此時(shí),你突然接到一個(gè)電話(huà)說(shuō)有個(gè)很?chē)?yán)重的問(wèn)題需要緊急修補(bǔ),那么可以按照下面的方式處理:

  1. 返回到原先已經(jīng)發(fā)布到生產(chǎn)服務(wù)器上的分支。
  2. 為這次緊急修補(bǔ)建立一個(gè)新分支,并在其中修復(fù)問(wèn)題。
  3. 通過(guò)測(cè)試后,回到生產(chǎn)服務(wù)器所在的分支,將修補(bǔ)分支合并進(jìn)來(lái),然后再推送到生產(chǎn)服務(wù)器上。
  4. 切換到之前實(shí)現(xiàn)新需求的分支,繼續(xù)工作。

分支的新建與切換

首先,我們假設(shè)你正在項(xiàng)目中愉快地工作,并且已經(jīng)提交了幾次更新(見(jiàn)圖 3-10)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0310-tn.png" alt="" />

圖 3-10. 一個(gè)簡(jiǎn)短的提交歷史

現(xiàn)在,你決定要修補(bǔ)問(wèn)題追蹤系統(tǒng)上的 #53 問(wèn)題。順帶說(shuō)明下,Git 并不同任何特定的問(wèn)題追蹤系統(tǒng)打交道。這里為了說(shuō)明要解決的問(wèn)題,才把新建的分支取名為 iss53。要新建并切換到該分支,運(yùn)行 git checkout 并加上 -b 參數(shù):

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

這相當(dāng)于執(zhí)行下面這兩條命令:

$ git branch iss53
$ git checkout iss53

圖 3-11 示意該命令的執(zhí)行結(jié)果。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0311-tn.png" alt="" />

圖 3-11. 創(chuàng)建了一個(gè)新分支的指針

接著你開(kāi)始嘗試修復(fù)問(wèn)題,在提交了若干次更新后,iss53 分支的指針也會(huì)隨著向前推進(jìn),因?yàn)樗褪钱?dāng)前分支(換句話(huà)說(shuō),當(dāng)前的 HEAD 指針正指向 iss53,見(jiàn)圖 3-12):

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

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0312-tn.png" alt="" />

圖 3-12. iss53 分支隨工作進(jìn)展向前推進(jìn)

現(xiàn)在你就接到了那個(gè)網(wǎng)站問(wèn)題的緊急電話(huà),需要馬上修補(bǔ)。有了 Git ,我們就不需要同時(shí)發(fā)布這個(gè)補(bǔ)丁和 iss53 里作出的修改,也不需要在創(chuàng)建和發(fā)布該補(bǔ)丁到服務(wù)器之前花費(fèi)大力氣來(lái)復(fù)原這些修改。唯一需要的僅僅是切換回 master 分支。

不過(guò)在此之前,留心你的暫存區(qū)或者工作目錄里,那些還沒(méi)有提交的修改,它會(huì)和你即將檢出的分支產(chǎn)生沖突從而阻止 Git 為你切換分支。切換分支的時(shí)候最好保持一個(gè)清潔的工作區(qū)域。稍后會(huì)介紹幾個(gè)繞過(guò)這種問(wèn)題的辦法(分別叫做 stashing 和 commit amending)。目前已經(jīng)提交了所有的修改,所以接下來(lái)可以正常轉(zhuǎn)換到 master 分支:

$ git checkout master
Switched to branch 'master'

此時(shí)工作目錄中的內(nèi)容和你在解決問(wèn)題 #53 之前一模一樣,你可以集中精力進(jìn)行緊急修補(bǔ)。這一點(diǎn)值得牢記:Git 會(huì)把工作目錄的內(nèi)容恢復(fù)為檢出某分支時(shí)它所指向的那個(gè)提交對(duì)象的快照。它會(huì)自動(dòng)添加、刪除和修改文件以確保目錄的內(nèi)容和你當(dāng)時(shí)提交時(shí)完全一樣。

接下來(lái),你得進(jìn)行緊急修補(bǔ)。我們創(chuàng)建一個(gè)緊急修補(bǔ)分支 hotfix 來(lái)開(kāi)展工作,直到搞定(見(jiàn)圖 3-13):

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 3a0874c] fixed the broken email address
 1 files changed, 1 deletion(-)

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0313-tn.png" alt="" />

圖 3-13. hotfix 分支是從 master 分支所在點(diǎn)分化出來(lái)的

有必要作些測(cè)試,確保修補(bǔ)是成功的,然后回到 master 分支并把它合并進(jìn)來(lái),然后發(fā)布到生產(chǎn)服務(wù)器。用 git merge 命令來(lái)進(jìn)行合并:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 README | 1 -
 1 file changed, 1 deletion(-)

請(qǐng)注意,合并時(shí)出現(xiàn)了“Fast forward”的提示。由于當(dāng)前 master 分支所在的提交對(duì)象是要并入的 hotfix 分支的直接上游,Git 只需把 master 分支指針直接右移。換句話(huà)說(shuō),如果順著一個(gè)分支走下去可以到達(dá)另一個(gè)分支的話(huà),那么 Git 在合并兩者時(shí),只會(huì)簡(jiǎn)單地把指針右移,因?yàn)檫@種單線(xiàn)的歷史分支不存在任何需要解決的分歧,所以這種合并過(guò)程可以稱(chēng)為快進(jìn)(Fast forward)。

現(xiàn)在最新的修改已經(jīng)在當(dāng)前 master 分支所指向的提交對(duì)象中了,可以部署到生產(chǎn)服務(wù)器上去了(見(jiàn)圖 3-14)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0314-tn.png" alt="" />

圖 3-14. 合并之后,master 分支和 hotfix 分支指向同一位置。

在那個(gè)超級(jí)重要的修補(bǔ)發(fā)布以后,你想要回到被打擾之前的工作。由于當(dāng)前 hotfix 分支和 master 都指向相同的提交對(duì)象,所以 hotfix 已經(jīng)完成了歷史使命,可以刪掉了。使用 git branch-d 選項(xiàng)執(zhí)行刪除操作:

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

現(xiàn)在回到之前未完成的 #53 問(wèn)題修復(fù)分支上繼續(xù)工作(圖 3-15):

$ git checkout iss53
Switched to branch 'iss53'
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
 1 file changed, 1 insertion(+)

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0315-tn.png" alt="" />

圖 3-15. iss53 分支可以不受影響繼續(xù)推進(jìn)。

值得注意的是之前 hotfix 分支的修改內(nèi)容尚未包含到 iss53 中來(lái)。如果需要納入此次修補(bǔ),可以用 git merge master 把 master 分支合并到 iss53;或者等 iss53 完成之后,再將 iss53 分支中的更新并入 master。

分支的合并

在問(wèn)題 #53 相關(guān)的工作完成之后,可以合并回 master 分支。實(shí)際操作同前面合并 hotfix 分支差不多,只需回到 master 分支,運(yùn)行 git merge 命令指定要合并進(jìn)來(lái)的分支:

$ git checkout master
$ git merge iss53
Auto-merging README
Merge made by the 'recursive' strategy.
 README | 1 +
 1 file changed, 1 insertion(+)

請(qǐng)注意,這次合并操作的底層實(shí)現(xiàn),并不同于之前 hotfix 的并入方式。因?yàn)檫@次你的開(kāi)發(fā)歷史是從更早的地方開(kāi)始分叉的。由于當(dāng)前 master 分支所指向的提交對(duì)象(C4)并不是 iss53 分支的直接祖先,Git 不得不進(jìn)行一些額外處理。就此例而言,Git 會(huì)用兩個(gè)分支的末端(C4 和 C5)以及它們的共同祖先(C2)進(jìn)行一次簡(jiǎn)單的三方合并計(jì)算。圖 3-16 用紅框標(biāo)出了 Git 用于合并的三個(gè)提交對(duì)象:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0316-tn.png" alt="" />

圖 3-16. Git 為分支合并自動(dòng)識(shí)別出最佳的同源合并點(diǎn)。

這次,Git 沒(méi)有簡(jiǎn)單地把分支指針右移,而是對(duì)三方合并后的結(jié)果重新做一個(gè)新的快照,并自動(dòng)創(chuàng)建一個(gè)指向它的提交對(duì)象(C6)(見(jiàn)圖 3-17)。這個(gè)提交對(duì)象比較特殊,它有兩個(gè)祖先(C4 和 C5)。

值得一提的是 Git 可以自己裁決哪個(gè)共同祖先才是最佳合并基礎(chǔ);這和 CVS 或 Subversion(1.5 以后的版本)不同,它們需要開(kāi)發(fā)者手工指定合并基礎(chǔ)。所以此特性讓 Git 的合并操作比其他系統(tǒng)都要簡(jiǎn)單不少。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0317-tn.png" alt="" />

圖 3-17. Git 自動(dòng)創(chuàng)建了一個(gè)包含了合并結(jié)果的提交對(duì)象。

既然之前的工作成果已經(jīng)合并到 master 了,那么 iss53 也就沒(méi)用了。你可以就此刪除它,并在問(wèn)題追蹤系統(tǒng)里關(guān)閉該問(wèn)題。

$ git branch -d iss53

遇到?jīng)_突時(shí)的分支合并

有時(shí)候合并操作并不會(huì)如此順利。如果在不同的分支中都修改了同一個(gè)文件的同一部分,Git 就無(wú)法干凈地把兩者合到一起(譯注:邏輯上說(shuō),這種問(wèn)題只能由人來(lái)裁決。)。如果你在解決問(wèn)題 #53 的過(guò)程中修改了 hotfix 中修改的部分,將得到類(lèi)似下面的結(jié)果:

$ 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 作了合并,但沒(méi)有提交,它會(huì)停下來(lái)等你解決沖突。要看看哪些文件在合并時(shí)發(fā)生沖突,可以用 git status 查閱:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

任何包含未解決沖突的文件都會(huì)以未合并(unmerged)的狀態(tài)列出。Git 會(huì)在有沖突的文件里加入標(biāo)準(zhǔn)的沖突解決標(biāo)記,可以通過(guò)它們來(lái)手工定位并解決這些沖突??梢钥吹酱宋募?lèi)似下面這樣的部分:

<<<<<<< HEAD
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
  please contact us at support@github.com
</div>
>>>>>>> iss53

可以看到 ======= 隔開(kāi)的上半部分,是 HEAD(即 master 分支,在運(yùn)行 merge 命令時(shí)所切換到的分支)中的內(nèi)容,下半部分是在 iss53 分支中的內(nèi)容。解決沖突的辦法無(wú)非是二者選其一或者由你親自整合到一起。比如你可以通過(guò)把這段內(nèi)容替換為下面這樣來(lái)解決:

<div id="footer">
please contact us at email.support@github.com
</div>

這個(gè)解決方案各采納了兩個(gè)分支中的一部分內(nèi)容,而且我還刪除了 <<<<<<<,=======>>>>>>> 這些行。在解決了所有文件里的所有沖突后,運(yùn)行 git add 將把它們標(biāo)記為已解決狀態(tài)(譯注:實(shí)際上就是來(lái)一次快照保存到暫存區(qū)域。)。因?yàn)橐坏捍?,就表示沖突已經(jīng)解決。如果你想用一個(gè)有圖形界面的工具來(lái)解決這些問(wèn)題,不妨運(yùn)行 git mergetool,它會(huì)調(diào)用一個(gè)可視化的合并工具并引導(dǎo)你解決所有沖突:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

如果不想用默認(rèn)的合并工具(Git 為我默認(rèn)選擇了 opendiff,因?yàn)槲以?Mac 上運(yùn)行了該命令),你可以在上方"merge tool candidates"里找到可用的合并工具列表,輸入你想用的工具名。我們將在第七章討論怎樣改變環(huán)境中的默認(rèn)值。

退出合并工具以后,Git 會(huì)詢(xún)問(wèn)你合并是否成功。如果回答是,它會(huì)為你把相關(guān)文件暫存起來(lái),以表明狀態(tài)為已解決。

再運(yùn)行一次 git status 來(lái)確認(rèn)所有沖突都已解決:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   index.html

如果覺(jué)得滿(mǎn)意了,并且確認(rèn)所有沖突都已解決,也就是進(jìn)入了暫存區(qū),就可以用 git commit 來(lái)完成這次合并提交。提交的記錄差不多是這樣:

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.
#

如果想給將來(lái)看這次合并的人一些方便,可以修改該信息,提供更多合并細(xì)節(jié)。比如你都作了哪些改動(dòng),以及這么做的原因。有時(shí)候裁決沖突的理由并不直接或明顯,有必要略加注解。

分支的管理

到目前為止,你已經(jīng)學(xué)會(huì)了如何創(chuàng)建、合并和刪除分支。除此之外,我們還需要學(xué)習(xí)如何管理分支,在日后的常規(guī)工作中會(huì)經(jīng)常用到下面介紹的管理命令。

git branch 命令不僅僅能創(chuàng)建和刪除分支,如果不加任何參數(shù),它會(huì)給出當(dāng)前所有分支的清單:

$ git branch
  iss53
* master
  testing

注意看 master 分支前的 * 字符:它表示當(dāng)前所在的分支。也就是說(shuō),如果現(xiàn)在提交更新,master 分支將隨著開(kāi)發(fā)進(jìn)度前移。若要查看各個(gè)分支最后一個(gè)提交對(duì)象的信息,運(yùn)行 git branch -v

$ git branch -v
  iss53   93b412c fix javascript issue
* master  7a98805 Merge branch 'iss53'
  testing 782fd34 add scott to the author list in the readmes

要從該清單中篩選出你已經(jīng)(或尚未)與當(dāng)前分支合并的分支,可以用 --merged--no-merged 選項(xiàng)(Git 1.5.6 以上版本)。比如用 git branch --merged 查看哪些分支已被并入當(dāng)前分支(譯注:也就是說(shuō)哪些分支是當(dāng)前分支的直接上游。):

$ git branch --merged
  iss53
* master

之前我們已經(jīng)合并了 iss53,所以在這里會(huì)看到它。一般來(lái)說(shuō),列表中沒(méi)有 * 的分支通常都可以用 git branch -d 來(lái)刪掉。原因很簡(jiǎn)單,既然已經(jīng)把它們所包含的工作整合到了其他分支,刪掉也不會(huì)損失什么。

另外可以用 git branch --no-merged 查看尚未合并的工作:

$ git branch --no-merged
  testing

它會(huì)顯示還未合并進(jìn)來(lái)的分支。由于這些分支中還包含著尚未合并進(jìn)來(lái)的工作成果,所以簡(jiǎn)單地用 git branch -d 刪除該分支會(huì)提示錯(cuò)誤,因?yàn)槟菢幼鰰?huì)丟失數(shù)據(jù):

$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

不過(guò),如果你確實(shí)想要?jiǎng)h除該分支上的改動(dòng),可以用大寫(xiě)的刪除選項(xiàng) -D 強(qiáng)制執(zhí)行,就像上面提示信息中給出的那樣。

利用分支進(jìn)行開(kāi)發(fā)的工作流程

現(xiàn)在我們已經(jīng)學(xué)會(huì)了新建分支和合并分支,可以(或應(yīng)該)用它來(lái)做點(diǎn)什么呢?在本節(jié),我們會(huì)介紹一些利用分支進(jìn)行開(kāi)發(fā)的工作流程。而正是由于分支管理的便捷,才衍生出了這類(lèi)典型的工作模式,你可以根據(jù)項(xiàng)目的實(shí)際情況選擇一種用用看。

長(zhǎng)期分支

由于 Git 使用簡(jiǎn)單的三方合并,所以就算在較長(zhǎng)一段時(shí)間內(nèi),反復(fù)多次把某個(gè)分支合并到另一分支,也不是什么難事。也就是說(shuō),你可以同時(shí)擁有多個(gè)開(kāi)放的分支,每個(gè)分支用于完成特定的任務(wù),隨著開(kāi)發(fā)的推進(jìn),你可以隨時(shí)把某個(gè)特性分支的成果并到其他分支中。

許多使用 Git 的開(kāi)發(fā)者都喜歡用這種方式來(lái)開(kāi)展工作,比如僅在 master 分支中保留完全穩(wěn)定的代碼,即已經(jīng)發(fā)布或即將發(fā)布的代碼。與此同時(shí),他們還有一個(gè)名為 developnext 的平行分支,專(zhuān)門(mén)用于后續(xù)的開(kāi)發(fā),或僅用于穩(wěn)定性測(cè)試 — 當(dāng)然并不是說(shuō)一定要絕對(duì)穩(wěn)定,不過(guò)一旦進(jìn)入某種穩(wěn)定狀態(tài),便可以把它合并到 master 里。這樣,在確保這些已完成的特性分支(短期分支,比如之前的 iss53 分支)能夠通過(guò)所有測(cè)試,并且不會(huì)引入更多錯(cuò)誤之后,就可以并到主干分支中,等待下一次的發(fā)布。

本質(zhì)上我們剛才談?wù)摰?,是隨著提交對(duì)象不斷右移的指針。穩(wěn)定分支的指針總是在提交歷史中落后一大截,而前沿分支總是比較靠前(見(jiàn)圖 3-18)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0318-tn.png" alt="" />

圖 3-18. 穩(wěn)定分支總是比較老舊。

或者把它們想象成工作流水線(xiàn),或許更好理解一些,經(jīng)過(guò)測(cè)試的提交對(duì)象集合被遴選到更穩(wěn)定的流水線(xiàn)(見(jiàn)圖 3-19)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0319-tn.png" alt="" />

圖 3-19. 想象成流水線(xiàn)可能會(huì)容易點(diǎn)。

你可以用這招維護(hù)不同層次的穩(wěn)定性。某些大項(xiàng)目還會(huì)有個(gè) proposed(建議)或 pu(proposed updates,建議更新)分支,它包含著那些可能還沒(méi)有成熟到進(jìn)入 nextmaster 的內(nèi)容。這么做的目的是擁有不同層次的穩(wěn)定性:當(dāng)這些分支進(jìn)入到更穩(wěn)定的水平時(shí),再把它們合并到更高層分支中去。再次說(shuō)明下,使用多個(gè)長(zhǎng)期分支的做法并非必需,不過(guò)一般來(lái)說(shuō),對(duì)于特大型項(xiàng)目或特復(fù)雜的項(xiàng)目,這么做確實(shí)更容易管理。

特性分支

在任何規(guī)模的項(xiàng)目中都可以使用特性(Topic)分支。一個(gè)特性分支是指一個(gè)短期的,用來(lái)實(shí)現(xiàn)單一特性或與其相關(guān)工作的分支??赡苣阍谝郧暗陌姹究刂葡到y(tǒng)里從未做過(guò)類(lèi)似這樣的事情,因?yàn)橥ǔ?chuàng)建與合并分支消耗太大。然而在 Git 中,一天之內(nèi)建立、使用、合并再刪除多個(gè)分支是常見(jiàn)的事。

我們?cè)谏瞎?jié)的例子里已經(jīng)見(jiàn)過(guò)這種用法了。我們創(chuàng)建了 iss53hotfix 這兩個(gè)特性分支,在提交了若干更新后,把它們合并到主干分支,然后刪除。該技術(shù)允許你迅速且完全的進(jìn)行語(yǔ)境切換 — 因?yàn)槟愕墓ぷ鞣稚⒃诓煌牧魉€(xiàn)里,每個(gè)分支里的改變都和它的目標(biāo)特性相關(guān),瀏覽代碼之類(lèi)的事情因而變得更簡(jiǎn)單了。你可以把作出的改變保持在特性分支中幾分鐘,幾天甚至幾個(gè)月,等它們成熟以后再合并,而不用在乎它們建立的順序或者進(jìn)度。

現(xiàn)在我們來(lái)看一個(gè)實(shí)際的例子。請(qǐng)看圖 3-20,由下往上,起先我們?cè)?master 工作到 C1,然后開(kāi)始一個(gè)新分支 iss91 嘗試修復(fù) 91 號(hào)缺陷,提交到 C6 的時(shí)候,又冒出一個(gè)解決該問(wèn)題的新辦法,于是從之前 C4 的地方又分出一個(gè)分支 iss91v2,干到 C8 的時(shí)候,又回到主干 master 中提交了 C9 和 C10,再回到 iss91v2 繼續(xù)工作,提交 C11,接著,又冒出個(gè)不太確定的想法,從 master 的最新提交 C10 處開(kāi)了個(gè)新的分支 dumbidea 做些試驗(yàn)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0320-tn.png" alt="" /> 圖 3-20. 擁有多個(gè)特性分支的提交歷史。

現(xiàn)在,假定兩件事情:我們最終決定使用第二個(gè)解決方案,即 iss91v2 中的辦法;另外,我們把 dumbidea 分支拿給同事們看了以后,發(fā)現(xiàn)它竟然是個(gè)天才之作。所以接下來(lái),我們準(zhǔn)備拋棄原來(lái)的 iss91 分支(實(shí)際上會(huì)丟棄 C5 和 C6),直接在主干中并入另外兩個(gè)分支。最終的提交歷史將變成圖 3-21 這樣:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0321-tn.png" alt="" />

圖 3-21. 合并了 dumbidea 和 iss91v2 后的分支歷史。

請(qǐng)務(wù)必牢記這些分支全部都是本地分支,這一點(diǎn)很重要。當(dāng)你在使用分支及合并的時(shí)候,一切都是在你自己的 Git 倉(cāng)庫(kù)中進(jìn)行的 — 完全不涉及與服務(wù)器的交互。

遠(yuǎn)程分支

遠(yuǎn)程分支(remote branch)是對(duì)遠(yuǎn)程倉(cāng)庫(kù)中的分支的索引。它們是一些無(wú)法移動(dòng)的本地分支;只有在 Git 進(jìn)行網(wǎng)絡(luò)交互時(shí)才會(huì)更新。遠(yuǎn)程分支就像是書(shū)簽,提醒著你上次連接遠(yuǎn)程倉(cāng)庫(kù)時(shí)上面各分支的位置。

我們用 (遠(yuǎn)程倉(cāng)庫(kù)名)/(分支名) 這樣的形式表示遠(yuǎn)程分支。比如我們想看看上次同 origin 倉(cāng)庫(kù)通訊時(shí) master 分支的樣子,就應(yīng)該查看 origin/master 分支。如果你和同伴一起修復(fù)某個(gè)問(wèn)題,但他們先推送了一個(gè) iss53 分支到遠(yuǎn)程倉(cāng)庫(kù),雖然你可能也有一個(gè)本地的 iss53 分支,但指向服務(wù)器上最新更新的卻應(yīng)該是 origin/iss53 分支。

可能有點(diǎn)亂,我們不妨舉例說(shuō)明。假設(shè)你們團(tuán)隊(duì)有個(gè)地址為 git.ourcompany.com 的 Git 服務(wù)器。如果你從這里克隆,Git 會(huì)自動(dòng)為你將此遠(yuǎn)程倉(cāng)庫(kù)命名為 origin,并下載其中所有的數(shù)據(jù),建立一個(gè)指向它的 master 分支的指針,在本地命名為 origin/master,但你無(wú)法在本地更改其數(shù)據(jù)。接著,Git 建立一個(gè)屬于你自己的本地 master 分支,始于 originmaster 分支相同的位置,你可以就此開(kāi)始工作(見(jiàn)圖 3-22):

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0322-tn.png" alt="" />

圖 3-22. 一次 Git 克隆會(huì)建立你自己的本地分支 master 和遠(yuǎn)程分支 origin/master,并且將它們都指向 origin 上的 master 分支。

如果你在本地 master 分支做了些改動(dòng),與此同時(shí),其他人向 git.ourcompany.com 推送了他們的更新,那么服務(wù)器上的 master 分支就會(huì)向前推進(jìn),而與此同時(shí),你在本地的提交歷史正朝向不同方向發(fā)展。不過(guò)只要你不和服務(wù)器通訊,你的 origin/master 指針仍然保持原位不會(huì)移動(dòng)(見(jiàn)圖 3-23)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0323-tn.png" alt="" />

圖 3-23. 在本地工作的同時(shí)有人向遠(yuǎn)程倉(cāng)庫(kù)推送內(nèi)容會(huì)讓提交歷史開(kāi)始分流。

可以運(yùn)行 git fetch origin 來(lái)同步遠(yuǎn)程服務(wù)器上的數(shù)據(jù)到本地。該命令首先找到 origin 是哪個(gè)服務(wù)器(本例為 git.ourcompany.com),從上面獲取你尚未擁有的數(shù)據(jù),更新你本地的數(shù)據(jù)庫(kù),然后把 origin/master 的指針移到它最新的位置上(見(jiàn)圖 3-24)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0324-tn.png" alt="" />

圖 3-24. git fetch 命令會(huì)更新 remote 索引。

為了演示擁有多個(gè)遠(yuǎn)程分支(在不同的遠(yuǎn)程服務(wù)器上)的項(xiàng)目是如何工作的,我們假設(shè)你還有另一個(gè)僅供你的敏捷開(kāi)發(fā)小組使用的內(nèi)部服務(wù)器 git.team1.ourcompany.com。可以用第二章中提到的 git remote add 命令把它加為當(dāng)前項(xiàng)目的遠(yuǎn)程分支之一。我們把它命名為 teamone,以便代替完整的 Git URL 以方便使用(見(jiàn)圖 3-25)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0325-tn.png" alt="" />

圖 3-25. 把另一個(gè)服務(wù)器加為遠(yuǎn)程倉(cāng)庫(kù)

現(xiàn)在你可以用 git fetch teamone 來(lái)獲取小組服務(wù)器上你還沒(méi)有的數(shù)據(jù)了。由于當(dāng)前該服務(wù)器上的內(nèi)容是你 origin 服務(wù)器上的子集,Git 不會(huì)下載任何數(shù)據(jù),而只是簡(jiǎn)單地創(chuàng)建一個(gè)名為 teamone/master 的遠(yuǎn)程分支,指向 teamone 服務(wù)器上 master 分支所在的提交對(duì)象 31b8e(見(jiàn)圖 3-26)。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0326-tn.png" alt="" />

圖 3-26. 你在本地有了一個(gè)指向 teamone 服務(wù)器上 master 分支的索引。

推送本地分支

要想和其他人分享某個(gè)本地分支,你需要把它推送到一個(gè)你擁有寫(xiě)權(quán)限的遠(yuǎn)程倉(cāng)庫(kù)。你創(chuàng)建的本地分支不會(huì)因?yàn)槟愕膶?xiě)入操作而被自動(dòng)同步到你引入的遠(yuǎn)程服務(wù)器上,你需要明確地執(zhí)行推送分支的操作。換句話(huà)說(shuō),對(duì)于無(wú)意分享的分支,你盡管保留為私人分支好了,而只推送那些協(xié)同工作要用到的特性分支。

如果你有個(gè)叫 serverfix 的分支需要和他人一起開(kāi)發(fā),可以運(yùn)行 git push (遠(yuǎn)程倉(cāng)庫(kù)名) (分支名)

$ git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new branch]      serverfix -> serverfix

這里其實(shí)走了一點(diǎn)捷徑。Git 自動(dòng)把 serverfix 分支名擴(kuò)展為 refs/heads/serverfix:refs/heads/serverfix,意為“取出我在本地的 serverfix 分支,推送到遠(yuǎn)程倉(cāng)庫(kù)的 serverfix 分支中去”。我們將在第九章進(jìn)一步介紹 refs/heads/ 部分的細(xì)節(jié),不過(guò)一般使用的時(shí)候都可以省略它。也可以運(yùn)行 git push origin serverfix:serverfix 來(lái)實(shí)現(xiàn)相同的效果,它的意思是“上傳我本地的 serverfix 分支到遠(yuǎn)程倉(cāng)庫(kù)中去,仍舊稱(chēng)它為 serverfix 分支”。通過(guò)此語(yǔ)法,你可以把本地分支推送到某個(gè)命名不同的遠(yuǎn)程分支:若想把遠(yuǎn)程分支叫作 awesomebranch,可以用 git push origin serverfix:awesomebranch 來(lái)推送數(shù)據(jù)。

接下來(lái),當(dāng)你的協(xié)作者再次從服務(wù)器上獲取數(shù)據(jù)時(shí),他們將得到一個(gè)新的遠(yuǎn)程分支 origin/serverfix,并指向服務(wù)器上 serverfix 所指向的版本:

$ git fetch origin
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (15/15), done.
From git@github.com:schacon/simplegit
 * [new branch]      serverfix    -> origin/serverfix

值得注意的是,在 fetch 操作下載好新的遠(yuǎn)程分支之后,你仍然無(wú)法在本地編輯該遠(yuǎn)程倉(cāng)庫(kù)中的分支。換句話(huà)說(shuō),在本例中,你不會(huì)有一個(gè)新的 serverfix 分支,有的只是一個(gè)你無(wú)法移動(dòng)的 origin/serverfix 指針。

如果要把該遠(yuǎn)程分支的內(nèi)容合并到當(dāng)前分支,可以運(yùn)行 git merge origin/serverfix。如果想要一份自己的 serverfix 來(lái)開(kāi)發(fā),可以在遠(yuǎn)程分支的基礎(chǔ)上分化出一個(gè)新的分支來(lái):

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

這會(huì)切換到新建的 serverfix 本地分支,其內(nèi)容同遠(yuǎn)程分支 origin/serverfix 一致,這樣你就可以在里面繼續(xù)開(kāi)發(fā)了。

跟蹤遠(yuǎn)程分支

從遠(yuǎn)程分支 checkout 出來(lái)的本地分支,稱(chēng)為 跟蹤分支 (tracking branch)。跟蹤分支是一種和某個(gè)遠(yuǎn)程分支有直接聯(lián)系的本地分支。在跟蹤分支里輸入 git push,Git 會(huì)自行推斷應(yīng)該向哪個(gè)服務(wù)器的哪個(gè)分支推送數(shù)據(jù)。同樣,在這些分支里運(yùn)行 git pull 會(huì)獲取所有遠(yuǎn)程索引,并把它們的數(shù)據(jù)都合并到本地分支中來(lái)。

在克隆倉(cāng)庫(kù)時(shí),Git 通常會(huì)自動(dòng)創(chuàng)建一個(gè)名為 master 的分支來(lái)跟蹤 origin/master。這正是 git pushgit pull 一開(kāi)始就能正常工作的原因。當(dāng)然,你可以隨心所欲地設(shè)定為其它跟蹤分支,比如 origin 上除了 master 之外的其它分支。剛才我們已經(jīng)看到了這樣的一個(gè)例子:git checkout -b [分支名] [遠(yuǎn)程名]/[分支名]。如果你有 1.6.2 以上版本的 Git,還可以用 --track 選項(xiàng)簡(jiǎn)化:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

要為本地分支設(shè)定不同于遠(yuǎn)程分支的名字,只需在第一個(gè)版本的命令里換個(gè)名字:

$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

現(xiàn)在你的本地分支 sf 會(huì)自動(dòng)將推送和抓取數(shù)據(jù)的位置定位到 origin/serverfix 了。

刪除遠(yuǎn)程分支

如果不再需要某個(gè)遠(yuǎn)程分支了,比如搞定了某個(gè)特性并把它合并進(jìn)了遠(yuǎn)程的 master 分支(或任何其他存放穩(wěn)定代碼的分支),可以用這個(gè)非常無(wú)厘頭的語(yǔ)法來(lái)刪除它:git push [遠(yuǎn)程名] :[分支名]。如果想在服務(wù)器上刪除 serverfix 分支,運(yùn)行下面的命令:

$ git push origin :serverfix
To git@github.com:schacon/simplegit.git
 - [deleted]         serverfix

咚!服務(wù)器上的分支沒(méi)了。你最好特別留心這一頁(yè),因?yàn)槟阋欢〞?huì)用到那個(gè)命令,而且你很可能會(huì)忘掉它的語(yǔ)法。有種方便記憶這條命令的方法:記住我們不久前見(jiàn)過(guò)的 git push [遠(yuǎn)程名] [本地分支]:[遠(yuǎn)程分支] 語(yǔ)法,如果省略 [本地分支],那就等于是在說(shuō)“在這里提取空白然后把它變成[遠(yuǎn)程分支]”。

分支的衍合

把一個(gè)分支中的修改整合到另一個(gè)分支的辦法有兩種:mergerebase(譯注:rebase 的翻譯暫定為“衍合”,大家知道就可以了。)。在本章我們會(huì)學(xué)習(xí)什么是衍合,如何使用衍合,為什么衍合操作如此富有魅力,以及我們應(yīng)該在什么情況下使用衍合。

基本的衍合操作

請(qǐng)回顧之前有關(guān)合并的一節(jié)(見(jiàn)圖 3-27),你會(huì)看到開(kāi)發(fā)進(jìn)程分叉到兩個(gè)不同分支,又各自提交了更新。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0327-tn.png" alt="" />

圖 3-27. 最初分叉的提交歷史。

之前介紹過(guò),最容易的整合分支的方法是 merge 命令,它會(huì)把兩個(gè)分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)進(jìn)行三方合并,合并的結(jié)果是產(chǎn)生一個(gè)新的提交對(duì)象(C5)。如圖 3-28 所示:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0328-tn.png" alt="" />

圖 3-28. 通過(guò)合并一個(gè)分支來(lái)整合分叉了的歷史。

其實(shí),還有另外一個(gè)選擇:你可以把在 C3 里產(chǎn)生的變化補(bǔ)丁在 C4 的基礎(chǔ)上重新打一遍。在 Git 里,這種操作叫做衍合(rebase)。有了 rebase 命令,就可以把在一個(gè)分支里提交的改變移到另一個(gè)分支里重放一遍。

在上面這個(gè)例子中,運(yùn)行:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是回到兩個(gè)分支最近的共同祖先,根據(jù)當(dāng)前分支(也就是要進(jìn)行衍合的分支 experiment)后續(xù)的歷次提交對(duì)象(這里只有一個(gè) C3),生成一系列文件補(bǔ)丁,然后以基底分支(也就是主干分支 master)最后一個(gè)提交對(duì)象(C4)為新的出發(fā)點(diǎn),逐個(gè)應(yīng)用之前準(zhǔn)備好的補(bǔ)丁文件,最后會(huì)生成一個(gè)新的合并提交對(duì)象(C3'),從而改寫(xiě) experiment 的提交歷史,使它成為 master 分支的直接下游,如圖 3-29 所示:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0329-tn.png" alt="" />

圖 3-29. 把 C3 里產(chǎn)生的改變到 C4 上重演一遍。

現(xiàn)在回到 master 分支,進(jìn)行一次快進(jìn)合并(見(jiàn)圖 3-30):

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0330-tn.png" alt="" />

圖 3-30. master 分支的快進(jìn)。

現(xiàn)在的 C3' 對(duì)應(yīng)的快照,其實(shí)和普通的三方合并,即上個(gè)例子中的 C5 對(duì)應(yīng)的快照內(nèi)容一模一樣了。雖然最后整合得到的結(jié)果沒(méi)有任何區(qū)別,但衍合能產(chǎn)生一個(gè)更為整潔的提交歷史。如果視察一個(gè)衍合過(guò)的分支的歷史記錄,看起來(lái)會(huì)更清楚:仿佛所有修改都是在一根線(xiàn)上先后進(jìn)行的,盡管實(shí)際上它們?cè)臼峭瑫r(shí)并行發(fā)生的。

一般我們使用衍合的目的,是想要得到一個(gè)能在遠(yuǎn)程分支上干凈應(yīng)用的補(bǔ)丁 — 比如某些項(xiàng)目你不是維護(hù)者,但想幫點(diǎn)忙的話(huà),最好用衍合:先在自己的一個(gè)分支里進(jìn)行開(kāi)發(fā),當(dāng)準(zhǔn)備向主項(xiàng)目提交補(bǔ)丁的時(shí)候,根據(jù)最新的 origin/master 進(jìn)行一次衍合操作然后再提交,這樣維護(hù)者就不需要做任何整合工作(譯注:實(shí)際上是把解決分支補(bǔ)丁同最新主干代碼之間沖突的責(zé)任,化轉(zhuǎn)為由提交補(bǔ)丁的人來(lái)解決。),只需根據(jù)你提供的倉(cāng)庫(kù)地址作一次快進(jìn)合并,或者直接采納你提交的補(bǔ)丁。

請(qǐng)注意,合并結(jié)果中最后一次提交所指向的快照,無(wú)論是通過(guò)衍合,還是三方合并,都會(huì)得到相同的快照內(nèi)容,只不過(guò)提交歷史不同罷了。衍合是按照每行的修改次序重演一遍修改,而合并是把最終結(jié)果合在一起。

有趣的衍合

衍合也可以放到其他分支進(jìn)行,并不一定非得根據(jù)分化之前的分支。以圖 3-31 的歷史為例,我們?yōu)榱私o服務(wù)器端代碼添加一些功能而創(chuàng)建了特性分支 server,然后提交 C3 和 C4。然后又從 C3 的地方再增加一個(gè) client 分支來(lái)對(duì)客戶(hù)端代碼進(jìn)行一些相應(yīng)修改,所以提交了 C8 和 C9。最后,又回到 server 分支提交了 C10。

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0331-tn.png" alt="" />

圖 3-31. 從一個(gè)特性分支里再分出一個(gè)特性分支的歷史。

假設(shè)在接下來(lái)的一次軟件發(fā)布中,我們決定先把客戶(hù)端的修改并到主線(xiàn)中,而暫緩并入服務(wù)端軟件的修改(因?yàn)檫€需要進(jìn)一步測(cè)試)。這個(gè)時(shí)候,我們就可以把基于 client 分支而非 server 分支的改變(即 C8 和 C9),跳過(guò) server 直接放到 master 分支中重演一遍,但這需要用 git rebase--onto 選項(xiàng)指定新的基底分支 master

$ git rebase --onto master server client

這好比在說(shuō):“取出 client 分支,找出 client 分支和 server 分支的共同祖先之后的變化,然后把它們?cè)?master 上重演一遍”。是不是有點(diǎn)復(fù)雜?不過(guò)它的結(jié)果如圖 3-32 所示,非??幔ㄗg注:雖然 client 里的 C8, C9 在 C3 之后,但這僅表明時(shí)間上的先后,而非在 C3 修改的基礎(chǔ)上進(jìn)一步改動(dòng),因?yàn)?serverclient 這兩個(gè)分支對(duì)應(yīng)的代碼應(yīng)該是兩套文件,雖然這么說(shuō)不是很?chē)?yán)格,但應(yīng)理解為在 C3 時(shí)間點(diǎn)之后,對(duì)另外的文件所做的 C8,C9 修改,放到主干重演。):

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0332-tn.png" alt="" />

圖 3-32. 將特性分支上的另一個(gè)特性分支衍合到其他分支。

現(xiàn)在可以快進(jìn) master 分支了(見(jiàn)圖 3-33):

$ git checkout master
$ git merge client

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0333-tn.png" alt="" />

圖 3-33. 快進(jìn) master 分支,使之包含 client 分支的變化。

現(xiàn)在我們決定把 server 分支的變化也包含進(jìn)來(lái)。我們可以直接把 server 分支衍合到 master,而不用手工切換到 server 分支后再執(zhí)行衍合操作 — git rebase [主分支] [特性分支] 命令會(huì)先取出特性分支 server,然后在主分支 master 上重演:

$ git rebase master server

于是,server 的進(jìn)度應(yīng)用到 master 的基礎(chǔ)上,如圖 3-34 所示:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0334-tn.png" alt="" />

圖 3-34. 在 master 分支上衍合 server 分支。

然后就可以快進(jìn)主干分支 master 了:

$ git checkout master
$ git merge server

現(xiàn)在 clientserver 分支的變化都已經(jīng)集成到主干分支來(lái)了,可以刪掉它們了。最終我們的提交歷史會(huì)變成圖 3-35 的樣子:

$ git branch -d client
$ git branch -d server

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0335-tn.png" alt="" />

圖 3-35. 最終的提交歷史

衍合的風(fēng)險(xiǎn)

呃,奇妙的衍合也并非完美無(wú)缺,要用它得遵守一條準(zhǔn)則:

一旦分支中的提交對(duì)象發(fā)布到公共倉(cāng)庫(kù),就千萬(wàn)不要對(duì)該分支進(jìn)行衍合操作。

如果你遵循這條金科玉律,就不會(huì)出差錯(cuò)。否則,人民群眾會(huì)仇恨你,你的朋友和家人也會(huì)嘲笑你,唾棄你。

在進(jìn)行衍合的時(shí)候,實(shí)際上拋棄了一些現(xiàn)存的提交對(duì)象而創(chuàng)造了一些類(lèi)似但不同的新的提交對(duì)象。如果你把原來(lái)分支中的提交對(duì)象發(fā)布出去,并且其他人更新下載后在其基礎(chǔ)上開(kāi)展工作,而稍后你又用 git rebase 拋棄這些提交對(duì)象,把新的重演后的提交對(duì)象發(fā)布出去的話(huà),你的合作者就不得不重新合并他們的工作,這樣當(dāng)你再次從他們那里獲取內(nèi)容時(shí),提交歷史就會(huì)變得一團(tuán)糟。

下面我們用一個(gè)實(shí)際例子來(lái)說(shuō)明為什么公開(kāi)的衍合會(huì)帶來(lái)問(wèn)題。假設(shè)你從一個(gè)中央服務(wù)器克隆然后在它的基礎(chǔ)上搞了一些開(kāi)發(fā),提交歷史類(lèi)似圖 3-36 所示:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0336-tn.png" alt="" />

圖 3-36. 克隆一個(gè)倉(cāng)庫(kù),在其基礎(chǔ)上工作一番。

現(xiàn)在,某人在 C1 的基礎(chǔ)上做了些改變,并合并他自己的分支得到結(jié)果 C6,推送到中央服務(wù)器。當(dāng)你抓取并合并這些數(shù)據(jù)到你本地的開(kāi)發(fā)分支中后,會(huì)得到合并結(jié)果 C7,歷史提交會(huì)變成圖 3-37 這樣:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0337-tn.png" alt="" />

圖 3-37. 抓取他人提交,并入自己主干。

接下來(lái),那個(gè)推送 C6 上來(lái)的人決定用衍合取代之前的合并操作;繼而又用 git push --force 覆蓋了服務(wù)器上的歷史,得到 C4'。而之后當(dāng)你再?gòu)姆?wù)器上下載最新提交后,會(huì)得到:

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0338-tn.png" alt="" />

圖 3-38. 有人推送了衍合后得到的 C4',丟棄了你作為開(kāi)發(fā)基礎(chǔ)的 C4 和 C6。

下載更新后需要合并,但此時(shí)衍合產(chǎn)生的提交對(duì)象 C4' 的 SHA-1 校驗(yàn)值和之前 C4 完全不同,所以 Git 會(huì)把它們當(dāng)作新的提交對(duì)象處理,而實(shí)際上此刻你的提交歷史 C7 中早已經(jīng)包含了 C4 的修改內(nèi)容,于是合并操作會(huì)把 C7 和 C4' 合并為 C8(見(jiàn)圖 3-39):

http://wiki.jikexueyuan.com/project/pro-git/images/18333fig0339-tn.png" alt="" />

圖 3-39. 你把相同的內(nèi)容又合并了一遍,生成一個(gè)新的提交 C8。

C8 這一步的合并是遲早會(huì)發(fā)生的,因?yàn)橹挥羞@樣你才能和其他協(xié)作者提交的內(nèi)容保持同步。而在 C8 之后,你的提交歷史里就會(huì)同時(shí)包含 C4 和 C4',兩者有著不同的 SHA-1 校驗(yàn)值,如果用 git log 查看歷史,會(huì)看到兩個(gè)提交擁有相同的作者日期與說(shuō)明,令人費(fèi)解。而更糟的是,當(dāng)你把這樣的歷史推送到服務(wù)器后,會(huì)再次把這些衍合后的提交引入到中央服務(wù)器,進(jìn)一步困擾其他人(譯注:這個(gè)例子中,出問(wèn)題的責(zé)任方是那個(gè)發(fā)布了 C6 后又用衍合發(fā)布 C4' 的人,其他人會(huì)因此反饋雙重歷史到共享主干,從而混淆大家的視聽(tīng)。)。

如果把衍合當(dāng)成一種在推送之前清理提交歷史的手段,而且僅僅衍合那些尚未公開(kāi)的提交對(duì)象,就沒(méi)問(wèn)題。如果衍合那些已經(jīng)公開(kāi)的提交對(duì)象,并且已經(jīng)有人基于這些提交對(duì)象開(kāi)展了后續(xù)開(kāi)發(fā)工作的話(huà),就會(huì)出現(xiàn)叫人沮喪的麻煩。

小結(jié)

讀到這里,你應(yīng)該已經(jīng)學(xué)會(huì)了如何創(chuàng)建分支并切換到新分支,在不同分支間轉(zhuǎn)換,合并本地分支,把分支推送到共享服務(wù)器上,使用共享分支與他人協(xié)作,以及在分享之前進(jìn)行衍合。