先搞清楚冲突到底是怎么发生的
冲突不是 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"