分支意味着偏離開發主線並繼續你自己的工作而不影響主線開發。Git的分支模型被稱為是Git的“殺手鐧特性”,而這也使得Git在眾多版本系統中,脫穎而出。
3.1 分支機制簡述
首先,我們應該深刻理解一些Git存儲數據的原理,當你發起一次提交,Git存儲的是提交對象,其中包含了指向暫存區快照的指針。提交對象也包含作者姓名和郵箱地址、已輸入的提交信息以及指向其父提交的指針。初始提交沒有父提交,而一般的提交會有一個父提交;對於兩個或者更多分支的和並提交來說,存在着多個父提交。
Git的分支只不過是一個指向某次提交的輕量級的可移動指針。Git默認的分支名稱是master。當你發起提交時,就有了一個指向最后一次提交的master分支。每次提交時,都會自動向前移動。(Git中的master分支並不是一個特殊的分支,它與其他分支並沒有什么區別,幾乎每個Git倉庫都有該分支,這只是因為git init命令會默認創建該分支,而大多數人都懶得去更改它)。
3.1.1 創建新分支
可以通過git branch來實現:
$ git branch testing
這會創建一個指向當前提交的新指針。
Git如何知道你當前處於哪一個分支上呢?Git實際上維持着一個名為HEAD的特殊指針。HEAD指向當前所在的本地分支的指針。在上述操作后,你仍然可以處在master分支上,因為這是git branch命令只會創建新分支,而不會切換到新的分支上去。
可以簡單的通過git log命令來查看各個分支當前所指的對象,這需要用到選項--decorate
$ git log --oneline --decorate
3.1.2 切換分支
要切換已有的分支,可以執行git checkout命令,現在讓我們切換到testing分支上。
$ git checkout testing
這條命令會改變HEAD指針,使其指向testing分支。
然后我們再提交一次。
$ vim test.rb
$ git commit -a -m 'made a change'
現在testing分支已經向前移動,然而master分支仍然指向之前執行git checkout切換分支時所在的提交,讓我們再切換到master分支:
$ git checkout master
這條命令一共做了兩件事。它把HEAD指針移回到master分支,還會把工作目錄的文件恢復到master分支指向的快照的狀態。這就意味着,你從現在開始所作的修改將基於項目較老的版本。總而言之,上述操作回滾了你在testing分支上所做的操作,是你能向另一個方向進行開發工作。
注意:分支切換會更改工作目錄文件,如果你切換到較舊的分支,工作目錄會被恢復到該分支上最后一次提交的狀態。如果Git在當前狀態下無法干凈地完成恢復操作,就不會允許你切換分支。
然后我們在當前分支再做出一些改動之后,項目的歷史就會產生分叉,你在新創建的分支上做了一些修改,然后在主分支上又做了一些修改,這兩次修改是在不同的分支上做出的,彼此互相分離。你可以在分支間自由切換。
3.2 基本的分支與合並操作
現在我們展示一個簡單的分支和合並案例。
- 在開發某個項目;
- 為新需求創建分支;
- 在新分支上展開工作;
這是,你接到電話,說項目有個嚴重的問題需要緊急修復,你隨后會這樣做:
- 切換到你的生產環境分支;
- 創建新的分支來進行此問題的熱修補工作;
- 通過測試后,合並熱修補分支並推送到生產環境中;
- 切換回之前的需求分支上繼續工作。
3.2.1 基本的分支操作
首先,假設你所在的工作的項目已經完成了一些提交。
這時,你決定要修復公司所用的問題跟蹤系統中的#53問題。可以使用帶有-b選項的git checkout命令來創建並切換到新分支上:
$ git checkout -b iss53
上面這條命令相當於:
$ git branch iss53
$ git checkout iss53
接下來繼續工作,並又進行了幾次提交。這么做會讓iss53分支指針向前移動,這時因為你當前所在的就是iss53分支(即HEAD指針當前就指向這條分支);
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
現在,你接到一個電話,說網站有個問題需要立即修復。如果沒有Git,你可能需要花費大量的精力去恢復之前針對iss53所作的工作,方便后續讓你制作的修復補丁單獨上線,而現在,你只需要切換回master分支即可。
首先,在切換分支之前,需要注意,如果你的工作目錄或者暫存區存在着未提交的更改,並且這些更改與你要切換到的分支沖突,Git就不允許你切換分支。在切換分支時,最好是保持一個干凈的工作區域。
假如現在你已經提交了所有的修改,這樣你就可以切換回master分支了:
$ git checkout master
此時項目的工作目錄就與你開始處理#53問題之前的狀態一模一樣了,你就可以集中精力制作熱補丁了。這里有一點要強調:當你切換分支時,Git會把工作目錄恢復到你切換到的分支上最后一次提交時的狀態。
接下來需要制作熱補丁。我們創建hotfix分支並在這個分支上展開修復工作:
$ git checkout -b hotfix
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
你可以運行測試來確保熱補丁的效果無誤,然后將其合並到master分支,以便部署到生產環境。使用git merge命令來完成上述操作:
$ git checkout master
$ git merge hotfix
你會注意到合並時出現了“fast-forward"的提示。由於當前所在的master分支所指向的提交是要並入的hotfix分支的直接上游,因而Git會將master分支指針向前移動。換句話說,當你試圖去合並兩個不同的提交,而順着其中一個提交的歷史可以直接到達另一個提交時,Git就會簡化合並操作,直接把分支指針向前移動,因為這種單線歷史不存在有分歧的工作。這就叫做”fast-forward“。
現在你的變更已經進入了master分支所指向的提交快照,可以部署補丁了。
在部署了這次極其重要的補丁之后,你准備要切換回之前被打斷的工作上去。不過先別急,首先你要把已經用不着的hotfix分支刪除,該分支和master分支指向的位置相同。使用git branch的-d選項來刪除這個分支:
$ git branch -d hotfix
現在你可以切換回之前未完成的#53問題分支,並且繼續進行工作:
$ git checkout iss53
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
值得注意的是,iss53分支並不包括你在hotfix分支上所作的工作。如果需要上述修補工作並入iss53,就需要執行git merge master使得master分支合並到iss53中,或者可以等到要把iss53合並回master分支時再把熱修補的工作整合進來。
3.2.2 基本的合並操作
假設現在#53的工作已經完工,可以合並回master分支了。這次合並操作實現起來與之前合並hotfix分支的操作差不多。只需要切換到master分支上,並執行git merge命令即可。
$ git checkout master
$ git merge iss53
這次合並看起來與之前hotfix的合並有點不一樣。在這次合並中,開發歷史從某個早先的時間點開始有了分叉。由於當時master分支指向的提交(C4)並不是iss53分支的直接祖先,因為Git必須要做一些額外的工作。本例中,Git執行的操作時簡單的三方合並。三方合並操作會使用兩個待合並分支上最新提交的快照,以及這兩個分支的共同祖先的提交快照(如下圖所示)。
與之前的簡單地向前移動分支指針地做法不同,這次Git會基於三方合並的結果創造新的快照,然后在創建一個提交指向新建的快照。這個操作叫做”合並提交“。合並提交的特殊性在於它不止擁有一個父提交。
值得注意的是,Git會自己判斷最優的共同祖先並將其作為合並基礎。
現在你的工作成果已經合並進來了,你就不需要iss53分支了。你可以在問題追蹤系統里面關閉這個問題並刪除分支。
$ git branch -d iss53
3.2.3 基本的合並沖突處理
有時候,上述合並過程並不會那么順利。如果你要在合並的兩個分支上都改了同一個文件的同一部分內容,Git就沒辦法干凈地合並這兩個分支。假設你在#53問題上的工作和在hotfix分支上的工作都修改了同一文件的同一部分,那么就會引起合並沖突,你會看到類似下面的輸出:
$ git merge iss53
Auto-merge index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
Git並沒有自動創建新的合並提交。它會暫停整個合並過程,等待你來解決沖突。在發生了合並沖突后,要查看哪些文件沒有被合並,可以執行git status:
$ 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會給這些有沖突的文件添加標准的待解決沖突標記,以便你手動打開這些文件來解決沖突。
你可以選擇任一版本的內容或者是自己整合兩者的內容來解決沖突。這種解決方式實際上是把兩個版本的內容各取一部分整合到一起。在解決了每個沖突文件的所有沖突部分后,就可以執行git add來把每個文件標記為沖突已解決狀態。在Git中,這可以通過把文件添加到暫存區來實現。
若要使用圖形化工具解決沖突,可以執行git mergetool, 該命令會啟動相應的圖形化合並工具,並引導你一步步解決沖突。當退出合並工具時,Git會詢問合並是否已經成功完成。如果合並成功完成,它就會將合並后的文件添加到暫存區,並將其標記為沖突已解決的狀態。可以再次執行git status 來確認所有的沖突都已解決。
如果覺得滿意了,並確認了所有沖突都已解決,相應的文件也進入了暫存區,就可以通過git commit命令來完成此次合並提交。
如果想給審批此次合並的人一點提示,那么可以修改上述的合並信息,提供更多的關於你如何進行此次合並的細節,比如你做了什么,你為什么要這樣做。
3.3 分支管理
現在,我們來試試一些分支管理工具,這些工具在經常使用分支時會很有用。
git branch命令不只是可以用來創建和刪除分支。如果你執行不帶參數的git branch命令,就會得到當前所有分支的簡短列表,如下所示:
$ git branch iss53 * master testing
請留意master前面的*字符,它表明了你當前所在的分支(即HEAD指向的分支)。這意味着如果你現在進行一次提交,master分支指針會隨着你的新提交向前移動。要看到每個分支上的最新提交,可以執行git branch -v。
另外兩個有用的選項:--merged是篩選已並入當前分支的所有分支;
--no-merged,篩選尚未並入的所有分支。
要查看哪些分支已經並入當前分支,可以執行git branch --merged:
$ git branch --merged
iss53
* master
由於之前iss53已被合並,因此它出現在了上述列表中。一般來說,對於前面沒有*的分支,可以使用git branch -d把它們全部刪除。你已經把這些分支上的工作全部納入到了其他分支中,所以不會因此而丟失任何東西。
要查看尚未合並的工作的所有分支,可以使用git branch --no-merged:
$ git branch --no-merged testing
上述命令會顯示出另一個分支。因為該分支包含了尚未合並到主線的工作,所以git branch -d並不能成功刪除它:
$ git branch
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.
如果你確實想要刪除該分支並丟棄其上的所有工作,可以按照上述輸出的提示信息使用-D選項強制刪除。
3.4 與分支有關的工作流
現在我們講一些簡單的工作流。他們之所以存在,得益於Git的輕量級分支機制。你可以根據自己項目的實際情況來自由選用他們。
3.4.1 長期分支
由於Git簡潔的三方合並機制,在較長的一段時間內多次把一個分支合並到另一個分支是很容易的操作。這意味着你可以擁有多個開放的分支,以用於開發周期的不同階段;你也可以經常性地把其中某些分支合並到其他的分支中去。
很多使用Git的開發者都喜歡用這種方式構建他們自己的工作流。例如,其中一種流程就是在master分支只存放穩定版本的代碼,即已經發布版本或即將發布版本的代碼。他們還會使用另一個叫做develop或next的平行分支用於開發,或是用於測試代碼的穩定性。這個分支不會一直保持穩定版本,不過一旦它達到穩定版本的狀態,就可以把它合並到master分支去。這樣的分支也被用來接受主題分支(短期分支,例如之前的iss53分支)的合並,來確保這些新開發的特性能夠通過所有的測試而不會引發新的錯誤。
實際上,我們剛才談論的是隨着你的提交操作而不斷移動的分支指針。穩定的分支會在提交歷史中較為靠后,而前沿的開發分支會較為靠前。
可以把這些分支認為是不同的工作簡倉(work silo),幾組提交經過完整的測試后,就會從一個簡倉移動到另一個更穩定的簡倉中去。
可以按照上述 方式構建幾個不同穩定級別的分支。有些大型項目有名為proposed(提議)或pu(proposed updates,提議的更新)的分支。這個分支會整合那些還沒有准備好並入next或master的分支。這么做背后的緣由是不同的分支擁有不同程度的穩定性。當分支達到更高的穩定程度時,它就被合並到更高級別的分支中去。所以,雖然擁有多個長期分支並非必須,但這樣很實用,特別是當你開發大型項目或復雜項目時更是如此。
3.4.2 主題分支
與上述長期分支有所不同,在任何規模的項目上主題分支(topic branch)都非常有用。主題分支是指短期的、用於實現某一特定功能及其相關工作的分支...
待續,