GIT團隊合作探討之三--使用分支

世有因果知因求果發表於2016-03-25

這篇文章是一個作為對git branch的綜合介紹。首先,我們會看看建立branch,這有點像是請求一個新的專案歷史。然後,我們看看git checkout是如何能夠被用來選擇一個branch,最後看看git merge是如何整合不同分支的李四的。

注意一點:git branch和svn branch是有很大不同的。svn branch僅僅被用於獲取偶然型的大規模開發effort,而git branch卻在你的每日工作流中都要使用。

git branch

分支代表著開發的一條線,分支實際上可以座位edit/stage/commit流程的一個抽象。你可以把他想想為希望申請建立一個全新的工作目錄(workding directory),快照區(staging area)和專案歷史(project history)。新的commit將在歷史日誌中轉為當前分支而存在,這也會產生一個專案歷史的fork。

git branch命令允許你建立,列表,重新命名和刪除分支。它不會允許你在不同分支間切換或者將forked history同時再次取回。也正是這個原因,git branch緊密地和git checkout/git merge命令整合在一起。

用法:

git branch      //理出當前repo的所有分支;
git branch <branch> //建立一個新的命名為<branch>的分支,注意這條命令不會checkout 
git branch -d <branch> //刪除指定的分支。如果還有一些unmerged changes,git是不允許你刪除一個分支的。
git branch -D <branch> //強制刪除一個分支,即使該分支有未merge的變更。
git branch -m <branch> //rename current branch to <branch>

探討:

在git中,分支是你每日工作流的重要組成部分。當你想新增一個feature或者修復一個bug,無論該工作是多大或多小,你都應該建立一個新的分支來封裝你的變更。這種工作模式也就隨時確保不穩定的程式碼永遠不會被扔到主分支上去,同時他也給你一個很好的機會在你merge feature程式碼到主分支之前來清理你的feature開發歷史!

比如:在上面的圖中,我們選了這麼一種典型情況:有兩條開發線獨立存在,一個是little feature,而另外一個是一個需要長時間開發的big feature兩個分支。這種開發策略使得不僅可以在這兩個feature上冰心開發,而且也能保證master分支不會被不穩定的程式碼所汙染。

Branch Tips

在GIT的背後,分支實現的方法相比於SVN的分支功能則要輕量很多。SVN的分支模型完全是把專案檔案從一個目錄拷貝到另外一個目錄,而git則把branch視為對一個commit的引用。也就是說,一個分支代表這一系列commit的頂端(tip),(注意:branch並不是commit的容器,而只是這組commit頭部的指標!)。一個分支的歷史完全由這些commit之間的關係來描述。

這對於git的合併模型也有著戲劇性的影響。在SVN中,merge的工作是以檔案為單位基礎的,而GIT中的merge則工作在更高層次更大粒度的commit層次上。你可以將專案歷史中的merge合併視作是兩條獨立的commit歷史線的join.

例子:

建立分支:非常重要的一點是:你必須理解分支就是指向一些commits的指標。當你建立一個branch,git要做的所有事情就是建立一個新的pointer---git並不會對repo做任何其他的改動。所以,如果你起初有以下歷史資訊的repo,然後,你執行

git branch crazy-expriment

 

建立一個新的branch,那麼repo的歷史並不會變化。你所獲得的是指向當前commit的一個指標。

注意在這裡我們僅僅建立了新的branch。為了建立新的commit到這個分支上去,你必須git checkout,然後使用標準的git add/git commit命令來提交commit。

刪除分支:

一旦你完成了一個分支上的feature開發或者bug修復,並且將這個分支上的所有改動merge到了主分支master上去,那麼你就可以刪除這個分支並且不會丟失任何歷史資訊。

git branch -d crazy-experiment //然而,如果branch沒有被merged,則這條命令會產生下面的錯誤:
error: The branch 'crazy-experiment' is not fully merged.
If you are sure you want to delete it, run 'git branch -D crazy-experiment'.

這樣的人性化機制確保你不會丟失這些commit,否則如果你使用-D選項,你將永遠丟失那條線上的所有開發工作。

 git checkout

git checkout命令讓你可以在不同的分支間進行任意切換。checkout一個分支將會更新在工作目錄中的檔案以便反映出在那個branch上儲存的對應檔案版本,同時這個checkout分支的動作也告訴git以後所有新的commit都須要記錄在那個branch上。把這個過程想象為你可以在不同的開發線上進行選擇和切換。

