Git 分支
對于任何一個文件,在Git內(nèi)都只有三種狀態(tài):已提交(committed),已修改(modified)和已暫存(staged)。已提交表示該文件已經(jīng)被安全地保存在本地數(shù)據(jù)庫中了;已修改表示修改了某個文件,但還沒有提交保存;已暫存表示把已修改的文件放在下次提交時要保存的清單中。
由此我們看到Git管理項目時,文件流轉(zhuǎn)的三個工作區(qū)域:
Git的工作目錄,暫存區(qū)域,以及本地倉庫

基本的Git工作流程如下:
1.在工作目錄中修改某些文件。
2.對修改后的文件進(jìn)行快照,然后保存到暫存區(qū)域。
3.提交更新,將保存在暫存區(qū)域的文件快照永久轉(zhuǎn)儲到Git目錄中。
所以,我們可以從文件所處的位置來判斷狀態(tài):如果是Git目錄中保存著的特定版本文件,就屬于已提交狀態(tài);如果作了修改并已放入暫存區(qū)域,就屬于已暫存狀態(tài);如果自上次取出后,作了修改但還沒有放到暫存區(qū)域,就是已修改狀態(tài)。
Git是如何儲存數(shù)據(jù):
在Git中提交時,會保存一個提交(commit)對象,該對象包含一個指向暫存內(nèi)容快照的指針,包含本次提交的作者等相關(guān)附屬信息,包含零個或多個指向該提交對象的父對象指針:首次提交是沒有直接祖先的,普通提交有一個祖先,由兩個或多個分支合并產(chǎn)生的提交則有多個祖先。
為直觀起見,我們假設(shè)在工作目錄中有三個文件,準(zhǔn)備將它們暫存后提交。暫存操作會對每一個文件計算校驗和(即第一章中提到的SHA-1哈希字串),然后把當(dāng)前版本的文件快照保存到Git倉庫中(Git使用blob類型的對象存儲這些快照),并將校驗和加入暫存區(qū)域:
$git add README test.rb LICENSE $ git commit -m 'initial commit of myproject'
當(dāng)使用 gitcommit 新建一個提交對象前,Git會先計算每一個子目錄(本例中就是項目根目錄)的校驗和,然后在Git倉庫中將這些目錄保存為樹(tree)對象。之后Git創(chuàng)建的提交對象,除了包含相關(guān)提交信息以外,還包含著指向這個樹對象(項目根目錄)的指針,如此它就可以在將來需要的時候,重現(xiàn)此次快照的內(nèi)容了。
現(xiàn)在,Git倉庫中有五個對象:三個表示文件快照內(nèi)容的blob對象;一個記錄著目錄樹內(nèi)容及其中各個文件對應(yīng)blob對象索引的tree對象;以及一個包含指向tree對象(根目錄)的索引和其他提交信息元數(shù)據(jù)的commit對象。

初次沒有parent指針,第二次以后提交的對象會包含一個指向上次提交對象的指針(譯注:即下圖中的parent對象)。兩次提交后

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

新建分支就是在當(dāng)前commit對象上新建個指針

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

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

這樣的實現(xiàn)方式會給我們帶來什么好處呢?好吧,現(xiàn)在不妨再提交一次:
$git commit -a -m 'made a change'
圖展示了提交后的結(jié)果。每次提交后HEAD隨著分支一起向前移動

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

我們做些修改然后再提交,我們的項目提交歷史產(chǎn)生了分叉,因為剛才我們創(chuàng)建了一個分支,轉(zhuǎn)換到其中進(jìn)行了一些工作,然后又回到原來的主分支進(jìn)行了另外一些工作。這些改變分別孤立在不同的分支里:我們可以在不同分支里反復(fù)切換,并在時機(jī)成熟時把它們合并到一起。而所有這些工作,僅僅需要branch和 checkout這兩條命令就可以完成

分支的合并

