Git 合并冲突怎么解决?用 AI 处理冲突的流程、Prompt 和 4 个易错点

0 阅读10分钟

@TOC


合并冲突本身不难,删掉几行标记、留下该留的代码而已。

难的是看不出两边原本想干什么。默认的冲突块里只有两段代码:你的和对方的。缺了最关键的一段——它们分叉之前长什么样。人缺这段信息要靠猜,AI 缺这段信息也在猜,而且猜得很自信。

这篇记我现在处理冲突的流程:合并前先预判,冲突了先把信息补齐,再分清哪些交给 AI、哪些自己拍板,最后用几条命令验收。附一段可复制的 Prompt。


一、合并之前:先看会冲突在哪

冲突最好在合并之前就知道。Git 2.38 起有个不碰工作区的「预演」命令:

git merge-tree --write-tree --name-only main feature

它不动工作区、索引和当前分支,只在后台把两个分支试合一遍:输出第一行是合并结果的树对象,可以忽略;后面列出的就是会冲突的文件。有冲突时退出码是 1,也能直接放进脚本里用。

开 PR 之前,我还会在 wescode 里问一句「我的分支和 main 有没有改到同一个函数」,把两边都改过的函数列出来,提前找改同一处的同事对一下。

⟦截图:wescode 里询问「我的分支和 main 有潜在冲突吗」的回答,说明文字「合并前按函数预判冲突」⟧


二、先改一个配置:让冲突块带上 base

默认的冲突标记长这样:

<<<<<<< HEAD
timeout := 30 * time.Second
=======
timeout := cfg.Timeout
>>>>>>> feature/config

只看这两段,没法判断该留谁。是你把超时改成了 30 秒,还是对方?原来是多少?

打开 zdiff3(Git 2.35 及以上;更老的版本用 diff3):

git config --global merge.conflictStyle zdiff3

同一个冲突会变成这样:

<<<<<<< HEAD
timeout := 30 * time.Second
||||||| 8a9b3c1
timeout := 60 * time.Second
=======
timeout := cfg.Timeout
>>>>>>> feature/config

||||||| 和 ======= 之间多出来的这段,就是两边分叉前的原始版本(base)。现在一眼能看清:你把默认超时从 60 秒改成了 30 秒,对方把硬编码改成了读配置。两边的意图并不冲突,正确的合并大概率是:保留读配置,同时把配置里的默认值改成 30 秒。

这一条对 AI 同样关键。没有 base,它只能在两段代码里二选一或者硬拼;有了 base,它能分别说出两边各改了什么。

已经冲突了才想起来没开?可以对单个文件重新生成带 base 的冲突标记:

git checkout --conflict=zdiff3 -- path/to/file

注意:这会丢掉你在这个文件里已经做的解决,最好一开始就执行。


三、再补一层:两边为什么改

冲突块只告诉你改了什么,不告诉你为什么。下面几条命令补齐剩下的信息:

# 列出所有冲突文件
git diff --name-only --diff-filter=U

# 两边动过这个文件的提交
git log --merge --oneline -- path/to/file

# 连同具体改动一起看
git log --merge -p -- path/to/file

# 分别查看三个版本的完整文件
git show :1:path/to/file   # base
git show :2:path/to/file   # ours(当前分支)
git show :3:path/to/file   # theirs(合进来的分支)

commit message 写得好不好,这时候最能看出来。fix(pay): 回调超时从 60s 改为 30s,避免上游重试堆积 能直接回答「为什么」;update 只能去翻 PR 或者找人问。


四、哪些冲突可以交给 AI

冲突类型典型样子处理方式
两边各加了不同的 import / 依赖同一位置各加了几行都保留,去重、排序
两边往同一个列表里加了不同的项路由注册、配置项、枚举值都保留,确认顺序是否有含义
一边改格式,一边改逻辑缩进换行 vs 实际改动以逻辑改动为准,合完再格式化
一边改注释,一边改代码文档注释 vs 函数体合完检查注释是否还对得上

共同点:两边的意图不冲突,只是改到了相邻的行。这类交给 AI 能省不少时间,风险也低。我在 wescode 里遇到这类冲突基本直接交给它合,合完扫一眼 diff 就提交。


五、哪些冲突必须自己拍板

1. 两边改了同一段逻辑,目的不同

比如一边把重试次数从 3 改成 5,另一边把重试整个换成了指数退避。AI 可以给方案,但「要不要保留、参数取多少」是业务判断,不是文本合并。

2. 一边删了,一边改了

git status 里显示 deleted by us 或 deleted by them。这种冲突没有冲突块可看,本质是一个决定:对方删掉这个文件的理由,是否盖过了你这次的修改。先问删文件的人。

3. lock 文件和生成文件

go.sum、package-lock.json、pnpm-lock.yaml、*.pb.go、mock 文件——别让 AI 手工拼哈希或者合生成代码。先解决源头文件,再用工具重新生成:

# Go:先解决 go.mod,go.sum 取任意一边后整理
git checkout --ours -- go.sum && go mod tidy

# npm / pnpm:先解决 package.json,再重新安装生成 lock
git checkout --ours -- package-lock.json && npm install
git checkout --ours -- pnpm-lock.yaml && pnpm install

# 生成代码:先解决 .proto 或接口定义,再跑项目自己的生成命令

4. 数据库迁移文件

两个分支各加了一个迁移文件,文件名不同,Git 通常不报冲突。但序号撞了、或者执行顺序和依赖关系错了,要到部署时才暴露。合并后顺手看一眼迁移目录的顺序。


六、可复制 Prompt

你在帮我解决一个 Git 合并冲突。只处理我给出的冲突块,冲突块以外的代码不要动。

