版本控制

Git 合并与变基冲突实战:读懂冲突标记并分场景解决

面向日常频繁分支协作的开发者,用可复现的冲突案例讲清 <<<<<<< ======= >>>>>>> 标记怎么读,以及合并、变基、重命名冲突各自怎么处理。

作者:巧匠团队·8 分钟阅读·更新于 2026-09-06

先搞清楚冲突到底是怎么发生的

冲突不是 Git 出错,而是两个提交都在同一片代码上做了不同修改,Git 无法自动决定谁赢。它把双方内容都留下来,交给你裁决。理解这一点,处理冲突时的焦虑会少一半。

最常见的触发点是 merge(merge feature into main)和 rebase(rebase feature onto main)。merge 把两份历史合并成一个新提交,rebase 把当前分支的提交逐个“搬到”目标分支顶端,两者冲突符号和解决的思路几乎一样,只是产出历史不同。

动手前先确认自己处于哪种操作中,因为这决定了冲突结束后是继续 merge 还是跑 git rebase --continue。用 git status 就能看到当前状态,区分 Merging 还是 Rebasing。

git merge feature
# -> conflict in src/user.ts

# or

git rebase main
# -> error: could not apply 3a2b1c0 ...

git status
# On branch feature
# You have unmerged paths.
#   both modified: src/user.ts

读懂冲突标记:三行分隔符各代表什么

冲突区域用 <<<<<<< 、 ======= 、 >>>>>>> 三行分隔。<<<<<<< 到 ======= 之间的段落是“当前分支”(HEAD,即正在接收合并的那一侧),======= 到 >>>>>>> 之间的段落是“被并入的提交”(incoming,即对方分支那侧)。

注意在 rebase 里方向是反的:<<<<<<< 一侧是你正在重放的提交(来自被 rebase 的分支),======= 到 >>>>>>> 一侧是 main 上的版本。如果不确定,看 git log 确认哪个是你的新提交。

记住基本规则:除了少数路径/文件重命名冲突,绝大多数冲突你要做的是选一边、糅合两边、或都重写,然后把分隔符和其中一方的内容删干净,只留想留下的最终版本。

<<<<<<< HEAD            <- 当前分支(merge 时为接收方)
const version = 2;
=======
const version = 3;
>>>>>>> feature        <- 被并入的分支

# 你想取对方的新版本:
const version = 3;

错误示范:随手删一行导致静默回归

最危险的做法是看到冲突就烦躁,随手把一边全删了完事。你保留的可能是有负面效应的旧逻辑,或者干脆丢了另一个分支新加的防御判断,跑起来没报错但行为错。这种回归往往在几周后的测试里才暴露。

另一个常见错误是把分隔符残留:只删了一边内容忘了删 <<<<<<< 或 >>>>>>>,代码一般立刻语法错误(文件里有遗留的 <<<<<<<),能被快速发现,但 rebase 时它可能混在被 `--continue` 的提交里。

正确姿势:先读两边代码各自干什么,再决定。若两边逻辑都重要,手动合并成一段;若只想保留一边,删干净另一边和全部标记,然后跑测试再继续操作。

# 错误示范:删错一边
<<<<<<< HEAD
if (user.balance < cost) throw new Error("insufficient");
=======
if (!user.valid) throw new Error("invalid user");
>>>>>>> feature

# 修复:不是二选一,而是判断完整
if (!user.valid) throw new Error("invalid user");
if (user.balance < cost) throw new Error("insufficient");

三类高频冲突场景与解法:文本、重命名、语义冲突

文本冲突就是两边改了同几行,解法见上。重命名冲突是 Git 把同一文件报告为 deleted/modified 或 rename/rename,用 git status 看更清晰,纯文本场景下 Git 很难自动处理,常需手动指定保留哪个名字并更新引用。

最容易被忽略的是“语义冲突”:根本没冲突标记,但两边改动叠加后逻辑矛盾。例如 A 分支把 config.timeout 从秒改成毫秒,B 分支新增了一个用秒计算的调用。代码能合并、能编译、能跑,就是数值错了。

语义冲突的拦截靠合并后立刻跑测试、看 git diff 时关注交互点,以及 review 时主动检查被合入的分支改动。不要只盯着有冲突标记的文件。

# 重命名冲突:一个文件被双方重命名成不同名字
git status
#   both renamed: src/old.ts -> src/newA.ts
#   and src/old.ts -> src/newB.ts

# 保留 A 的名字,B 的内容手动合并后提交新文件
# 删除多余文件并让引用指向最终名
rm src/newB.ts
mv src/newA.ts src/final.ts

合并 vs 变基:损失信息最少的那条路

merge 保留真实分支拓扑和双方提交,冲突解决一次;rebase 重写你的提交,历史更线性,但每条提交都可能被逐个重放,多次冲突要反复解。团队共享 main 时严禁 rebase main,否则其他人的引用会被改写。

worktree 或独立分支上,rebase 能获得整洁历史;但一旦你的分支已推送给别人合并,用 merge 更安全。判断标准是“这个分支是否已经被人拉走”:已分享就 merge,私有则可选 rebase。

进行 rebase 前先把手头晦涩改动 commit,避免未提交改动被忽略;--continue 前每步都 git add 冲突文件,再 git rebase --continue,最后 git push --force-with-lease 更新远端(仅私有分支)。

# 合并风格
git merge main
# ... resolve ...
git merge --continue

# 变基风格(私有分支)
git rebase main
# ... resolve file A ...
git add fileA
git rebase --continue
# ... resolve file B ...
git add fileB
git rebase --continue

git push --force-with-lease origin feature

事后验证:确认没有悄悄弄丢改动

解决冲突不等于完成。先跑一遍测试与 lint,然后重点验证“被你手动合并的那几处”是否符合预期——它们最容易出错。

用 git diff 在解完冲突后立刻看改动是否只剩你的意图;用 git log --merge 或 git log -p 对比合并前后相关文件的变化,能抓住多删多留的地方。平时显著减小冲突距离的做法是频繁、小步提交并定期同步目标分支,让冲突总量变小。

# 确认冲突是否全部解决
git status   # 不再出现 unmerged paths 即完成

git diff --stat

git log --oneline --graph -10    # 查看合并后的拓扑

# 验证你手改的那段最终代码
git show HEAD:src/user.ts | grep -n "user.valid"

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。