Git 全景課 / 第二部分 分支與歷史 · M5

M5 合併與衝突

分支讓大家各自工作;合併把工作收回來。本單元分清楚兩種合併,看懂 Git 怎樣逐行決定結果,並學會冷靜處理衝突:衝突不是錯誤,只是 Git 請你做一個它無法替你做的決定。
約 75 分鐘沙盒 6 個任務 · 互動 ×2 · 小測 4 題先修:M4
學完本單元,你能夠:
  1. 分辨 fast-forward 和三方合併,並預測 git merge 會產生哪一種;
  2. 用合併基準解釋 Git 怎樣逐行決定合併結果,以及甚麼時候會衝突;
  3. 讀懂衝突標記,按步驟解決衝突,或放棄合併;
  4. 寫出不容易衝突的文件(知識工作的寫法)。

1合併:把另一條分支的工作收進來 What merging does

git merge feature 的意思是:把 feature 的工作,合併進我現在所在的分支。方向很重要:被改變的是目前的分支(HEAD 所指的那條),feature 本身不會動。所以合併之前,先切換到「要收工作的那條分支」,通常是 main。

git switch main        # 先到要收工作的分支
git merge feature      # 把 feature 收進來

Git 會按兩條分支的關係,選擇兩種做法之一。

2fast-forward:只是把標籤往前推 Fast-forward

如果 main 在 feature 開出之後沒有新的 commit,main 就是 feature 的祖先:feature 的歷史已經包含了 main 的全部。這時 Git 不需要真正「合併」,只要把 main 這張標籤往前推到 feature 的位置。

fast-forward 合併 main 指向 B,feature 指向 D,D 在 B 之後。合併後 main 直接移到 D,沒有新 commit。 合併前 main feature git merge feature 之後 main feature main 是 feature 的祖先:沒有分岔。 main 往前推到 D;沒有新 commit,歷史仍是一直線。
圖 1. fast-forward 只移動標籤(藍色=這一步改變了的 ref)。終端機會顯示 Fast-forward。

3三方合併:用合併基準逐行決定 Three-way merge

如果兩條分支都有新的 commit(歷史分岔了),Git 就要真正合併內容。它找出三個版本:

有了基準,Git 就能判斷每一處是「誰改的」:

基準 → ours基準 → theirs結果
沒改沒改保持原樣
改了沒改用 ours 的改動
沒改改了用 theirs 的改動
改了改了,而且改得一樣用那個共同的改動
改了改了,改得不一樣衝突:Git 不知道該用哪個,交給你決定

合併成功後,Git 建立一個合併 commit:它和普通 commit 一樣是一張快照,唯一的分別是它有兩個 parent,把兩條歷史接回一起。

為甚麼需要基準 只比較 ours 和 theirs,你只知道兩者「不同」,不知道是誰改的。有了基準,Git 才知道「這一行 ours 沒動、theirs 改了」,於是放心採用 theirs。這就是「三方」的意思:ours、theirs、基準三個版本。

互動 1 預測合併結果

每一行給出基準、ours、theirs 三個版本。選出 Git 的合併結果,再按「對答案」。

4衝突:Git 請你做決定 Merge conflicts

兩邊都改了同一處、改得又不一樣時,Git 會停下來,在檔案裏用衝突標記把兩個版本並排寫出:

研究背景:AI 工具在校園普及。
<<<<<<< HEAD                 ← 從這裏開始是 ours(目前分支)
樣本數:118 人(剔除 2 份無效問卷)。
=======                      ← 分隔線
樣本數:120 人。
>>>>>>> data                 ← 到這裏為止是 theirs(被合併的分支 data)
結論:多數學生每天使用。

衝突期間,倉庫處於「合併中」狀態:.git/MERGE_HEAD 記着被合併的分支,git status 會列出 both modified 的檔案。沒有衝突的部分,Git 已經自動合併好。

「同一處」比你想像的大 Git 以「一段連續被改動的行」為單位判斷。兩邊即使改的是相鄰的兩行(例如一邊改第 2 行、另一邊改第 3 行),也算衝突;中間隔着至少一行沒改動,才能自動合併。這是用真實 Git 驗證過的行為,沙盒也照此模擬。

5沙盒:合併與解決衝突 Sandbox: merge and resolve

互動 2 合併沙盒