背景:
- 当前分支(ours):<分支名>,目的:<一句话>
- 合入分支(theirs):<分支名>,目的:<一句话>
- 相关提交(git log --merge --oneline 的输出):
<粘贴>

冲突块(zdiff3 格式,含 base):
<粘贴>

要求:
1. 先分别说明:相对 base,ours 改了什么、theirs 改了什么
2. 判断两边能否同时保留;如果语义互斥,停下来说明分歧,不要替我选
3. 给出合并后的代码,并标注每一处来自 ours / theirs / 新写
4. 不要为了「兼容两边」把作用相同的两份逻辑都留下
5. 列出合并后需要我验证的点(调用方、测试、配置)

第 3 条最有用:标了来源,review 时一眼就能看出它有没有悄悄丢掉某一边。

我在 wescode 里的做法是把冲突文件 @ 进对话,再贴上面这段。冲突文件多的时候,先让它排处理顺序:依赖文件(go.mod、package.json)最先,配置其次,业务代码最后;每解决一步就跑一次编译,编译不过就停下来,别等全部解完才发现问题。

⟦截图:wescode 对话里 @ 冲突文件并贴入 Prompt 后的分析结果,说明文字「每一处都标了来源的合并结果」⟧


七、4 个易错点

1. 两边都留,结果做了两遍

「两边都保留」是 AI 最常给的「安全」解法。重复的 import、重复的函数定义,编译器会报错;但同一个中间件注册两次、同一条配置写两遍、同一个事件发两次,编译器不管。

看到「两边都保留」的方案,专门检查一下:是不是同一件事被做了两遍。

2. 悄悄丢掉一边

反过来,它也可能整块采用了一边,另一边的修改就这么没了。冲突标记删得干干净净,看起来一切正常。

解决完、提交前,对每个冲突文件跑这两条:

git diff HEAD --stat -- path/to/file         # 合并结果 vs ours
git diff MERGE_HEAD --stat -- path/to/file   # 合并结果 vs theirs

哪一条的输出是空的,就说明合并结果和那一边一模一样——另一边对这个文件的改动被整块丢了。要么确认是有意为之,要么回去查。rebase 时把 MERGE_HEAD 换成 REBASE_HEAD。

3. rebase 时 ours 和 theirs 是反的

  • merge 时:ours = 当前分支,theirs = 合进来的分支
  • rebase 时反过来:ours = 你要变基到的目标分支(比如 main),theirs = 正在重放的你自己的提交

所以在 rebase 过程中执行 git checkout --theirs,保留的是你自己的改动。人经常搞混,AI 也一样。给 AI 贴冲突时别只写「保留 ours」,直接说清楚「保留 main 上的版本」还是「保留我这个提交的版本」。

4. 没报冲突的地方,才最危险

一个分支把 GetUser 改名成 FindUser,并更新了所有调用;另一个分支新写了一处 GetUser 调用。两边改的不是同一行,Git 自动合并,一个冲突都不报。

编译型语言会在编译时报错,还算幸运。更糟的是只改语义、不改签名:一边把 CalcFee 的返回值从「元」改成了「分」,另一边新增的调用还在按「元」用。编译通过,测试没覆盖到,就这么上线了。

这类问题在冲突块里根本看不到,只能靠两件事:

  • 合并后做完整的编译和测试,不只跑冲突文件相关的那部分
  • 把对方分支改过签名或语义的函数列出来,逐个查它们在你这边的新增调用

第二件我是在 wescode 里直接问:「feature 分支上改过签名的函数有哪些,main 这边谁在调用它们?」然后逐个点过去看。用命令行的话,先 git diff main...feature 找出对方改过签名的函数,再 git grep -n 函数名 搜调用方;通过接口间接调用的地方要额外留意。


八、合并完的验收清单

# 1. 残留的冲突标记:git add 不会拦你,所以 add 已解决的文件之后再查一遍
git diff --cached --check

# 2. 两边的改动都在(对每个冲突文件)
git diff HEAD --stat -- path/to/file
git diff MERGE_HEAD --stat -- path/to/file

# 3. 完整构建和测试(换成你项目的命令)
go build ./... && go test ./...

git diff --check 也会报行尾空格这类问题,重点看 leftover conflict marker 那几行。

长期分支反复 rebase、同一处冲突一次次重复解的,可以打开 rerere,让 Git 记住你的解法:

git config --global rerere.enabled true

下次遇到同样的冲突会自动套用。但它复用的也可能是你上次的错误解法,所以上面的验收一步都别省。


九、什么时候别让 AI 碰

冲突一大片的时候。 长期分支合 main,一下冲突几十个文件。这时先 git merge --abort,考虑按 main 上的提交分几段合进来,每次解一小批。规模一大,AI 逐个文件处理很容易前后不一致。

你不知道对方为什么这么改的时候。 去问写代码的人,比问 AI 靠谱。AI 只能从代码推测意图,推测错了还很有说服力。

lock 文件和生成文件。 交给工具重新生成,前面说过了。


小结

处理合并冲突,信息比技巧重要:

  1. 合并前先用 git merge-tree 预判哪些文件会冲突
  2. 打开 zdiff3,让冲突块带上 base
  3. 用 git log --merge 看两边为什么改
  4. 机械冲突交给 AI,语义冲突自己拍板,lock 文件和生成文件交给工具
  5. 验收三件事:没有残留标记、两边的改动都在、完整编译测试通过

AI 在这件事上的价值,是把「读懂两边各改了什么」这一步做快。至于该保留谁,仍然是人的决定。

上面的流程我是在 wescode 里跑的,官网是 weisyn.com。你们团队处理冲突有什么约定,比如谁的分支谁负责解,欢迎评论区聊聊。