深入理解 Git 分支合并的两种模式(快进合并与三方合并),掌握冲突解决流程与 merge 策略选择,让团队协作更顺畅。

分支合并 — 把多条开发线路汇聚到一起

分支是 Git 最强大的功能,但分支开发完最终要合并回主线。合并方式选不对、冲突处理不熟练,是团队协作中最常见的问题来源。


一、两种合并模式

git merge 会根据分支的历史关系,自动选择合并方式:

模式英文名触发条件是否产生合并提交
快进合并Fast-forward目标分支没有新提交不产生
三方合并Three-way merge两个分支都有新提交产生

1.1 快进合并(Fast-forward)

当目标分支自你创建功能分支以来没有新提交时,Git 直接把指针”快进”到最新提交:

1
2
3
4
5
6
7
# 合并前
main: A → B → C
feature: A → B → C → D → E

# git merge feature 后
main: A → B → C → D → E ← main 指针直接移到 E
feature: A → B → C → D → E
1
2
3
4
5
6
git checkout main
git merge feature

# 输出:
# Updating c3a1b2d..e5f6g7h
# Fast-forward

1.2 三方合并(Three-way merge)

当两个分支都有新提交时,Git 找到它们的共同祖先,把三方改动合并成一个新提交:

1
2
3
4
5
6
7
8
9
# 合并前
main: A → B → C → F → G
\
feature: D → E

# git merge feature 后
main: A → B → C → F → G → H (Merge commit)
\ /
feature: D → E ──────┘
1
2
3
4
5
6
7
git checkout main
git merge feature

# 输出:
# Merge made by the 'ort' strategy.
# src/login.js | 15 ++++++++++++
# 1 file changed, 15 insertions(+)

二、基本操作

1
2
3
4
5
6
7
8
# 1. 切换到目标分支(要合入的分支)
git checkout main

# 2. 合并功能分支
git merge feature/login

# 3. 推送到远程
git push

常用选项

选项作用
--no-ff强制创建合并提交(即使可以快进)
--ff-only只允许快进合并,否则中止
--squash把功能分支的所有提交压成一个
--no-commit合并后不自动提交,先检查代码
-m "信息"指定合并提交信息

三、–no-ff:保留分支历史

--no-ff(no fast-forward)强制创建一个合并提交,即使快进合并是可能的。

对比

1
2
3
4
5
6
7
# 不用 --no-ff(默认快进)
main: A → B → C → D → E ← 看不出哪些提交属于功能分支

# 使用 --no-ff
main: A → B ────────→ H (Merge) ← 一眼看出功能分支的范围
\ /
feature: C → D → E
1
git merge --no-ff feature/login -m "Merge branch 'feature/login'"

什么时候用 --no-ff

  • 合并功能分支时,想在历史中保留分支的痕迹
  • 方便日后 git revert 撤销整个功能
  • 团队协作中,让其他人看到功能开发的边界

四、合并冲突

当两个分支修改了同一文件的同一区域时,Git 无法自动判断该保留哪个,就会产生冲突。

4.1 冲突标记

冲突文件的长这样:

1
2
3
4
5
<<<<<<< HEAD
这是 main 分支上的代码
=======
这是 feature 分支上的代码
>>>>>>> feature/login
标记含义
<<<<<<< HEAD当前分支的内容开始
=======两个分支的分界线
>>>>>>> feature/login被合入分支的内容结束

4.2 冲突解决流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 执行合并
git merge feature/login

# 2. Git 提示有冲突
# CONFLICT (content): Merge conflict in src/login.js
# Automatic merge failed; fix conflicts and then commit the result.

# 3. 查看哪些文件有冲突
git status

# 4. 打开冲突文件,手动解决冲突标记
vim src/login.js

# 5. 标记为已解决
git add src/login.js

# 6. 提交合并结果
git commit -m "Merge branch 'feature/login' - 解决登录模块冲突"

4.3 解决冲突的四种策略

策略做法适用场景
保留当前分支删除 ======= 以下到 >>>>>>> 的内容当前分支的代码是正确的
保留合入分支删除 <<<<<<<======= 的内容合入分支的代码是正确的
手动合并把两边的代码手动整合在一起两边都有需要的部分
重新编写删除所有冲突标记,写一段新代码两边都不太对

