git log --graph 中清晰的提交线图方便 code review 和回退定位。两条互补的策略:git pull --rebase 把无意义的分叉抹平,git merge --no-ff 把一组相关提交刻意保留成一个"鼓包"。看起来矛盾,目的一致 —— 让线图表达意图,而不是过程。
pull –rebase:抹掉无意义的分叉
默认 pull 出了什么
本地和远端各自有了新提交,pull 时若不是 fast-forward,Git 默认 merge,会多出一个没有任何代码意义的"Merge branch ‘main’ of …“提交:

假设 pull 前:
A---B---C origin/main
/
D---E---F---G main
git pull(默认 merge)后多出 H:
A---B---C
/ \
D---E---F---G---H main, origin/main
git pull --rebase 后线图变平,F G 被基于 C 重放成 F’ G’:
D---E---A---B---C---F'---G' main, origin/main
写进配置,不要每次记得加
git config --global pull.rebase true # pull 默认 rebase
git config --global pull.ff only # 不能 fast-forward 就拒绝(避免误产生 merge commit)
git config --global rebase.autostash true # rebase 前自动 stash 未提交修改
Git 2.27+ 起,未显式配置 pull.rebase 时每次 pull 会警告,等于在催你做一次明确选择。
什么时候别用 rebase
- 冲突预期很多 —— rebase 每个被重放的 commit 都可能触发冲突,merge 只触发一次。
- 已 push 出去的本地分支 —— rebase 会改写 SHA,强推会把基于它工作的同事埋掉。
- 想保留"这一组改动是同一时间合入"的语境 —— 那是
--no-ff的活。
merge –no-ff:把一组提交保留成"鼓包”
git merge --no-ff <branch> 在可以 fast-forward 时也强制生成一个 merge commit,把整条 feature 分支保留为可识别的形状。
先看 ff 与 no-ff 在线图上的差别
合并前(feature 从 A 拉出,main 期间没动 —— ff 的前提):
A ─ main
\
B ─ C ─ feature
git merge feature(默认,能 ff 就 ff):
A ─ B ─ C ─ main, feature ← main 指针滑到 C,无新 commit,feature 拓扑消失
git merge --no-ff feature(强制不 ff):
A ─────────── M ─ main ← M 是新 merge commit,两个 parent: A、C
\ / git log --graph 能看到 feature 这一段"鼓包"
B ─ C ─────

--no-ff只在 ff 可行时才有意义。main 在 feature 期间也有新提交(拓扑已经分叉)时,Git 无论如何都会生成 merge commit,加不加--no-ff一样。
鼓包带来的两个隐性价值
- 语义边界。merge commit 的 message(默认
Merge branch 'feature/xxx')天然标注"以下这串提交都属于同一个特性"。code review 时一眼分组;git log --merges直接列出所有 feature 合并点做 release notes;git bisect区分"哪个特性引入了 bug"也更精确。 - 整体回滚。
git revert -m 1 <merge-sha>一条命令撤回整个特性,-m 1指定保留第一个 parent(main 侧),feature 侧的所有改动整体反转。比一笔笔git revert简单一个量级。
强制全局 no-ff(团队约定后再开):
git config --global merge.ff false # 自己手动 merge 时强制不 ff
git config --global merge.ff only # 反过来:只允许 ff,禁止生成 merge commit(线性派)
你们项目可能本来就是 no-ff
走 PR / MR 流程的团队,服务端的 merge 策略比本地 merge.ff 更优先。常见平台默认值:
| 平台 / 选项 | 等价行为 |
|---|---|
| GitLab Merge method = Merge commit(默认) | --no-ff |
| GitLab Merge method = Fast-forward merge | ff(要求先 rebase) |
| GitHub PR = Create a merge commit(默认) | --no-ff |
| GitHub PR = Rebase and merge | rebase + ff |
| GitHub / GitLab Squash | 整 PR 压成 1 commit + ff |
判断你们用哪种:git log --graph --oneline main 看一眼。一堆 Merge branch ... 鼓包 = no-ff;纯一条直线 = ff(多半还 squash 了)。
合并前先 rebase,让线图更干净
将 feature 合入 dev 前的标准动作:先把 feature 重排到最新 dev 之后,再 --no-ff merge,分叉点会上移,鼓包紧凑:
git fetch origin
git switch feature
git rebase origin/dev # 把 feature 重放到最新 dev 之后
git switch dev && git pull
git merge --no-ff feature # 鼓包合入
判断 feature 是否落后于 dev:git log feature..dev 列出"dev 有而 feature 没有"的提交,有输出 = feature 落后,需要先 rebase;无输出可以直接合。

--rebase=merges:rebase 时保留 merge 鼓包
普通 rebase 会把 merge commit 抹平。如果一段历史里有重要的 --no-ff 鼓包要保留,用:
git rebase --rebase-merges origin/main # 旧名 --preserve-merges 已废弃
平台 squash merge 改变了什么
GitHub / GitLab / Bitbucket 的 “Squash and merge” 把整个 PR 压成一条 commit 合入主干。结果是:
- 主干线图天生干净,本地的
--rebase/--no-ff都变得次要。 - 但本地分支 push 后回到 GitHub 看到的还是原始 commit,PR 描述应当承担起"语义边界"的责任 —— 这部分原本是 merge commit 在干的事。
- 整体回滚也变成了
git revert <squashed-sha>,一条命令同样能撤。
结论:用 PR squash merge 的团队,本地策略可以放宽到只关心 pull.rebase = true;自托管直接 push 到主干的团队,--no-ff + git rebase --rebase-merges 的组合仍然是首选。
决策表
| 场景 | 命令 | 目的 |
|---|---|---|
| 日常 pull | git pull --rebase | 抹平无意义分叉 |
| 合入 feature 分支 | git merge --no-ff feature | 保留语义鼓包 |
| 合并前同步基 | git rebase origin/dev | 让鼓包紧凑 |
| rebase 含 merge 的历史 | git rebase --rebase-merges | 保留已有鼓包 |
| 团队走 PR squash | 只配 pull.rebase=true | 平台已处理线图 |
References
– EOF –