合并回 master分支。只需回到master分支,運行g(shù)it merge命令指定要合并進(jìn)來的分支:
$git merge iss53
請注意,由于當(dāng)前 master分支所指向的提交對象(C4)并不是 iss53分支的直接祖先,Git不得不進(jìn)行一些額外處理。就此例而言,Git會用兩個分支的末端(C4和C5)以及它們的共同祖先(C2)進(jìn)行一次簡單的三方合并計算。用紅框標(biāo)出了Git用于合并的三個提交對象:
紅色框為分支合并自動識別出最佳的同源合并點這次,Git沒有簡單地把分支指針右移,而是對三方合并后的結(jié)果重新做一個新的快照,并自動創(chuàng)建一個指向它的提交對象(C6)。這個提交對象比較特殊,它有兩個祖先(
C4和C5)。值得一提的是Git可以自己裁決哪個共同祖先才是最佳合并基礎(chǔ);它們需要開發(fā)者手工指定合并基礎(chǔ)。所以此特性讓Git的合并操作比其他系統(tǒng)都要簡單不少。

遇到?jīng)_突時的分支合并
有時候合并操作并不會如此順利。如果在不同的分支中都修改了同一個文件的同一部分,Git就無法干凈地把兩者合到一起將得到類似下面的結(jié)果:
$git merge iss53 Auto-merging index.html CONFLICT (content): Mergeconflict in index.html Automatic merge failed; fix conflicts and thencommit the result.
Git作了合并,但沒有提交,它會停下來等你解決沖突。要看看哪些文件在合并時發(fā)生沖突,可以用 gitstatus查閱:
[master*]
$git status
index.html: needs merge
# On branch master
# Changed butnot updated:
# (use "git add ..." to update what willbe committed)
# (use "git checkout -- ..." to discardchanges in working directory) # # unmerged: index.html #
任何包含未解決沖突的文件都會以未合并(unmerged)的狀態(tài)列出。Git會在有沖突的文件里加入標(biāo)準(zhǔn)的沖突解決標(biāo)記,可以通過它們來手工定位并解決這些沖突。可以看到此文件包含類似下面這樣的部分:
<<<<<<<HEAD:index.html
contact: email.support@github.com
=======
pleasecontact us at support@github.com
>>>>>>>iss53:index.html
可以看到 =======隔開的上半部分,是 HEAD(即 master分支,在運行merge命令時所切換到的分支)中的內(nèi)容,下半部分是在 iss53分支中的內(nèi)容。解決沖突的辦法無非是二者選其一或者由你親自整合到一起。比如你可以通過把這段內(nèi)容替換為下面這樣來解決:
pleasecontact us at email.support@github.com
這個解決方案各采納了兩個分支中的一部分內(nèi)容,而且我還刪除了 <<<<<<<
,=======和 >>>>>>>這些行。在解決了所有文件里的所有沖突后,運行
gitadd將把它們標(biāo)記為已解決狀態(tài)(譯注:實際上就是來一次快照保存到暫存區(qū)域。)。因為一旦暫存,就表示沖突已經(jīng)解決。再運行一次 gitstatus來確認(rèn)所有沖突都已解決:
$git status
# On branch master
# Changes to be committed:
# (use "gitreset HEAD ..." to unstage)
# # modified: index.html #
如果覺得滿意了,并且確認(rèn)所有沖突都已解決,也就是進(jìn)入了暫存區(qū),就可以用 git commit來完成這次合并提交。提交的記錄差不多是這樣:
Mergebranch 'iss53' Conflicts: index.html #
# It looks like you may becommitting a MERGE.
# If this is not correct, please remove the file
# .git/MERGE_HEAD # and try again. #
如果想給將來看這次合并的人一些方便,可以修改該信息,提供更多合并細(xì)節(jié)。比如你都作了哪些改動,以及這么做的原因。有時候裁決沖突的理由并不直接或明顯,有必要略加注解。分支的管理日后的常規(guī)工作中會經(jīng)常用到下面介紹的管理命令。git branch命令不僅僅能創(chuàng)建和刪除分支,如果不加任何參數(shù),它會給出當(dāng)前所有分支的清單:
$git branch iss53 * master testing
注意看 master分支前的 *字符:它表示當(dāng)前所在的分支。也就是說,如果現(xiàn)在提交更新,master分支將隨著開發(fā)進(jìn)度前移。若要查看各個分支最后一個提交對象的信息,運行$gitbranch -v:
$git branch -v
iss53 93b412c fix javascript issue * master 7a98805
Merge branch 'iss53' testing 782fd34 add scott to the author list inthe readmes
要從該清單中篩選出你已經(jīng)(或尚未)與當(dāng)前分支合并的分支,可以用
--merge和 --no-merged選項(Git1.5.6以上版本)。比如用gitbranch --merge 查看哪些分支已被并入當(dāng)前分支(譯注:也就是說哪些分支是當(dāng)前分支的直接上游。):
$git branch --merged iss53 * master
之前我們已經(jīng)合并了 iss53,所以在這里會看到它。一般來說,列表中沒有*
的分支通常都可以用 gitbranch -d來刪掉。原因很簡單,既然已經(jīng)把它們所包含的工作整合到了其他分支,刪掉也不會損失什么。另外可以用 gitbranch --no-merged查看尚未合并的工作:
$git branch --no-merged testing
它會顯示還未合并進(jìn)來的分支。由于這些分支中還包含著尚未合并進(jìn)來的工作成果,所以簡單地用 gitbranch -d刪除該分支會提示錯誤,因為那樣做會丟失數(shù)據(jù):
$git branch -d testing
error: The branch 'testing' is not an ancestorof your current HEAD.
If you are sure you want to delete it, run 'gitbranch -D testing'.
不過,如果你確實想要刪除該分支上的改動,可以用大寫的刪除選項 -D強(qiáng)制執(zhí)行,就像上面提示信息中給出的那樣。
遠(yuǎn)程分支
遠(yuǎn)程分支(remotebranch)是對遠(yuǎn)程倉庫中的分支的索引。它們是一些無法移動的本地分支;只有在Git進(jìn)行網(wǎng)絡(luò)交互時才會更新。遠(yuǎn)程分支就像是書簽,提醒著你上次連接遠(yuǎn)程倉庫時上面各分支的位置。我們用 (遠(yuǎn)程倉庫名
)/(分支名)這樣的形式表示遠(yuǎn)程分支。比如我們想看看上次同 origin倉庫通訊時master的樣子,就應(yīng)該查看 origin/master分支。如果你和同伴一起修復(fù)某個問題,但他們先推送了一個iss53分支到遠(yuǎn)程倉庫,雖然你可能也有一個本地的 iss53分支,但指向服務(wù)器上最新更新的卻應(yīng)該是 origin/iss53分支。
可能有點亂,我們不妨舉例說明。假設(shè)你們團(tuán)隊有個地址為git.ourcompany.com的Git服務(wù)器。如果你從這里克隆,Git會自動為你將此遠(yuǎn)程倉庫命名為origin,并下載其中所有的數(shù)據(jù),建立一個指向它的 master分支的指針,在本地命名為 origin/master,但你無法在本地更改其數(shù)據(jù)。接著,Git建立一個屬于你自己的本地master分支,始于 origin上 master分支相同的位置,你可以就此開始工作如果你在本地 master分支做了些改動,與此同時,其他人向 git.ourcompany.com推送了他們的更新,那么服務(wù)器上的master分支就會向前推進(jìn),而于此同時,你在本地的提交歷史正朝向不同方向發(fā)展。不過只要你不和服務(wù)器通訊,你的 origin/master

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

