← [ BACK_TO_NOTES ]

GitHub Pull Request 的三种合并方式

对比 GitHub PR 的 Create a merge commit、Squash and merge、Rebase and merge 三种集成策略,重点解释提交图、SHA、历史可读性、回滚与分支删除行为。

#synthesis #growing #git #git #github #github

[!info] related notes

GitHub Pull Request 的三种合并方式

覆盖范围

这篇笔记只讨论 GitHub Pull Request 最终进入目标分支时的三种标准合并策略:

  1. Create a merge commit
  2. Squash and merge
  3. Rebase and merge

它们解决的是同一个问题:

PR 已经通过 Review / CI 后,GitHub 应该以什么形态把 feature branch 的变化放进 main?

它们和日常在本地执行的 git merge、git rebase 有直接关系,但并不完全等价。Git 本身的基础差异见 Git中的merge和rebase。

[!important] “默认合并”不是固定规则 GitHub UI 中最常被叫作“普通合并”的选项正式名称是 Create a merge commit。仓库管理员可以在 Repository Settings 中启用或禁用三种方式,因此实际默认显示哪一种取决于仓库配置,而不是 GitHub 永远固定一种。

为什么三种方式值得单独理解

三种方式都能把最终代码内容放进 main,真正不同的是它们如何处理 Git 历史:

同一个 PR
  ↓
代码结果可能相同
  ↓
但 commit graph、SHA、回滚方式、历史可读性可能完全不同

因此选择合并方式,本质上是在决定:

  • feature branch 的原始 commit 是否保留;
  • main 是否保留一个明确的“PR 合并节点”;
  • 一个 PR 最终对应一个 commit 还是多个 commit;
  • 历史是否保持线性;
  • 后续 revert、bisect、删除分支时 Git 如何理解 ancestry。

先建立一个示例提交图

假设 main 当前是:

A---B---C  main

开发者从 C 创建 feature branch,并提交三次:

A---B---C  main
         \
          D---E---F  feature

PR 的内容就是 D + E + F 相对于 C 的变化。

下面三种方式,最终得到的历史完全不同。


1. Create a merge commit

GitHub UI:

Merge pull request
→ Create a merge commit

GitHub CLI:

gh pr merge <pr-number> --merge

提交图

合并后通常变成:

A---B---C-------M  main
         \     /
          D---E---F

M 是一个新的 merge commit,同时有两个 parent:

parent 1 = C
parent 2 = F

因此 Git 能明确看到:

D → E → F 是一条 feature branch,并且在 M 这个节点整体进入了 main。

原始 commit SHA 会怎样

D、E、F 原封不动保留。

也就是说:

feature branch 上的 D
=
main history 里的 D

没有重写 SHA。

这也是它和另外两种方法最重要的差异之一。

优点

  • 完整保留 feature branch 的真实历史;
  • PR 的分支拓扑清晰;
  • 所有原始 commit SHA 都还在;
  • 多人共享、长期 feature branch 比较安全;
  • 可以清楚看到“哪个 merge commit 对应哪个 PR”。

缺点

如果团队每个 PR 都有很多琐碎 commit:

fix typo
try again
fix test
address review
format

它们都会永久进入 main。

大量 PR 后,历史可能变成:

--M------M---------M----M---
 /      /         /    /
...    ...       ...  ...

commit graph 信息丰富,但阅读起来不一定简洁。

适合什么情况

优先考虑:

  • feature branch 本身的每个 commit 都有历史价值;
  • 多人共享同一分支;
  • 长周期 feature / release branch;
  • 团队希望保留真实分支拓扑;
  • 不希望重写任何已推送 commit。

分支删除行为

因为 main 的 merge commit M 把 D/E/F 纳入 ancestry:

D ∈ main history
E ∈ main history
F ∈ main history

所以合并后运行:

git switch main
git pull --ff-only origin main
git branch -d feature

Git 通常能够直接确认:

feature branch 已经 merged into HEAD。


2. Squash and merge

GitHub UI:

Merge pull request
→ Squash and merge

GitHub CLI:

gh pr merge <pr-number> --squash

这是短生命周期 feature / bugfix PR 中非常常见的选择。

提交图

feature branch 原来有:

D---E---F

GitHub 会把它们的最终 diff压成一个新的 commit,例如 S:

A---B---C---S  main
         \
          D---E---F  feature

从代码内容来看:

S ≈ D + E + F 的最终效果

但从 Git 历史来看:

S != D
S != E
S != F

S 是新 commit,有自己的新 SHA。

核心心智模型

feature branch 的“开发过程”
D → E → F

          squash
             ↓

main 只保留“最终结果”
             S

这是一种非常典型的:

feature branch 可以 messy,main history 必须 clean。

