# Pull Request 到底是什么：Git 里没有这个命令，页面上的“合并”又是谁在合

PR 是托管平台上的编号记录，合并仍是服务器上的一次 merge——三种合并按钮各留下什么，以及为什么合完本地还得自己 pull

> Git 远程仓库 · Git 合并 · Git 基础操作 · 约 6 分钟 · 10 月 21 日

## 本篇要点

1. PR 是托管平台上的一条带编号的记录，写的是“把 head 分支合进 base 分支”，评论、评审和检查都挂在它上面；Git 本身没有这个概念。
2. 平台会为每个 PR 额外造出只读引用 refs/pull/N/head 和 refs/pull/N/merge，所以别人的 PR 也能 fetch 到本地，说明 PR 的内容仍然是提交和引用。
3. “能不能合并”由服务器在它自己的仓库里算，用的是与本地 merge 相同的三路合并逻辑，而且只代表此刻的状态。
4. 三种合并按钮的差别只在于服务器在 base 上留下什么：默认的合并提交保留你原来的每个提交，squash 压成一个新提交，rebase and merge 逐个重放并换掉全部哈希。
5. 合并改写的只是服务器仓库的分支指针，本地 main 和 origin/main 都不会自动更新，必须自己 fetch 再 pull。
6. 用 squash 或 rebase and merge 之后，你本地的提交对象没有被 main 引用，所以删分支可能被拒，继续在同一分支上开新 PR 还会把旧提交带出来。

---

你已经会 `add`、`commit`、`push` 了。真正进团队那天，第一件事通常是“开个 PR 吧”。

在 GitHub 上填个标题点一下，页面出现编号 #7，底下写着 `feature → main`，还有一串检查状态和一个绿按钮 Merge pull request。你点了，再回本地，`git status` 说你的 `main` 落后 1 个提交。

两个地方想不通。第一，Git 的命令表里没有 `pull-request`，那这个按钮是谁在合并？第二，合都合完了，为什么我的电脑什么都不知道？

这篇就回答这两个问题。答案的关键是一句话：**PR 是托管平台上的东西，合并仍然是 Git 的合并，只是执行的人换成了服务器。**

## Git 在两台机器之间只会干两件事

先回想你已经用熟的部分。`push` 是把你这边对方没有的提交送过去，然后请求对方挪一个指针；`fetch` 是把对方的提交取下来，然后挪你本地的 `origin/main`。都是“传提交 + 挪指针”，方向相反而已。

这里没有任何“申请”“评审”“批准”的位置。Git 是一个传输协议，它不关心你为什么要挪这个指针。历史上这套流程是靠邮件完成的：写好改动、导出成补丁发到邮件列表、等维护者回一句“应用了”。（`git request-pull` 是那个年代的遗留物，只负责拼一段说明文字。）

## PR 是平台在仓库边上多加的一层记录

GitHub、GitLab 除了跑一个 Git 服务，还跑着一个网站和一个数据库。你点“New pull request”时，它做的事是：在数据库里新建一条记录，写明 base 分支（要合进哪儿）和 head 分支（从哪儿来），给它编号 7，然后把之后的评论、评审意见、CI 结果统统挂在这条记录上。

这条记录不属于 Git。GitLab 管它叫 merge request，指的是同一个位置上的同一样东西，只是名字不同。

但它也不是一张纯表单。你开 PR 的那一刻，平台顺手在仓库里造了两条**只读**引用：

- `refs/pull/7/head`，指向 head 分支的最新提交；
- `refs/pull/7/merge`，指向一个“假装现在就合并”的模拟合并提交（有冲突时这条不生成）。

所以别人的 PR 你能拿到本地跑：

```bash
git fetch origin pull/7/head:review7
git switch review7
```

这说明 PR 里的内容仍然是提交和引用，只是平台替你多存了一份，还给它编了个号。这两条引用只读，你往里推会被拒（`deny updating a hidden ref`）。

## “能不能合并”是服务器自己算的

PR 页面上那句 “This branch has no conflicts with the base branch”，不是装饰。服务器在它自己那份仓库里算了一遍：head 是不是 base 的后代？能快进吗？如果不能，两边的共同祖先是谁，三路合并有没有冲突？

