Git歷險(xiǎn)記(5):Git里的分支與合并
51CTO編者按:本文是《Git歷險(xiǎn)記》系列的第五篇,譯者劉輝通過翻譯《Git Community Book》為我們具體講解Git里的分支與合并以及如何處理沖突的方法。如果你對Git還不了解,可以參看《Git歷險(xiǎn)記(1):初識版本控制系統(tǒng)Git》。以下是正文。
分支與合并
在Git里面我們可以創(chuàng)建不同的分支,來進(jìn)行調(diào)試、發(fā)布、維護(hù)等不同工作,而互不干擾。下面我們還是來創(chuàng)建一個試驗(yàn)倉庫,看一下Git分支運(yùn)作的臺前幕后:
$rm -rf test_branch_proj $mkdir test_branch_proj $cd test_branch_proj $git init Initialized empty Git repository in /home/test/test_branch_proj/.git/
我們?nèi)缫酝粯?,?chuàng)建一個“readme.txt”文件并把它提交到倉庫中:
$echo "hello, world" > readme.txt $git add readme.txt $git commit -m "project init" [master (root-commit) 0797f4f] project init 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 readme.txt
我們來看一下工作目錄(working tree)的當(dāng)前狀態(tài):
$git status # On branch master nothing to commit (working directory clean)
大家如果注意的話,可以看到“# On branch master”這么一行,這表示我們現(xiàn)在正在主分支(master)上工作。當(dāng)我們新建了一個本地倉庫,一般就是默認(rèn)處在主分支(master)上。下面我們一起看一下Git是如何存儲一個分支的:
$cd .git $cat HEAD ref: refs/heads/master
“.git/HEAD”這個文件里保存的是我們當(dāng)前在哪個分支上工作的信息。
在Git中,分支的命名信息保存在“.git/refs/heads”目錄下:
$ls refs/heads master
我們可以看到目錄里面有一個名叫“master”文件,我們來看一下里面的內(nèi)容:
$cat refs/heads/master 12c875f17c2ed8c37d31b40fb328138a9027f337
大家可以看到這是一個“SHA1哈希串值”,也就是一個對象名,我們再看看這是一個什么類型的對象:
$cat refs/heads/master | xargs git cat-file -t commit
是的,這是一個提交(commit),“master”文件里面存有主分支(master)最新提交的“對象名”;我們根據(jù)這個“對象名”就可以可找到對應(yīng)的樹對象(tree)和二進(jìn)制對象(blob),簡而言之就是我能夠按“名”索引找到這個分支里所有的對象。
讀者朋友把我們文章里的示例在自己的機(jī)器上執(zhí)行時會發(fā)現(xiàn),“cat refs/heads/master”命令的執(zhí)行結(jié)果和和文章中的不同。在本文里這個提交(commit)的名字是: “12c875f17c2ed8c37d31b40fb328138a9027f337”,前面我講Git是根據(jù)對象的內(nèi)容生成“SHA1哈希串值”作為 名字,只要內(nèi)容一樣,那么的對應(yīng)的名字肯定是一樣的,為什么這里面會不一樣呢? Git確實(shí)根據(jù)內(nèi)容來生成名字的,而且同名(SHA1哈希串值)肯定會有 相同內(nèi)容,但是提交對象(commit)和其它對象有點(diǎn)不一樣,它里面會多一個時間戳(timestamp),所以在不同的時間生成的提交對象,即使內(nèi)容 完全一樣其名字也不會相同。
下面命令主是查看主分支最新提交的內(nèi)容:
$cat refs/heads/master | xargs git cat-file -p tree 0bd1dc15d804534cf25c5cb53260fd03c84fd4b9 author liuhui998 1300697913 +0800 committer liuhui998 1300697913 +0800 project init
“1300697913 +0800”這就是時間戳(timestamp)。
現(xiàn)在查看此分支里面所包含的數(shù)據(jù)(blob)
$cat refs/heads/master | xargs git cat-file -p | head -n 1 | cut -b6-15 | xargs git cat-file -p 100644 blob 4b5fa63702dd96796042e92787f464e28f09f17d readme.txt
查看當(dāng)前的readme.txt
$git cat-file -p 4b5fa63 hello, world $cd ..
好的,前面是在主分支(master)里面玩,下面我們想要創(chuàng)建一個自己的測試分支來玩一下。git branch命令可以創(chuàng)建一個新的分支,也可以查看當(dāng)前倉庫里有的分支。下面先創(chuàng)建一個叫“test”的分支: $git branch test
再來看一下當(dāng)前項(xiàng)目倉庫中有幾個分支:
$git branch * master test
我們現(xiàn)在簽出“test”分支到工作目錄里:
$git checkout test
現(xiàn)在再來看一下我們處在哪個分支上:
$git branch master * test
好的,我們現(xiàn)在在“test”分支里面了,那么我們就修改一下“readme.txt”這個文件,再把它提交到本地的倉庫里面支:
$echo "In test branch" >> readme.txt $git add readme.txt $git commit -m "test branch modified" [test 7f3c997] test branch modified 1 files changed, 1 insertions(+), 0 deletions(-)
當(dāng)看當(dāng)前版本所包含的blob:
$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
我們現(xiàn)在再像前面一樣的看看Git如何存儲“test”這個分支的,先來看看“.git/HEAD”這個文件是否指向了新的分支:
$cd .git $cat HEAD ref: refs/heads/test
沒錯,“.git/HEAD”確實(shí)指向的“test”分支。再來看看“.git/refs/heads”目錄里的內(nèi)容:
$ls refs/heads master test
我們可以看到目錄里面多了一個名叫“test”文件,我們來看一下里面的內(nèi)容:
$cat refs/heads/test 7f3c9972577a221b0a30b58981a554aafe10a104
查看測試分支(test)最新提交的內(nèi)容:
$cat refs/heads/test | xargs git cat-file -p tree 7fa3bfbeae072063c32621ff08d51f512a3bac53 parent b765df9edd4db791530f14c2e107aa40907fed1b author liuhui998 1300698655 +0800 committer liuhui998 1300698655 +0800 test branch modified
再來查看此分支里面所包含的數(shù)據(jù)(blob):
$cat refs/heads/test | xargs git cat-file -p | head -n 1 | cut -b6-15 | xargs git cat-file -p 100644 blob ebe01d6c3c2bbb74e043715310098d8da2baa4bf readme.txt
查看當(dāng)前”readme.txt”文件里的內(nèi)容:
$git cat-file -p ebe01d6 hello, world In test branch cd ..
我們再回到主分支里面:
$git checkout master Switched to branch 'master' $git checkout master $cat readme.txt hello, world
如我們想看看主分支(master)和測試分支(test)之間的差異,可以使用git diff命令來查看它們之間的diff:
$git diff test diff --git a/readme.txt b/readme.txt index ebe01d6..4b5fa63 100644 --- a/readme.txt +++ b/readme.txt @@ -1,2 +1 @@ hello, world -In test branch
大家可以以到當(dāng)前分支與測試分支(test)相比,少了一行內(nèi)容:“-In test branch”。
如果執(zhí)行完git diff命令后認(rèn)為測試分支(test)的修改無誤,能合并時,可以用git merge命令把它合并到主分支(master)中:
$git merge test Updating b765df9..7f3c997 Fast-forward readme.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
“Updating b765df9..7f3c997”表示現(xiàn)在正在更新合并“b765df9”和“7f3c997”兩個提交(commit)之間的內(nèi)容;“b765df9”代表著主分支(master),“7f3c997”代表測試分支(test)。
“Fast-forward”在這里可以理解為順利合并,沒有沖突。“readme.txt | 1 +”表示這個文件有一行被修改,“1 files changed, 1 insertions(+), 0 deletions(-)”,表示這一次合并只有一個文件被修改,一行新數(shù)據(jù)插入,0 行被刪除。
我們現(xiàn)在看一下合并后的“readme.txt”的內(nèi)容:
$cat readme.txt hello, world In test branch
內(nèi)容沒有錯,是“master”分支和“test”分支合并后的結(jié)果,再用“git status”看一下,當(dāng)前工作目錄的狀態(tài)也是干凈的(clean)。
$git status # On branch master nothing to commit (working directory clean)
好的,現(xiàn)在測試分支(test)結(jié)束了它的使命,沒有存在的價(jià)值的,可以用“git branch -d”命令把這個分支刪掉:
$git branch -d test Deleted branch test (was 61ce004).
如果你想要刪除的分支還沒有被合并到其它分支中去,那么就不能用“git branch -d”來刪除它,需要改用“git branch -D”來強(qiáng)制刪除。
#p#
如何處理沖突(conflict)
前面說了分支的一些事情,還簡單地合并了一個分支。但是平時多人協(xié)作的工作過程中,幾乎沒有不碰到?jīng)_突(conflict)的情況,下面的示例就是剖析一下沖突成因及背后的故事:
還是老規(guī)矩,新建一個空的Git倉庫作試驗(yàn):
$rm -rf test_merge_proj $mkdir test_merge_proj $cd test_merge_proj $git init Initialized empty Git repository in /home/test/test_merge_proj/.git/
在主分支里建一個“readme.txt”的文件,并且提交本地倉庫的主分支里(master):
$echo "hello, world" > readme.txt $git add readme.txt $git status # On branch master # # Initial commit # # Changes to be committed: # (use "git rm --cached ..." to unstage) # # new file: readme.txt # git commit -m "project init" [master (root-commit) d58353e] project init 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 readme.txt
當(dāng)看當(dāng)前版本所包含的blob:
$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p 100644 blob 4b5fa63702dd96796042e92787f464e28f09f17d readme.txt
雖然前面把“readme.txt”這個文件提交了,但是暫存區(qū)里還是會暫存一下,直到下次“git add”時把它沖掉:
$git ls-files --stage 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 readme.txt
然后再創(chuàng)建測試分支(test branch),并且切換到測試分支下工作:
$git branch test $git checkout test Switched to branch 'test'
再在測試分支里改寫“readme.txt”的內(nèi)容,并且提交到本地倉庫中:
$echo "hello, mundo" > readme.txt $git add readme.txt $git commit -m "test branch modified" [test 7459649] test branch modified 1 files changed, 1 insertions(+), 1 deletions(-)
現(xiàn)在看一下當(dāng)前分支里的“readme.txt”的“SHA1哈希串值”確實(shí)不同了:
$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p 100644 blob 034a81de5dfb592a22039db1a9f3f50f66f474dd readme.txt
暫存區(qū)里的東東也不一樣了:
$git ls-files --stage 100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 0 readme.txt
現(xiàn)在我們切換到主分支(master)下工作,再在“readme.txt”上作一些修改,并把它提交到本地的倉庫里面:
$git checkout master Switched to branch 'master' $git add readme.txt echo "hola,world" > readme.txt $git add readme.txt $git commit -m "master branch modified" [master 269ef45] master branch modified 1 files changed, 1 insertions(+), 1 deletions(-)
現(xiàn)在再來看一下當(dāng)前分支里的“readme.txt”的“SHA1哈希串值”:
$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p 100644 blob aac629fb789684a5d9c662e6548fdc595608c002 readme.txt
暫存區(qū)里的內(nèi)容也改變了:
$git ls-files --stage 100644 aac629fb789684a5d9c662e6548fdc595608c002 0 readme.txt
主分支(master) 和測試分支(test)里的內(nèi)容已經(jīng)各自改變了(diverged),我們現(xiàn)在用“git merge”命令來把兩個分支合一下看看:
$git merge test Auto-merging readme.txt CONFLICT (content): Merge conflict in readme.txt Automatic merge failed; fix conflicts and then commit the result.
合并命令的執(zhí)行結(jié)果不是“Fast-foward”,而是“CONFLICT”。是的,兩個分支的內(nèi)容有差異,致使它們不能自動合并(Auto-merging)。
還是先看一下工作目錄的狀態(tài):
$git status # On branch master # Unmerged paths: # (use "git add/rm ..." as appropriate to mark resolution) # # both modified: readme.txt # no changes added to commit (use "git add" and/or "git commit -a")
現(xiàn)在Git提示當(dāng)前有一個文件“readme.txt”沒有被合并,原因是“both modified”。
再看一下暫存區(qū)里的內(nèi)容:
$git ls-files --stage 100644 4b5fa63702dd96796042e92787f464e28f09f17d 1 readme.txt 100644 aac629fb789684a5d9c662e6548fdc595608c002 2 readme.txt 100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 3 readme.txt
看一下里面的每個blob對象的內(nèi)容:
$git cat-file -p 4b5fa6 hello, world $git cat-file -p aac629 hola,world $git cat-file -p 034a81 hello, mundo
我們不難發(fā)現(xiàn),“aac629”是當(dāng)前主分支的內(nèi)容,“034a81”是測試分支里的內(nèi)容,而“4b5fa6”是它們共同父對象(Parent)里的內(nèi)容。因?yàn)樵诤喜⑦^程中出現(xiàn)了錯誤,所以Git把它們?nèi)齻€放到了暫存區(qū)了。
現(xiàn)在我們再來看一下工作目錄里的“readme.txt”文件的內(nèi)容:
$cat readme.txt <<<<<<< HEAD hola,world ======= hello, mundo >>>>>>> test
“<<<<<<< HEAD“下面就是當(dāng)前版本里的內(nèi)容;而“=======”之下,“>>>>>>> test”之上則表示測試分支里與之對應(yīng)的有沖突的容。修復(fù)沖突時我們要做的,一般就是把“ <<<<<<< HEAD”,“=======”和“ >>>>>>> test”這些東東先去掉,然后把代碼改成我們想要的內(nèi)容。
假設(shè)我們用編輯器把“readme.txt“改成了下面的內(nèi)容:
$cat readme.txt hola, mundo
然再把改好的“readme.txt”用“git add”添加到暫存區(qū)中,最后再用“git commit”提交到本地倉庫中,這個沖突(conflict)就算解決了:
$git add readme.txt $git commit -m "fix conflict" [master ebe2f18] fix conflict
這里看起來比較怪異的地方是Git解決了沖突的辦法:怎么用“git add”添加到暫存區(qū)去,“git add”不是用來未暫存文件的吧,怎么又來解決沖突了。不過我想如果你仔細(xì)讀過上一篇文章的話就不難理解,因?yàn)镚it是一個“snapshot”存儲系統(tǒng),所有新增加的內(nèi)容都是直接存儲的,而不是和老版本作一個比較后存儲新舊版本間的差異。
Git里面合并兩個版本之間的同一文件,如果兩者間內(nèi)容相同則不作處理,兩者間內(nèi)容不同但是可以合并則產(chǎn)生一個新的blob對象,兩者間內(nèi)容不同但是合并時產(chǎn)生了沖突,那么我們解決了沖突后要把文件“git add”到暫存區(qū)中再“git commit”提交到本地倉庫即可,這就和前面一樣產(chǎn)生一個新的blob對象。
假設(shè)我們對合并的結(jié)果不滿意,可以用下面的命令來撤消前面的合并:
$git reset --hard HEAD^ HEAD is now at 050d890 master branch modified
從git reset(2)命令的輸出結(jié)果可以看到,主分支已經(jīng)回到了合并前的狀態(tài)了。
我們再用下面的命令看一下“readme.txt”文件,確認(rèn)一下文件改回來沒有:
$cat readme.txt hola,world
小結(jié)
由于Git采用了“SHA1哈希串值內(nèi)容尋值”、“快照存儲(snapshot)”等方法, Git中創(chuàng)建分支代價(jià)是很小的速度很快;也這是因?yàn)槿绱?,它處理合并沖突的方法與眾不同。
在這里我想起了“C語言就是匯編(計(jì)算機(jī)硬件)的一個馬甲”這句話,其實(shí)Git也就是底層文件系統(tǒng)的一個馬甲,只不過它帶了版本控制功能,而且更加高效。Git里有些命令可能不是很好理解(如解決合并沖突用git add),但是對于系統(tǒng)層而言,它是最高效的,就像是C語言的數(shù)組下標(biāo)從0開始一樣。
原文連接:http://www.infoq.com/cn/news/2011/03/git-adventures-branch-merge
【編輯推薦】