深入理解 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
|
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
|
二、
基本操作
1 2 3 4 5 6 7 8
| git checkout main
git merge feature/login
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
| git merge feature/login
git status
vim src/login.js
git add src/login.js
git commit -m "Merge branch 'feature/login' - 解决登录模块冲突"
|
4.3 解决冲突的四种策略
| 策略 | 做法 | 适用场景 |
|---|
| 保留当前分支 | 删除 ======= 以下到 >>>>>>> 的内容 | 当前分支的代码是正确的 |
| 保留合入分支 | 删除 <<<<<<< 到 ======= 的内容 | 合入分支的代码是正确的 |
| 手动合并 | 把两边的代码手动整合在一起 | 两边都有需要的部分 |
| 重新编写 | 删除所有冲突标记,写一段新代码 | 两边都不太对 |
小技巧:VS Code / IDEA 等编辑器会自动识别冲突标记,提供”保留当前””保留传入””两边都保留”的按钮,比手动编辑方便得多。
4.4 合并时指定优先方
如果你明确知道要保留哪一方的代码:
1 2 3 4 5
| git merge feature/login -X ours
git merge feature/login -X theirs
|
注意:-X ours 和 -X theirs 只在冲突的部分生效,没有冲突的改动仍会正常合并。
五、
–squash:压缩合并
--squash 把功能分支的所有提交压成一个提交再合入,让主线历史更干净。
1 2 3 4 5
| 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
两种方式都能把功能分支的代码合入主线,但理念和效果完全不同:
| 特性 | merge | rebase |
|---|
| 操作方式 | 创建合并提交,两条线汇合 | 把提交”搬”到目标分支后面 |
| 历史记录 | 保留真实历史 | 改写成线性历史 |
| 分支痕迹 | 保留(特别是 --no-ff) | 消除分支痕迹 |
| 安全性 | 不改写任何提交 | 改写提交 hash |
| 适合场景 | 公共分支、保留上下文 | 个人分支、整理历史 |
什么时候选哪个?
1 2 3 4 5 6 7 8
| 要把功能分支合入 main,且想保留开发记录? └── merge(推荐 --no-ff)
要同步 main 的最新代码到你的功能分支? └── rebase(比 merge 更干净)
要推送到远程的公共分支? └── merge(绝对不要 rebase 公共分支)
|
黄金法则:永远不要对已推送到远程的公共分支执行 rebase!rebase 只适用于本地的、个人的分支。
七、
放弃合并
合并过程中发现冲突太多,或者合并错了分支,可以中止:
安全
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
| git checkout feature/login git pull origin feature/login
git fetch origin git rebase origin/main
git checkout main git pull origin main
git merge --no-ff feature/login -m "Merge branch 'feature/login'"
git push origin main
|
场景:撤销一次已提交的合并
1 2 3 4 5 6
| git reset --hard HEAD~1
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 干净) │ └────────────────┴─────────────────────────────────────────┘
|