Pro Git 3.2 基本的分支與合併

出自DILA Wiki

現在讓我們來看一個簡單的分支與合併(merge)的例子,實際工作中大體也會用到這樣的工作流程:

  1. 開發某個網站。
  2. 為實現某個新的需求,創建一個分支。
  3. 在這個分支上開展工作。

假設此時,你突然接到一個電話說有個很嚴重的問題需要緊急修補,那麼可以按照下面的方式處理:

  1. 返回到原先已經發佈到生產伺服器上的分支。
  2. 為這次緊急修補建立一個新分支。
  3. 測試通過後,將此修補分支合併,再推送到生產伺服器上。
  4. 切換到之前實現新需求的分支,繼續工作。

基本分支

首先,我們假設你正在專案中愉快地工作,並且已經提交了幾次更新(見圖 3-10)。

Pro-git-3-10.png
圖 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 示意該命令的結果。

Pro-git-3-11.png
圖 3-11. 創建了一個新的分支指標

接下來,你在網站專案上繼續工作並作了一次提交。這會使 iss53 分支的指標隨著提交向前推進,因為它處於檢出狀態(或者說,你的 HEAD 指標目前正指向它,見圖3-12):

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'

Pro-git-3-12.png
圖 3-12. iss53 分支隨工作進展向前推進

現在你就接到了那個網站問題的緊急電話,需要馬上修補。有了 Git ,我們就不需要同時發佈這個補丁和 iss53 裡作出的修改,也不需要在創建和發佈該補丁到伺服器之前花費很多努力來復原這些修改。唯一需要的僅僅是切換回 master 分支。

不過在此之前,留心你的暫存區或者工作目錄裡,那些還沒有提交的修改,它會和你即將檢出(check out)的分支產生衝突從而阻止 Git 為你轉換分支。轉換分支的時候最好保持一個清潔的工作區域。稍後會介紹幾個繞過這種問題的辦法(分別叫做 stashing 和 amending)。目前已經提交了所有的修改,所以接下來可以正常轉換到 master 分支:

$ git checkout master
Switched to branch "master"

此時工作目錄中的內容和你在解決問題 #53 之前一模一樣,你可以集中精力進行緊急修補。這一點值得牢記:Git 會把工作目錄的內容恢復為檢出某分支時它所指向的那個 commit 的快照。它會自動添加、刪除和修改檔案以確保目錄的內容和你上次提交時完全一樣。

接下來,你得進行緊急修補。我們創建一個緊急修補分支(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]: created 3a0874c: "fixed the broken email address"
 1 files changed, 0 insertions(+), 1 deletions(-)

Pro-git-3-13.png
圖 3-13. hotfix 分支是從 master 分支所在點分化出來的

有必要作些測試,確保修補是成功的,然後把它合併到 master 分支並發佈到生產伺服器。用 git merge 命令來進行合併:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast forward
 README |    1 -
 1 files changed, 0 insertions(+), 1 deletions(-)

請注意,合併時出現了 “Fast forward”(快進)提示。由於當前 master 分支所在的 commit 是要併入的 hotfix 分支的直接上游,Git 只需把指針直接右移。換句話說,如果順著一個分支走下去可以到達另一個分支,那麼 Git 在合併兩者時,只會簡單地把指標前移,因為沒有什麼分歧需要解決,所以這個過程叫做快進(Fast forward)。

現在的目錄變為當前 master 分支指向的 commit 所對應的快照,可以發佈了(見圖 3-14)。

Pro-git-3-14.png
圖 3-14. 合併之後,master 分支和 hotfix 分支指向同一位置。

在那個超級重要的修補發佈以後,你想要回到被打擾之前的工作。因為現在 hotfix 分支和 master 指向相同的提交,現在沒什麼用了,可以先刪掉它。使用 git branch 的 -d 選項表示刪除:

$ git branch -d hotfix
Deleted branch hotfix (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]: created ad82d7a: "finished the new footer [issue 53]"
 1 files changed, 1 insertions(+), 0 deletions(-)

Pro-git-3-15.png
圖 3-15. iss53 分支可以不受影響繼續推進。

不用擔心 hotfix 分支的內容還沒包含在 iss53 中。如果確實需要納入此次修補,可以用 git merge master 把 master 分支合併到 iss53,或者等完成後,再將 iss53 分支中的更新併入 master。

基本合併

在問題 #53 相關的工作完成之後,可以合併回 master 分支,實際操作同前面合併 hotfix 分支差不多,只需檢出想要更新的分支(master),並運行 git merge 命令指定來源:

$ git checkout master
$ git merge iss53
Merge made by recursive.
 README |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

請注意,這次合併的實現,並不同於之前 hotfix 的併入方式。這一次,你的開發歷史是從更早的地方開始分叉的。由於當前 master 分支所指向的 commit (C4)並非想要併入分支(iss53)的直接祖先,Git 不得不進行一些處理。就此例而言,Git 會用兩個分支的末端(C4 和 C5)和它們的共同祖先(C2)進行一次簡單的三方合併計算。圖 3-16 標出了 Git 在用於合併的三個更新快照:

Pro-git-3-16.png
圖 3-16. Git 為分支合併自動識別出最佳的同源合併點。

Git 沒有簡單地把分支指標右移,而是對三方合併的結果作一新的快照,並自動創建一個指向它的 commit(C6)(見圖 3-17)。我們把這個特殊的 commit 稱作合併提交(merge commit),因為它的祖先不止一個。

值得一提的是 Git 可以自己裁決哪個共同祖先才是最佳合併基礎;這和 CVS 或 Subversion(1.5 以後的版本)不同,它們需要開發者手工指定合併基礎。所以此特性讓 Git 的合併操作比其他系統都要簡單不少。

Pro-git-3-17.png
圖 3-17. Git 自動創建了一個包含了合併結果的 commit 物件。

既然你的工作成果已經合併了,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 查閱:

[master*]$ git status
index.html: needs merge
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	unmerged:   index.html
#

任何包含未解決衝突的檔案都會以未合併(unmerged)狀態列出。Git 會在有衝突的檔案裡加入標準的衝突解決標記,可以通過它們來手工定位並解決這些衝突。可以看到此檔包含類似下面這樣的部分:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
  please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

可以看到 ======= 隔開的上半部分,是 HEAD(即 master 分支,在運行 merge 命令時檢出的分支)中的內容,下半部分是在 iss53 分支中的內容。解決衝突的辦法無非是二者選其一或者由你親自整合到一起。比如你可以通過把這段內容替換為下面這樣來解決:

<div id="footer">
please contact us at email.support@github.com
</div>

這個解決方案各採納了兩個分支中的一部分內容,而且我還刪除了 <<<<<<<,=======,和>>>>>>> 這些行。在解決了所有檔案裡的所有衝突後,執行 git add 將把它們標記為已解決(resolved)。因為一旦暫存,就表示衝突已經解決。如果你想用一個有圖形介面的工具來解決這些問題,不妨執行 git mergetool,它會調用一個視覺化的合併工具並引導你解決所有衝突:

$ git mergetool
merge tool candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff
Merging the files: index.html

Normal merge conflict for 'index.html':
  {local}: modified
  {remote}: modified
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.
#

如果想給將來看這次合併的人一些方便,可以修改該資訊,提供更多合併細節。比如你都作了哪些改動,以及這麼做的原因。有時候裁決衝突的理由並不直接或明顯,有必要略加注解。