章節 ▾ 第二版

3.2 Git 分支 - 基礎分支與合併

基礎分支與合併

讓我們透過一個簡單的分支與合併的例子,來演示你在實際工作中可能使用的流程。你將遵循以下步驟:

  1. 對網站進行一些工作。

  2. 為你正在處理的新使用者故事建立一個分支。

  3. 在該分支上進行一些工作。

在這個階段,你會接到一個電話,說另一個問題很緊急,需要你立即修復(hotfix)。你將執行以下操作:

  1. 切換到你的生產分支。

  2. 建立一個分支來新增熱修復。

  3. 測試通過後,合併熱修復分支,並推送到生產環境。

  4. 切換回你原來的使用者故事分支並繼續工作。

基礎分支

首先,假設你正在你的專案上工作,並且 master 分支上已經有一些提交。

A simple commit history
圖 18. 簡單的提交歷史

你決定在公司使用的任何問題跟蹤系統中處理問題 #53。要同時建立一個新分支並切換到它,你可以執行帶有 -b 選項的 git checkout 命令:

$ git checkout -b iss53
Switched to a new branch "iss53"

這相當於執行以下命令:

$ git branch iss53
$ git checkout iss53
Creating a new branch pointer
圖 19. 建立新的分支指標

你在網站上工作並進行了一些提交。這樣做會使 iss53 分支向前移動,因為你已經切換到該分支(也就是說,你的 HEAD 指向它)。

$ vim index.html
$ git commit -a -m 'Create new footer [issue 53]'
The `iss53` branch has moved forward with your work
圖 20. iss53 分支隨著你的工作向前移動

現在你接到電話說網站有問題,需要你立即修復。使用 Git,你不必將你的修復與你已做的 iss53 更改一起部署,也無需在將修復應用到生產環境之前花費大量精力來回滾這些更改。你只需切換回你的 master 分支即可。

然而,在你這樣做之前,請注意,如果你的工作目錄或暫存區有未提交的更改與你即將切換的分支衝突,Git 將不允許你切換分支。在切換分支時,最好保持工作區是乾淨的。有幾種方法可以解決這個問題(即暫存和修改提交),我們將在 暫存與清理 中稍後介紹。現在,我們假設你已經提交了所有更改,因此你可以切換回你的 master 分支:

$ git checkout master
Switched to branch 'master'

此時,你的專案工作目錄與你開始處理問題 #53 之前完全一樣,你可以專注於你的熱修復。這是一個重要的記憶點:當你切換分支時,Git 會將你的工作目錄重置為你在該分支上最後一次提交時的樣子。它會自動新增、刪除和修改檔案,以確保你的工作副本是你上次提交到該分支時的狀態。

接下來,你有一個熱修復要做。讓我們建立一個 hotfix 分支,直到完成修復為止:

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'Fix broken email address'
[hotfix 1fb7853] Fix broken email address
 1 file changed, 2 insertions(+)
Hotfix branch based on `master`
圖 21. 基於 master 的熱修復分支

你可以執行測試,確保熱修復是你想要的,最後將 hotfix 分支合併回你的 master 分支以部署到生產環境。你使用 git merge 命令來完成此操作:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

你會注意到該合併中出現“快進”(fast-forward)字樣。因為你合併的 hotfix 分支所指向的提交 C4 直接位於你當前所在的提交 C2 的前方,Git 只是簡單地將指標向前移動。換句話說,當你嘗試將一個提交與可以透過跟蹤第一個提交的歷史記錄到達的提交進行合併時,Git 透過向前移動指標來簡化操作,因為沒有發散的工作需要合併——這被稱為“快進”。

你的更改現在位於 master 分支指向的提交快照中,你可以部署修復了。

`master` is fast-forwarded to `hotfix`
圖 22. master 快進到 hotfix

在你的重要修復部署後,你就可以切換回中斷之前正在進行的工作了。然而,首先你要刪除 hotfix 分支,因為你不再需要它——master 分支指向了相同的位置。你可以使用 git branch 命令的 -d 選項來刪除它:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

現在你可以切換回你正在處理的問題 #53 分支並繼續工作。

$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'Finish the new footer [issue 53]'
[iss53 ad82d7a] Finish the new footer [issue 53]
1 file changed, 1 insertion(+)
Work continues on `iss53`
圖 23. iss53 上的工作繼續進行

值得注意的是,你在 hotfix 分支中所做的工作不包含在你 iss53 分支的檔案中。如果你需要將其合併進來,可以透過執行 git merge mastermaster 分支合併到你的 iss53 分支中,或者你可以等到稍後決定將 iss53 分支合併回 master 時再整合這些更改。

基礎合併

假設你已經決定問題 #53 的工作已完成,並準備合併到你的 master 分支。為此,你將把 iss53 分支合併到 master,就像你之前合併 hotfix 分支一樣。你所要做的就是檢出你希望合併到的分支,然後執行 git merge 命令:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

這看起來與你之前做的 hotfix 合併有些不同。在這種情況下,你的開發歷史從某個較舊的點開始分叉。因為你當前所在分支上的提交不是你正在合併的分支的直接祖先,Git 必須做一些工作。在這種情況下,Git 會執行一次簡單的三方合併,使用分支尖端指向的兩個快照以及兩者的共同祖先。

Three snapshots used in a typical merge
圖 24. 典型合併中使用的三個快照

Git 不僅僅是將分支指標向前移動,它還會建立三方合併所產生的新快照,並自動建立一個指向該快照的新提交。這被稱為合併提交(merge commit),其特殊之處在於它有多個父提交。

A merge commit
圖 25. 合併提交

現在你的工作已合併,你不再需要 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")

任何有合併衝突且尚未解決的檔案都將列為未合併。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 分支,因為這是你執行合併命令時檢出的分支)是該塊的頂部(======= 之上的所有內容),而你 iss53 分支中的版本看起來像底部所有內容。為了解決衝突,你必須選擇其中一邊或將內容自行合併。例如,你可能會透過將整個塊替換為以下內容來解決此衝突:

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

此解決方案包含每個部分的一小部分,並且 <<<<<<<=======>>>>>>> 行已被完全刪除。在你解決了每個衝突檔案中所有這些部分之後,對每個檔案執行 git add 以將其標記為已解決。暫存檔案會將其在 Git 中標記為已解決。

如果你想使用圖形工具來解決這些問題,可以執行 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 是因為命令在 macOS 上執行)之外的合併工具,你可以在“以下工具之一”之後看到頂部列出的所有支援的工具。只需輸入你想要使用的工具名稱即可。

注意

如果你需要更高階的工具來解決複雜的合併衝突,我們將在 高階合併 中介紹更多有關合並的內容。

退出合併工具後,Git 會詢問你合併是否成功。如果你告訴指令碼已成功,它會為你暫存檔案以將其標記為已解決。你可以再次執行 git status 來驗證所有衝突是否都已解決:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    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.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#	modified:   index.html
#

如果你認為這對於將來檢視此合併的其他人會有幫助,你可以修改此提交訊息,詳細說明如何解決合併,並解釋你進行這些更改的原因(如果這些原因不明顯)。

scroll-to-top