Git 代码冲突:全部原因 + 解决方法(附沙箱实测)
本文所有报错信息、冲突文件内容、命令输出均为 Git 2.43 真实运行截图 ,不是凭记忆写的。
配套可复现脚本:
git-conflict-lab/demo.sh(bash demo.sh一键重跑全部场景)。
一、先搞懂:Git 到底为什么会冲突
一句话原理
Git 的合并基于三路合并(three-way merge):
base(共同祖先版本)
/ \
ours(你的) theirs(对方的)
\ /
合并结果
Git 的判断逻辑只有三条:
| 情况 | 谁改了 | 结果 |
|---|---|---|
| 只有一边改了这一行 | ours 或 theirs | ✅ 自动采用改动的那边 |
| 两边都没改 | 都不 | ✅ 保持原样 |
| 两边都改了同一块 | ours 和 theirs | ❌ 冲突,必须人来裁决 |
| 一边改、一边删 | 冲突类型不同 | ❌ 冲突(modify/delete) |
关键认知 :Git 冲突的本质不是"Git 不行",而是 Git 无法替你判断"到底该听谁的" 。
所以解决冲突不是技术活,是决策活------你要读懂两边的意图,然后做决定。
冲突文件长什么样(实测输出)
public class OrderService {
public void cancel(Long id) {
<<<<<<< HEAD
System.out.println("【master】取消订单并释放库存");
=======
System.out.println("【feature】超时未支付自动取消,发短信通知");
>>>>>>> feature/order-cancel
}
}
三个标记,记住它们:
| 标记 | 含义 |
|---|---|
<<<<<<< HEAD |
冲突开始,HEAD 后面是当前分支的内容 |
======= |
分界线 |
>>>>>>> feature/order-cancel |
冲突结束,下面是被合并进来的内容 |
⚠️ 7 个字符,一个都不能少 :<<<<<<< 是 7 个 <,>>>>>>> 是 7 个 >,======= 是 7 个 =。
(很多人手写时少写一个导致文件损坏。)
二、冲突的 16 种原因(按类型分类)
A 类:内容冲突(代码本身撞车)
1️⃣ 两边改了同一文件的同一行 / 相邻行 ------ 占所有冲突的 90%
报错原文(实测)
$ git merge feature/order-cancel
Auto-merging OrderService.java
CONFLICT (content): Merge conflict in OrderService.java
Automatic merge failed; fix conflicts and then commit the result.
触发原因:两个分支基于同一个提交,改了同一段代码。
⚠️ 即使是相邻行 (比如你改第 10 行,对方改第 11 行)也可能冲突,
因为 Git 的合并单位是"hunk"(上下文相关的代码块,默认上下各 3 行),不是严格的行。
2️⃣ 一方修改、一方删除(modify/delete)
报错原文(实测)
$ git merge feature/clean
CONFLICT (modify/delete): LegacyUtil.java deleted in feature/clean and modified in HEAD.
Version HEAD of LegacyUtil.java left in tree.
Automatic merge failed; fix conflicts and then commit the result.
$ git status
Unmerged paths:
(use "git add/rm <file>..." as appropriate to mark resolution)
deleted by them: LegacyUtil.java
决策点:这个文件到底该留还是该删?
- 留 → 说明对方的删除是错的,或者你这个文件还有用
- 删 → 说明你的改动白做了,应该跟着删
3️⃣ 二进制文件两边都改(png/jar/xlsx/docx)
报错原文(实测)
$ git merge feature
Auto-merging logo.png
CONFLICT (content): Merge conflict in logo.png
为什么特殊 :Git 无法对二进制做文本 diff,所以不会产生 <<<<<<< 标记,只能二选一。
bash
git checkout --ours logo.png # 保留我的
git checkout --theirs logo.png # 保留对方的
实测结果:--ours 后文件内容确实是 master 的 fake-C。
4️⃣ 新增了同名文件(add/add)
CONFLICT (add/add): Merge conflict in UserService.java
典型场景 :你和同事各自新建了同名文件(比如都新建了 OrderVO.java),但内容完全不同。
常见于大家同时按同一个需求文档开工。
5️⃣ 重命名冲突(rename/modify、rename/rename)
CONFLICT (rename/modify): OrderDto.java renamed to OrderVO.java in HEAD, but modified in feature
CONFLICT (rename/rename): a.java renamed to b.java in HEAD, renamed to c.java in feature
典型场景:你重命名了类(IDEA 的 Refactor → Rename),同事同时改了这个类的内容。
B 类:操作方式导致的冲突
6️⃣ git pull 冲突(本地提交 vs 远程提交分叉)
$ git pull
CONFLICT (content): Merge conflict in pom.xml
Automatic merge failed; fix conflicts and then commit the result.
原因 :git pull = git fetch + git merge。你的本地有新提交,远程也有新提交,两条线分叉了。
💡 实习生最常踩的坑 :
git pull之前不先git fetch看一眼,直接 pull 就把冲突糊脸上了。
7️⃣ git rebase 冲突 ------ 同一个冲突可能要解决 N 次
报错原文(实测)
$ git rebase master
Rebasing (1/2)
Auto-merging cfg.txt
CONFLICT (content): Merge conflict in cfg.txt
error: could not apply 9a315ab... feat: feature 改第2行
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
为什么比 merge 痛苦 :rebase 是把你的提交逐个应用到目标分支上 。
如果你有 5 个提交都碰了同一处,你要解 5 次。
解决 → git add → git rebase --continue → 又冲突 → 解决 → add → continue → ...
实测最终效果:成功 rebase 后历史是一条直线
* 02a644d feat: feature 新增第4行
* 3793fae feat: feature 改第2行
* 5c4ad3f fix: master 改第2行 ← 你的提交被"挪"到了 master 之后
* faca9f5 init
8️⃣ git cherry-pick 冲突
$ git cherry-pick abc1234
error: could not apply abc1234... feat: xxx
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' and run "git cherry-pick --continue"
原因 :把某个提交抄到另一个分支,但那个分支的上下文已经变了。
解法 :同 rebase,git cherry-pick --continue / --abort。
9️⃣ git stash pop 冲突
$ git stash pop
CONFLICT (content): Merge conflict in OrderService.java
原因 :你 stash 了改动 → 期间别人改了同一处 → pop 回来撞车。
注意 :stash pop 冲突时,stash 不会被自动删除 ,解决完要手动 git stash drop。
🔟 工作区有未提交改动,直接被拒绝(不是冲突,但很像)
报错原文(实测,真实踩到的)
$ git merge feature/clean
error: Your local changes to the following files would be overwritten by merge:
LegacyUtil.java
Please commit your changes or stash them before you merge.
Aborting
三种选择
bash
git commit -am "wip: 先存一下" # ① 提交(推荐)
git stash # ② 暂存起来,事后 git stash pop
git checkout -- LegacyUtil.java # ③ 丢弃(危险,改动找不回)
C 类:环境 / 配置导致的"假冲突"(最冤)
1️⃣1️⃣ 换行符不一致(CRLF vs LF)------ Windows 用户专属噩梦
现象 :明明只改了一行,却提示整个文件都冲突。
原因 :Windows 用 \r\n,Linux/Mac 用 \n。你提交时把整个文件的换行符都改了。
解决
bash
# Windows 用户全局配置(提交时转 LF,检出时转 CRLF)
git config --global core.autocrlf true
# Linux / macOS
git config --global core.autocrlf input
# 项目根目录加 .gitattributes(推荐,团队统一)
*.java text eol=lf
*.md text eol=lf
*.png binary
1️⃣2️⃣ IDE 自动格式化 / 自动 import 导致大面积冲突
现象:你只改了 3 行,diff 出来 200 行。
原因 :IDEA 的 Reformat Code、Optimize Imports 把同事的代码也重排了;
或者同事用了不同的 code style(缩进 2 空格 vs 4 空格)。
解决 :团队统一导入同一份 .editorconfig + IDEA code style 配置文件。
ini
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
[*.java]
indent_size = 4
1️⃣3️⃣ 文件名大小写不一致
现象 :本地好好的,推到服务器上 CI 就冲突/报错。
原因 :Windows 和 macOS 默认不区分文件名大小写 ,OrderVO.java 和 OrderVo.java 是同一个文件;
Linux 区分 ,是两个文件。
解决 :git config core.ignorecase false(Linux 上),并统一命名规范。
1️⃣4️⃣ 文件权限变更(mode change)
diff --git a/deploy.sh b/deploy.sh
old mode 100644
new mode 100755
解决 :git config core.fileMode false(Windows 上推荐关掉,否则 chmod 会被当改动)。
1️⃣5️⃣ 子模块(submodule)冲突
CONFLICT (submodule): Merge conflict in libs/common
原因 :两边引用了子模块的不同 commit。
解决:
bash
git checkout --ours libs/common # 或 --theirs
git add libs/common
git submodule update --init --recursive
1️⃣6️⃣ 目录 / 文件类型冲突
CONFLICT (file/directory): There is a directory with name config in feature.
Adding config as config~HEAD
原因 :你建了个 config 目录,同事建了个 config 文件。
解决 :手动决定保留哪个,git rm 掉另一个。
三、解决方法(从通用到专用)
🔧 通用五步法(适用于 merge / pull)
bash
# ① 看清楚哪些文件冲突了
git status
# ② 打开冲突文件,找到 <<<<<<< ======= >>>>>>> 标记
# 手动改成你想要的最终内容(删掉所有标记!)
# ③ 标记为已解决
git add <文件>
# ④ 完成合并
git commit # 会自动生成 Merge 提交信息
# 或 git commit -m "merge: 融合 xxx 与 xxx"
# ⑤ 验证
git log --oneline --graph --all
🚨 最容易犯的错 :改完文件忘记
git add,直接git commit→ 报Committing is not possible because you have unmerged files。或者更糟:把
<<<<<<< HEAD这些标记提交上去了,代码直接编译不过。
提交前自检
bash
grep -rn "<<<<<<<\|>>>>>>>\|=======" --include="*.java" .
# 有输出 = 还有标记没清理干净,别提交!
🛠 三种解决策略
| 策略 | 命令 | 适用场景 |
|---|---|---|
| 听我的 | git checkout --ours <file> |
确认自己的改动是最新最对的 |
| 听对方的 | git checkout --theirs <file> |
对方是重构后版本,我的要废弃 |
| 融合两边 | 手动编辑 | 两边的功能都要保留(最常见、最正确) |
批量处理(冲突文件很多时)
bash
git checkout --ours . # 全部用我的(谨慎!)
git checkout --theirs . # 全部用对方的(谨慎!)
git diff --name-only --diff-filter=U | xargs git checkout --ours # 只处理冲突文件
用 -X 参数在合并时就指定策略(不进冲突状态,直接自动选边)
bash
git merge -X ours feature # 冲突时一律用我的(其他部分照常合并)
git merge -X theirs feature # 冲突时一律用对方的
⚠️ 高频陷阱:rebase 时 --ours/--theirs 是反的
这是最容易误删自己代码的地方,我做了实测验证:
=== 在 feature 分支上执行 git rebase master ===
--- git checkout --ours 取到的内容: MASTER版本 ← 直觉以为是 feature,其实是 master!
--- git checkout --theirs 取到的内容: FEATURE版本 ← 你的代码在这边!
=== 对照:在 master 分支上执行 git merge feature ===
--- git checkout --ours 取到的内容: MASTER版本
--- git checkout --theirs 取到的内容: FEATURE版本
记忆口诀:
ours= 当前HEAD所在的那一边,不是"我的分支"。
- merge 时 HEAD 在你当前分支 → ours = 你的
- rebase 时 Git 把 HEAD 切到了目标分支(master),再逐个重放你的提交 → ours = master,你的代码在 theirs
血泪教训 :rebase 冲突时想保留自己的代码,要用 --theirs,用错了就把自己的活全丢了。
🖥 图形化工具解决(Java 开发首选 IDEA)
IDEA 操作路径
-
冲突时 IDEA 自动弹出
Conflicts对话框 -
或右键项目 →
Git→Resolve Conflicts -
双击文件,进入三栏合并界面:
┌──────────────┬──────────────┬──────────────┐
│ 左边 │ 中间 │ 右边 │
│ Yours/当前 │ Result │ Theirs/传入 │
│ (本地) │ (最终结果) │ (对方分支) │
└──────────────┴──────────────┴──────────────┘
↑ >> 接受左边 ↑ 中间可直接编辑 接受右边 << ↑ -
也可以直接在中间的 Result 栏手打(推荐,最灵活)
-
点
Apply→ 文件自动git add -
全部解决完 →
git commit
IDEA 快捷键 :冲突文件上按 Alt+M(Win)/ ⌥+M(Mac)快速打开合并界面。
VS Code :冲突文件会显示 Accept Current Change / Accept Incoming Change / Accept Both Changes / Compare Changes 四个按钮,一键解决。
命令行党的 mergetool
bash
git mergetool # 调用配置的外部工具(vimdiff / beyond compare / meld)
git config --global merge.tool vimdiff
🔁 rebase 冲突专用流程
bash
git rebase master
# 冲突了 ↓
git status # 看哪个文件
# 手工改文件
git add <文件>
git rebase --continue # 继续下一个提交
# 如果这个提交不想要了
git rebase --skip
# 彻底放弃,回到 rebase 之前
git rebase --abort # ← 救命命令,随时可用
实测关键输出
$ git rebase --continue
[detached HEAD 3793fae] feat: feature 改第2行
1 file changed, 1 insertion(+), 1 deletion(-)
Rebasing (2/2) Successfully rebased and updated refs/heads/feature.
🎯 高级技巧:git rerere(让 Git 记住你怎么解的冲突)
适用场景:长期分支反复合并、rebase 反复冲突同一个地方。
bash
git config --global rerere.enabled true
实测效果
--- 第 1 次 merge 冲突,我人工改成了 MERGED ---
CONFLICT (content): Merge conflict in f.txt
Recorded preimage for 'f.txt' ← Git 记录冲突现场
Recorded resolution for 'f.txt' ← Git 记录我的解法
--- 回滚这次 merge,再合并一次 ---
$ git merge topic
CONFLICT (content): Merge conflict in f.txt
Resolved 'f.txt' using previous resolution. ← 自动复用了上次的解法!
$ cat f.txt
a
b MERGED ← 直接就是我上次解出来的结果,不用动手
c
注意:rerere 只自动应用解法 ,仍需你
git add+git commit确认。
四、后悔药:搞砸了怎么撤
场景 1:合并中,还没 commit,想重来
bash
git merge --abort # merge 冲突中
git rebase --abort # rebase 冲突中
git cherry-pick --abort # cherry-pick 冲突中
场景 2:merge 已经 commit 了,但解错了(还没 push)
bash
git reset --hard ORIG_HEAD # ← 最优雅,ORIG_HEAD 永远指向"危险操作前"的位置
实测
merge 前 HEAD(ORIG_HEAD) = 4eff645 当前 HEAD = 4eff645
$ git reset --hard ORIG_HEAD
回滚后 f.txt 内容:MASTER ← 完美回到合并前
ORIG_HEAD是 Git 自动维护的"上一次危险操作前的位置",merge / rebase / reset / pull都会更新它。
场景 3:merge 已 commit 且已经 push,不能 reset
bash
git revert -m 1 <merge提交号>
实测
错误 merge 已提交:
* 9d459ed merge: 解错了
|\
| * e05e701 f
* | 4eff645 m
$ git revert -m 1 HEAD
revert 后 f.txt = MASTER ← 内容被反向撤销了
历史记录:
* b74ef7b Revert "merge: 解错了" ← 新增一个反向提交
* 9d459ed merge: 解错了 ← 原记录保留(不破坏历史,别人不会受影响)
|\
-m 1的意思是"保留第 1 个父分支(即主干)的内容"。已 push 的公共分支一定用 revert,不要用 reset(reset 会改写历史,坑全组人)。
场景 4:什么办法都没了,找 reflog
bash
git reflog # 看 HEAD 的所有移动记录
git reset --hard HEAD@{5} # 回到第 5 步之前
reflog 是 Git 的"后悔药中的后悔药",90 天内的操作基本都能找回(包括被 reset 掉的 commit)。
五、面试 30 秒背诵版
Q:Git 冲突是怎么产生的?怎么解决?
【原因】
Git 合并用的是三路合并,比较的是"共同祖先、你的版本、对方版本"三个版本。
只有当两边都改了同一块代码 时 Git 无法判断该听谁的,才会冲突。
常见冲突类型有:同一行内容冲突、一方修改一方删除、二进制文件冲突、重命名冲突,
以及换行符 CRLF/LF、IDE 自动格式化这类"假冲突"。
【解决】
先 git status 看哪些文件冲突,打开冲突文件会看到
<<<<<<< HEAD、 =======、>>>>>>> 分支名 三组标记,
上半部分是我的、下半部分是对方的。
手工改成最终想要的内容、删掉所有标记,然后 git add 标记为已解决,最后 git commit 完成合并。
如果冲突中想放弃,用 git merge --abort 或 git rebase --abort 一键回退。
【加分点】
- rebase 冲突时
--ours指的是目标分支而不是我自己的分支,用错会丢代码 - 已 push 的错误合并用
git revert -m 1撤销,不能用 reset - 开
rerere.enabled可以让 Git 记住解法,重复冲突自动解决 - 冲突解决完要
grep检查有没有残留的<<<<<<<标记
六、预防冲突的 8 条实战规范
| # | 规范 | 为什么 |
|---|---|---|
| 1 | 开工前先 git pull,每天至少一次 |
减少与你本地的分叉程度 |
| 2 | 一个功能一个分支,功能做完立刻合并删除 | 分支活越久,冲突越多 |
| 3 | 小步提交,别攒 3 天的改动一次提 | 提交越小冲突越好解 |
| 4 | 不要改别人的代码格式化 | 格式冲突是最没价值的冲突 |
| 5 | 团队统一 .editorconfig + .gitattributes |
根除换行符和缩进假冲突 |
| 6 | 公共文件(pom.xml、配置文件、常量类)改动先在群里说一声 | 这类文件全组都在改,是冲突重灾区 |
| 7 | 长期分支定期 git rebase master 保持同步 |
把大冲突拆成小冲突 |
| 8 | 合并前 git fetch + git log HEAD..origin/master 看一眼别人改了啥 |
提前心里有数 |
推荐工作流(实习生版)
bash
# 每天早上
git checkout master
git pull
git checkout -b feature/订单导出 # 开新分支
# 开发中,频繁小提交
git add . && git commit -m "feat(order): 新增导出接口"
# 提交 MR 前,先同步主干(把冲突在本地解决干净)
git fetch origin
git rebase origin/master # ← 冲突在这里解决,别等 MR 页面上解
git push -f origin feature/订单导出 # rebase 后需要强推自己的分支
# MR 被 approve 后合并,删分支
git branch -d feature/订单导出
⚠️
git push -f只能强推你自己的 feature 分支 ,永远不要
git push -f origin master------这是能让你当天离职的操作。
七、速查命令表
| 你想要什么 | 命令 |
|---|---|
| 看哪些文件冲突 | git status |
| 看冲突的 3 个版本 | git ls-files -u(1=base 2=ours 3=theirs) |
| 保留我的版本 | git checkout --ours <file> |
| 保留对方版本 | git checkout --theirs <file> |
| 放弃 merge | git merge --abort |
| 放弃 rebase | git rebase --abort |
| rebase 继续 | git add . && git rebase --continue |
| rebase 跳过这个提交 | git rebase --skip |
| 回到合并前 | git reset --hard ORIG_HEAD |
| 撤销已 push 的 merge | git revert -m 1 <commit> |
| 找丢失的提交 | git reflog |
| 检查残留冲突标记 | grep -rn "<<<<<<<" . |
| 看分支图 | git log --oneline --graph --all |
| 开启冲突记忆 | git config --global rerere.enabled true |
| 暂存手头改动 | git stash / git stash pop |
配套文件 :git-conflict-lab/demo.sh ------ 5 个冲突场景的完整复现脚本,bash demo.sh 即可重跑。