小技巧:VS Code / IDEA 等编辑器会自动识别冲突标记,提供”保留当前””保留传入””两边都保留”的按钮,比手动编辑方便得多。

4.4 合并时指定优先方

如果你明确知道要保留哪一方的代码:

1
2
3
4
5
# 冲突时优先保留当前分支(ours = main)
git merge feature/login -X ours

# 冲突时优先保留合入分支(theirs = feature)
git merge feature/login -X theirs

注意-X ours-X theirs 只在冲突的部分生效,没有冲突的改动仍会正常合并。


五、–squash:压缩合并

--squash 把功能分支的所有提交压成一个提交再合入,让主线历史更干净。

1
2
3
4
5
# 把 feature 分支的所有提交压成一个,暂存到工作区
git merge --squash feature/login

# 自己写一个干净的提交信息
git commit -m "feat: 添加用户登录功能"

效果对比

1
2
3
4
5
6
7
8
# 普通 merge
main: A → B → H (Merge) ← 历史中保留了 C、D、E
\ /
feature: C → D → E

# squash merge
main: A → B → S ← 只有一个干净的提交 S
feature: C → D → E ← 原分支不受影响

squash 合并后的注意事项:因为 Git 不认为两个分支已经合并过(没有 merge commit),下次再合并同一个分支时可能产生冲突。适合一次性功能分支,不适合长期维护的分支。


六、merge vs rebase

两种方式都能把功能分支的代码合入主线,但理念和效果完全不同:

特性mergerebase
操作方式创建合并提交,两条线汇合把提交”搬”到目标分支后面
历史记录保留真实历史改写成线性历史
分支痕迹保留(特别是 --no-ff消除分支痕迹
安全性不改写任何提交改写提交 hash
适合场景公共分支、保留上下文个人分支、整理历史

什么时候选哪个?

1
2
3
4
5
6
7
8
要把功能分支合入 main,且想保留开发记录?
└── merge(推荐 --no-ff)

要同步 main 的最新代码到你的功能分支?
└── rebase(比 merge 更干净)

要推送到远程的公共分支?
└── merge(绝对不要 rebase 公共分支)

黄金法则:永远不要对已推送到远程的公共分支执行 rebase!rebase 只适用于本地的、个人的分支。


七、放弃合并

合并过程中发现冲突太多,或者合并错了分支,可以中止:

1
2
# 放弃正在进行的合并,回到合并前的状态
git merge --abort
安全

merge --abort 是完全安全的,它会把你恢复到执行 git merge 之前的状态,不会丢失任何代码。

如果合并已经提交了但还没 push:

1
2
# 撤销合并提交
git reset --hard HEAD~1

八、实战:完整合并流程

场景:把功能分支合并到 main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 确保功能分支代码是最新的
git checkout feature/login
git pull origin feature/login

# 2. 先 rebase main,解决可能的冲突
git fetch origin
git rebase origin/main
# 如果有冲突:解决 → git add → git rebase --continue

# 3. 切回 main,合并
git checkout main
git pull origin main

# 4. 合并功能分支(用 --no-ff 保留记录)
git merge --no-ff feature/login -m "Merge branch 'feature/login'"

# 5. 推送
git push origin main

场景:撤销一次已提交的合并

1
2
3
4
5
6
# 还没 push
git reset --hard HEAD~1

# 已经 push 了,用 revert 生成一个"反向提交"
git revert -m 1 <merge_commit_hash>
git push

总结:分支合并速查卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──────────────────────────────────────────────────────────┐
│ Git Merge 速查 │
├────────────────┬─────────────────────────────────────────┤
│ 普通合并 │ git merge <branch> │
│ 保留分支记录 │ git merge --no-ff <branch> │
│ 压缩合并 │ git merge --squash <branch> │
│ 只允许快进 │ git merge --ff-only <branch> │
├────────────────┼─────────────────────────────────────────┤
│ 冲突时选当前 │ git merge <branch> -X ours │
│ 冲突时选对方 │ git merge <branch> -X theirs │
│ 放弃合并 │ git merge --abort │
├────────────────┼─────────────────────────────────────────┤
│ 合并到公共分支 │ merge(不要 rebase) │
│ 同步到个人分支 │ rebase(比 merge 干净) │
└────────────────┴─────────────────────────────────────────┘