推送本地分支
要想和其他人分享某個本地分支,你需要把它推送到一個你擁有寫權(quán)限的遠(yuǎn)程倉庫。如果你有個叫 serverfix的分支需要和他人一起開發(fā),可以運行g(shù)itpush (遠(yuǎn)程倉庫名)(分支名):
$git push origin serverfix
這其實有點像條捷徑。Git自動把 serverfix分支名擴(kuò)展為refs/heads/serverfix:refs/heads/serverfix,意為“取出我在本地的
serverfix分支,推送到遠(yuǎn)程倉庫的serverfix分支中去”。也可以運行
git push origin serverfix:serferfix
來實現(xiàn)相同的效果,它的意思是“上傳我本地的serverfix分支到遠(yuǎn)程倉庫中去,仍舊稱它為serverfix分支”。通過此語法,你可以把本地分支推送到某個命名不同的遠(yuǎn)程分支:若想把遠(yuǎn)程分支叫作awesomebranch,可以用
git push origin serverfix:awesomebranch
來推送數(shù)據(jù)。
接下來,當(dāng)你的協(xié)作者再次從服務(wù)器上獲取數(shù)據(jù)時,他們將得到一個新的遠(yuǎn)程分支 origin/serverfix:
$git fetch origin
值得注意的是,在 fetch操作下載好新的遠(yuǎn)程分支之后,你仍然無法在本地編輯該遠(yuǎn)程倉庫中的分支。換句話說,在本例中,你不會有一個新的serverfix分支,有的只是一個你無法移動的 origin/serverfix指針。如果要把該內(nèi)容合并到當(dāng)前分支,可以運行
git merge origin/serverfix
。如果想要一份自己的 serverfix來開發(fā),可以在遠(yuǎn)程分支的基礎(chǔ)上分化出一個新的分支來:
$git checkout -b serverfix origin/serverfix
Branch serverfix set up totrack remote branch refs/remotes/origin/serverfix. Switched to a newbranch "serverfix"
這會切換到新建的 serverfix 本地分支,其內(nèi)容同遠(yuǎn)程分支 origin/serverfix一致,這樣你就可以在里面繼續(xù)開發(fā)了。跟蹤遠(yuǎn)程分支從遠(yuǎn)程分支 checkout出來的本地分支,稱為跟蹤分支(trackingbranch)_。跟蹤分支是一種和遠(yuǎn)程分支有直接聯(lián)系的本地分支。在跟蹤分支里輸入gitpush,Git會自行推斷應(yīng)該向哪個服務(wù)器的哪個分支推送數(shù)據(jù)。反過來,在這些分支里運行 gitpull會獲取所有遠(yuǎn)程索引,并把它們的數(shù)據(jù)都合并到本地分支中來。
在克隆倉庫時,Git通常會自動創(chuàng)建一個名為 master的分支來跟蹤origin/master。這正是gitpush和 gitpull一開始就能正常工作的原因。當(dāng)然,你可以隨心所欲地設(shè)定為其它跟蹤分支,比如origin上除了 master之外的其它分支。剛才我們已經(jīng)看到了這樣的一個例子:
gitcheckout -b [分支名][遠(yuǎn)程名]/[分支名]。如果你有1.6.2以上版本的Git,還可以用--track選項簡化:
$git checkout --track origin/serverfix
Branch serverfix set up totrack remote branch refs/remotes/origin/serverfix. Switched to a newbranch "serverfix"
要為本地分支設(shè)定不同于遠(yuǎn)程分支的名字,只需在前個版本的命令里換個名字:
$git checkout -b sf origin/serverfix
Branch sf set up to track remotebranch refs/remotes/origin/serverfix. Switched to a new branch "sf"
現(xiàn)在你的本地分支 sf會自動向 origin/serverfix推送和抓取數(shù)據(jù)了。
分支的衍合
把一個分支整合到另一個分支的辦法有兩種:merge和rebase(譯注:rebase的翻譯暫定為“衍合”,大家知道就可以了。)。在本章我們會學(xué)習(xí)什么是衍合,如何使用衍合,為什么衍合操作如此富有魅力,以及我們應(yīng)該在什么情況下使用衍合。

