版本控制git之二-分支

有人把 Git 的分支模型稱爲它的`‘必殺技特性’',也正因爲這一特性,使得 Git 從衆多版本控制系統中脫穎而出。 爲何 Git 的分支模型如此出衆呢? Git 處理分支的方式可謂是難以置信的輕量,創建新分支這一操作幾乎能在瞬間完成,並且在不同分支之間的切換操作也是一樣便捷。 與許多其它版本控制系統不同,Git 鼓勵在工作流程中頻繁地使用分支與合併,哪怕一天之內進行許多次。 理解和精通這一特性,你便會意識到 Git 是如此的強大而又獨特,並且從此真正改變你的開發方式。

分支簡介

在進行提交操作時,Git 會保存一個提交對象(commit object)。知道了 Git 保存數據的方式,我們可以很自然的想到——該提交對象會包含一個指向暫存內容快照的指針。 但不僅僅是這樣,該提交對象還包含了作者的姓名和郵箱、提交時輸入的信息以及指向它的父對象的指針。首次提交產生的提交對象沒有父對象,普通提交操作產生的提交對象有一個父對象,而由多個分支合併產生的提交對象有多個父對象,

爲了更加形象地說明,我們假設現在有一個工作目錄,裏面包含了三個將要被暫存和提交的文件。 暫存操作會爲每一個文件計算校驗和(使用我們在 起步 中提到的 SHA-1 哈希算法),然後會把當前版本的文件快照保存到 Git 倉庫中(Git 使用 blob 對象來保存它們),最終將校驗和加入到暫存區域等待提交:

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

當使用 git commit 進行提交操作時,Git 會先計算每一個子目錄(本例中只有項目根目錄)的校驗和,然後在 Git 倉庫中這些校驗和保存爲樹對象。 隨後,Git 便會創建一個提交對象,它除了包含上面提到的那些信息外,還包含指向這個樹對象(項目根目錄)的指針。如此一來,Git 就可以在需要的時候重現此次保存的快照。

現在,Git 倉庫中有五個對象:三個 blob 對象(保存着文件快照)、一個樹對象(記錄着目錄結構和 blob 對象索引)以及一個提交對象(包含着指向前述樹對象的指針和所有提交信息)。

你已經知道,每次提交,Git都把它們串成一條時間線,這條時間線就是一個分支。截止到目前,只有一條時間線,在Git裏,這個分支叫主分支,即master分支。HEAD嚴格來說不是指向提交,而是指向mastermaster纔是指向提交的,所以,HEAD指向的就是當前分支。

一開始的時候,master分支是一條線,Git用master指向最新的提交,再用HEAD指向master,就能確定當前分支,以及當前分支的提交點:

git-br-initial

分支創建

Git 是怎麼創建新分支的呢? 很簡單,它只是爲你創建了一個可以移動的新的指針。 比如,創建一個 testing 分支, 你需要使用 git branch 命令:

$ git branch testing

分支切換

要切換到一個已存在的分支,你需要使用 git checkout 命令。 我們現在切換到新創建的 testing 分支去:

$ git checkout testing

那麼,這樣的實現方式會給我們帶來什麼好處呢? 現在不妨再提交一次:

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

首先,我們創建dev分支,然後切換到dev分支:

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

git checkout命令加上-b參數表示創建並切換,相當於以下兩條命令:

$ git branch dev
$ git checkout dev
Switched to branch 'dev'

然後,用git branch命令查看當前分支:

$ git branch
* dev
  master

git branch命令會列出所有分支,當前分支前面會標一個*號。

然後,我們就可以在dev分支上正常提交,比如對readme.txt做個修改,加上一行:

Creating a new branch is quick.

分支刪除

你可以使用帶 -d選項的 git branch 命令來刪除分支:

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

分支的合併

假設你已經修正了 #53 問題,並且打算將你的工作合併入 master 分支。 爲此,你需要合併 iss53 分支到 master 分支,這和之前你合併 hotfix 分支所做的工作差不多。 你只需要檢出到你想合併入的分支,然後運行 git merge 命令:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

這和你之前合併 hotfix 分支的時候看起來有一點不一樣。 在這種情況下,你的開發歷史從一個更早的地方開始分叉開來(diverged)。 因爲,master 分支所在提交並不是 iss53 分支所在提交的直接祖先,Git 不得不做一些額外的工作。 出現這種情況的時候,Git 會使用兩個分支的末端所指的快照(C4C5)以及這兩個分支的工作祖先(C2),做一個簡單的三方合併。

一次典型合併中所用到的三個快照。

遇到衝突時的分支合併

有時候合併操作不會如此順利。 如果你在兩個不同的分支中,對同一個文件的同一個部分進行了不同的修改,Git 就沒法乾淨的合併它們。 如果你對 #53 問題的修改和有關 hotfix 的修改都涉及到同一個文件的同一處,在合併它們的時候就會產生合併衝突:

$ 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 做了合併,但是沒有自動地創建一個新的合併提交。 Git 會暫停下來,等待你去解決合併產生的衝突。 你可以在合併衝突後的任意時刻使用 git status 命令來查看那些因包含合併衝突而處於未合併(unmerged)狀態的文件:

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

任何因包含合併衝突而有待解決的文件,都會以未合併狀態標識出來。 Git 會在有衝突的文件中加入標準的衝突解決標記,這樣你可以打開這些包含衝突的文件然後手動解決衝突。 出現衝突的文件會包含一些特殊區段,看起來像下面這個樣子:

<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
 please contact us at [email protected]
</div>
>>>>>>> iss53:index.html

這表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因爲你在運行 merge 命令的時候已經檢出到了這個分支)在這個區段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。 爲了解決衝突,你必須選擇使用由 ======= 分割的兩部分中的一個,或者你也可以自行合併這些內容。 例如,你可以通過把這段內容換成下面的樣子來解決衝突:

<div id="footer">
please contact us at [email protected]
</div>

上述的衝突解決方案僅保留了其中一個分支的修改,並且 &lt;&lt;&lt;&lt;&lt;&lt;&lt; , ======= , 和 &gt;&gt;&gt;&gt;&gt;&gt;&gt; 這些行被完全刪除了。 在你解決了所有文件裏的衝突之後,對每個文件使用 git add 命令來將其標記爲衝突已解決。 一旦暫存這些原本有衝突的文件,Git 就會將它們標記爲衝突已解決。

分支管理

現在已經創建、合併、刪除了一些分支,讓我們看看一些常用的分支管理工具。

git branch 命令不只是可以創建與刪除分支。 如果不加任何參數運行它,會得到當前所有分支的一個列表:

$ git branch
  iss53
* master
  testing

注意 master 分支前的 * 字符:它代表現在檢出的那一個分支(也就是說,當前 HEAD 指針所指向的分支)。 這意味着如果在這時候提交,master 分支將會隨着新的工作向前移動。 如果需要查看每一個分支的最後一次提交.

Bug分支

軟件開發中,bug就像家常便飯一樣。有了bug就需要修復,在Git中,由於分支是如此的強大,所以,每個bug都可以通過一個新的臨時分支來修復,修復後,合併分支,然後將臨時分支刪除。

當你接到一個修復一個代號101的bug的任務時,很自然地,你想創建一個分支issue-101來修復它,但是,等等,當前正在dev上進行的工作還沒有提交:

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

    new file:   hello.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   readme.txt

並不是你不想提交,而是工作只進行到一半,還沒法提交,預計完成還需1天時間。但是,必須在兩個小時內修復該bug,怎麼辦?

幸好,Git還提供了一個stash功能,可以把當前工作現場“儲藏”起來,等以後恢復現場後繼續工作:

$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge

現在,用git status查看工作區,就是乾淨的(除非有沒有被Git管理的文件),因此可以放心地創建分支來修復bug。

首先確定要在哪個分支上修復bug,假定需要在master分支上修復,就從master創建臨時分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

$ git checkout -b issue-101
Switched to a new branch 'issue-101'

現在修復bug,需要把“Git is free software ...”改爲“Git is a free software ...”,然後提交:

$ git add readme.txt 
$ git commit -m "fix bug 101"
[issue-101 4c805e2] fix bug 101
 1 file changed, 1 insertion(+), 1 deletion(-)

修復完成後,切換到master分支,並完成合並,最後刪除issue-101分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
 readme.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

太棒了,原計劃兩個小時的bug修復只花了5分鐘!現在,是時候接着回到dev分支幹活了!

$ git checkout dev
Switched to branch 'dev'

$ git status
On branch dev
nothing to commit, working tree clean

工作區是乾淨的,剛纔的工作現場存到哪去了?用git stash list命令看看:

$ git stash list
stash@{0}: WIP on dev: f52c633 add merge

工作現場還在,Git把stash內容存在某個地方了,但是需要恢復一下,有兩個辦法:

一是用git stash apply恢復,但是恢復後,stash內容並不刪除,你需要用git stash drop來刪除;

另一種方式是用git stash pop,恢復的同時把stash內容也刪了:

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

    new file:   hello.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   readme.txt

Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)

再用git stash list查看,就看不到任何stash內容了:

$ git stash list

你可以多次stash,恢復的時候,先用git stash list查看,然後恢復指定的stash,用命令:

$ git stash apply stash@{0}

dev分支

軟件開發中,總有無窮無盡的新的功能要不斷添加進來。

添加一個新功能時,你肯定不希望因爲一些實驗性質的代碼,把主分支搞亂了,所以,每添加一個新功能,最好新建一個dev分支,在上面開發,完成後,合併,最後,刪除該dev分支。

現在,你終於接到了一個新任務:開發代號爲Vulcan的新功能,該功能計劃用於下一代星際飛船。

於是準備開發:

$ git checkout -b dev-vulcan
Switched to a new branch 'dev-vulcan'

5分鐘後,開發完畢:

$ git add vulcan.py

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

    new file:   vulcan.py

$ git commit -m "add feature vulcan"
[feature-vulcan 287773e] add feature vulcan
 1 file changed, 2 insertions(+)
 create mode 100644 vulcan.py

切回dev,準備合併:

$ git checkout dev

一切順利的話,feature分支和bug分支是類似的,合併,然後刪除。

但是!

就在此時,接到上級命令,因經費不足,新功能必須取消!

雖然白乾了,但是這個包含機密資料的分支還是必須就地銷燬:

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

銷燬失敗。Git友情提醒,dev-vulcan分支還沒有被合併,如果刪除,將丟失掉修改,如果要強行刪除,需要使用大寫的-D參數。。

現在我們強行刪除:

$ git branch -D dev-vulcan
Deleted branch dev-vulcan (was 287773e).

終於刪除成功!

漸進穩定分支的工作流(“silo”)視圖。

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