在前面的模組中,我們可拿到過git checkout可以用來檢視老的commits,checkout一個分支也是類似的,也就是會更新工作目錄為相應的版本。然而不同的是checkout 分支會導致後續新的變更儲存在專案歷史中。

用法:

git checkout <existing-branch> //這條命令checkout已經存在的一個分支,更新工作目錄為對應分支版本;
git checkout -b <new-branch> //以當前分支head commit為起點建立並且checkout到new-branch
git checkout -b <new-branch> <existing-branch> //以指定<exisiting-branch>的head commit為起點建立一個new-branch

探討:

git checkout和git branch是密切配合工作的。當你希望開始一個新的feature開發,你需要通過git branch命令來建立一個新的branch,然後checkout這個新的branch.你可以在一個repo中通過checkout branch來切換到多個不同feature上去工作。

為每一個新的feature都開一個專有的分支來隔離開發,這種模式對於傳統的SVN工作流來說是一個巨大的轉變。也正是這種工作模式使得任意發揮你的想象力做新的嘗試,而不用擔心你會破壞已有功能,這也使得同時工作在許多並無關聯的feature上成為現實。而且,分支也能有效地促進了合作工作流。

Detached HEADs

現在我們已經看到git checkout的三個主要應用,我們可以談談“detached head"這個狀態的含義了。

記住HEAD是git用於參考當前snapshot的方法。內部原理上,git checkout命令實際上僅僅簡單更新了HEAD來指向指定的branch或者一個特定的commit.當HEAD指向了一個分支,git並不會有任何complain,但是當你直接checkout一個commit時,git就會切換進入一個"detached HEAD"狀態。

這個detached head的警告是在告訴你你現在做的一切都是和專案開發工作的其他部分完全分離的。如果你仍然希望在這種detached head狀態下開發新的feature,那麼將不會有任何分支來讓你後續返回它。如果你checkout到另外一個branch了,那麼將沒有任何辦法能夠reference你在detached head下開發的feature.

這裡要指出的是,你的開發工作應該永遠發生在一個branch上,而不能在detached HEAD狀態下來開發。因為只有在一個branch上開發遞交新的commit,你才能夠reference到你新的commits。然而,如果你僅僅希望看看老的commit當時的快照,你儘管使用這種方式。

例子:

下面的例子演示了基本的git分支使用過程。

1.當你希望開始一個新的功能開發時,你基於master/develop分支建立一個新的branch,並且切換到這個分支上。

git branch new-feature
git checkout new-feature 
//上面兩條命令等價於:
git checkout -b new-feature

2.隨後你commit你的新快照:

# Edit some files
git add <file>
git commit -m "Started work on a new feature"
# Repeat

注意:所有上面第2.步的commit行為被記錄在new-feature這個分支上,而這個分支和master分支是完全獨立的。你可以不用擔心與此同時,其他的分支到底發生了什麼,你是工作在一個隔離的環境中。

3.當是時間返回到"official" code base時,你只需要checkout master即可。

git checkout master

這條命令使得你返回在你開始new-feature開發之前的master狀態。在master分支上,你可以將new-feature這個分支merge過來,或者重新建立一個新的完全獨立的另外一個新feature branch,或者在master分支上做一些其他的工作。

git merge:

merge是git用於將分開的歷史重新合併的方法(putting a forked history back together again). git merge命令允許你將以分支來代表的獨立的開發線整合合併到一個branch上。

需要指出的是:下面所有的命令都會merge到當前分支。而當前分支會被更新以便反映merge的結果。但是注意target branch不會做任何的改變。

用法:

git merge <branch> //將<branch>分支merge到當前分支。git會自動決定merge演算法
git merge --no-ff <branch> //將<branch>合併merge到當前分支,但是必須產生一個merge commit(即使這個merge是fast-forward merge)。
//這個--no-ff選項對於歸檔所有的merge動作很有用,否則我們可能看不清楚這些程式碼從哪裡來的。

 

探討:

一旦你在一個獨立分支下完成了feature開發,非常重要的一點是你可以將這個開發工作合入到主分支中去。依賴於你的repo的不同結構情況,git可能會有幾種不同的合併演算法來完成這個目的:

