現在讓我們來看一個簡單的分支與合併的例子,實際工作中大體也會用到這樣的工作流程:
- 開發某個網站。
- 爲實現某個新的需求,創建一個分支。
- 在這個分支上開展工作。
假設此時,你突然接到一個電話說有個很嚴重的問題需要緊急修補,那麼可以按照下面的方式處理:
- 返回到原先已經發布到生產服務器上的分支。
- 爲這次緊急修補建立一個新分支,並在其中修復問題。
- 通過測試後,回到生產服務器所在的分支,將修補分支合併進來,然後再推送到生產服務器上。
- 切換到之前實現新需求的分支,繼續工作。
分支的新建與切換
首先,我們假設你正在項目中愉快地工作,並且已經提交了幾次更新(見圖 3-10)。
圖 3-10. 一個簡短的提交歷史
現在,你決定要修補問題追蹤系統上的 #53 問題。順帶說明下,Git 並不同任何特定的問題追蹤系統打交道。這裏爲了說明要解決的問題,才把新建的分支取名爲 iss53。要新建並切換到該分支,運行 git checkout
並加上 -b
參數:
$ git checkout -b iss53
Switched to a new branch 'iss53'
這相當於執行下面這兩條命令:
$ git branch iss53
$ git checkout iss53
圖 3-11 示意該命令的執行結果。
圖 3-11. 創建了一個新分支的指針
接着你開始嘗試修復問題,在提交了若干次更新後,iss53
分支的指針也會隨着向前推進,因爲它就是當前分支(換句話說,當前的 HEAD
指針正指向 iss53
,見圖 3-12):
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
圖 3-12. iss53 分支隨工作進展向前推進
現在你就接到了那個網站問題的緊急電話,需要馬上修補。有了 Git ,我們就不需要同時發佈這個補丁和 iss53
裏作出的修改,也不需要在創建和發佈該補丁到服務器之前花費大力氣來複原這些修改。唯一需要的僅僅是切換回 master
分支。
不過在此之前,留心你的暫存區或者工作目錄裏,那些還沒有提交的修改,它會和你即將檢出的分支產生衝突從而阻止 Git 爲你切換分支。切換分支的時候最好保持一個清潔的工作區域。稍後會介紹幾個繞過這種問題的辦法(分別叫做 stashing 和 commit amending)。目前已經提交了所有的修改,所以接下來可以正常轉換到 master
分支:
$ git checkout master
Switched to branch 'master'
此時工作目錄中的內容和你在解決問題 #53 之前一模一樣,你可以集中精力進行緊急修補。這一點值得牢記:Git 會把工作目錄的內容恢復爲檢出某分支時它所指向的那個提交對象的快照。它會自動添加、刪除和修改文件以確保目錄的內容和你當時提交時完全一樣。
接下來,你得進行緊急修補。我們創建一個緊急修補分支 hotfix
來開展工作,直到搞定(見圖 3-13):
$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 3a0874c] fixed the broken email address
1 files changed, 1 deletion(-)
圖 3-13. hotfix 分支是從 master 分支所在點分化出來的
有必要作些測試,確保修補是成功的,然後回到 master
分支並把它合併進來,然後發佈到生產服務器。用 git merge
命令來進行合併:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
README | 1 -
1 file changed, 1 deletion(-)
請注意,合併時出現了“Fast forward”的提示。由於當前 master
分支所在的提交對象是要併入的 hotfix
分支的直接上游,Git 只需把 master
分支指針直接右移。換句話說,如果順着一個分支走下去可以到達另一個分支的話,那麼 Git 在合併兩者時,只會簡單地把指針右移,因爲這種單線的歷史分支不存在任何需要解決的分歧,所以這種合併過程可以稱爲快進(Fast forward)。
現在最新的修改已經在當前 master
分支所指向的提交對象中了,可以部署到生產服務器上去了(見圖 3-14)。
圖 3-14. 合併之後,master 分支和 hotfix 分支指向同一位置。
在那個超級重要的修補發佈以後,你想要回到被打擾之前的工作。由於當前 hotfix
分支和 master
都指向相同的提交對象,所以 hotfix
已經完成了歷史使命,可以刪掉了。使用 git branch
的 -d
選項執行刪除操作:
$ git branch -d hotfix
Deleted branch hotfix (was 3a0874c).
現在回到之前未完成的 #53 問題修復分支上繼續工作(圖 3-15):
$ git checkout iss53
Switched to branch 'iss53'
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)
圖 3-15. iss53 分支可以不受影響繼續推進。
值得注意的是之前 hotfix
分支的修改內容尚未包含到 iss53
中來。如果需要納入此次修補,可以用 git merge master
把 master 分支合併到 iss53
;或者等 iss53
完成之後,再將 iss53
分支中的更新併入 master
。
分支的合併
在問題 #53 相關的工作完成之後,可以合併回 master
分支。實際操作同前面合併 hotfix
分支差不多,只需回到 master
分支,運行 git merge
命令指定要合併進來的分支:
$ git checkout master
$ git merge iss53
Auto-merging README
Merge made by the 'recursive' strategy.
README | 1 +
1 file changed, 1 insertion(+)
請注意,這次合併操作的底層實現,並不同於之前 hotfix
的併入方式。因爲這次你的開發歷史是從更早的地方開始分叉的。由於當前 master
分支所指向的提交對象(C4)並不是 iss53
分支的直接祖先,Git 不得不進行一些額外處理。就此例而言,Git 會用兩個分支的末端(C4 和 C5)以及它們的共同祖先(C2)進行一次簡單的三方合併計算。圖 3-16 用紅框標出了 Git 用於合併的三個提交對象:
圖 3-16. Git 爲分支合併自動識別出最佳的同源合併點。
這次,Git 沒有簡單地把分支指針右移,而是對三方合併後的結果重新做一個新的快照,並自動創建一個指向它的提交對象(C6)(見圖 3-17)。這個提交對象比較特殊,它有兩個祖先(C4 和 C5)。
值得一提的是 Git 可以自己裁決哪個共同祖先纔是最佳合併基礎;這和 CVS 或 Subversion(1.5 以後的版本)不同,它們需要開發者手工指定合併基礎。所以此特性讓 Git 的合併操作比其他系統都要簡單不少。
圖 3-17. Git 自動創建了一個包含了合併結果的提交對象。
既然之前的工作成果已經合併到 master
了,那麼 iss53
也就沒用了。你可以就此刪除它,並在問題追蹤系統裏關閉該問題。
$ git branch -d iss53
解決衝突
人生不如意之事十之八九,合併分支往往也不是一帆風順的。
準備新的feature1
分支,繼續我們的新分支開發:
$ git checkout -b feature1
Switched to a new branch 'feature1'
修改readme.txt
最後一行,改爲:
Creating a new branch is quick AND simple.
在feature1
分支上提交:
$ git add readme.txt
$ git commit -m "AND simple"
[feature1 14096d0] AND simple
1 file changed, 1 insertion(+), 1 deletion(-)
切換到master
分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Git還會自動提示我們當前master
分支比遠程的master
分支要超前1個提交。
在master
分支上把readme.txt
文件的最後一行改爲:
Creating a new branch is quick & simple.
提交:
$ git add readme.txt
$ git commit -m "& simple"
[master 5dc6824] & simple
1 file changed, 1 insertion(+), 1 deletion(-)
現在,master
分支和feature1
分支各自都分別有新的提交,變成了這樣:
這種情況下,Git無法執行“快速合併”,只能試圖把各自的修改合併起來,但這種合併就可能會有衝突,我們試試看:
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
果然衝突了!Git告訴我們,readme.txt
文件存在衝突,必須手動解決衝突後再提交。git status
也可以告訴我們衝突的文件:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")
我們可以直接查看readme.txt的內容:
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
Git用<<<<<<<
,=======
,>>>>>>>
標記出不同分支的內容,我們修改如下後保存:
Creating a new branch is quick and simple.
再提交:
$ git add readme.txt
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed
現在,master
分支和feature1
分支變成了下圖所示:
用帶參數的git log
也可以看到分支的合併情況:
$ git log --graph --pretty=oneline --abbrev-commit
* cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file
最後,刪除feature1
分支:
$ git branch -d feature1
Deleted branch feature1 (was 14096d0).
工作完成。