说起来有点丢人,我最早提 PR 的时候,干过不少让维护者看了直摇头的事:往 master 分支直接推代码、Commit message 写 "update"、"fix bug" 这种看了等于没看的描述、PR 描述里一个字都不写就点创建。当时我还觉得"代码能跑不就行了",直到被一个项目维护者在评论里礼貌地教育了一顿,我才开始认真琢磨一件事:到底什么样的 PR 才算"规范"。
后来参与的开源项目多了,自己也做了维护者,收到过几百个 PR,我才真正明白:规范地提交 PR 不是为了好看,而是为了降低协作成本。你和维护者之间隔着网络、隔着时区、隔着不认识的人,你的 PR 能不能被顺利合入,很大程度上取决于你替对方省了多少麻烦。这篇文章我就把一套完整、实用的 PR 提交流程给你捋清楚,配合图文说明,从 fork 仓库一直讲到 review 反馈后的更新,帮你从一开始就走对路子。适合刚接触开源贡献、或者提过几次 PR 但总被要求修改的同学。
1. 从"能提PR"到"会提PR":规范到底规范在哪
先想一个问题:PR 的本质是什么?它不是"我把代码发给你",而是"我基于你的项目做了一些改动,请你审查并决定是否合并"。这是一个请求,不是一次提交。既然是请求,姿态和方式就很重要。
很多刚入门的同学对 PR 有个误解,觉得"代码写对就行了"。其实在真实的开源项目里,维护者审查一个 PR,看的顺序通常是:先看 PR 描述清不清楚,再看分支和提交历史干不干净,然后才看 diff(代码改动),最后自己跑一遍测试。你的代码写得再好,如果前面几步一塌糊涂,很可能被拖着迟迟不合并,甚至直接被关闭。
那"规范"具体指什么?我把它拆成四个层面:
- 流程层面:不用 master 分支直接改,而是用独立的功能分支;保持分支和上游仓库同步;不把无关改动混进同一个 PR。
- 提交层面:Commit message 语义清晰,遵循项目的提交规范;每个 commit 只做一件事;历史整洁,没有一堆 "fix typo" 之类的垃圾提交。
- 描述层面:PR 标题和描述写清楚"做了什么、为什么做、怎么验证的",必要时附带截图和测试结果。
- 沟通层面:收到 review 反馈后及时响应、礼貌回复,更新 PR 的方式要正确,不覆盖协作者的工作。
这四个层面不是各自独立的,它们共同服务于一个目标:让维护者用最少的时间理解你的改动,并放心地把它合并进去。说白了,提交 PR 是一次社交行为 + 工程行为,规范本身就是你专业度的体现。
顺便说一个我当维护者之后特别有感触的现象:很多 PR 本身代码质量不错,但败在了"包装"上。比如分支只写了一串乱码似的哈希,或者描述里只有一句"fix issue",我得点开 diff 才能猜他想干什么。反之,那些模板填得工整、commit 历史清晰的 PR,我会下意识地更认真地 review,也更愿意在评论区多给一些指导。人都是这样,你尊重对方的时间,对方也会尊重你的代码。
这篇文章后面所有的内容,都是围绕这四个层面展开的。你按这套流程走,提 PR 这件事基本就不会再出大问题。
2. 提PR前的地基工作:仓库准备与分支管理
很多教程一上来就讲怎么点 New Pull Request,但我觉得那一步其实是最简单的,点两下按钮而已。真正决定你 PR 体验的,是之前一个小时的准备工作。这部分做得好,后面所有环节都顺。
2.1 Fork:把项目复制到你名下
进入你感兴趣的开源项目主页,点右上角的Fork按钮,在弹出界面确认一下仓库名和描述,然后 Create fork。这一步是在 GitHub 服务器端把原仓库复制一份到你的账号下,你自己的这份副本你想怎么折腾都行,不影响原项目。
Fork 完成之后,你需要在本地把代码拉下来。这里有个细节:Fork 出来的仓库地址和你 clone 的仓库地址,要分清。很多人只 clone 了自己 fork 的那个地址,后面想同步原仓库的最新代码时会一脸懵。我建议的做法是,fork 完成后直接执行:
# 用你自己的 fork 地址 clone 到本地 git clone git@github.com:你的用户名/项目名.git # 进入项目目录 cd 项目名clone 用的是 SSH 还是 HTTPS 取决于你本地是否配置了 SSH key。个人强烈建议配置 SSH,因为这个地址是git@github.com:...,后面 push 代码免密、安全,不会像 HTTPS 那样每次要输入账号密码(或者用到 Personal Access Token,稍微麻烦一些)。
2.2 设置 upstream:给本地仓库装一个"上游雷达"
clone 完之后有个一步必做但特别容易被新手忽略的操作:把原仓库添加为 upstream 远程源。
git remote add upstream git@github.com:原组织名/项目名.git注意原组织名不是你的用户名,是项目所属的组织或作者。执行完可以看一下当前配置确认无误:
git remote -v正常情况下你应该看到四个远程地址:两个指向你 clone 时的远程(通常是 origin),两个指向 upstream。这就是你这台电脑连接的"两端":origin 是你自己的 fork,upstream 是项目本体。
为什么必须配 upstream?因为开源项目每天都在更新。你 fork 下来的版本是个快照,过几天就跟不上原仓库了。没有 upstream,你就没法在本地拉取项目的最新代码,提的 PR 很容易跟最新代码冲突。有了它,一条命令就能同步:
git fetch upstream2.3 分支管理:永远不在 master 上动手术
接下来是分支策略。我见过的所有资深开源贡献者,几乎都有一个共同的铁律:永远不要在你的 master/main 分支上直接改代码、直接推提交。
原因很朴素:master 分支是用于跟 upstream 保持同步的"镜像分支",它应该是干净的。如果你在它上面改了一批代码,之后再想git pull upstream main同步最新代码,就会和你的本地改动纠缠在一起,冲突处理非常痛苦。
正确做法是每个 PR 开一个独立的新分支。比如你要修一个文档里的小 bug,或者加一个新功能:
# 先确保 master 是最新的 git checkout master git pull upstream master # 创建并切换到新的功能分支,名字要有语义 git checkout -b fix/docs-typo分支命名的规范这里多说两句。不要用patch-1、update-2这种 GitHub 网页端自动生成的名字(虽然网页端改文件确实会这样,但本地操作完全可以避免),用"类型/简短描述"的格式,一眼能看出这个分支是干什么的:
fix/login-page-crashfeature/user-avatar-uploaddocs/update-install-guiderefactor/cleanup-api-client
这样做有两个好处:你切分支时知道自己在干什么,维护者看你的 PR 时也能从分支名初步判断改动方向。另外一个小技巧是分支名里不要出现中文字符和特殊符号,纯字母、数字、连字符最安全。
2.4 保持分支同步:PR 前的关键一步
假设你开这个分支已经两周了,期间上游项目被别人合入了大量新代码。你提 PR 之前必须同步一次,否则大概率冲突。
同步方式有两种思路:
思路 A:把上游最新的改动合并到你的功能分支
git checkout fix/docs-typo git fetch upstream git merge upstream/main思路 B:把功能分支 rebase 到上游最新代码之上
git checkout fix/docs-typo git fetch upstream git rebase upstream/mainmerge 和 rebase 的区别一句话能说清:merge 会产生一个"分叉再汇合"的节点,历史不是直线;rebase 是把你分支上的提交"拔起来"重新种到目标分支最新提交之上,历史是一条直线。虽然网上关于该用哪个吵翻天,但我在给开源项目做贡献时更倾向 rebase,因为 PR 的历史看起来会非常干净——你提的 PR 里就只包含你自己改的那几个 commit,跟主线的最新状态严丝合缝。
提示:rebase 会改写你本地提交的历史,如果你已经把分支 push 到远程 fork 了,并且知道可能有别人基于你的分支在协作(多人在同一 fork 上开发),那 rebase 前要谨慎,以免改写掉别人的提交。单人做贡献的场景基本没这个问题。
同步完之后记得验证一下当前代码能不能正常跑,至少把已有的测试跑一遍再提 PR。千万不要同步完连构建都没跑就把 PR 扔出去,那是把风险往维护者身上推。
3. Commit信息的规范:让项目历史像一本清晰的账本
代码是写给机器执行的,但 commit message 是写给人看的。维护者在 review 你的 PR 时,第一眼看到的不是代码,而是提交历史。如果你的提交历史是一团乱麻,对方的耐心会迅速耗尽。
3.1 为什么 commit message 如此重要
想象这样一个场景:一个项目发布了一个新版本,用户反馈某个功能坏了,维护者需要快速定位哪个提交引入了问题。如果每条 commit 都写得清清楚楚,git bisect二分查找几分钟就能定位;如果提交信息全是 "update"、"fix",你连排查的欲望都没有。
更现实的是,commit message 是 PR 审查者的阅读地图。他看到一个 commit 标题写着fix: correct token refresh race condition,心里就有数了,知道该重点看哪段逻辑;如果看到的是update,他只能自己去 diff 里猜,这种 PR 谁看了都头疼。
3.2 Conventional Commits 规范
社区里最有共识的提交规范是Conventional Commits(约定式提交)。它的精炼格式是:
<type>(<scope>): <subject>常见的<type>类型我整理了一张表:
| 类型 | 含义 | 典型场景 |
|---|---|---|
| feat | 新功能 | 新增接口、新增页面、新增组件 |
| fix | 修复 bug | 修复崩溃、修复逻辑错误 |
| docs | 文档变更 | 更新 README、注释 |
| style | 格式调整 | 修改缩进、补分号,不涉及逻辑 |
| refactor | 重构 | 重命名变量、拆分函数,行为不变 |
| test | 测试相关 | 新增测试用例、修改测试代码 |
| chore | 杂务 | 更新依赖、修改构建脚本 |
| perf | 性能优化 | 优化查询速度、减少内存占用 |
| build | 构建系统 | 修改 Dockerfile、CI 配置 |
| ci | 持续集成 | 修 GitHub Actions 工作流 |
<scope>是可选的,写影响的范围,比如feat(api)、fix(auth)。<subject>用祈使句描述这次改动做了什么,不要写得像描述状态,要写"做了什么事",比如 "add user login endpoint" 而不是 "user login endpoint added"。
完整的 commit 还可以有 body(正文)和 footer(脚注)。body 说明为什么要这么做、改动的思路,footer 常用BREAKING CHANGE:标记破坏性变更,或者用Closes #123关联 issue。一个规范的例子:
fix(auth): correct token refresh race condition The previous implementation checked the token expiry before the async refresh completed, which could cause a race condition under concurrent requests. Move the refresh into a single mutex-protected queue to ensure only one refresh happens. Closes #1024对比一个反面教材:
update这种 commit 等于没写。如果你用了git commit时没填信息,Git 默认会让你进入编辑器,新手常常直接 :wq 退出导致提交信息是空的。每次提交前,花一分钟把信息写好,是成本最低的协作贡献。
3.3 一个提交只做一件事
与 commit message 规范配套的是提交粒度。你最好不要把一个 PR 里所有改动塞进一个巨大的 commit,也不要把一个改动拆成五个互相纠缠的 commit。理想状态是:每个 commit 是一个逻辑独立的改动单元,可以单独被理解、被 revert。
举个例子:你想给一个项目加"用户头像上传"功能,顺手发现了登录页有个 display bug。正确做法是开两个分支或者至少分两个 commit,一个 commit 只加头像上传,另一个 commit 只修 bug。如果你把两个毫无关系的改动塞在一起,维护者 review 时很难聚焦,万一第一个功能需要大改,第二个修好的 bug 也被拖着合不进去。
如果历史已经写乱了,怎么办?别慌,还有补救手段:
# 把最近 n 个 commit 合并成一个,并重新编辑提交信息 git rebase -i HEAD~n进入交互界面后,把非首个 commit 前的pick改成squash(简写s),保存后 Git 会把你写的所有提交信息汇总成一个编辑器让你重新写。这就是所谓的"整理历史",在你 push 之前做多少次都行。
3.4 commit 里不要出现的内容
再说几条我踩过坑之后的硬性注意点:
- 不要把密钥、token、密码提交进代码。一旦 push 到公开仓库,密钥就等于泄露了。这在审查时是非常严重的红线,大项目会直接关闭 PR。
- 不要把 IDE 配置文件、编译产物提交进去。确保
.gitignore正确配置。很多新手的 PR 里混入.idea/、.vscode/、node_modules/之类的文件,维护者看到就会皱眉。 - 不要用
git add .无脑添加所有文件。先git status看清楚改了什么,再git add单一文件或文件列表,最后git diff --cached检查一遍再提交。
提示:每次 commit 前养成的习惯是:
git status(看改了哪些文件)→git diff(看具体改动内容)→git add(精确添加)→git commit(写清楚信息)。这一步流程多花两分钟,能帮你在后面省两小时。
4. 推送代码与创建PR的完整流程
到了这一步,你的本地功能分支上有几个干净的 commit,你已经 fetch 过 upstream,测试也跑过了。现在终于可以推到远程并创建 PR 了。
4.1 推送分支到远程
推送之前先确认你当前的分支:
git branch --show-current然后执行:
git push -u origin fix/docs-typo这里的-u意思是把本地分支和远程分支关联起来,后续你直接用git push就能推,不用再写全参数。推送成功后,终端的输出里会直接给你一个链接,类似:
remote: Create a pull request for 'fix/docs-typo' on GitHub by visiting: remote: https://github.com/你的用户名/项目名/pull/new/fix/docs-typo直接复制这个链接到浏览器打开,GitHub 会直接把你带到创建 PR 的页面。
如果在推送时提示因为分支落后而 push 失败,那说明远端有人在你之前 push 过更新的代码。处理方式是先git pull --rebase origin fix/docs-typo拉取并变基,再重新推送。这里我再三提醒:不要用git push --force去覆盖远程分支,除非你明确知道自己在做什么。
4.2 创建 PR 的页面细节
进入 New Pull Request 页面后,你会看到三块核心区域:
base 与 compare 的选择器:这是最容易选错的地方。
base repository和base应该选原项目(不是你的 fork),分支选原项目的main(或master,看默认分支);compare repository选你的 fork,compare选你的功能分支。我的手速记忆法是:"左边是别人的 main,右边是我的 branch"。选对了,GitHub 会显示 "Able to merge",代表没有冲突;如果显示 "There isn’t anything to compare",多半是选反了。标题(Title):直接沿用你 commit 里最核心的那条信息即可。规范的做法是
fix(auth): correct token refresh race condition,简洁、语义清晰,跟你的提交规范保持一致。描述(Description):这里就是下一章节要重点讲的 PR 描述。先别急着点绿色按钮,确认你的描述够不够信息量。
创建 PR 时还有一个实用的弹层选项:"Create draft pull request"。如果你这个 PR 只是半成品、还在开发中,可以先发 Draft PR,等于昭告天下"这个 PR 先别合,我还想改"。把功能做完、测试通过后,点 "Ready for review" 把它转为正式 PR。这样做法既展示了进展,又不会给维护者添乱。
4.3 创建后的自查动作
很多新手创建完 PR 就觉得"完事了",其实发布后你需要立刻做两步自查:
第一,重新看一遍 Files changed 标签页。GitHub 会列出你所有改动过的文件和具体 diff。翻一遍,特别留意有没有不小心提交的无关文件(比如编译产物、锁文件乱改动)。这一步相当于你提交到公开 review 前的最后一道自查关卡。
第二,看 GitHub Actions 是否跑起来了。现在主流开源项目都配置了 CI,你的 PR 刚提交,云端会自动跑 lint、测试、构建。如果红色 ✗ 出现了,别等维护者来催,自己先去看构建日志修好它。我对维护者催 CI 这件事特别有执念:一个 CI 挂掉的 PR,就等于在跟维护者说"我连验证都没做",对方的信任度会立刻打折。
5. PR描述怎么写才专业:结构与模板
代码已经上去了,维护者会点开你的 PR,首先看到的就是标题和描述。现实中大量 PR 的描述都是空白或者只有一句话,这其实浪费了最好的沟通机会。一个合格的 PR 描述应该让维护者不点开代码也能大致判断你的改动。
5.1 维护者最想知道的四件事
我将自己在 review PR 时的内心诉求做了一个总结,输出为一套 PR 描述应该回答的问题:
| 问题 | 意思 |
|---|---|
| 这个 PR 做了什么? | 改动内容的一句话概述 |
| 为什么需要这个改动? | 修复了什么 bug、满足什么需求 |
| 怎么验证的? | 跑过什么测试、手动测试过什么 |
| 有什么潜在影响? | 是否会破坏现有功能、涉及哪些模块 |
如果你能把四件事写清楚,维护者的 80% 疑问都在描述里解决了,剩下的只需要看代码确认。这就是所谓的"替对方着想"。
5.2 一个可以直接抄的 PR 模板
很多成熟项目会在.github/PULL_REQUEST_TEMPLATE.md里提供自己的 PR 模板,你提 PR 时会自动带入。但如果没有,你可以参照我常用的这套结构:
## 概述 修复了登录模块中 token 刷新存在竞态条件的问题。 ## 改动内容 - 将 token 刷新过程改为通过互斥队列串行执行 - 补充了并发刷新场景下的单元测试 ## 验证方式 - 本地运行 `npm test`,全部 128 个测试通过 - 模拟 10 个并发请求刷新 token,未再出现重复刷新问题 ## 影响的模块 - `src/auth/tokenManager.ts` - `tests/auth/tokenManager.test.ts` ## 关联 issue Closes #1024注意其中"关联 issue"这一项的重要性。GitHub 支持在描述里写Closes #编号,PR 合并后会自动关闭对应 issue。这个机制能让项目追踪"某个 issue 是被哪个 PR 解决的",维护者非常看重。如果你的 PR 是对某个 issue 的回应,一定记得写上。
5.3 截图和演示:胜过一千行文字
对于前端项目、UI 改动或者涉及交互的功能,描述里贴截图或者 GIF 动图是极大的加分项。GitHub 支持直接把图片拖拽进描述框,会自动上传。我在审查一个 CSS 改动的 PR 时,如果对方贴了"修改前后对比截图",我基本不需要再花时间自己起项目去验证样式;反之没有截图的话,我只能 pull 下来跑一遍,成本高出不少。
测试结果的汇总也可以用简洁的方式展示。比如表格:
| 检查项 | 结果 | |--------|------| | 单元测试 | 128 passed | | E2E 测试 | 16 passed | | ESLint | 0 error | | 构建 | 成功 |格式不重要,表达"我验证过了"这个信息最重要。
5.4 标题的几个禁忌
最后提醒一下标题的坑。不要让 PR 标题变成另一个简短的问题描述,比如 "fix bug"、"update",这跟空描述没啥区别。标题推荐格式就是 commit message 的主体部分,比如:
feat(checkout): add promo code supportfix(login): prevent redirect loop when session expiresdocs: clarify installation steps for Windows
这种标题里带的feat、fix等前缀,跟你的 commit 规范呼应,保持一致性后,项目的 PR 列表整体看起来会非常专业。有些项目甚至会在合并时用这些前缀自动生成 changelog(版本更新记录),所以标题写规范是实打实有价值的。
6. 收到Review反馈之后:更新PR的规范姿势
PR 提交后,你可能会收到维护者的 comments,也可能有其他贡献者在你的 PR 下展开讨论。这个过程不可怕,被要求修改太正常了。我提了这么多 PR,几乎没有一次是一次通过的。真正重要的是:收到反馈之后,你的应对方式是否规范。
6.1 先理解,再动手
收到 review 意见后,我的习惯是通读全部评论,区分出三类:
- 必须改的:比如逻辑错误、风格不合规、测试不过。
- 可讨论的:比如"我觉得这个命名更容易理解",这时候可以礼貌回复自己的理由,也可以选择接受对方建议。
- 纯夸奖/锦上添花的:比如 "Great work!",适当回应即可。
如果是可讨论的,我建议至少先给出一个回应,说明你的思路和原因,而不是默默按对方说的改完——那样看起来好像是"我迫不得已才改"。当然,如果对方说得合理,接受建议并说一句 "Good point, updated." 是非常得体的沟通。
6.2 更新分支的两种姿势:新增 commit vs 改写历史
这是 PR 更新环节最核心的一个抉择:你的新修改,是直接叠加一个新的 commit,还是把原来的 commit 全部改写重组?
如果在 PR 的早期阶段,维护者还在做整体逻辑 review,建议直接新增一个 commit 来响应修改意见。原因很简单:新的 commit 让维护者能清楚地看到"从上一个版本到你最新版本之间改了哪些东西"——GitHub 的更新 diff 就是基于这个对比的。如果你把历史 rewrite 得干干净净,对方反而看不出你响应改动的过程。
如果在 PR 已经基本 review 完成、只差几个小问题时,可以走 rebase 把历史整理得干净一些再推进。常见做法是把所有响应性的小修改 squash 进对应的原始 commit,让最终合并时的历史没有一堆 "fix review comment" 之类的碎提交。
如果是自己本地独享的分支,推荐用 rebase 流程统一整理。以"把最近三个 commit 合并成一个"为例:
git rebase -i HEAD~3在弹出的编辑器里,将后两个pick改成s(squash),保存,然后在第二个界面上统一写一份完整的提交信息。完成后,如果你已经 push 过旧版本,就需要用强推来更新远程分支:
git push --force-with-lease origin fix/docs-typo6.3 force push 的正确打开方式
git push --force是危险操作,但有时候没法避免。这里我强烈推荐使用--force-with-lease而不是直接--force。区别在于:--force无条件覆盖远程分支;--force-with-lease会先检查远程分支是否和你本地记录的一致,如果不一致(说明别人可能已经推了新代码),它就拒绝执行。这是保护队友最后一根稻草,也能避免把协作伙伴的提交整消失。
需要强调一个常见误区:如果 PR 分支是你和另外一个人共用的(比如你们一个负责实现、一个负责测试,共用同一 fork 分支),改写历史 + force push 很容易把另一个人的提交冲掉。这种协作场景下,尽量用新增 commit 的方式来更新,不要频繁改写历史。
6.4 在 GitHub 上逐条回复评论
在评论区对每一条 review 意见做简要回复,是一个非常良性的沟通动作。哪怕只是 "Done." 或在代码里说明已修复位置,维护者也会觉得你认真对待了反馈。GitHub 的 review 评论如果被回复,会进入已解决/待解决的状态流转,方便对方二次审查。
另外我建议在把所有修改 push 上去后,在 PR 里追加一条总括性的评论,比如:刚刚根据 review 意见做了 N 处调整,主要集中在 X 和 Y,测试已全部通过,麻烦再 review 一下。这样维护者会主动回来看更新。不回应、不评论地默默 push,对方可能根本不知道你已经改了。
6.5 冲突来了怎么办
如果你的 PR 存在合并冲突,GitHub 页面会显示红色 "This branch has conflicts that must be resolved"。处理流程也很标准:
git checkout fix/docs-typo git fetch upstream git rebase upstream/mainrebase 过程中 Git 会标记冲突文件,你手动解决后:
git add 冲突解决后的文件 git rebase --continue完成后测试验证,然后强推更新 PR 分支。整个过程在本地操作即可,GitHub 上那个红字不用等它自己消失,它会在你的分支更新后自动重新计算合并状态。
7. 我踩过的那些PR坑:真实案例与避坑清单
最后一部分,我不打算再讲流程,而是给你一份"我亲手踩过、也亲眼见过别人踩过"的 PR 避坑清单。每一条都是真实教训,不看流程正规,但看这些坑,能少走很多弯路。
7.1 默认分支上直接开改
我认识的一位朋友第一次给开源项目贡献时,图省事没开分支,直接在 clone 下来的 master 上改了代码,然后 push 到了他自己 fork 的 master,又把这个分支和原仓库的 master 去比对创建 PR。结果就是 PR 里出现了大量不相关的历史差异,因为他的 master 落后了原仓库几百个 commit,GitHub 无法正确计算 diff。最后他只能把 fork 删了重新来一遍,白费了一个下午。
正确做法永远是:在干净的 master/main 基础上,拉一个独立的功能分支。我之前在 2.3 节讲的那个流程,就是从这一类惨痛教训里总结出来的。
7.2 忘掉同步 upstream,导致大量冲突
我有个习惯是提 PR 前一定会做一次git fetch upstream确认自己的分支不是落后状态。曾经有一次我没有同步,交上去的 PR 涉及到一个文件,而那个文件上游已经重构过三轮了。review 时维护者很客气地让我解决冲突,我本地却因为缺少最新的上游代码,处理得痛苦万分。
同步不是一次就够。你的 PR 周期拖得越长,越要记得时常git fetch upstream && git rebase upstream/main。把这个动作当作一个常规步骤,而不是出问题时的急救手段。
7.3 把"review 修改"压成乱麻
还有一种情况很常见:你反复提交了十几个 commit 来回应 review 意见,历史看起来像这样:fix comment 1、fix comment 2、add missing import、oops forgot this。这类碎提交会让 final 合并时的历史极其难看。
如果你和协作者没有共用分支,建议在 PR 接近完成时做一次 squash 整理,把全部改动整合成一两个干净的 commit。本地上操作是git rebase -i origin/main,进入交互模式把所有提交按需合并,然后git push --force-with-lease。整理完之后再看 PR 的 Files changed,等于是一个全新的、聚焦的 diff。
7.4 PR 里混入了与主题无关的改动
我 review 过一个"修复登录 bug"的 PR,结果 diff 里除了登录模块,还顺带把整个项目的代码格式化了一遍、升级了依赖版本、调整了两个 CSS 变量。不能说那些改动是错的,但它们和 PR 主题完全无关,导致我无法判断哪些改动会影响登录行为。
正确做法是,无关改动要么拆成单独 PR,要么干脆先不动,提 issue 告诉维护者。如果你只是帮忙修了一个旁边的 typo,可以顺便,但牵扯到格式化、重构、依赖升级这类大规模改动,一定要分开。PR 越小,review 越快,被合并的概率越高。这一条怎么强调都不过分。
7.5 测试没跑就提交
这个坑我已经提过多次,但因为它太重要了,还是放在避坑清单里压轴。我犯过一次最蠢的错误:改了一个配置文件的路径,本地没生效验证,直接 push 提 PR,结果 CI 构建直接挂掉。维护者找我说 "Build is broken",我只能马上修、重新推,整个过程极其尴尬。
我的建议是,在推送之前固定跑三件事:git status(看文件改动是否正常)、相关测试命令(npm test或项目文档里的命令)、lint 命令(如果有)。如果你改了文档或者纯注释,至少也要git diff检查一下格式。让 CI 挂掉,是比代码写得烂更让人对你的判断力失去信心的信号。
7.6 把 issue 和 PR 的关系写清楚
还有一个小建议:如果这个 PR 是为了解决某个 issue,在 PR 描述里写Closes #123会非常方便后续追踪。很多人知道这个语法但在描述里写了个寂寞,维护者还得手动去关联。这种"举手之劳"的事情,做到位了,你的贡献会显得格外上心。
我从第一次提 PR 的懵懂到现在偶尔review别人的PR,最深的感受是:**规范的 PR 流程不是束缚,而是一种互相尊重。**你尊重维护者的时间,他们也会更尊重你的劳动。把这篇文章里讲的流程走一遍,你的第一个 PR 或者下一个 PR,体验一定会完全不一样。