本文所有报错信息、冲突文件内容、命令输出均为 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,所以不会产生 <<<<<<< 标记,只能二选一。
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。
10、工作区有未提交改动,直接被拒绝(不是冲突,但很像)
报错原文(实测,真实踩到的)
$ 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
三种选择
git commit -am "wip: 先存一下" # ① 提交(推荐) git stash # ② 暂存起来,事后 git stash pop git checkout -- legacyutil.java # ③ 丢弃(危险,改动找不回)
c 类:环境 / 配置导致的"假冲突"(最冤)
11、换行符不一致(crlf vs lf)—— windows 用户专属噩梦
现象:明明只改了一行,却提示整个文件都冲突。
原因:windows 用 \r\n,linux/mac 用 \n。你提交时把整个文件的换行符都改了。
解决
# 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
12、ide 自动格式化 / 自动 import 导致大面积冲突
现象:你只改了 3 行,diff 出来 200 行。
原因:idea 的 reformat code、optimize imports 把同事的代码也重排了;
或者同事用了不同的 code style(缩进 2 空格 vs 4 空格)。
解决:团队统一导入同一份 .editorconfig + idea code style 配置文件。
# .editorconfig root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space [*.java] indent_size = 4
13、文件名大小写不一致
现象:本地好好的,推到服务器上 ci 就冲突/报错。
原因:windows 和 macos 默认不区分文件名大小写,ordervo.java 和 ordervo.java 是同一个文件;
linux 区分,是两个文件。
解决:git config core.ignorecase false(linux 上),并统一命名规范。
14、文件权限变更(mode change)
diff --git a/deploy.sh b/deploy.sh old mode 100644 new mode 100755
解决:git config core.filemode false(windows 上推荐关掉,否则 chmod 会被当改动)。
15、子模块(submodule)冲突
conflict (submodule): merge conflict in libs/common
原因:两边引用了子模块的不同 commit。
解决:
git checkout --ours libs/common # 或 --theirs git add libs/common git submodule update --init --recursive
16、目录 / 文件类型冲突
conflict (file/directory): there is a directory with name config in feature. adding config as config~head
原因:你建了个 config 目录,同事建了个 config 文件。
解决:手动决定保留哪个,git rm 掉另一个。
三、解决方法(从通用到专用)
通用五步法(适用于 merge / pull)
# ① 看清楚哪些文件冲突了 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 这些标记提交上去了,代码直接编译不过。
提交前自检
grep -rn "<<<<<<<\|>>>>>>>\|=======" --include="*.java" . # 有输出 = 还有标记没清理干净,别提交!
三种解决策略
| 策略 | 命令 | 适用场景 |
|---|---|---|
| 听我的 | git checkout --ours <file> | 确认自己的改动是最新最对的 |
| 听对方的 | git checkout --theirs <file> | 对方是重构后版本,我的要废弃 |
| 融合两边 | 手动编辑 | 两边的功能都要保留(最常见、最正确) |
批量处理(冲突文件很多时)
git checkout --ours . # 全部用我的(谨慎!) git checkout --theirs . # 全部用对方的(谨慎!) git diff --name-only --diff-filter=u | xargs git checkout --ours # 只处理冲突文件
用 -x 参数在合并时就指定策略(不进冲突状态,直接自动选边)
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
git mergetool # 调用配置的外部工具(vimdiff / beyond compare / meld) git config --global merge.tool vimdiff
rebase 冲突专用流程
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 反复冲突同一个地方。
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,想重来
git merge --abort # merge 冲突中 git rebase --abort # rebase 冲突中 git cherry-pick --abort # cherry-pick 冲突中
场景 2:merge 已经 commit 了,但解错了(还没 push)
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
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
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 看一眼别人改了啥 | 提前心里有数 |
推荐工作流(实习生版)
# 每天早上 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 即可重跑。
以上就是git代码冲突原因与解决方法全解的详细内容,更多关于git代码冲突原因与解决的资料请关注代码网其它相关文章!
发表评论