右上角是工作目錄裏的 report.md,按某一行旁邊的「改寫」等於在編輯器裏改那一行(也可以用命令 edit 3 新的內容)。藍色標示未提交的改動;衝突會以黃色顯示,下面附處理按鈕。圖上的編號是真實的 SHA-1;雙圈的是合併 commit。

應用線:

6解決衝突的步驟 Resolving step by step

  1. 看狀態:git status,找出 both modified 的檔案。
  2. 找標記:在編輯器裏搜尋 <<<<<<<。
  3. 做決定:保留 ours、保留 theirs、兩者結合,或者寫一個新版本。這是內容上的判斷,不確定就問寫那部分的人。
  4. 刪標記:把 <<<<<<<、=======、>>>>>>> 三行全部刪掉。
  5. 標記已解決:git add <檔案>。
  6. 完成合併:git commit(Git 已預備好說明 Merge branch '…')。

處理到一半想重來?git merge --abort 會讓一切回到合併前的樣子。

不要把衝突標記提交進去 Git 不會檢查你 add 的檔案還有沒有標記。提交前跑一次 git diff --check:它會指出殘留的衝突標記。很多編輯器(例如 VS Code 的 merge editor)會把衝突顯示成「接受目前/接受傳入/接受兩者」的按鈕,原理和沙盒的三個按鈕一樣;也可以用 git mergetool 開啟你設定的合併工具。

7知識工作:寫出不容易衝突的文件 Writing conflict-friendly prose

Git 以「行」為單位合併。程式碼天生一行一句;文章卻常常一整段寫成一行。結果是兩個人改同一段裏不同的句子,Git 也會判為衝突。

互動 3 整段一行 vs 一句一行

同一段文字,A 和 B 各改一句。切換寫法和 B 改的句子,看 Git 能不能自動合併。

寫法:
B 改:

基準

A(ours)改第 1 句

B(theirs)

合併結果

所以用 Git 管理文章時,最實用的習慣是一句一行(semantic line breaks):Markdown 顯示時會把相鄰的行接成同一段,讀者看不出分別,但合併和 diff 都會清楚得多。M12 會再談知識庫的其他做法。

8容易混淆的對照 Easily confused

fast-forward 與 --no-ff

fast-forward(預設,條件許可時)git merge --no-ff
做了甚麼只把標籤往前推即使可以 fast-forward,也建立合併 commit
歷史形狀一直線,看不出曾經有分支保留「這幾個 commit 是一起從某分支合進來的」
撤銷整批工作要逐個找出那幾個 commit撤銷那一個合併 commit 即可(M6)
常見於個人工作、小修改團隊經 PR 合併功能分支(GitHub 的「Create a merge commit」)
常見誤解
  • 「git merge feature 會把 main 合進 feature。」相反。被改變的是你現在所在的分支。
  • 「衝突代表有人做錯了。」不是。兩個人改了同一處,本來就需要一個人判斷;Git 只是誠實地說它判斷不了。
  • 「合併後 feature 分支就消失了。」不會。它仍指着原來的 commit;確認不再需要後用 git branch -d 刪除(已合併,所以不會被拒絕)。
  • 「沒有衝突就代表合併結果一定正確。」不一定。兩邊各自正確的改動合在一起,仍然可能互相矛盾(例如一邊改了函數名稱,另一邊新增了舊名稱的呼叫)。合併後要測試或重讀。

9命令速查 Cheat sheet

命令作用
git merge feature把 feature 合併進目前分支(能 fast-forward 就 fast-forward)
git merge --no-ff feature一定建立合併 commit
git merge --abort放棄進行中的合併,回到合併前
git status合併中:列出 both modified 的檔案
git add <檔案>標記該檔案的衝突已解決
git diff --check找出殘留的衝突標記
git merge-base main feature顯示兩條分支的合併基準
git log --oneline --graph --all在終端機看分岔與合併
git branch -d feature刪除已合併的分支

10小測 Quiz

1. 你在 main 上執行 git merge feature,而 main 自 feature 開出後沒有新 commit。結果是:
2. 合併基準裏某行是「樣本數:100」。ours 沒改,theirs 改成「樣本數:120」。結果是:
3. 出現衝突後,你修改好檔案並刪除了標記。下一步是:
4. A 改了段落的第 1 句,B 改了同一段的第 3 句。整段寫在同一行。合併時會怎樣?

本單元重點 Summary

延伸閱讀