2026-06-18 · conflict · hunk · 衝突標記 · 解衝突流程 · mergetool
<<<<<<< ======= >>>>>>> 標記怎麼讀、以及解衝突的完整流程。看懂後你會發現,衝突不是錯誤,而是 Git 誠實地把選擇權交還給你。
最常見的誤解是「兩個人改了同一個檔案就會衝突」。錯。Git 比對的單位細到檔案裡的一個個區塊(hunk,可粗略想成連續的幾行),而不是整個檔案。(hunk 不是 Git 原創、其來歷與運作見 附錄 A · diff/patch 原理)回到第 5 章三方合併的邏輯:Git 拿共同祖先當基準,逐段問「相對基準,這段誰動過?」——
下圖把這兩種情況並排。同一個檔案有三段,左例兩邊動的是不同段、右例動的是同一段——結果天差地遠:
不過「不同段」這件事 Git 判得比你想的保守。以下幾種邊角,即使「邏輯上」不該算衝突,文字層面仍會被判為衝突:
| 情況 | 會不會衝突 | 為什麼 |
|---|---|---|
| 兩邊改的行緊鄰(上下文重疊) | ⚠️ 多半會 | Git 比對是看「連同上下文的整塊」,改動貼太近時兩塊重疊,寧可保守判為衝突讓你確認。 |
| 兩邊改同一行的不同部分 | ⚠️ 會 | 判定到「行」這層;即使一個人改行首、一個人改行尾,文字層面仍是同一行被兩種版本覆蓋。 |
| 一邊改內容、另一邊刪掉整檔(modify/delete) | ⚠️ 會 | Git 無法判斷「該保留你的修改,還是尊重對方的刪除」,標為特殊的 modify/delete 衝突,要你選留檔或刪檔。 |
上面提到「一邊改內容、另一邊刪掉整個檔案」(modify/delete)。這種衝突跟一般的內容衝突,解法有什麼不同?
差很多,根源在於衝突發生的層級不同。一般衝突發生在檔案內容(同一個 hunk/行),Git 在問你「這段內容該長怎樣」;modify/delete 發生在檔案存在與否,Git 在問的是「這個檔到底還要不要」。
| 一般(內容)衝突 | modify/delete 衝突 | |
|---|---|---|
| 衝突層級 | 檔案內容(hunk/行) | 檔案存在與否(整檔) |
<<< === >>> 標記 | 有,兩版並排寫進檔裡讓你拼 | 沒有(見下方說明) |
| 工作目錄狀態 | 檔案裡是夾著標記的混合體 | 檔案原樣留著修改方那一版,無標記 |
| 解法本質 | 編輯內容、挑/重寫,再 git add | 檔案層級二選一:留檔 或 刪檔 |
為什麼沒有衝突標記?一般衝突時 Git 兩邊都有「這幾行的內容」,可以塞成 <<< 我方 === 對方 >>>。但 modify/delete 是「檔案內容」對上「這檔不該存在」——兩者無法在同一個檔裡並排(你沒辦法寫「內容 vs 不存在」)。所以 Git 不放標記,而是把修改方的整版檔案留在工作目錄,改用 git status 的文字告訴你:
CONFLICT (modify/delete): config.yml deleted in feature
and modified in HEAD.
# 措辭會是 deleted by them(對方刪、你改)或 deleted by us(你刪、對方改)
解法不是編輯內容,而是下決定。因為沒有標記可清,你用指令表態「要哪一邊」:
git add config.yml # 採用「修改方」→ 保留這個檔(就用留著的那版)
git rm config.yml # 採用「刪除方」→ 真的把它刪掉
git commit # 表態完照常收尾
對比一般衝突的流程(見 6.3):那邊是「編輯檔案挑內容、刪掉標記、git add」;這裡跳過「編輯 + 刪標記」,直接 git add(留)或 git rm(刪)做檔案層級的裁決。
一句話記:一般衝突是「拼內容」,modify/delete 是「保留還是刪除,二選一」——前者用編輯器,後者用 git add / git rm 表態。⚠️ 小陷阱:忘了表態就直接 git commit,Git 會擋下來(還有未解的 unmerged 路徑),一定要先對那個檔 add 或 rm。
<<< === >>> 怎麼讀碰到衝突時,Git 不會擅自二選一,而是把兩邊的版本都原封不動寫進檔案,中間用三組標記隔開,等你來裁決。打開那個檔案,衝突的那段會長這樣:
<<<<<<< HEAD 到 =======:你目前分支(HEAD,常被稱作 ours)的版本。======= 到 >>>>>>> feature:被合併進來那一方(常被稱作 theirs)的版本,標記尾端就是來源分支名。<<<、===、>>> 三行全部刪掉,只留你要的內容。忘了刪,等於把這些亂碼提交進程式碼裡(編譯器/直譯器多半會直接報錯)。git diff 在解衝突期間也會特別把這些標記凸顯出來,提交前可再掃一眼。
圖 6-2 只列了我方 / 對方兩段,但第 5 章說過三方合併真正靠的是共同祖先(merge base)當基準——執行一次 git config merge.conflictStyle diff3,衝突標記就會多一段,變成三段式:
diff3 後,======= 分隔線前會多插入 ||||||| 到 ======= 這一段,內容是共同祖先當初的版本。三段對照起來就是我方 / 祖先 / 對方——有了祖先版本當基準,才看得出「兩邊各自相對原始內容動了什麼」,而不是只看到兩個沒有上下文的結果,對判斷「這行到底該留誰的」特別有幫助。新版 Git 還有加強版 zdiff3(git config merge.conflictStyle zdiff3),會把兩邊沒動、其實相同的行自動摺疊,三段對照更乾淨。這個設定是全域偏好,不影響衝突判定本身,純粹是看得更清楚。
衝突發生時,git merge 會停下來(合併處於「進行中」狀態),git status 會列出 Unmerged paths。流程其實很固定,四步:
git add 標記已解 → git commit。中途反悔用 git merge --abort幾個關鍵指令與心法:
| 指令 | 用途 |
|---|---|
git status | 列出 Unmerged paths——哪些檔還沒解;解衝突期間最常看的指令。 |
git add <檔> | 解衝突的「確認鍵」:把檔案 add 進暫存區,等於告訴 Git「這檔我搞定了」。衝突標記沒清乾淨也照 add,所以 add 前要自己檢查。 |
git commit | 所有衝突檔都 add 後,提交即生成 merge commit(訊息已預填,通常直接存檔)。也可用 git merge --continue。 |
git merge --abort | 後悔藥:放棄這次合併,工作目錄回到 merge 之前的乾淨狀態,當沒發生過。 |
git mergetool | 叫出設定好的三方比對工具(VS Code、Beyond Compare、vimdiff…),用「我方 / 對方 / 結果」三欄圖形化挑選,比手刪標記直覺。 |
git restore --ours/--theirs <檔> | 整檔直接採用某一方(ours = 你目前分支,theirs = 併進來那方),適合「這檔我確定全聽某一邊」的情況。這是「還原檔案內容」,呼應第 5 章拆分後的職責分工,理當用 restore;舊版寫法 git checkout --ours/--theirs <檔> 效果相同,兩者都通,新代碼建議用前者。 |
下面是一個含衝突的 greet.js。試著當一次裁判:你要留我方、留對方、兩者都留,還是自己重寫?點按鈕看解完後檔案長怎樣——注意每種選擇三行標記都會一併消失。
| 概念 | 一句話記住 |
|---|---|
| 判定單位 | 衝突看的是檔案內的同一段(hunk/行),不是整個檔案——同檔不同段不衝突 |
| 保守判定 | 改動緊鄰、同一行各改各的、modify/delete,即使邏輯不衝突仍會被標衝突 |
| 衝突標記 | <<< HEAD 到 === 是我方,到 >>> 分支 是對方;三行都是真文字,解完必刪 |
| 解衝突流程 | 停 → 編輯挑內容刪標記 → git add(確認鍵)→ git commit |
| 後悔與工具 | merge --abort 整個放棄;mergetool / --ours / --theirs 加速裁決 |