Git 合并与冲突
上一章学会了开分支,这一章讲怎么把分支"合回去"。合并(merge)是把两条分支的改动汇到一起的操作,它会遇到两种情况:顺利自动合并、以及让人头疼的冲突(conflict)。理解合并的两种模式和冲突的解决套路,你就能从容应对任何协作场景。
1. git merge 的两种模式
合并时,Git 会根据"目标分支有没有新提交"自动选择两种模式之一。
fast-forward(快进合并)
如果 main 分支自你分出 feature 后没有新提交,Git 只需把 main 指针"快进"到 feature 的最新位置,不产生任何合并提交:
# 场景 A:fast-forward 合并(目标分支没新提交)
# main 指向 A,你在 feature 分支加了 B、C 两个提交
#
# 合并前:
# A (main, feature 都在这)
# 合并 feature 时 main 直接"快进"到 C
# A -> B -> C (main, feature 都指向 C)
git switch main
git merge feature-login
# 输出:Updating a1b2c3d..e4f5g6h
# Fast-forward
# login.html | 1 +
# 1 file changed, 1 insertion(+)
# 特点:没有产生合并提交,历史是一条直线
# 缺点:看不出"这些改动是在 feature 分支上做的"
# 想保留"分支痕迹",强制生成合并提交:
git merge --no-ff feature-login -m "merge: 合并登录功能"三方合并(three-way merge)
如果 main 和 feature 都有各自的新提交(从共同祖先分叉),Git 会找一个"共同祖先"作为基准,做三方比较,自动生成一个合并提交(merge commit),它有两个父提交:
# 场景 B:三方合并(目标分支也有新提交)
# main 有 D,feature 有 B、C,从 A 分叉
#
# A - D (main)
# \
# B - C (feature)
#
# 合并会产生一个新的"合并提交" M,有两个父提交
git switch main
git merge feature-login
# 输出:Merge made by the 'ort' strategy.
# login.html | 5 +++++
# 1 file changed, 5 insertions(+)
# 历史变成:
# A - D ----- M (main)
# \ /
# B - C --- (feature)
# 这种合并保留了分支历史,适合"大功能合并"2. 合并冲突(conflict)
当两条分支修改了同一文件的同一区域,Git 不知道该保留哪个版本,就会产生冲突,暂停合并等你手动决定。这是协作里最常见、也最让新手慌的场景。
# 冲突产生:main 和 feature 改了"同一文件的同一区域"
git switch main
git merge feature-login
# 输出:
# Auto-merging config.js
# CONFLICT (content): Merge conflict in config.js
# Automatic merge failed; fix conflicts and then commit the result.
# 打开冲突文件,会看到这样的标记:
# <<<<<<< HEAD
# const theme = "dark"; # 当前分支(main)的内容
# =======
# const theme = "light"; # 要合并进来的(feature)的内容
# >>>>>>> feature-login
# 解决步骤:
# 1. 手动编辑文件,留下正确的版本(删掉冲突标记)
# 比如改成: const theme = "auto";
# 2. 标记冲突已解决
git add config.js
# 3. 继续合并(会自动生成合并提交)
git commit
# 4. 如果想放弃这次合并
git merge --abort冲突标记分三块,要认清:
<<<<<<<到=======之间是当前分支(HEAD)的内容。=======到>>>>>>>之间是要合并进来的分支的内容。- 行尾
feature-login标明对方分支名。
解决冲突就是手动编辑文件,留下正确的版本,删掉所有冲突标记,然后 git add 标记已解决。VS Code 打开冲突文件时会有"Accept Current / Accept Incoming / Accept Both"按钮,点一下就行,非常方便。
3. 冲突解决技巧
# 查看哪些文件冲突了
git status
# both modified: config.js
# 用 VS Code 打开冲突文件,会有"接受当前/接受传入/接受双方"的按钮
code config.js
# 用 mergetool(图形化三方比较)
git mergetool
# 查看冲突双方的差异
git diff --diff-filter=U
# 一次性"全部接受我方"
git checkout --ours config.js
# 一次性"全部接受对方"
git checkout --theirs config.js
git add config.js几个实战建议:
- 冷静看 diff:用
git diff看清双方改了什么,理解意图,别盲目选一边。 - 避免"全接受我方/对方":除非确实是一方完全正确,否则手动融合才是对的。
- 冲突太多说明分支拖太久:下次开发勤拉取主干,定期把 main 合并进 feature,减少最终冲突。
- 实在搞不定就 abort:
git merge --abort回到合并前状态,重新理清思路。
4. squash merge — 压扁合并
有时候 feature 分支上有一堆"修了又改"的零散提交(比如 10 个 "wip"、"fix typo"),直接合并会把 main 的历史搞乱。用 --squash 把它们压成一个干净提交:
# squash merge:把 feature 的多个零散提交压成一个
# 适合:feature 上有 10 个"修了又改"的杂乱提交,
# 合并到 main 时只留一个干净的"feat: 添加登录功能"
git switch main
git merge --squash feature-login
# 注意:squash 不会自动 commit,只是把改动放进暂存区
# 你需要手动提交一次(写个总结性的 message)
git commit -m "feat: 添加登录功能(含表单、校验、API)"
# 历史效果:main 上只有 1 个提交,看不到 feature 内部的 10 次折腾这是 GitHub 网页上 "Squash and merge" 按钮背后的原理。适合"功能完整、但过程杂乱"的场景。
5. merge 的黄金法则
- 永远从目标分支发起 merge:要合并 feature 到 main,就先
switch main再merge feature。顺序反了会把 main 的提交带到 feature 上。 - 合并前先 pull:确保本地 main 是最新的,避免和远程冲突。
- 合并完立刻测:合完跑一遍测试,确认没把功能合坏,再 push。
- 团队约定合并策略:有人爱 merge(保留分支历史),有人爱 squash(历史干净),团队内部统一一种。
← 上一篇 Git 分支
下一篇 Git 远程仓库 →