就是第三篇[讲合并](https://pangzhengboyin.com/articles/git-merge-fast-forward-vs-merge-commit-31113dee)时的那套算法，只是执行的人从你的笔记本换成了服务器。

注意它算的是**此刻**。别人的 PR 先合进去，你这个判断就过期了，页面会变成有冲突，或者提示你 Update branch。这不是平台在故意刁难，是共同祖先确实变了。

## 点下去那一下，服务器动了哪个指针

点 Merge pull request，服务器在自己的仓库上执行一次合并，然后挪 `refs/heads/main`。三种按钮的区别，就在于它执行哪一种：

```mermaid
flowchart LR
  PR["PR #7：feature → main"] --> A["Create a merge commit（默认）"]
  PR --> B["Squash and merge"]
  PR --> C["Rebase and merge"]
  A --> A1["保留你原来的每个提交，另加一个合并提交"]
  B --> B1["压成一个新提交，没有合并提交"]
  C --> C1["逐个重放，哈希全变，没有合并提交"]
```

- **Create a merge commit** 是默认项，等于 `--no-ff`：哪怕能快进，也硬造一个有两个 parent 的合并提交，你原来那些提交全部保留。
- **Squash and merge** 把 PR 里所有提交压成一个新提交放到 base 头上。这个新提交的父提交就是当前 base 的头，所以指针直接前移，属于快进。
- **Rebase and merge** 把每个提交挨个重放到 base 上，也是快进，但每个提交都换了新哈希。它和本地 `git rebase` 还有一点细微差别：GitHub 会重写 committer 信息、丢掉原本就是空的提交。

三者共同的地方是：真正被改写的只有服务器仓库里的 `refs/heads/main`。“PR 被合并了”在 Git 层面就是一句话——服务器上那个指针挪到了一个包含你改动的位置。

## 所以本地必须自己同步一次

合并发生在服务器的仓库里，跟你这台机器没有任何通信。合并那一刻，你本地什么都没有变：`main` 还指着开分支之前那个提交，`origin/main` 还停在上次通信时的位置。

后一件事在讲远程仓库那篇里说过：`origin/main` 只是本地书签，只在 `fetch` 和 `push` 时更新（[远程仓库到底连的是什么](https://pangzhengboyin.com/articles/git-remote-origin-fetch-vs-pull-9d6d92b7)）。服务器挪了指针，不会有人来通知你，也没有谁该通知——两个仓库之间没有这种推送关系。

```bash
git switch main
git pull            # 拉下新提交，把 main 指针前移
```

如果你当时还在功能分支上，只跑 `git fetch origin`，那更新的只是 `origin/main`，你的本地 `main` 依旧是旧的，得再把它快进过去才会动。

## 一个跟着来的坑：squash 之后，你本地的提交还在

用合并提交那种方式，你原来的提交对象进了 `main`，从 `main` 能走过去。squash 和 rebase and merge 都是造了新提交，你本地那些旧提交则留在原地，从 `main` 一条路也走不到它们——虽然内容已经在里面了。

这会造成几个很别扭的现象：

- `git branch -d feature` 可能拒绝删除，因为它判断的是“这些提交对象是否已被合并”，而不是“内容是否已在里面”；
- 你如果接着在这个分支上改，再开一个 PR，之前已经合过的提交会重新出现在新 PR 的提交列表里，还可能重复撞上同样的冲突。GitHub 官方文档专门提醒了这一点；
- 想强推清理，就回到[推送被拒那篇](https://pangzhengboyin.com/articles/why-git-push-rejected-force-with-lease-f816f580)的规则：分支是你自己的，`--force-with-lease` 可以；分支是共享的，就老实重新开一条。

省事的做法是：合并后就在页面上把远端分支删掉，本地切回 `main`、`pull`，再 `git branch -D feature`，下次从最新的 `main` 重新开分支。

## 收束

PR 是平台上的流程记录：编号、描述、评论、评审、检查，这些都真实存在，也确实是团队协作真正在用的东西。但“合并”不是它的特权。它替你做的还是 merge 那套事——找共同祖先、比对三方、挪指针——只是发生在服务器上。

想通这一点，那一整串现象就好解释了：合并后本地要自己 `pull`；PR 关闭不等于分支被删；反过来，你在本地手工 `merge` 再 `push` 上去，效果和点按钮一样（前提是你有写权限，而且推得上去）。

## 术语表

- Pull Request（Merge Request）：托管平台上的一条编号记录，声明把 head 分支合进 base 分支，评论、评审和检查都附在它上面，Git 本身没有这个东西。
- base 与 head：PR 里两个分支的叫法，base 是改动要合进去的目标分支，head 是带来改动的那条分支。
- refs/pull/N/head：平台为一个 PR 额外创建的只读引用，指向 head 分支的最新提交，让任何人都能把别人的 PR 取到本地。
- Squash and merge：把 PR 里所有提交压成一个新提交放到 base 上，原来那些提交对象不再被 main 引用，内容在但历史不在。

## 来源

1. [GitHub Docs: Pull request merges — 合并提交、squash、rebase and merge 三种方式各在历史里留下什么](https://docs.github.com/en/pull-requests/reference/pull-request-merges)
2. [GitHub Docs: About pull requests — refs/pull/N/head 与 refs/pull/N/merge 两条只读引用的定义与用法](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)
3. [GitHub Docs: Merging a pull request — 合并需要写权限，squash 与 rebase 走快进，rebase and merge 产生新哈希](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/merging-a-pull-request)

---

原文：https://pangzhengboyin.com/articles/what-is-a-pull-request-d2584b00

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