最容易的整合分支的方法是 merge 命令,它會把兩個分支最新的快照(C3
和C4)以及二者最新的共同祖先(C2)進(jìn)行三方合并,合并的結(jié)果是產(chǎn)生一個新的提交對象(C5)

還有另外一個選擇:你可以把在C3里產(chǎn)生的變化補(bǔ)丁在C4的基礎(chǔ)上重新打一遍。在Git里,這種操作叫做衍合(rebase)。有了 rebase 命令,就可以把在一個分支里提交的改變移到另一個分支里重放一遍。
在上面這個例子中,運行:
$git checkout experiment
$git rebase master
First, rewinding head to replay your work on top ofit... Applying: added staged command
它的原理是回到兩個分支最近的共同祖先,根據(jù)當(dāng)前分支(也就是要進(jìn)行衍合的分支 experiment)后續(xù)的歷次提交對象(這里只有一個C3),生成一系列文件補(bǔ)丁,然后以基底分支(也就是主干分支master)最后一個提交對象(C4)為新的出發(fā)點,逐個應(yīng)用之前準(zhǔn)備好的補(bǔ)丁文件,最后會生成一個新的合并提交對象(C3’),從而改寫 experiment 的提交歷史,使它成為
master 分支的直接下游

把C3里產(chǎn)生的改變到C4上重演一遍?,F(xiàn)在回到 master分支,進(jìn)行一次快進(jìn)合并

