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

冲突标记分三块,要认清:

解决冲突就是手动编辑文件,留下正确的版本,删掉所有冲突标记,然后 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

几个实战建议:

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 的黄金法则

← 上一篇 Git 分支

下一篇 Git 远程仓库

✈️💬