要麼是一個fast-forward merge,要麼可能是一個3-way merge.

fast-forward merge:

這種merge策略在下面這種情況下merge時應用:當從當前branch的tip到featurebracnh的tip是一個線性的路徑時。 在這種情況下,git並不會實際上去做分支的merge,git要做的僅僅是通過移動(fast forward)當前分支的tip到featurebranch的tip,這樣就實現了整合歷史(integrate the histories)。這個動作效能上就合併了histories,因為所有能通過feature branch達到的歷史commit現在都可以通過當前分支也能訪問達到了!下面這張圖說明了這個fast forward merge的過程:

然而,如果兩個分支產生了分叉(diverged),則fast-forward merge是不可能的了。當從當前分支的tip到featurebranch沒有一個線性路徑時,為了merge,git別無選擇,只能通過 3-way merge策略來實現合併。3-way merge使用一個專有的commit來將兩條歷史(histories)結合起來。這個3-way merge術語來源於以下的事實:git使用三個commit來產生這個merge commit: 兩個branch tips commit + 兩個branch他們共同的祖先commit

你雖然可以使用上面兩種merge策略中的任何一種,但是一般性的最佳實踐是:

對於小的feature或者bug fix,往往使用fast-forward merge策略(通過merge前做rebase動作來保證f-f merge這一點);對於longer-running feature的整合合並則使用3-way merge策略。對於後者來說,那個merge commit將作為合併兩條分支的符號。

解決衝突:

如果你要merge的兩條分支都對同一個檔案的同一個部分做了修改,git無法得知應該使用哪個版本,這時git將會在merge commit之前停止下來,以便你手工解決這些衝突。

在衝突解決過程中,git merge過程依然使用你已經熟悉的edit/stage/commit工作流來解決衝突。當你碰到merge conflict時,使用git status命令來檢視哪些檔案需要解決衝突。例如,如果兩個branch都對hello.py做了修改,你可能看到下面的資訊:

# On branch master
# Unmerged paths:
# (use "git add/rm ..." as appropriate to mark resolution)
#
# both modified: hello.py
#

然後你需要手工解決這些衝突,隨後你執行git add hello.py來告訴git,你已經完成了衝突解決。然後git commit來產生這個merge commit。

merge具體例項

fast-forward merge

git checkout -b new-feature master
#edit some files
git add <file>
git commit -m "結束了new-feature"
git checkout master
git merge --no-ff new-feature  //依然產生一個merge動作以便檢查歷史
git branch -d new-feature //這時可以直接刪除掉new-feature

上面的這個例子對於short-lived topic branch是非常常見的workflow,這種情況下branch更多是作為一個隔離開發的工具,而不是組織長期存在feature的開發協調工具。

需要指出的是:git 在branch -d時並不會complain因為new-feature現在完全可以通過master branch來遍歷歷史。但是為了在歷史log中儲存這些短期的資訊,我們可以使用--no-ff這個引數,以便即使在這種fast-forward merge情況下依然能有資訊可以回溯到歷史上的這個短暫branch資訊。

3-way merge

下面這個例子也非常類似,但是我們需要一個3-way merge策略,因為master分支在new-feature分支前進時也有了前進。這對於大型feature或者有多個開發人員同時工作的專案更加普遍。

git checkout -b new-feature master //在master分支tip處建立new-feature分支用以記錄該feature開發歷史
# edit some files
git add <file>
git commit-m "start the new-feature"
# edit some files
git add <file>
git commit-m "Finish the new-feature"

#Develop the master branch
git checkout master
#edit some file on master branch
git add <file>
git commit -m"make some super-stable changes to master"

//#merge in the new-feature branch with 3-way merge
git merge new-feature
git branch -d new-feature

注意在這種場景下,git是不可能執行一個fast-forward merge的,因為無法在不回退的前提下,master頭直接能夠移動到new-feature分支的頭,也就是說master和new-feature發生了diverged分散。

在這種情況下,大多數workflow中,new-feature會是一個相當大的feature,也會耗費比較長的時間來開發,這也是為什麼同時在master上可能也會向前進的原因(比如其他小feature的成熟合入)。如果你的feature branch實際上是非常小的話,那麼你可能折中地通過rebase到master上,然後做一個fast forward merge.這種模式可以阻止一些不必要的merge commits從而汙染我們的專案歷史。

 

相關文章