現在讓我們來看一個簡單的分支與合并的例子,實際工作中大體也會用到這樣的工作流程:
1. 開發某個網站。
2. 為實現某個新的需求,創建一個分支。
3. 在這個分支上開展工作。
假設此時,你突然接到一個電話說有個很嚴重的問題需要緊急修補,那么可以按照下面的方式處理:
1. 返回到原先已經發布到生產服務器上的分支。
2. 為這次緊急修補建立一個新分支,并在其中修復問題。
3. 通過測試后,回到生產服務器所在的分支,將修補分支合并進來,然后再推送到生產服務器上。
4. 切換到之前實現新需求的分支,繼續工作。
## 分支的新建與切換
首先,我們假設你正在項目中愉快地工作,并且已經提交了幾次更新(見圖 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`
## 遇到沖突時的分支合并
有時候合并操作并不會如此順利。如果在不同的分支中都修改了同一個文件的同一部分,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 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")
~~~
任何包含未解決沖突的文件都會以未合并(unmerged)的狀態列出。Git 會在有沖突的文件里加入標準的沖突解決標記,可以通過它們來手工定位并解決這些沖突。可以看到此文件包含類似下面這樣的部分:
~~~
<<<<<<< HEAD
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53
~~~
可以看到` ======= `隔開的上半部分,是 HEAD(即 master 分支,在運行 merge 命令時所切換到的分支)中的內容,下半部分是在 iss53 分支中的內容。解決沖突的辦法無非是二者選其一或者由你親自整合到一起。比如你可以通過把這段內容替換為下面這樣來解決:
~~~
<div id="footer">
please contact us at email.support@github.com
</div>
~~~
這個解決方案各采納了兩個分支中的一部分內容,而且我還刪除了 `<<<<<<<,=======` 和` >>>>>>>` 這些行。在解決了所有文件里的所有沖突后,運行 `git add `將把它們標記為已解決狀態(譯注:實際上就是來一次快照保存到暫存區域。)。因為一旦暫存,就表示沖突已經解決。如果你想用一個有圖形界面的工具來解決這些問題,不妨運行 `git mergetool`,它會調用一個可視化的合并工具并引導你解決所有沖突:
~~~
$ git mergetool
This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html
Normal merge conflict for 'index.html':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (opendiff):
~~~
如果不想用默認的合并工具(Git 為我默認選擇了 opendiff,因為我在 Mac 上運行了該命令),你可以在上方"`merge tool candidates`"里找到可用的合并工具列表,輸入你想用的工具名。我們將在第七章討論怎樣改變環境中的默認值。
退出合并工具以后,Git 會詢問你合并是否成功。如果回答是,它會為你把相關文件暫存起來,以表明狀態為已解決。
再運行一次 `git status` 來確認所有沖突都已解決:
~~~
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
~~~
如果覺得滿意了,并且確認所有沖突都已解決,也就是進入了暫存區,就可以用 `git commit` 來完成這次合并提交。提交的記錄差不多是這樣:
~~~
Merge branch 'iss53'
Conflicts:
index.html
It looks like you may be committing a merge.
If this is not correct, please remove the file
.git/MERGE_HEAD
and try again.
~~~
如果想給將來看這次合并的人一些方便,可以修改該信息,提供更多合并細節。比如你都作了哪些改動,以及這么做的原因。有時候裁決沖突的理由并不直接或明顯,有必要略加注解。
- 1. 起步
- 1.1 關于版本控制
- 1.2 Git 簡史
- 1.3 Git 基礎
- 1.4 安裝 Git
- 1.5 初次運行 Git 前的配置
- 1.6 獲取幫助
- 1.7 小結
- 2. Git基礎
- 2.1 取得項目的 Git 倉庫
- 2.2 記錄每次更新到倉庫
- 2.3 查看提交歷史
- 2.4 撤消操作
- 2.5 遠程倉庫的使用
- 2.6 打標簽
- 2.7 技巧和竅門
- 2.8 小結
- 3. Git分支
- 3.1 何謂分支
- 3.2 分支的新建與合并
- 3.3 分支的管理
- 3.4 利用分支進行開發的工作流程
- 3.5 遠程分支
- 3.6 分支的衍合
- 3.7 小結
- 4. 服務器上的Git
- 4.1 協議
- 4.2 在服務器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設服務器
- 4.5 公共訪問
- 4.6 GitWeb
- 4.7 Gitosis
- 4.8 Gitolite
- 4.9 Git 守護進程
- 4.10 Git 托管服務
- 4.11 小結
- 5. 分布式Git
- 5.1 分布式工作流程
- 5.2 為項目作貢獻
- 5.3 項目的管理
- 5.4 小結
- 6. Git工具
- 6.1 修訂版本(Revision)選擇
- 6.2 交互式暫存
- 6.3 儲藏(Stashing)
- 6.4 重寫歷史
- 6.5 使用 Git 調試
- 6.6 子模塊
- 6.7 子樹合并
- 6.8 總結
- 7. 自定義Git
- 7.1 配置 Git
- 7.2 Git屬性
- 7.3 Git掛鉤
- 7.4 Git 強制策略實例
- 7.5 總結
- 8. Git與其他系統
- 8.1 Git 與 Subversion
- 8.2 遷移到 Git
- 8.3 總結
- 9. Git 內部原理
- 9.2 Git 對象
- 9.3 Git References
- 9.4 Packfiles
- 9.5 The Refspec
- 9.6 傳輸協議
- 9.7 維護及數據恢復
- 9.8 總結
- 9.1 底層命令 (Plumbing) 和高層命令 (Porcelain)