优点

  • 一个 PR 最终通常对应一个 commit;
  • main 非常容易阅读;
  • 临时修复 commit 不污染永久历史;
  • 单个 PR 很容易整体 revert;
  • 对短生命周期 Bug / feature branch 非常合适。

例如 main 最终可以保持:

A
↓
feat(web): add onboarding
↓
fix(api): reject stale revision
↓
fix(web): scope temporal actions by fact semantics
↓
feat(ai): add diagnosis replay

而不是把开发过程中几十个 WIP commit 全带进去。

缺点

  • feature branch 原始 commit 不进入 main;
  • 单个 commit 的精细历史被压掉;
  • 如果 feature branch 每个 commit 都经过精心设计并有独立意义,squash 会丢失这种结构;
  • Git 的 ancestry 不会认为原 feature commit 已经成为 main 的祖先。

BodySense 的真实例子

Bug 修复分支:

fix/144-body-state-temporal-actions

feature branch 原 commit:

303cb509
fix(web): scope temporal actions by fact semantics

PR #145 使用 Squash Merge 后,main 上得到的是一个新的 commit:

653aaa5f
fix(web): scope temporal actions by fact semantics (#145)

因此:

303cb509 != 653aaa5f

但两个节点对应的是同一项 PR 修复的开发版本与最终集成版本。

为什么 Squash Merge 后删除分支可能出现 warning

这次实际执行:

git branch -d fix/144-body-state-temporal-actions

Git 提示:

warning: deleting branch 'fix/144-body-state-temporal-actions'
that has been merged to
'refs/remotes/origin/fix/144-body-state-temporal-actions',
but not yet merged to HEAD

原因不是代码没有进入 main,而是 Git 的安全判断基于 commit ancestry。

Squash 后:

main:
A---B---C---653aaa5f

feature:
         \
          303cb509

main 中没有 303cb509 这个原始 SHA,因此:

303cb509 ∉ ancestors(main)

Git 无法只通过 commit graph 判断:

“653aaa5f 的代码其实已经包含了 303cb509 的最终效果。”

所以会出现 warning。

真正应该先验证的是:

git switch main
git pull --ff-only origin main
git log -5 --oneline

以及 GitHub PR 是否已经是 Merged。

CLI 也可以验证:

gh pr view <pr-number>

确认 PR 已合并后,再删除 feature branch 即可。

[!warning] 不要仅因为 git branch -d / -D 能执行,就把它当成“代码已经进入 main”的证明。对于 squash / rebase 类型的集成,先确认 PR merge 和 main 上的最终 commit。

适合什么情况

非常适合:

  • GitHub Flow;
  • Bugfix branch;
  • 小到中型 feature PR;
  • 一个 PR 希望对应一个业务变化;
  • 开发过程中有 WIP / review-fix / test-fix commit;
  • 希望 main 保持简单、线性、易回滚。

这也是很多个人项目、应用项目和 PR 驱动仓库的常用默认选择。


3. Rebase and merge

GitHub UI:

Merge pull request
→ Rebase and merge

GitHub CLI:

gh pr merge <pr-number> --rebase

提交图

假设 feature 分支上有:

D---E---F

GitHub 会把这些 commit 逐个重新应用到当前 base branch 顶部。

最终类似:

A---B---C---D'---E'---F'  main

注意:

D' != D
E' != E
F' != F

逻辑 commit 被保留下来了,但因为 parent 改了,每个 commit 的 SHA 都会重新计算。

为什么 SHA 一定会变

Git commit 的 SHA 不只是文件内容的 hash,它还包含:

  • tree;
  • parent commit;
  • author / committer metadata;
  • commit message 等。

Rebase 改变了 parent:

原来:D parent = C
重放后:D' parent = 最新 base

所以会产生新 commit 对象和新 SHA。

优点

  • main 完全线性;
  • 不产生 merge commit;
  • 不像 squash 那样把所有 commit 合成一个;
  • 可以保留 feature branch 中“每个 commit 的逻辑边界”。

最终历史像:

A---B---C---D'---E'---F'---G---H

非常适合希望:

main 是线性的,同时每个 commit 本身也有意义。

缺点

  • 原始 feature commit SHA 不保留;
  • 多个 commit 都会进入 main,因此 feature branch 的 commit 质量必须高;
  • 如果 feature branch 里有 fix typo、try again、address CI,这些也会成为永久历史;
  • 对审计“原始远端 commit SHA”不友好;
  • 删除原 feature branch 时,同样可能遇到 ancestry 相关提示。

适合什么情况

优先考虑:

  • 团队强烈要求线性 main;
  • feature branch 的 commit 已经精心整理;
  • 每个 commit 都应该可以独立理解 / review / bisect;
  • 不希望有 merge commit;
  • 但又不想 squash 掉 commit 边界。

三种方式直接对比

维度Create a merge commitSquash and mergeRebase and merge
main 是否产生 merge commit是否否
feature 原始 commit 数量是否保留是否,压成 1 个逻辑上保留
feature 原始 SHA 是否保留是否否
main 是否线性否,保留分支图是是
一个 PR 最终通常对应几个 commitN + 1 个 merge commit1 个N 个
是否保留 feature 分支拓扑是否否
对 WIP commit 的容忍度中等高低
整体 revert 一个 PR较方便,但 merge revert 有 parent 语义最简单多 commit 时更麻烦
main 历史阅读体验信息完整但可能复杂最简洁线性且细粒度
合并后原 branch SHA 属于 main ancestry是否否

三种提交图放在一起看

Merge commit

A---B---C-------M
         \     /
          D---E---F

保留:

D E F + branch topology + M

Squash

A---B---C---S

保留:

D + E + F 的最终结果

但不保留:

D / E / F 的原始 commit identity

Rebase

A---B---C---D'---E'---F'

保留:

D / E / F 的逻辑拆分

但每个都是新 commit。


Rebase and merge 和“先 rebase feature 到 main”不是一回事

这是一个很容易混淆的地方。

开发过程中你可能执行:

git fetch origin
git rebase origin/main

这是在做:

同步 / 整理 feature branch。

而 GitHub PR 中选择:

Rebase and merge

是在做:

最终把 PR 集成进 base branch 的策略。

因此完全可能有:

feature 先 rebase 到最新 main
        ↓
PR 最终仍然 Squash and merge

这两件事不冲突。

同理:fast-forward merge 也不是 GitHub PR UI 中单独列出的第四种标准按钮。GitHub 的 Rebase and merge 虽然得到线性历史,但它通过新的 rebased commits 形成最终历史,不应简单理解成“把原 feature branch 原封不动 fast-forward 进 main”。

如何选择

可以用一个简单决策树:

这个 PR 的每个 commit 都值得永久保留吗?
│
├─ 否
│   ↓
│  Squash and merge
│
└─ 是
    │
    ├─ 是否需要保留真实 branch topology / 原始 SHA?
    │   ↓
    │  是 → Create a merge commit
    │
    └─ 否,只想要线性历史
        ↓
       Rebase and merge

对于典型 GitHub Flow 应用仓库,可以进一步简化:

Bugfix / 普通 feature
→ Squash and merge

精心设计的一组独立 commits
→ Rebase and merge

长期共享分支 / 需要保留分支历史
→ Create a merge commit

对短生命周期 Bug 分支的推荐

一次标准 Bug 修复通常经历:

Issue
→ fix branch
→ 测试
→ commit
→ PR
→ CI / Review
→ merge
→ 删除 branch

这类分支的目标不是永久保存开发过程,而是永久保存修复本身。

因此常见推荐是:

Squash and merge

最终 main 会留下一个容易理解的节点:

fix(web): scope temporal actions by fact semantics (#145)

详细闭环见 Bug 修复完整工作流。

Merge 策略不会替代 CI / Branch Protection

无论最终选择哪一种:

Create a merge commit
Squash and merge
Rebase and merge

都不应该绕过:

  • required status checks;
  • branch protection / rulesets;
  • Review requirement;
  • merge queue(如果仓库启用);
  • commit policy。

Merge Strategy 解决的是:

已经允许合并之后,历史怎么落进 main。

它不负责证明代码本身已经安全。

Issue 自动关闭和 Merge Strategy 无关

PR body 中写:

Fixes #144

当 PR 合并进入默认分支后,GitHub 可以自动关闭对应 Issue。

这和选择:

merge / squash / rebase

没有本质关系。

合并后的标准收尾

无论使用哪种方式,都建议:

git switch main
git pull --ff-only origin main

先确认 main 已经拿到最终 PR 结果。

再检查:

git log --oneline -5

然后删除本地 feature branch:

git branch -d <feature-branch>

如果是 squash / rebase,并且 Git 因 ancestry 无法证明已合并,不要马上使用 -D。

先确认:

GitHub PR = MERGED
main = 最新 origin/main
main 上存在本 PR 的最终 commit / diff

确认无误后再进行清理。

远端分支不再需要时:

git push origin --delete <feature-branch>
git fetch --prune

最短记忆模型

Merge commit
= 保留整个分支历史

Squash
= 保留 PR 的最终结果

Rebase
= 保留 commit 逻辑,但重写历史

再加一条 SHA 规则:

Merge commit → 原 feature SHA 保留
Squash      → 原 feature SHA 不进入 main
Rebase      → 原 feature SHA 被重写

理解这两组规则,基本就能解释 GitHub PR 三种合并方式的大多数行为。

创建于 2026/8/31 更新于 2026/8/31
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本