Git裏的分支&合併

分支與合併

在Git裏面我們可以創建不同的分支,來進行調試、發佈、維護等不同工作,而互不干擾。下面我們還是來創建一個試驗倉庫,看一下Git分支運作的臺前幕後:

$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/ 

我們如以往一樣,創建一個“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)的當前狀態:

$git status 
# On branch master 
nothing to commit (working directory clean) 

大家如果注意的話,可以看到“# On branch master”這麼一行,這表示我們現在正在主分支(master)上工作。當我們新建了一個本地倉庫,一般就是默認處在主分支(master)上。下面我們一起看一下Git是如何存儲一個分支的:

$cd .git 
$cat HEAD 
ref: refs/heads/master 

“.git/HEAD”這個文件裏保存的是我們當前在哪個分支上工作的信息。

在Git中,分支的命名信息保存在“.git/refs/heads”目錄下:

$ls refs/heads 
master 

我們可以看到目錄裏面有一個名叫“master”文件,我們來看一下里面的內容:

$cat refs/heads/master 
12c875f17c2ed8c37d31b40fb328138a9027f337 

大家可以看到這是一個“SHA1哈希串值”,也就是一個對象名,我們再看看這是一個什麼類型的對象:

$cat refs/heads/master | xargs git cat-file -t 
commit 

是的,這是一個提交(commit),“master”文件裏面存有主分支(master)最新提交的“對象名”;我們根據這個“對象名”就可以可找到對應的樹對象(tree)和二進制對象(blob),簡而言之就是我能夠按“名”索引找到這個分支裏所有的對象。

讀者朋友把我們文章裏的示例在自己的機器上執行時會發現,“cat refs/heads/master”命令的執行結果和和文章中的不同。在本文裏這個提交(commit)的名字是: “12c875f17c2ed8c37d31b40fb328138a9027f337”,前面我講Git是根據對象的內容生成“SHA1哈希串值”作爲 名字,只要內容一樣,那麼的對應的名字肯定是一樣的,爲什麼這裏面會不一樣呢? Git確實根據內容來生成名字的,而且同名(SHA1哈希串值)肯定會有 相同內容,但是提交對象(commit)和其它對象有點不一樣,它裏面會多一個時間戳(timestamp),所以在不同的時間生成的提交對象,即使內容 完全一樣其名字也不會相同。

下面命令主是查看主分支最新提交的內容:

$cat refs/heads/master | xargs git cat-file -p 
tree 0bd1dc15d804534cf25c5cb53260fd03c84fd4b9 
author liuhui998 <[email protected]> 1300697913 +0800 
committer liuhui998 <[email protected]> 1300697913 +0800     project init 

“1300697913 +0800”這就是時間戳(timestamp)。

