解決合併衝突 (Merge Conflicts)

當兩個分支修改了同一個檔案的同一個部分,Git 無法自動決定要保留哪個版本,就會產生合併衝突。

為什麼會有衝突?

假設 mainfeature 都修改了 index.js 的第 10 行:

  • main 把它改成 const name = "Alice";
  • feature 把它改成 const name = "Bob";

Git 不知道該用哪一個,所以需要你手動處理。

衝突發生時

當你執行 git merge 遇到衝突:

git merge feature

會看到:

Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.

此時 Git 會在衝突的檔案中標記衝突的位置。

衝突標記

打開有衝突的檔案,會看到類似這樣的標記:

function greet() {
<<<<<<< HEAD
    const name = "Alice";
=======
    const name = "Bob";
>>>>>>> feature
    console.log("Hello, " + name);
}

說明:

  • <<<<<<< HEAD:當前分支(你正在合併到的分支)的內容開始
  • =======:分隔線
  • >>>>>>> feature:要合併進來的分支的內容結束

解決衝突

手動編輯

編輯檔案,決定最終要保留什麼:

// 選項一:保留 HEAD 的版本
function greet() {
    const name = "Alice";
    console.log("Hello, " + name);
}

// 選項二:保留 feature 的版本
function greet() {
    const name = "Bob";
    console.log("Hello, " + name);
}

// 選項三:結合兩者
function greet() {
    const name = "Alice and Bob";
    console.log("Hello, " + name);
}

記得刪除衝突標記(<<<<<<<=======>>>>>>>)。

完成解決

# 1. 編輯好衝突的檔案

# 2. 標記衝突已解決
git add index.js

# 3. 完成合併
git commit

Git 會自動產生合併 commit 訊息。

使用工具解決衝突

使用 VS Code

VS Code 會自動識別衝突,並提供按鈕:

  • Accept Current Change:保留 HEAD 的版本
  • Accept Incoming Change:保留合併進來的版本
  • Accept Both Changes:保留兩者
  • Compare Changes:比較差異

使用 Git mergetool

git mergetool

這會開啟你設定的合併工具(例如 VS Code、Meld、Beyond Compare)。

設定 VS Code 為 mergetool:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

使用命令列選項

直接選擇一方的版本:

# 使用我們的版本
git checkout --ours index.js

# 使用他們的版本
git checkout --theirs index.js

查看衝突狀態

查看哪些檔案有衝突

git status

輸出:

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   index.js
        both modified:   style.css

查看衝突差異

git diff

取消合併

如果你想放棄這次合併,回到合併前的狀態:

git merge --abort

複雜衝突情境

檔案被刪除 vs 修改

一方刪除了檔案,另一方修改了它:

CONFLICT (modify/delete): index.js deleted in feature and modified in HEAD.

解決方式:

# 保留檔案
git add index.js

# 或刪除檔案
git rm index.js

檔案被重新命名

一方重新命名了檔案,另一方修改了它:

CONFLICT (rename/delete): old-name.js renamed to new-name.js in HEAD.

二進制檔案衝突

圖片等二進制檔案無法合併內容:

# 保留我們的版本
git checkout --ours image.png

# 保留他們的版本
git checkout --theirs image.png

git add image.png

避免衝突的方法

  1. 經常同步:定期 git pull 保持和遠端同步

  2. 小步提交:避免大範圍的修改

  3. 溝通協調:團隊成員避免同時修改相同的檔案

  4. 模組化程式碼:好的程式架構可以減少多人修改同一檔案的機會

衝突解決策略

策略選項

在合併時可以指定策略:

# 有衝突時優先用我們的版本
git merge -X ours feature

# 有衝突時優先用他們的版本
git merge -X theirs feature

注意:這和 git merge -s ours 不同:

  • -X ours:有衝突時用我們的,沒衝突的正常合併
  • -s ours:完全忽略對方的變更,只保留我們的

實際範例

完整的衝突解決流程

# 1. 嘗試合併
git merge feature
# CONFLICT (content): Merge conflict in index.js

# 2. 查看狀態
git status
# both modified: index.js

# 3. 打開檔案,手動編輯解決衝突
code index.js

# 4. 標記已解決
git add index.js

# 5. 完成合併
git commit -m "Merge feature branch, resolve conflicts"

# 6. 推送
git push

多個檔案衝突

git merge feature
# CONFLICT: index.js
# CONFLICT: style.css
# CONFLICT: app.js

# 一個一個解決
code index.js
git add index.js

code style.css
git add style.css

code app.js
git add app.js

# 完成
git commit

小技巧

查看衝突的三方版本

# 查看共同祖先的版本
git show :1:index.js

# 查看我們的版本
git show :2:index.js

# 查看他們的版本
git show :3:index.js

重新開始解決衝突

如果解決到一半搞砸了:

# 重置單一檔案
git checkout -m index.js

# 或整個重來
git merge --abort
git merge feature