Git 分支合并操作入门:merge、squash、rebase 一文学懂
每天都要
git merge、git rebase,但你真的知道它们的区别吗?同样的需求,为什么有时候历史是一条直线,有时候长出一堆"分叉"?为什么有人说"rebase 危险"? 本教程由浅入深讲透 Git 的三种合并方式——merge / squash / rebase——以及 cherry-pick、fast-forward、冲突处理等周边操作。核心目标:看清三种方式对提交历史的"改写"程度,从此合并不再纠结。
📑 目录
全篇共 9 章 · 总阅读约 50 分钟 · 难度从 ⭐ 到 ⭐⭐⭐ 递进 阅读路线:🟢 新手 → 第 1、2、3、4、5、9 章 | 🟠 进阶 → 全篇通读(第 6、7、8 章)
| 章节 | 难度 | 时长 | 简介 |
|---|---|---|---|
| 一、三个核心概念:提交、分支、分叉(入门篇) | ⭐ 入门 | 5 分钟 | 看懂提交图,才能看懂三种合并的本质区别 |
| 二、merge:保留完整历史的"合并提交"(核心篇) | ⭐⭐ 核心 | 7 分钟 | 非线性历史的由来,fast-forward 与 merge commit |
| 三、squash merge:把一堆提交压成一个(核心篇) | ⭐⭐ 核心 | 7 分钟 | GitHub PR 默认操作,主干干净但细节尽失 |
| 四、rebase:改写历史,线性重放(核心篇) | ⭐⭐⭐ 核心 | 10 分钟 | 最强大也最危险,交互式 rebase 与黄金法则 |
| 五、三者终极对比与选型(实战篇) | ⭐⭐⭐ 实战 | 5 分钟 | 一张全对比表 + 决策流程图,照着选不会错 |
| 六、补充操作:fast-forward、cherry-pick、revert(进阶篇) | ⭐⭐ 进阶 | 6 分钟 | merge 家族的其他成员,各有各的用途 |
| 七、冲突处理:合并绕不开的坎(实战篇) | ⭐⭐⭐ 实战 | 8 分钟 | 冲突长什么样、怎么解决,merge 与 rebase 的区别 |
| 八、常见坑与避坑(避坑篇) | ⭐⭐ 避坑 | 4 分钟 | rebase 已推送分支等 6 个大坑 |
| 九、总结与练习 | ⭐ 收尾 | 2 分钟 | 记忆口诀 + 4 道自测题 + 速查表 |
一、三个核心概念:提交、分支、分叉(入门篇)
想看懂三种合并的区别,先补三个基础概念。
1.1 提交(commit):历史的"快照链"
每个提交记录一次代码变更,并指向它的父提交,形成一条链:
A ← B ← C ← (HEAD → main)提交唯一标识
每个提交有唯一的哈希值(如 3f8a2c1)。历史一旦被改写,哈希就会变——这是理解 rebase"危险"的关键。
1.2 分支(branch):可以移动的指针
分支本质上就是指向某个提交的指针。main 和 feature 各自指向不同的提交:
main: A ← B ← C
feature: A ← B ← C ← D ← E1.3 分叉(divergence):需要合并的根本原因
当两个分支从同一祖先各自前进时,就分叉了:
main: A ← B ← C
\
feature: D ← E现在的问题是
C 和 E 都基于 B 各自发展,谁也不知道对方改了什么。合并就是把分叉的历史"重新接上"。merge、squash、rebase 就是三种"接法",区别在于——接完之后,历史长什么样。
二、merge:保留完整历史的"合并提交"(核心篇)
git merge feature(在 main 上执行)把 feature 的改动并入 main,产生一个合并提交(merge commit),它有两个父提交:
main: A ← B ← C ─────── F (merge commit,双亲:C 和 E)
\ /
feature: D ← E ──什么时候没有 merge commit?
如果 feature 直接从 main 顶端长出、main 期间没有新提交(没有分叉),Git 直接移动指针 → fast-forward(快进合并),不产生合并提交:
main: A ← B ← C ← D ← E (指针直接前进,C 后面就接 D、E)想让分叉不存在时也保留合并记录,用 git merge --no-ff feature。
命令速记
git checkout main # 先切到目标分支
git merge feature # 合并 feature(默认 fast-forward 可用则快进)
git merge --no-ff feature # 强制生成 merge commit优点与缺点
| 说明 | |
|---|---|
| ✅ 优点 | 历史完整可追溯:每个提交、每次合并都记录在案;不改写任何提交,最安全 |
| ❌ 缺点 | 历史非线性:多人协作后提交图变成"毛线球",git log 很难读 |
什么时候用 merge?
长期分支(如 release、develop)之间的合并、需要完整审计历史的团队项目。GitHub 上"Create a merge commit"就是这种。
三、squash merge:把一堆提交压成一个(核心篇)
git merge --squash feature 把 feature 的所有提交压缩成一个新提交,挂在 main 顶端;feature 自身的提交历史不进入 main:
main: A ← B ← C ← S (单个新提交,内容 = D+E 的全部改动)# 实际执行(squash 不会自动提交,需要手动 commit)
git merge --squash feature # 把改动暂存(staged)
git commit -m "feat: 完成 X 功能" # 手动提交成一个这就是 GitHub PR 的默认行为
在 GitHub 合并 PR 时选 "Squash and merge" 按钮,就是这个操作:一个 PR = 一个提交。
优点与缺点
| 说明 | |
|---|---|
| ✅ 优点 | main 历史干净线性:一个功能一个提交,好读、好回滚(git revert 一次撤销整个功能) |
| ❌ 缺点 | 丢失过程细节:中间"改了两版、修了 5 次"的过程没了;想单独回退某个中间提交也不行 |
什么时候用 squash?
功能分支 → main 的标准姿势(团队 PR 合并首选)。前提:这个功能已经成熟,中间过程不需要保留。
四、rebase:改写历史,线性重放(核心篇)
git rebase main(在 feature 上执行):把 feature 的提交摘下来,在 main 顶端重新"播放"一遍,生成一批全新的提交(哈希全变):
之前: main: A ← B ← C
feature: D ← E
之后: main: A ← B ← C
feature: D' ← E' (D、E 的"重制版")结果:历史变成一条直线,没有 merge commit。但注意——D' 不是 D,E' 不是 E,它们是内容相同、哈希不同的克隆。
交互式 rebase:整理自己的历史
git rebase -i HEAD~3 # 交互式整理最近 3 个提交| 命令 | 作用 |
|---|---|
pick | 保留该提交 |
reword | 保留提交,但修改提交信息 |
squash | 把该提交合并进上一个提交 |
edit | 停下来编辑该提交 |
drop | 删除该提交 |
黄金法则(必须记住)
绝不要 rebase 已经推送到远程、且别人可能基于它的提交。 rebase 改写历史哈希,别人 rebase 过的分支一 pull 就"炸"(历史对不上、冲突满天飞)。只对自己的、未推送的提交使用 rebase。
优点与缺点
| 说明 | |
|---|---|
| ✅ 优点 | 历史完全线性,像"一个人从头写到尾",最好读;合并冲突可以逐个提交解决 |
| ❌ 缺点 | 改写历史,共享分支上用就是灾难;每个重放的提交都可能触发冲突 |
什么时候用 rebase?
- 本地把个人分支"跟上"最新 main(
git rebase main)后在看板上会很干净 - 用
-i整理自己还没推送的提交(合并碎提交、改提交信息)
五、三者终极对比与选型(实战篇)
全维度对比表
| 维度 | merge | squash | rebase |
|---|---|---|---|
| 历史形态 | 非线性(有合并分叉) | 线性(压缩成 1 个提交) | 线性(重放全部提交) |
| 提交粒度 | 保留全部提交 | 压缩为 1 个 | 保留全部(哈希改变) |
| 过程可追溯性 | ⭐⭐⭐ 最强 | ⭐ 无(只看结果) | ⭐⭐ 中间(提交还在但哈希变了) |
| 冲突解决 | 一次解决所有 | 一次解决所有 | 每个提交都可能要解决 |
| 改写历史? | ❌ 不改写 | 🟡 只加 1 个新提交 | ✅ 改写原提交 |
| 共享分支安全性 | 最安全 | 安全 | ❌ 危险 |
| 典型场景 | 长期分支合并 | 功能分支 PR 合并 | 本地个人分支整理 |
决策流程图
要合并的分支情况?
│
├─ 别人也要基于它继续开发(共享分支)→ 绝对用 merge(不要 rebase)
│
├─ 功能已完成,合并进 main,想要干净主干 → squash merge
│
├─ 个人开发分支,想跟上 main 且历史整洁 → rebase main
│
└─ 只要某个分支的"一个提交" → cherry-pick(见第六章)团队约定比个人偏好重要
具体用哪种,先看团队和远端仓库的约定(GitHub 仓库可强制只允许 squash)。个人偏好服从项目惯例。
六、补充操作:fast-forward、cherry-pick、revert(进阶篇)
6.1 fast-forward(快进合并)
没有分叉时 merge 的默认行为——指针直接前移,不产生合并提交:
main: A ← B ← C ← D ← E (feature 的提交直接"接"在 main 后面)想要"明明可以快进也记录一次合并"?用 --no-ff。
6.2 cherry-pick(摘取单个提交)
把某一个提交的改动复制到当前分支,常用于修复版分支:
git cherry-pick 3f8a2c1 # 把提交 3f8a2c1 的改动应用到当前分支典型场景
功能在 main 已经合并了,但 release 分支紧急要其中一个 bug 修复——cherry-pick 单个提交过去,不用整条分支合并。
6.3 revert vs reset(撤销操作)
| 操作 | 原理 | 历史 | 使用时机 |
|---|---|---|---|
git revert <hash> | 生成一个反向提交 | 保留原提交,安全 | 已推送的提交(团队协作) |
git reset --hard <hash> | 直接把指针移回去 | 删除后续提交 | 本地未推送的提交 |
记住一个原则
已推送的历史,用 revert;未推送的历史,才用 reset。
七、冲突处理:合并绕不开的坎(实战篇)
7.1 冲突长什么样
两个分支改了同一处代码,Git 不知道听谁的,就在文件里标记出来:
<<<<<<< HEAD
const version = '2.0.0'; // 当前分支的版本
=======
const version = '1.9.9'; // 被合并分支的版本
>>>>>>> feature7.2 解决流程(三步走)
# 1. 手动编辑冲突文件,保留想要的内容,删掉 <<<<<<< ======= >>>>>>> 标记
# 2. 标记为已解决
git add 冲突文件
# 3. 继续合并
git merge --continue # merge 时
git rebase --continue # rebase 时7.3 merge 冲突 vs rebase 冲突
| merge | rebase | |
|---|---|---|
| 冲突出现的次数 | 一次性解决所有差异 | 每个重放的提交都可能冲突 |
| 中途想放弃 | git merge --abort | git rebase --abort |
| 冲突范围 | 两个分支的全部差异 | 每个提交点位的差异 |
缓解冲突的策略
- 小步提交、频繁合并——分叉越久,冲突越多
- 合并前先
git fetch同步最新 main,减少分叉 - 解决冲突时先看对方为什么改,别盲目删
八、常见坑与避坑(避坑篇)
| # | 坑 | 后果 | 避坑方法 |
|---|---|---|---|
| 1 | rebase 已推送的分支 | 队友 pull 后历史错乱、冲突爆炸 | 共享分支只用 merge;rebase 只用于本地未推送提交 |
| 2 | squash 后想单独回滚一个中间提交 | 做不到(中间提交根本不存在了) | 需要细粒度回滚就不该 squash |
| 3 | 以为会 fast-forward,结果冒出 merge commit | 莫名出现分叉 | 先 git status 看是否分叉,或约定 --no-ff |
| 4 | 解决冲突时误删对方的代码 | 功能悄悄消失 | 用 IDE 的冲突工具,逐块确认 |
| 5 | rebase 中断后不会恢复 | 卡在"rebase in progress" | 记牢 git rebase --abort 一键回退 |
| 6 | 团队未约定策略,混用三种方式 | 历史一会线一会叉,无法管理 | 先定团队规范(如 PR 统一 squash) |
最强提醒
rebase 和 reset 都是改写历史,误用无法通过普通手段找回且会影响他人。拿不准时,merge 永远是最安全的选择。
九、总结与练习
记忆口诀
线下想清楚,线上别乱来:
merge 保历史,squash 压成堆,rebase 改历史只对本地;
共享分支只用 merge,已推历史只用 revert。核心要点回顾
- ✅ merge:保留完整历史,产生 merge commit,最安全
- ✅ squash:把分支提交压成 1 个,主干干净,丢失过程
- ✅ rebase:重放提交到目标顶端,历史线性,改写哈希,只用于未推送提交
- ✅ fast-forward:无分叉时的自动快进
- ✅ cherry-pick:摘单个提交;revert 撤销已推送历史
- ✅ 冲突解决:merge 一次解决,rebase 逐个解决;都有 --abort 兜底
自测题(先做再看答案)
- 下图是哪种合并的结果?
A ← B ← C ← S (内容 = D+E) - 你在 feature 分支上开发的提交已经 push 到远端、团队共用了,现在想回到 main 顶端,该用 merge 还是 rebase?
git merge feature什么时候不会产生 merge commit?- release 分支需要 main 上的某一个 bug 修复提交,用什么命令?
点击查看答案
- squash merge——多个提交被压缩成单个提交 S(无法确认,因为丢了中间过程)
- merge。已推送的分支属于共享历史,rebase 会改写哈希导致团队混乱;merge 安全
- 当 feature 直接基于 main 顶端、main 期间没有新提交(没有分叉)时,Git 会 fast-forward 直接移动指针
git cherry-pick <提交哈希>
Git 命令速查表
| 想做什么 | 命令 |
|---|---|
| 合并分支(保留历史) | git merge feature |
| 强制生成合并提交 | git merge --no-ff feature |
| 压缩合并(PR 风格) | git merge --squash feature + git commit |
| 线性重放(改历史) | git rebase main / git rebase -i HEAD~3 |
| 摘取单个提交 | git cherry-pick <hash> |
| 撤销已推送提交 | git revert <hash> |
| 撤销本地提交 | git reset --hard <hash> |
| 中断合并/变基 | git merge --abort / git rebase --abort |
| 看提交图 | git log --graph --oneline --all |
相关笔记
- GitHub 安装包的命名规则 —— GitHub 相关技能(下载安装与分支协作同属 Git 生态)
- 进阶方向:团队协作工作流(Git Flow / GitHub Flow)可继续深入