解決衝突
衝突意味著 Git 發現兩處對同一位置的修改,並且拒絕猜測。解決方案是關於意圖的決定,而安全的預設做法是保留雙方的意圖。透過刪除一方來解決問題只會讓標記消失,但問題變得更糟:建置仍然通過,但同事的修改已經消失。
1. 確認你處於哪個操作——ours 和 theirs 取決於此
git status # "rebase in progress" / "You have unmerged paths" / "cherry-pick in progress"
| 操作 | --ours / HEAD 側 | --theirs 側 |
|---|---|---|
merge | 你所在的分支 | 被合併進來的分支 |
rebase | 你正在變基到的分支(上游) | 你正在重播的自己的提交 |
cherry-pick、revert、stash pop | 你所在的分支 | 正在被應用的提交 |
變基(rebase)那一行是陷阱。在 git rebase main 期間,「ours」是 main,而「theirs」是使用者自己的工作,這與合併相反。在變基過程中執行 git checkout --theirs file 會保留你的修改,而丟棄上游的修改。在使用任一標誌之前,請重新閱讀此表格。
2. 查看衝突全貌,而不僅僅是標記
git diff --name-only --diff-filter=U # 未合併的檔案
git status # 類型:兩者皆修改 / 我們刪除 / 兩者皆新增
git config merge.conflictStyle zdiff3 # 也顯示共同祖先(git 2.35+;舊版本使用 diff3)
git checkout --conflict=zdiff3 -- <file> # 重新渲染單一檔案,顯示基礎版本(會丟棄已對該檔案進行的編輯)
有了基礎版本可見之後,標記有三個部分:你的側、原始、他們的側。當你能看到每方從什麼變成了什麼時,大多數看似衝突的情況就會變得明顯。
要了解每方為何修改:
git log --merge -p -- <file> # 從雙方觸及該檔案的提交(合併或變基進行中)
git show REBASE_HEAD # 在變基期間:正在重播的提交
git show :1:<file> :2:<file> :3:<file> # 基礎、「ours」、「theirs」作為完整檔案
閱讀提交訊息。如果你無法用一句話解釋每方的修改,那麼你做出的解決方案就是猜測。
3. 透過結合來解決
- 雙方因不同原因編輯了同一行 - 寫下能同時完成兩件事的版本。例如,一方新增了
tax_rate參數,另一方新增了discount參數:解決後的函式應同時接受兩者並同時應用。 - 一方移動或重新命名,另一方編輯 - 在新位置上應用編輯。不要同時保留舊檔案和新檔案。
- 一方刪除,另一方修改 - 這是真正的決策,而非機械性的操作。找出被刪除的原因;如果修改仍然重要,則必須繼承過來,否則刪除獲勝。如果不清楚,請詢問使用者。
- 產生的檔案和鎖定檔案(
package-lock.json、poetry.lock、go.sum、快照、編譯輸出) - 不要手動合併。任選一方,然後使用工具重新產生(npm install、poetry lock、go mod tidy、快照指令)。 - 二進位檔案 - 無法合併;有意識地選擇一方(注意上方的表格)並說明。
文字可以乾淨地合併但仍然錯誤:如果一方重新命名了函式而另一方對舊名稱新增了呼叫,Git 不會看到衝突。這就是為什麼步驟 4 包含建置。
全面性標誌隱藏了決策。 git checkout --ours .、-X ours / -X theirs(這會將每個衝突塊解析為向一方傾斜)以及 -s ours(這會完全丟棄另一個分支)都在未經審視的情況下丟棄了工作。僅在使用者明確要求且知道正在丟棄什麼時才使用它們。
4. 驗證,然後繼續
git diff --check # 檢查遺留的衝突標記和空白錯誤
grep -rnE '^(<<<<<<<|=======|>>>>>>>)' . # 雙重保險,針對 git diff-check 不會檢查的檔案
# 建置並執行最快相關的測試
git add <resolved-files>
使用匹配的指令繼續。在會開啟編輯器的情況下加上 GIT_EDITOR=true,因為編輯器會在非互動式 shell 中卡住:
git merge --continue # 或:git commit --no-edit
GIT_EDITOR=true git rebase --continue
GIT_EDITOR=true git cherry-pick --continue
變基會逐個提交重播,因此它可能會在下一個提交時再次停止。重複此循環。在衝突出現的最早提交處解決;後續的提交可能是同一個衝突在重播。
如果解決後測試失敗,失敗就是關於解決方案的資訊。不要忽略它繼續,也不要透過編輯測試來「修復」它,除非你知道測試正在保護哪一方的行為。
5. 何時停止並詢問
在以下情況下停止而非猜測:
- 雙方以不相容的方式更改了商業邏輯(兩種不同的稅務規則、兩種不同的重試策略);
- 你無法用一句話說明每方為何進行修改;
- 解決方案導致測試失敗且原因不明。
陳述兩個意圖,說明你會選擇什麼以及原因,然後讓使用者決定。在 --continue 之前的一切都是可逆的:git merge --abort、git rebase --abort、git cherry-pick --abort 會將樹狀結構恢復到開始時的狀態。
重複衝突
對於長時間的變基,其中相同的衝突反覆出現,或者一個分支反覆被合併然後放棄:
git config rerere.enabled true # 記錄每個解決方案並在下次重播它
git rerere diff # 它所記錄的內容
git rerere forget-path <file> # 丟棄一個記錄錯誤的解決方案
rerere 會不經詢問地重新應用解決方案,因此錯誤的解決方案會傳播開來。在經過 rerere 解決的衝突之後,仍然要檢查結果。
當一個分支存在時間長且遠遠落後時,合併主幹一次通常只會導致一輪衝突;而變基會逐個提交重播衝突。這個選擇在使用者。
護欄
- 永遠不要為了讓衝突消失而全面採用一方。 另一方是某人的工作。
- 不要僅僅因為費時就中止使用者要求你完成的變基;只有在發生錯誤時才中止,並說明原因。
- 未經要求不要推送解決方案。變基需要強制推送;請遵循
git-safe-history-rewrite。 - 報告每個衝突的檔案:雙方做了什麼、你保留了什麼,以及任何你無法驗證的事項。