- 引言 — PR 不是代码,而是沟通
- 小而目的单一的 PR
- 提交信息: Conventional Commits
- 正文里写"为什么",而不是"改了什么"
- 上传前先做自我审查
- 好的 PR 说明: 背景、变更、测试
- 对审阅者的共情
- 用堆叠 PR 拆分大工作
- 结语
- 参考资料
引言 — PR 不是代码,而是沟通
PR 提交上去,却好几天都没人来审阅——这种经历大概每个人都有过。反过来,有的 PR 一提交就被打上"LGTM",马上就合并了。这是代码能力的差距吗?未必如此。能快速合并的 PR,除了技术之外,还有一门沟通的技术。
核心的视角是这样的。你打开一个 PR 的那一刻,并不只是写了代码,而是向审阅者发出了一个请求——"请理解并批准这个变更"。审阅者不知道你脑子里在想什么,他手里只有 diff 和你的说明。所以一个好的 PR,归根结底是让审阅者用最少的努力获得最大确信的 PR。
这篇文章整理了写出这类 PR 和提交信息的实用习惯。如果 Git 命令还不太顺手,不妨先去本站的 Git 实验场用眼睛熟悉一下分支和提交的流程,再回来读也不迟。
小而目的单一的 PR
如果只能选一个效果最大的习惯,那无疑是把 PR 保持得小。在一个 1000 行的 PR 和十个 100 行的 PR 之间,审阅质量更高的一方压倒性地是后者。
原因在于人类的注意力。审阅者面对一个大 diff 时,会发生两件事。第一,注意力下降,漏掉 bug。第二,因为负担太大,审阅会被一拖再拖。研究和现场经验都表明,审阅发现缺陷的比率会随着变更规模增大而急剧下降。
所以原则是一个 PR 只装一个目的。
差的 PR: "用户资料功能"
- 新增 API 端点
- 顺手替换了无关的日志库
- 全文重新调整了缩进风格
- 修了 3 个错别字
→ 审阅者: 我该看什么? 这个风格改动是不是藏着 bug?
拆成 3 个好的 PR:
PR 1: 新增资料 API 端点 (功能)
PR 2: 替换日志库 (基础设施)
PR 3: 风格/错别字整理 (杂项)
→ 每个都能独立审阅、合并、回滚
把功能变更、重构和格式化混进同一个 PR,审阅者就得费力分辨哪些行是"真正的变更"、哪些只是"挪了个位置",很快就会疲惫。把格式化单独提交,重构拆到独立的 PR 里——光是这样做,审阅速度就会明显加快。
提交信息: Conventional Commits
提交信息也有一套广泛使用的惯例。Conventional Commits 规定在提交的第一行用前缀标出变更的种类。
<类型>(<范围>): <摘要>
feat(auth): add password reset via email
fix(api): handle null user in settlement job
docs(readme): clarify local setup steps
refactor(cart): extract price calculation
test(order): add cases for partial refunds
chore(deps): bump lodash to 4.17.21
常用的类型有 feat(功能)、fix(修 bug)、docs(文档)、refactor(不改变行为的改进)、test(测试)、chore(构建、依赖等杂项)。
这套规约的好处不止于统一格式而已——它让机器也能读懂提交。因为能区分 feat 和 fix,工具就能自动升版本号(语义化版本控制),自动生成变更日志(CHANGELOG)。对人也有好处,只要扫一眼 git log,这个项目发生过什么事情一目了然。
摘要行(第一行)还有几条惯例。用祈使语气写("add"而不是"added"),控制在 50 字左右,末尾不加句号。可以把它想成填空——"应用这个提交之后,会___"。
正文里写"为什么",而不是"改了什么"
这里是区分初级和中级的分水岭。新手的提交信息记录的是改了什么,但这一点 diff 本身已经展示过了。真正需要的是为什么改。
弱的信息(改了什么 - diff 里已经有):
fix: change timeout from 30 to 60
好的信息(为什么 - diff 里没有):
fix(upload): raise timeout to 60s for large video uploads
结账后将原始视频(最大 2GB)传给编码服务器时,
30 秒的超时经常被打破,导致上传失败。
实测 p95 传输时间为 48 秒,因此留出余量将其提到 60 秒。
根本性的解决方案(分块上传)另外在 issue #482 中处理。
下面这条信息,对六个月后再来看这段代码的人(通常是未来的你自己)是决定性的。它提前回答了"为什么恰好是 60 秒?30 秒不行吗?"这个问题,还说明了这不是根本解决方案,并链接了后续 issue。代码能说明它做了什么,但为什么会变成这样,只有人能记录下来。而提交正文,正是这份记录的位置。
归纳成规则就是: 摘要行之后空一行,下面的正文描述背景、原因和权衡。琐碎的提交(比如改错别字)可以不写正文,但只要提交里包含了某个决策,就一定要留下"为什么"。
上传前先做自我审查
在提交 PR 之前、在发给别人之前,从头到尾把自己的 diff 读一遍这个习惯,效果好得惊人。这就是把自己当成审阅者,去读自己的代码。
# 提交前,把自己改了什么整体再看一遍
git diff main...HEAD
自我审查抓到的东西通常都很琐碎,但恰恰是会让审阅者恼火的那类: 留下了调试用的 print 或 console.log,顺带混进来注释掉的死代码,不小心提交了临时文件,或者提交信息里有错别字。这些如果被审阅者发现并留下评论,就会多一次往返,合并也因此推迟。
自我审查提前消除了这次往返。想象一下"审阅者会在这里问什么",提前在代码旁边给出可能被问到的问题的回答。比如"这部分看起来可能有点奇怪,但外部 API 只会以这种格式响应,没有别的办法"这样的自我注释,能大大节省审阅者的时间。
好的 PR 说明: 背景、变更、测试
PR 说明是审阅者在读 diff 之前看到的第一个画面。在这里把方向交代清楚,审阅会顺畅很多。推荐按三个维度来组织。
## 背景 (Context / Why)
为什么需要这个变更。有什么问题或需求促成了它。
关联 issue 的链接。让审阅者不需要背景知识也能理解。
## 变更 (What changed)
改了什么、怎么改的。如果 diff 很大,按文件/区域简短说明。
如果有重要的设计决策,也说明其依据。
## 测试 (How it was tested)
是怎么验证的。新增的测试、手动确认的步骤、截图等。
让审阅者相信"这确实能跑"的依据。
这三个维度依次回答了审阅者必然会有的三个问题——"为什么要做这个?""改了什么?""真的能跑吗?"。尤其不要漏掉测试这一节。如果是 UI 变更,一张前后对比的截图胜过一百句解释。审阅者一旦确认"这个人验证过了",就会安心,并倾向于批准。
对审阅者的共情
贯穿这一切习惯的一个态度,就是对审阅者的共情。提交 PR 时,想象屏幕另一端有一个时间紧张的同事。任何能降低他认知负荷的行为,都会让你的 PR 更快通过。
具体来说是这样:
- 引导阅读顺序: "先看
parser.py,再看使用它的main.py,这样更容易理解"——这样一句话,就能成为审阅者的路标。 - 对重大决策提前说明依据: 有争议的选择(比如"为什么用 Y 而不是 X")要写在说明里,不要等审阅者来问。
- 让 PR 大小匹配审阅者的时间: 紧急的热修复更要小。即便是大功能,也需要拆成可审阅的单元这份体贴。
- 不要防御性地对待反馈: 审阅评论针对的是代码,不是人格。"好的,已经采纳了"比争辩更快。
审阅归根结底是人与人之间的事。持续提交能帮到审阅者的 PR,信任会逐渐积累,下一个 PR 也会通过得更快。
用堆叠 PR 拆分大工作
"保持小"这个原则和"必须做出一个大功能"这个现实经常发生冲突。这时有用的技巧是堆叠 PR(stacked PRs)。把大工作拆成一串相互依赖的小 PR,后一个 PR 叠在前一个之上。
main
└── PR 1: 数据库 schema + 迁移 (独立审阅)
└── PR 2: 在 PR 1 之上的仓储层
└── PR 3: 在 PR 2 之上的 API 端点
└── PR 4: 在 PR 3 之上的 UI 接入
每个 PR 都以紧邻它下面的那个 PR 作为基准分支。这样一来,审阅者依次审查四个 200 行的片段,每个片段都建立在前面的上下文之上理解——比一次性扔出 800 行要好得多。前面的 PR 合并之后,把后面 PR 的基准改回 main 继续。
堆叠 PR 在管理上需要一点勤快,因为前面的 PR 一旦被修改,后面的 PR 就得跟着变基。但大多数团队工具都能帮上忙,而"可审阅的大小"带来的收益,足以覆盖这份成本。一旦看出某个功能有变大的苗头,从一开始就按堆叠来设计,比日后再费力拆分一个庞大的 PR 要轻松得多。
结语
快速合并的 PR,秘诀不在于华丽的代码,而在于对审阅者的体贴。小而目的单一的 PR、用 Conventional Commits 整理出的历史、包含"为什么"的提交正文、自己动手做的自我审查、包含背景·变更·测试的说明,以及用于大工作的堆叠 PR——这一切指向的都是同一件事: 让审阅者用最少的努力获得最大的确信。
写代码只是一半的工作,剩下的一半,是让别人理解并信任这份代码。如果 Git 工作流还不熟悉,可以去 Git 实验场亲手操作分支、合并和变基,培养手感。工具用顺手之后,就能把更多精力花在打磨好的 PR 上。
参考资料
- Conventional Commits 规约: https://www.conventionalcommits.org/
- Google Engineering Practices, "The CL author's guide": https://google.github.io/eng-practices/review/developer/
- "How to Write a Git Commit Message" (Chris Beams): https://cbea.ms/git-commit/
- GitHub Docs, "About pull requests": https://docs.github.com/en/pull-requests
현재 단락 (1/73)
PR 提交上去,却好几天都没人来审阅——这种经历大概每个人都有过。反过来,有的 PR 一提交就被打上"LGTM",马上就合并了。这是代码能力的差距吗?未必如此。能快速合并的 PR,除了技术之外,还有一...