現在查看此分支裏面所包含的數據(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 

查看當前的readme.txt

$git cat-file -p 4b5fa63 
hello, world 
$cd .. 

好的,前面是在主分支(master)裏面玩,下面我們想要創建一個自己的測試分支來玩一下。git branch命令可以創建一個新的分支,也可以查看當前倉庫裏有的分支。下面先創建一個叫“test”的分支: $git branch test 

再來看一下當前項目倉庫中有幾個分支:

$git branch 
* master   test 

我們現在簽出“test”分支到工作目錄裏:

$git checkout test 

現在再來看一下我們處在哪個分支上:

$git branch   master 
* test 

好的,我們現在在“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(-) 

當看當前版本所包含的blob:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p 

我們現在再像前面一樣的看看Git如何存儲“test”這個分支的,先來看看“.git/HEAD”這個文件是否指向了新的分支:

$cd .git 
$cat HEAD 
ref: refs/heads/test 

沒錯,“.git/HEAD”確實指向的“test”分支。再來看看“.git/refs/heads”目錄裏的內容:

$ls refs/heads 
master 
test 

我們可以看到目錄裏面多了一個名叫“test”文件,我們來看一下里面的內容:

$cat refs/heads/test 
7f3c9972577a221b0a30b58981a554aafe10a104 

查看測試分支(test)最新提交的內容:

$cat refs/heads/test | xargs git cat-file -p 
tree 7fa3bfbeae072063c32621ff08d51f512a3bac53 
parent b765df9edd4db791530f14c2e107aa40907fed1b 
author liuhui998 <[email protected]> 1300698655 +0800 
committer liuhui998 <[email protected]> 1300698655 +0800      test branch modified 

再來查看此分支裏面所包含的數據(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 

查看當前”readme.txt”文件裏的內容:

$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 

大家可以以到當前分支與測試分支(test)相比,少了一行內容:“-In test branch”。

如果執行完git diff命令後認爲測試分支(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”表示現在正在更新合併“b765df9”和“7f3c997”兩個提交(commit)之間的內容;“b765df9”代表着主分支(master),“7f3c997”代表測試分支(test)。

“Fast-forward”在這裏可以理解爲順利合併,沒有衝突。“readme.txt | 1 +”表示這個文件有一行被修改,“1 files changed, 1 insertions(+), 0 deletions(-)”,表示這一次合併只有一個文件被修改,一行新數據插入,0 行被刪除。

我們現在看一下合併後的“readme.txt”的內容:

$cat readme.txt 
hello, world 
In test branch 

內容沒有錯,是“master”分支和“test”分支合併後的結果,再用“git status”看一下,當前工作目錄的狀態也是乾淨的(clean)。

$git status 
# On branch master 
nothing to commit (working directory clean) 

好的,現在測試分支(test)結束了它的使命,沒有存在的價值的,可以用“git branch -d”命令把這個分支刪掉:

$git branch -d test 
Deleted branch test (was 61ce004). 

如果你想要刪除的分支還沒有被合併到其它分支中去,那麼就不能用“git branch -d”來刪除它,需要改用“git branch -D”來強制刪除。

如何處理衝突(conflict)

前面說了分支的一些事情,還簡單地合併了一個分支。但是平時多人協作的工作過程中,幾乎沒有不碰到衝突(conflict)的情況,下面的示例就是剖析一下衝突成因及背後的故事:

還是老規矩,新建一個空的Git倉庫作試驗:

$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 <file>..." 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 

當看當前版本所包含的blob:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p 
100644 blob 4b5fa63702dd96796042e92787f464e28f09f17d  readme.txt 

雖然前面把“readme.txt”這個文件提交了,但是暫存區裏還是會暫存一下,直到下次“git add”時把它沖掉:

$git ls-files --stage 
100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 readme.txt 

然後再創建測試分支(test branch),並且切換到測試分支下工作:

$git branch test 
$git checkout test 
Switched to branch 'test' 

再在測試分支裏改寫“readme.txt”的內容,並且提交到本地倉庫中:

$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(-) 

現在看一下當前分支裏的“readme.txt”的“SHA1哈希串值”確實不同了:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p 
100644 blob 034a81de5dfb592a22039db1a9f3f50f66f474dd  readme.txt 

暫存區裏的東東也不一樣了:

$git ls-files --stage 
100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 0 readme.txt  

現在我們切換到主分支(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(-) 

現在再來看一下當前分支裏的“readme.txt”的“SHA1哈希串值”:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p 
100644 blob aac629fb789684a5d9c662e6548fdc595608c002  readme.txt 

暫存區裏的內容也改變了:

$git ls-files --stage 
100644 aac629fb789684a5d9c662e6548fdc595608c002 0 readme.txt 

主分支(master) 和測試分支(test)裏的內容已經各自改變了(diverged),我們現在用“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. 

合併命令的執行結果不是“Fast-foward”,而是“CONFLICT”。是的,兩個分支的內容有差異,致使它們不能自動合併(Auto-merging)。

還是先看一下工作目錄的狀態:

$git status 
# On branch master 
# Unmerged paths: 
# (use "git add/rm <file>..." as appropriate to mark resolution) 
# 
# both modified:    readme.txt 
# 
no changes added to commit (use "git add" and/or "git commit -a") 

現在Git提示當前有一個文件“readme.txt”沒有被合併,原因是“both modified”。

再看一下暫存區裏的內容:

$git ls-files --stage 
100644 4b5fa63702dd96796042e92787f464e28f09f17d 1 readme.txt 
100644 aac629fb789684a5d9c662e6548fdc595608c002 2 readme.txt 
100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 3 readme.txt 

看一下里面的每個blob對象的內容:

$git cat-file -p 4b5fa6 
hello, world 
$git cat-file -p aac629 
hola,world 
$git cat-file -p 034a81 
hello, mundo 

我們不難發現,“aac629”是當前主分支的內容,“034a81”是測試分支裏的內容,而“4b5fa6”是它們共同父對象(Parent)裏的內容。因爲在合併過程中出現了錯誤,所以Git把它們三個放到了暫存區了。

現在我們再來看一下工作目錄裏的“readme.txt”文件的內容:

$cat readme.txt 
<<<<<<< HEAD 
hola,world 
======= 
hello, mundo 
>>>>>>> test

“<<<<<<< HEAD“下面就是當前版本里的內容;而“=======”之下,“>>>>>>> test”之上則表示測試分支裏與之對應的有衝突的容。修復衝突時我們要做的,一般就是把“ <<<<<<< HEAD”,“=======”和“ >>>>>>> test”這些東東先去掉,然後把代碼改成我們想要的內容。

假設我們用編輯器把“readme.txt“改成了下面的內容:

$cat readme.txt 
hola, mundo 

然再把改好的“readme.txt”用“git add”添加到暫存區中,最後再用“git commit”提交到本地倉庫中,這個衝突(conflict)就算解決了:

$git add readme.txt 
$git commit -m "fix conflict" 
[master ebe2f18] fix conflict 

這裏看起來比較怪異的地方是Git解決了衝突的辦法:怎麼用“git add”添加到暫存區去,“git add”不是用來未暫存文件的吧,怎麼又來解決衝突了。不過我想如果你仔細讀過上一篇文章的話就不難理解,因爲Git是一個“snapshot”存儲系統,所有新增加的內容都是直接存儲的,而不是和老版本作一個比較後存儲新舊版本間的差異。

Git裏面合併兩個版本之間的同一文件,如果兩者間內容相同則不作處理,兩者間內容不同但是可以合併則產生一個新的blob對象,兩者間內容不同但是合併時產生了衝突,那麼我們解決了衝突後要把文件“git add”到暫存區中再“git commit”提交到本地倉庫即可,這就和前面一樣產生一個新的blob對象。

假設我們對合並的結果不滿意,可以用下面的命令來撤消前面的合併:

$git reset --hard HEAD^ 
HEAD is now at 050d890 master branch modified 

git reset(2)命令的輸出結果可以看到,主分支已經回到了合併前的狀態了。

我們再用下面的命令看一下“readme.txt”文件,確認一下文件改回來沒有:

$cat readme.txt 
hola,world 

小結

由於Git採用了“SHA1哈希串值內容尋值”、“快照存儲(snapshot)”等方法, Git中創建分支代價是很小的速度很快;也這是因爲如此,它處理合並衝突的方法與衆不同。

在這裏我想起了“C語言就是彙編(計算機硬件)的一個馬甲”這句話,其實Git也就是底層文件系統的一個馬甲,只不過它帶了版本控制功能,而且更加高效。Git裏有些命令可能不是很好理解(如解決合併衝突用git add),但是對於系統層而言,它是最高效的,就像是C語言的數組下標從0開始一樣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章