現(xiàn)在的C3’應(yīng)的快照,其實和普通的三方合并,即上個例子中的C5對應(yīng)的快照內(nèi)容一模一樣了。雖然最后整合得到的結(jié)果沒有任何區(qū)別,但衍合能產(chǎn)生一個更為整潔的提交歷史。如果視察一個衍合過的分支的歷史記錄,看起來會更清楚:仿佛所有修改都是在一根線上先后進(jìn)行的,盡管實際上它們原本是同時并行發(fā)生的。
一般我們使用衍合的目的,是想要得到一個能在遠(yuǎn)程分支上干凈應(yīng)用的補(bǔ)丁—比如某些項目你不是維護(hù)者,但想幫點忙的話,最好用衍合:先在自己的一個分支里進(jìn)行開發(fā),當(dāng)準(zhǔn)備向主項目提交補(bǔ)丁的時候,根據(jù)最新的
origin/master
進(jìn)行一次衍合操作然后再提交,這樣維護(hù)者就不需要做任何整合工作(譯注:實際上是把解決分支補(bǔ)丁同最新主干代碼之間沖突的責(zé)任,化轉(zhuǎn)為由提交補(bǔ)丁的人來解決。),只需根據(jù)你提供的倉庫地址作一次快進(jìn)合并,或者直接采納你提交的補(bǔ)丁。
請注意,合并結(jié)果中最后一次提交所指向的快照,無論是通過衍合,還是三方合并,都會得到相同的快照內(nèi)容,只不過提交歷史不同罷了。衍合是按照每行的修改次序重演一遍修改,而合并是把最終結(jié)果合在一起。
有趣的衍合
衍合也可以放到其他分支進(jìn)行,并不一定非得根據(jù)分化之前的分支。以圖歷史為例,我們?yōu)榱私o服務(wù)器端代碼添加一些功能而創(chuàng)建了特性分支server,然后提交C3和C4。然后又從C3的地方再增加一個client分支來對客戶端代碼進(jìn)行一些相應(yīng)修改,所以提交了C8和C9。最后,又回到server分支提交了C10。

從一個特性分支里再分出一個特性分支的歷史。
假設(shè)在接下來的一次軟件發(fā)布中,我們決定先把客戶端的修改并到主線中,而暫緩并入服務(wù)端軟件的修改(因為還需要進(jìn)一步測試)。這個時候,我們就可以把基于server分支而非master分支的改變(即C8和C9),跳過server直接放到master分支中重演一遍,但這需要用git rebase的--onto選項指定新的基底分支master:
$git rebase --onto master server client
這好比在說:“取出client分支,找出client分支和server分支的共同祖先之后的變化,然后把它們在master上重演一遍”。是不是有點復(fù)雜?不過它的結(jié)果如圖所示,非??幔ㄗg注:雖然client里的C8,C9在C3之后,但這僅表明時間上的先后,而非在C3修改的基礎(chǔ)上進(jìn)一步改動,因為server和client這兩個分支對應(yīng)的代碼應(yīng)該是兩套文件,雖然這么說不是很嚴(yán)格,但應(yīng)理解為在C3時間點之后,對另外的文件所做的C8,C9修改,放到主干重演。):

將特性分支上的另一個特性分支衍合到其他分支。
現(xiàn)在可以快進(jìn) master分支了
$git checkout master
$git merge client

master分支,使之包含client分支的變化?,F(xiàn)在我們決定把server分支的變化也包含進(jìn)來。我們可以直接把server分支衍合到master,而不用手工切換到server分支后再執(zhí)行衍合操作—gitrebase [主分支][特性分支]命令會先取出特性分支server,然后在主分支master上重演:
$git rebase master server
于是,server的進(jìn)度應(yīng)用到master的基礎(chǔ)上
在master分支上衍合server分支。然后就可以快進(jìn)主干分支master了:
$git checkout master
$git merge server
現(xiàn)在client和Server分支的變化都已經(jīng)集成到主干分支來了,可以刪掉它們了。最終我們的提交歷史會變成:
$git branch -d client
$git branch -d server

最終的提交歷史衍合的風(fēng)險呃,奇妙的衍合也并非完美無缺,要用它得遵守一條準(zhǔn)則:
一旦分支中的提交對象發(fā)布到公共倉庫,就千萬不要對該分支進(jìn)行衍合操作。

而在C8之后,你的提交歷史里就會同時包含C4和C4’,兩者有著不同的SHA-1校驗值,如果用gitlog查看歷史,會看到兩個提交擁有相同的作者日期與說明,令人費解