让 Claude Code、Cursor 或 Codex 跑了半小时,写出 6 个文件的改动,结果 git pull --rebase origin main 一打回车就满屏 CONFLICT (content): Merge conflict in src/...。最难受的不是冲突本身,而是 AI 同时改了空白、import 顺序和真正的业务逻辑,让你分不清哪段是”逻辑冲突”、哪段是”格式噪音”。
TL;DR(最快的修法)
- 先把 AI 的改动 commit 一次,让它成为可恢复的对象,再开一个备份分支。
- 重新跑 rebase,让 git 自动解决纯格式的 hunk:
git pull --rebase -X theirs origin main会在冲突 hunk 上保留 AI 的版本,同时照常拉进 main 上不冲突的提交。 - 对剩下的几个仍在冲突的文件做三路对比,只手工改真正的逻辑部分。
- 跑测试 / lint / build,然后
git rebase --continue。
关键坑: 在 rebase 过程中,ours 和 theirs 跟普通 merge 是反的。被重放的提交(也就是 AI 的改动)是 theirs,你要落到的目标分支(main)是 ours。本文后面全部按 rebase 的含义来写。如果你是通过 git merge 走到这一步的,请把下文所有 ours/theirs 反过来理解。
常见原因
按 AI 编辑产生 merge 冲突的命中率从高到低排序。
1. AI 改的文件 main 也动了
最常见的一种:你 fork 出去半小时,期间同事 push 了 3 个 commit 都碰了同一份 src/server/auth.ts。AI 不知道远端动了,按本地状态全文重写。
CONFLICT (content): Merge conflict in src/server/auth.ts
Auto-merging src/server/auth.ts
如何判断: git log origin/main --since="2 hours ago" -- <文件> 看远端最近是否有别人动过该文件。
2. import 自动整理顺序和团队不一致
Cursor / Copilot 经常在保存时跑 eslint --fix 或 Prettier 自动排序 import;如果团队配置和 AI 触发的配置不同(比如分组方式不同),整个 import 块会被整体重写,产生大段”看似全改”的冲突。
<<<<<<< HEAD
import { z } from "zod";
import { db } from "@/lib/db";
import type { User } from "@/types";
=======
import type { User } from "@/types";
import { db } from "@/lib/db";
import { z } from "zod";
>>>>>>> ai-branch
如何判断: 冲突 hunk 里前后两份 import 集合完全相同,只是顺序 / 空行不同。
3. 空白和换行被 AI 顺手改了
Aider / Windsurf 默认会让模型重写整段函数,模型有时把 tab→空格、CRLF→LF、行尾分号风格全部按它的偏好写一遍。git diff 显示”整文件全红全绿”,但 git diff -w 几乎为空。
如何判断: git diff --stat 显示大量增删行,但 git diff -w --stat 行数骤降到个位数。
4. AI 跨文件搬代码,main 也在搬
AI 决定把 utils/format.ts 里的 formatDate 搬到 lib/date.ts;同一时间 main 把同一个函数搬到 shared/format.ts。rebase 时两份”删除”加两份”新增”对不上,git 报 add/add 冲突。
CONFLICT (add/add): Merge conflict in lib/date.ts
如何判断: git status 里同时出现 both added 和 deleted by us / deleted by them。
5. AI 修了某个 bug,main 也修了同一个 bug
经典的”两边都对、但写法不同”。你让 AI 修 race condition 加了 mutex;同事用 atomic 写了别的方案。语义都对,但代码冲突。
如何判断: 冲突两边单独都能跑测试通过,只是实现思路不同。
6. lockfile / 生成文件冲突
package-lock.json、pnpm-lock.yaml、migrations/*.sql、schema.prisma 这类自动生成文件几乎一冲突就是几百到几千行。单独有一篇 AI 编辑后 lockfile 冲突 专门讲怎么处理。
如何判断: 冲突文件名是 lockfile / migration / generated 目录下的产物。
最短修复路径
按顺序做,每一步都可回退——最坏情况你可以 git rebase --abort 回到 Step 1。
Step 1:先把 AI 改动凝固成一个 commit
如果 AI 的改动还在 working tree 没提交,先 commit 一次,让它变成一个可对比、可 reset 的对象。
git add -A
git commit -m "WIP: AI edits before rebase"
git rev-parse HEAD # 记下这个 SHA,待会要 cherry-pick / 对比
接着新建一个安全分支保险:
git branch backup/ai-edits-$(date +%Y%m%d-%H%M)
这样即使 rebase 彻底搞砸,git checkout backup/... 一行就能回来。
Step 2:打开 rerere,让每个冲突只解一次
如果 AI 重跑(很常见:你 abort 掉、重新提示,它又生成了同样的改动),你会再撞上同一个冲突。打开 git rerere,让 git 记住你的解法,下次自动重放:
git config --global rerere.enabled true
merge 和 rebase 时 git 都会自动调用 rerere。要在乱糟糟的 rebase 之前打开,而不是之后。
Step 3:用 -X 策略让 git 自动吃掉纯格式冲突
很多冲突其实只是 import 顺序、空白、换行风格。先在 hunk 级别让 git 偏向某一边,再处理剩下的。记住 TL;DR 里那条 rebase 反转。
# 在冲突 hunk 上保留 AI 的版本,同时照常拉进 main 的其他提交。
# rebase 时,被重放的 AI 提交是 "theirs"。
git pull --rebase -X theirs origin main
# 或者反过来:在冲突 hunk 上偏向 main 的版本。
# rebase 时,你要落到的目标分支 main 是 "ours"。
git pull --rebase -X ours origin main
-X theirs / -X ours 只在冲突 hunk 上做取舍,不会丢掉只存在于某一边、没有重叠的新增代码。它是个决胜规则,不是删除。这一步通常能一次性消掉大部分纯格式冲突,只剩下少数真冲突需要手工解。
如果只想吃掉纯空白差异(默认的 ort 策略支持这个选项):
git rebase -X ignore-all-space origin/main
Step 4:剩下的冲突按类型分类处理
git status --short | grep "^UU" # 列出还在冲突的文件(两边都改了)
按下面三类分别处理。这些 git checkout 命令默认你正处在 rebase 中,所以侧别标签按 rebase 的含义来:
| 类型 | 处理方式 |
|---|---|
| 纯 import / 格式 | 保留 AI 版本:git checkout --theirs <file>,然后跑一次本地 formatter |
| lockfile(按 main 重新生成) | 取 main 版本:git checkout --ours <file>,然后 npm install 重新生成 |
| 真正的逻辑冲突 | 手工编辑,参考 Step 5 的三路对比 |
如果不确定自己是在 rebase 还是 merge,看一下:git status 会显示 interactive rebase in progress / rebase in progress,或者 git merge 之后的 You have unmerged paths。ours/theirs 的含义在这两种情况下是相反的。
Step 5:用三路对比看真正的逻辑冲突
对于剩下的逻辑冲突文件,打开三路视图:
git mergetool --tool=vimdiff # 或 vscode / meld
或手动取出每个 stage。:1 是共同祖先,:2 是 ours,:3 是 theirs。rebase 时这意味着 :2 是 main,:3 是你的 AI commit:
git show :1:src/server/auth.ts > /tmp/base.ts # 共同祖先
git show :2:src/server/auth.ts > /tmp/ours.ts # main 端(rebase 时)
git show :3:src/server/auth.ts > /tmp/theirs.ts # AI 端(rebase 时)
diff -u /tmp/base.ts /tmp/ours.ts # main 改了什么
diff -u /tmp/base.ts /tmp/theirs.ts # AI 改了什么
只有看到”main 改了什么”和”AI 改了什么”两份独立 diff,才能判断该如何合并,而不是盲选一边。
Step 6:解决完跑全套验证,再继续 rebase
git add <已解决的文件>
npm test && npm run lint && npm run build
git rebase --continue
如果中途想放弃:
git rebase --abort # 回到 Step 1 的 commit
如何确认已经修好
git status显示没有 unmerged paths,并且nothing to commit, working tree clean。git rebase --continue顺利跑完,不再反复弹冲突(或者本来就没有 rebase 在进行)。git diff origin/main...HEAD --stat只列出你打算改的文件,没有意外冒出来的 lockfile 或纯空白文件。- 测试、lint、build 在最终的整棵树上都通过,而不只是各自单边通过。
预防建议
- 让 AI 开始改之前先
git pull --rebase origin main,把分叉窗口缩到最短。 - 在
CLAUDE.md/.cursorrules/AGENTS.md里写明:“改前先读文件、不要重排 import、不要批量改空白”。 - AI commit 保持小而频繁(一次只做一件事),冲突时只损失一个 commit 的工作量。
- 仓库根加
.editorconfig和 pre-commit 的 formatter hook,让 AI 跑出来的代码风格自动和团队一致。 - lockfile / 生成文件加一条
.gitattributes规则,省掉手工对比。内置的union驱动(package-lock.json merge=union)会保留两边的行,之后用npm install重新生成即可。光写merge=ours是不行的,得先定义它:git config merge.ours.driver true。 - 全局打开一次
git config --global rerere.enabled true,让重复冲突自动套用上次的解法。 - 大改动让 AI 先写计划再执行,你确认文件范围后再让它动文件。
常见问题
为什么 git checkout --theirs 取出来的是反的版本?
因为你在 rebase,不是 merge。rebase 时标签是反的:theirs 是被重放的提交(AI 改动),ours 是你要落上去的分支(main)。如果你在 rebase 中想要 main 的版本,用 git checkout --ours。
-X theirs 会把 AI 新写的代码丢掉吗?
不会。-X theirs(和 -X ours)只决定冲突 hunk 上哪边胜出。只存在于某一边、没有重叠的代码,两种情况下都会保留。它是决胜规则,不是删除。
AI 每次重跑都撞上同一个冲突,怎么不用反复解?
在 rebase 之前打开 git rerere(git config --global rerere.enabled true)。git 会记下你手工解的结果,下次再出现完全相同的冲突时自动重放。
整个文件都显示冲突,但其实只是空白差异,最快怎么修?
先用 git diff -w --stat 确认(如果行数掉到接近 0,就是纯空白),然后用 git rebase -X ignore-all-space origin/main 重跑,让 git 不再把空白差异当冲突。
怎么不逐行读就分辨出真逻辑冲突和格式噪音?
对比 git diff --stat 和 git diff -w --stat。第二个很小就是空白问题。对于 import,看冲突 hunk 两边是不是同一批标识符、只是顺序不同。这两步过完还剩下的,才是值得做三路对比的真语义冲突。
能不能在不丢 AI 工作的前提下直接退出?
能。git rebase --abort 会回到 Step 1 的那个 commit,而你的 backup/ai-edits-* 分支不管怎样都是一份独立副本。