Git 分支
對于任何一個文件,在Git內(nèi)都只有三種狀態(tài):已提交(committed)疆柔,已修改(modified)和已暫存(staged)。已提交表示該文件已經(jīng)被安全地保存在本地數(shù)據(jù)庫中了锌钮;已修改表示修改了某個文件,但還沒有提交保存扑浸;已暫存表示把已修改的文件放在下次提交時要保存的清單中烧给。
由此我們看到Git管理項目時,文件流轉(zhuǎn)的三個工作區(qū)域:
Git的工作目錄喝噪,暫存區(qū)域础嫡,以及本地倉庫
基本的Git工作流程如下:
1.在工作目錄中修改某些文件。
2.對修改后的文件進行快照酝惧,然后保存到暫存區(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è)在工作目錄中有三個文件湖雹,準備將它們暫存后提交咏闪。暫存操作會對每一個文件計算校驗和(即第一章中提到的SHA-1哈希字串),然后把當前版本的文件快照保存到Git倉庫中(Git使用blob類型的對象存儲這些快照)摔吏,并將校驗和加入暫存區(qū)域:
$git add README test.rb LICENSE $ git commit -m 'initial commit of myproject'
當使用 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作為分支的默認名字茬暇。在若干次提交后,你其實已經(jīng)有了一個指向最后一次提交對象的master分支寡喝,它在每次提交的時候都會自動向前移動糙俗。
新建分支就是在當前commit對象上新建個指針
那么,Git是如何知道你當前在哪個分支上工作的呢预鬓?其實答案也很簡單巧骚,它保存著一個名為HEAD的特別指針。請注意它和你熟知的許多其他版本控制系統(tǒng)(比如Subversion或CVS)里的HEAD概念大不相同格二。在Git中劈彪,它是一個指向你正在工作中的本地分支的指針(譯注:將HEAD想象為當前分支的別名。)顶猜。運行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分支里作出的修改暫時取消喜德,這樣你就可以向另一個方向進行開發(fā)
我們做些修改然后再提交,我們的項目提交歷史產(chǎn)生了分叉酸役,因為剛才我們創(chuàng)建了一個分支住诸,轉(zhuǎn)換到其中進行了一些工作驾胆,然后又回到原來的主分支進行了另外一些工作。這些改變分別孤立在不同的分支里:我們可以在不同分支里反復(fù)切換贱呐,并在時機成熟時把它們合并到一起丧诺。而所有這些工作,僅僅需要branch和 checkout這兩條命令就可以完成
分支的合并
合并回 master分支奄薇。只需回到master分支驳阎,運行g(shù)it merge命令指定要合并進來的分支:
$git merge iss53
請注意,由于當前 master分支所指向的提交對象(C4)并不是 iss53分支的直接祖先馁蒂,Git不得不進行一些額外處理呵晚。就此例而言,Git會用兩個分支的末端(C4和C5)以及它們的共同祖先(C2)進行一次簡單的三方合并計算沫屡。用紅框標出了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會在有沖突的文件里加入標準的沖突解決標記,可以通過它們來手工定位并解決這些沖突肩榕。可以看到此文件包含類似下面這樣的部分:
<<<<<<<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將把它們標記為已解決狀態(tài)(譯注:實際上就是來一次快照保存到暫存區(qū)域。)股淡。因為一旦暫存身隐,就表示沖突已經(jīng)解決。再運行一次 gitstatus來確認所有沖突都已解決:
$git status
# On branch master
# Changes to be committed:
# (use "gitreset HEAD ..." to unstage)
# # modified: index.html #
如果覺得滿意了唯灵,并且確認所有沖突都已解決贾铝,也就是進入了暫存區(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. #
如果想給將來看這次合并的人一些方便垢揩,可以修改該信息,提供更多合并細節(jié)敛瓷。比如你都作了哪些改動叁巨,以及這么做的原因。有時候裁決沖突的理由并不直接或明顯呐籽,有必要略加注解俘种。分支的管理日后的常規(guī)工作中會經(jīng)常用到下面介紹的管理命令。git branch命令不僅僅能創(chuàng)建和刪除分支绝淡,如果不加任何參數(shù)宙刘,它會給出當前所有分支的清單:
$git branch iss53 * master testing
注意看 master分支前的 *字符:它表示當前所在的分支。也就是說牢酵,如果現(xiàn)在提交更新悬包,master分支將隨著開發(fā)進度前移。若要查看各個分支最后一個提交對象的信息馍乙,運行$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)(或尚未)與當前分支合并的分支布近,可以用
--merge和 --no-merged選項(Git1.5.6以上版本)。比如用gitbranch --merge 查看哪些分支已被并入當前分支(譯注:也就是說哪些分支是當前分支的直接上游丝格。):
$git branch --merged iss53 * master
之前我們已經(jīng)合并了 iss53撑瞧,所以在這里會看到它。一般來說显蝌,列表中沒有*
的分支通常都可以用 gitbranch -d來刪掉预伺。原因很簡單,既然已經(jīng)把它們所包含的工作整合到了其他分支曼尊,刪掉也不會損失什么酬诀。另外可以用 gitbranch --no-merged查看尚未合并的工作:
$git branch --no-merged testing
它會顯示還未合并進來的分支。由于這些分支中還包含著尚未合并進來的工作成果骆撇,所以簡單地用 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強制執(zhí)行肴裙,就像上面提示信息中給出的那樣趾唱。
遠程分支
遠程分支(remotebranch)是對遠程倉庫中的分支的索引。它們是一些無法移動的本地分支蜻懦;只有在Git進行網(wǎng)絡(luò)交互時才會更新甜癞。遠程分支就像是書簽,提醒著你上次連接遠程倉庫時上面各分支的位置阻肩。我們用 (遠程倉庫名
)/(分支名)這樣的形式表示遠程分支带欢。比如我們想看看上次同 origin倉庫通訊時master的樣子,就應(yīng)該查看 origin/master分支烤惊。如果你和同伴一起修復(fù)某個問題乔煞,但他們先推送了一個iss53分支到遠程倉庫,雖然你可能也有一個本地的 iss53分支柒室,但指向服務(wù)器上最新更新的卻應(yīng)該是 origin/iss53分支渡贾。
可能有點亂,我們不妨舉例說明雄右。假設(shè)你們團隊有個地址為git.ourcompany.com的Git服務(wù)器空骚。如果你從這里克隆,Git會自動為你將此遠程倉庫命名為origin擂仍,并下載其中所有的數(shù)據(jù)囤屹,建立一個指向它的 master分支的指針,在本地命名為 origin/master逢渔,但你無法在本地更改其數(shù)據(jù)肋坚。接著,Git建立一個屬于你自己的本地master分支肃廓,始于 origin上 master分支相同的位置智厌,你可以就此開始工作如果你在本地 master分支做了些改動,與此同時盲赊,其他人向 git.ourcompany.com推送了他們的更新铣鹏,那么服務(wù)器上的master分支就會向前推進,而于此同時哀蘑,你在本地的提交歷史正朝向不同方向發(fā)展诚卸。不過只要你不和服務(wù)器通訊,你的 origin/master
可以運行 git fetch origin來同步遠程服務(wù)器上的數(shù)據(jù)到本地递礼。該命令首先找到
origin是哪個服務(wù)器(本例為git.ourcompany.com)惨险,從上面獲取你尚未擁有的數(shù)據(jù),更新你本地的數(shù)據(jù)庫脊髓,然后把 origin/master的指針移到它最新的位置上
推送本地分支
要想和其他人分享某個本地分支,你需要把它推送到一個你擁有寫權(quán)限的遠程倉庫栅受。如果你有個叫 serverfix的分支需要和他人一起開發(fā)将硝,可以運行g(shù)itpush (遠程倉庫名)(分支名):
$git push origin serverfix
這其實有點像條捷徑恭朗。Git自動把 serverfix分支名擴展為refs/heads/serverfix:refs/heads/serverfix,意為“取出我在本地的
serverfix分支依疼,推送到遠程倉庫的serverfix分支中去”痰腮。也可以運行
git push origin serverfix:serferfix
來實現(xiàn)相同的效果,它的意思是“上傳我本地的serverfix分支到遠程倉庫中去律罢,仍舊稱它為serverfix分支”膀值。通過此語法,你可以把本地分支推送到某個命名不同的遠程分支:若想把遠程分支叫作awesomebranch误辑,可以用
git push origin serverfix:awesomebranch
來推送數(shù)據(jù)沧踏。
接下來,當你的協(xié)作者再次從服務(wù)器上獲取數(shù)據(jù)時巾钉,他們將得到一個新的遠程分支 origin/serverfix:
$git fetch origin
值得注意的是翘狱,在 fetch操作下載好新的遠程分支之后,你仍然無法在本地編輯該遠程倉庫中的分支砰苍。換句話說潦匈,在本例中,你不會有一個新的serverfix分支赚导,有的只是一個你無法移動的 origin/serverfix指針茬缩。如果要把該內(nèi)容合并到當前分支,可以運行
git merge origin/serverfix
吼旧。如果想要一份自己的 serverfix來開發(fā)凰锡,可以在遠程分支的基礎(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)容同遠程分支 origin/serverfix一致黍少,這樣你就可以在里面繼續(xù)開發(fā)了寡夹。跟蹤遠程分支從遠程分支 checkout出來的本地分支,稱為跟蹤分支(trackingbranch)_厂置。跟蹤分支是一種和遠程分支有直接聯(lián)系的本地分支菩掏。在跟蹤分支里輸入gitpush,Git會自行推斷應(yīng)該向哪個服務(wù)器的哪個分支推送數(shù)據(jù)昵济。反過來智绸,在這些分支里運行 gitpull會獲取所有遠程索引,并把它們的數(shù)據(jù)都合并到本地分支中來访忿。
在克隆倉庫時瞧栗,Git通常會自動創(chuàng)建一個名為 master的分支來跟蹤origin/master。這正是gitpush和 gitpull一開始就能正常工作的原因海铆。當然迹恐,你可以隨心所欲地設(shè)定為其它跟蹤分支,比如origin上除了 master之外的其它分支卧斟。剛才我們已經(jīng)看到了這樣的一個例子:
gitcheckout -b [分支名][遠程名]/[分支名]殴边。如果你有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è)定不同于遠程分支的名字,只需在前個版本的命令里換個名字:
$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)進行三方合并戒职,合并的結(jié)果是產(chǎn)生一個新的提交對象(C5)
還有另外一個選擇:你可以把在C3里產(chǎn)生的變化補丁在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ù)當前分支(也就是要進行衍合的分支 experiment)后續(xù)的歷次提交對象(這里只有一個C3),生成一系列文件補丁再来,然后以基底分支(也就是主干分支master)最后一個提交對象(C4)為新的出發(fā)點蒙兰,逐個應(yīng)用之前準備好的補丁文件,最后會生成一個新的合并提交對象(C3’)芒篷,從而改寫 experiment 的提交歷史搜变,使它成為
master 分支的直接下游
把C3里產(chǎn)生的改變到C4上重演一遍。現(xiàn)在回到 master分支针炉,進行一次快進合并
現(xiàn)在的C3’應(yīng)的快照挠他,其實和普通的三方合并,即上個例子中的C5對應(yīng)的快照內(nèi)容一模一樣了篡帕。雖然最后整合得到的結(jié)果沒有任何區(qū)別殖侵,但衍合能產(chǎn)生一個更為整潔的提交歷史。如果視察一個衍合過的分支的歷史記錄镰烧,看起來會更清楚:仿佛所有修改都是在一根線上先后進行的拢军,盡管實際上它們原本是同時并行發(fā)生的。
一般我們使用衍合的目的怔鳖,是想要得到一個能在遠程分支上干凈應(yīng)用的補丁—比如某些項目你不是維護者茉唉,但想幫點忙的話,最好用衍合:先在自己的一個分支里進行開發(fā),當準備向主項目提交補丁的時候赌渣,根據(jù)最新的
origin/master
進行一次衍合操作然后再提交魏铅,這樣維護者就不需要做任何整合工作(譯注:實際上是把解決分支補丁同最新主干代碼之間沖突的責任昌犹,化轉(zhuǎn)為由提交補丁的人來解決坚芜。),只需根據(jù)你提供的倉庫地址作一次快進合并斜姥,或者直接采納你提交的補丁鸿竖。
請注意,合并結(jié)果中最后一次提交所指向的快照铸敏,無論是通過衍合缚忧,還是三方合并,都會得到相同的快照內(nèi)容杈笔,只不過提交歷史不同罷了闪水。衍合是按照每行的修改次序重演一遍修改,而合并是把最終結(jié)果合在一起蒙具。
有趣的衍合
衍合也可以放到其他分支進行球榆,并不一定非得根據(jù)分化之前的分支。以圖歷史為例禁筏,我們?yōu)榱私o服務(wù)器端代碼添加一些功能而創(chuàng)建了特性分支server持钉,然后提交C3和C4。然后又從C3的地方再增加一個client分支來對客戶端代碼進行一些相應(yīng)修改篱昔,所以提交了C8和C9每强。最后,又回到server分支提交了C10州刽。
從一個特性分支里再分出一個特性分支的歷史空执。
假設(shè)在接下來的一次軟件發(fā)布中,我們決定先把客戶端的修改并到主線中穗椅,而暫緩并入服務(wù)端軟件的修改(因為還需要進一步測試)辨绊。這個時候,我們就可以把基于server分支而非master分支的改變(即C8和C9)房待,跳過server直接放到master分支中重演一遍邢羔,但這需要用git rebase的--onto選項指定新的基底分支master:
$git rebase --onto master server client
這好比在說:“取出client分支,找出client分支和server分支的共同祖先之后的變化桑孩,然后把它們在master上重演一遍”拜鹤。是不是有點復(fù)雜?不過它的結(jié)果如圖所示流椒,非趁舨荆酷(譯注:雖然client里的C8,C9在C3之后,但這僅表明時間上的先后,而非在C3修改的基礎(chǔ)上進一步改動惯裕,因為server和client這兩個分支對應(yīng)的代碼應(yīng)該是兩套文件温数,雖然這么說不是很嚴格,但應(yīng)理解為在C3時間點之后蜻势,對另外的文件所做的C8撑刺,C9修改,放到主干重演握玛。):
將特性分支上的另一個特性分支衍合到其他分支够傍。
現(xiàn)在可以快進 master分支了
$git checkout master
$git merge client
master分支,使之包含client分支的變化∧硬現(xiàn)在我們決定把server分支的變化也包含進來冕屯。我們可以直接把server分支衍合到master,而不用手工切換到server分支后再執(zhí)行衍合操作—gitrebase [主分支][特性分支]命令會先取出特性分支server拂苹,然后在主分支master上重演:
$git rebase master server
于是安聘,server的進度應(yīng)用到master的基礎(chǔ)上在master分支上衍合server分支。然后就可以快進主干分支master了:
$git checkout master
$git merge server
現(xiàn)在client和Server分支的變化都已經(jīng)集成到主干分支來了瓢棒,可以刪掉它們了浴韭。最終我們的提交歷史會變成:
$git branch -d client
$git branch -d server
最終的提交歷史衍合的風險呃,奇妙的衍合也并非完美無缺音羞,要用它得遵守一條準則:
一旦分支中的提交對象發(fā)布到公共倉庫囱桨,就千萬不要對該分支進行衍合操作。
而在C8之后嗅绰,你的提交歷史里就會同時包含C4和C4’舍肠,兩者有著不同的SHA-1校驗值,如果用gitlog查看歷史窘面,會看到兩個提交擁有相同的作者日期與說明翠语,令人費解