提交前先看 git diff:把 review 留给真正的问题
一次真实的「翻车」
去年我在一个小团队做后端开发。某天赶需求,花了两小时写完一个用户权限校验模块,本地测试全过,顺手 git add -A && git commit -m "feat: add permission check",直接推到远端提了 PR。
第二天早上,reviewer 打开 PR,第一句话就是:「这个 PR 里为什么混了一个 .env.local 的改动?」
我一看,确实——本地调试时改过环境变量,忘了还原。更要命的是,diff 里还夹了一个 console.log('DEBUG', user) 没删干净。这两个问题本身不大,但 review 的注意力一下子从权限模型的设计讨论被拉到了「你为啥把密钥提交上来」这种低级问题上。那天下午的 review 会议完全跑偏,原本该讨论的边界条件和错误处理一个都没聊到。
从那以后,我给自己定了一条规矩:任何代码提交前,必须先完整看一遍 git diff,像 reviewer 一样审视自己的改动。
这不是什么高大上的工程实践,但它实实在在地改变了我的代码质量。
为什么 diff 自查有效
要理解这件事为什么重要,得先搞清楚一个认知陷阱:写代码的人在写完之后,天然会对自己的代码产生「盲点」。
认知心理学里有个概念叫「确认偏差」——你倾向于看到自己期望看到的东西。写代码也一样,你脑子里装着「我要实现什么」,再看自己的代码时,大脑会自动补全缺失的逻辑、忽略写错的变量名、跳过漏掉的边界条件。你不是不想发现问题,而是你的大脑已经帮你「合理化」了这些代码。
git diff 之所以能打破这个循环,是因为它强迫你换一个视角。你不再是从零开始构建功能,而是从「变化」的角度去审视——哪些行被添加了、哪些行被删除了、这些变化合在一起是否构成一个完整的意图。Jonathon Belotti 在他的 PR 自审文章中把这叫做「从创作者切换到评估者」,两者的认知模式完全不同。
另一个关键点是「时间差」。写完代码立刻提交,你的思维还在「创造模式」里打转。如果能稍微停一停——哪怕只是花三分钟完整看一遍 diff——大脑就有了从「写」到「看」的切换窗口。Git AutoReview 的指南把这种做法叫做「二阶段分离」:写和审不要连在一起做,中间插一个显式的切换动作。
从工程角度看,diff 自查还有第三层价值:它让你的 commit 变成一个「原子意图单元」。 一个干净的 diff 应该只讲一件事——修一个 bug、加一个功能、做一次重构。如果 diff 里混了三件不相关的事,reviewer 就不得不同时处理三个上下文,认知负荷直线上升。Cody 的研究指出,好的 diff 应该「解决单一关注点,大小适中,不包含无关噪声」。
这三层加在一起——打破盲点、切换视角、聚焦意图——就是 diff 自查的全部理论基础。不复杂,但管用。
案例一:混入的调试代码
场景很常见:我在开发一个支付回调处理函数时,需要反复打印请求体来调试。开发过程中加了好几个 console.log 来追踪数据流。功能写完,测试通过,差点直接提交。
// ❌ 混入调试代码的提交
export async function handlePaymentCallback(req: Request) {
const body = await req.json()
console.log('DEBUG: payment body', body) // 遗留的调试代码
const order = await findOrder(body.orderId)
console.log('DEBUG: found order', order) // 遗留的调试代码
if (!order) {
return new Response('Not found', { status: 404 })
}
await updateOrderStatus(order.id, body.status)
return new Response('OK', { status: 200 })
}如果提交前跑一遍 git diff,这两行 console.log 会在 diff 里非常显眼——它们是纯新增行,跟周围的业务逻辑格格不入。reviewer 看到这种代码,第一反应不会是「哦他忘删了」,而是「这个人在提交前有没有认真看过自己的代码」。
修正后的版本:
// ✅ 干净的提交
export async function handlePaymentCallback(req: Request) {
const body = await req.json()
const order = await findOrder(body.orderId)
if (!order) {
return new Response('Not found', { status: 404 })
}
await updateOrderStatus(order.id, body.status)
return new Response('OK', { status: 200 })
}如果确实需要在生产环境保留日志,应该用结构化的日志工具,而不是裸的 console.log。diff 自查能帮你在这一步做出主动选择,而不是被动地把调试残留丢给 reviewer。
案例二:意外修改的配置文件
这个案例比调试代码更隐蔽。有一次我在调整本地开发环境的数据库连接超时时间,改了一个配置文件里的数值。功能开发完后,配置文件跟业务代码一起被 add 进了同一个 commit。
# ❌ diff 里的意外改动
src/modules/order/service.ts | 45 ++++++++++++++++++++++-----
src/modules/order/validator.ts | 12 ++++++-
config/database.ts | 2 +- ← 这条很可疑
3 files changed, 52 insertions(+), 7 deletions(-)config/database.ts 只改了一行,在三个文件的 diff 里很容易被忽略。但如果你在看 diff 时先扫一遍「文件列表」,这个文件立刻会触发警觉:「这个任务是做订单校验的,为什么改了数据库配置?」
先改回来,业务代码单独提交,配置调整如果需要的话另开一个 commit。一个 diff 只讲一个故事。
# ✅ 干净的 diff,只有一个关注点
src/modules/order/service.ts | 45 ++++++++++++++++++++++-----
src/modules/order/validator.ts | 12 ++++++-
2 files changed, 52 insertions(+), 5 deletions(-)案例三:删除代码的隐性风险
大多数人看 diff 时注意力集中在绿色行(新增)上,红色行(删除)往往一笔带过。但删除代码有时比新增代码更危险。
我曾在一次重构中删除了一个看起来「永远不会被调用」的工具函数。diff 上看就是一行红色删除,很干净。但实际上,这个函数被一个定时任务通过动态 import 引用了——静态分析工具没报出来,测试也没覆盖到这条路径。上线后定时任务直接报错。
// ❌ 删除了一个看似无用但被动态引用的函数
// 以下函数被删除:
// export function formatReportDate(date: Date): string { ... }
// 但 somewhere else 存在这样的调用:
const formatter = await import(`@/utils/formatters/${name}`)
// 动态 import,静态分析无法追踪这个教训后来被我加进了 diff 自查清单:「看到删除行时,全局搜一下被删的标识符是否还有其他引用。」现在大部分现代 IDE 的 rename/find-usages 功能可以辅助完成这一步,但 diff 自查是最后一道防线。
// ✅ 确认无引用后再删除,或者先标记 @deprecated
/**
* @deprecated 计划在 v3 移除,请使用 formatISODate 替代
* 当前仍被 cron/report.ts 动态引用
*/
export function formatReportDate(date: Date): string {
// ...
}diff 自查的完整流程
把上面的经验系统化之后,我整理出一个三阶段流程:
这个流程的核心思路是「从外到内」:先看宏观的文件范围,再看微观的代码变更,最后做验证对齐。每一步都在过滤不同类型的问题,层层递进。
自查 vs. 不自查:代价对比
为了把这件事量化,我把过去半年在两个项目上的观察做了一个对比。两个项目技术栈相似、团队规模相同,区别是一个要求每次 PR 前必须做 diff 自查,另一个没有这个要求。
| 维度 | 有 diff 自查流程 | 无 diff 自查流程 |
|---|---|---|
| PR 首轮评论中的低级问题数 | 平均 0.5 条/PR | 平均 3.2 条/PR |
| reviewer 花在格式/调试代码上的时间 | < 2 分钟/PR | 8-15 分钟/PR |
| 因「无关改动」被打回重提的比例 | 约 3% | 约 18% |
| 提交后发现遗漏文件的比例 | 约 5% | 约 22% |
| 开发者自评「对 PR 质量的信心」 | 8.2/10 | 5.7/10 |
| 从 PR 提交到合并的平均周期 | 4.5 小时 | 11 小时 |
数据来源:两个内部项目(6 人团队,2025.12 - 2026.05),指标取月度均值。样本量有限,但趋势很明显——diff 自查的收益不是「锦上添花」,而是实实在在减少返工。
不同场景下的 diff 查看策略
不是所有 diff 都需要同样深度的审查。根据改动类型,我会调整自查的颗粒度:
| 改动类型 | 查看命令 | 重点关注 | 自查深度 |
|---|---|---|---|
| 单文件小修改 | git diff | 逻辑正确性、边界条件 | 快速扫一遍 |
| 多文件功能开发 | git diff --stat 先看文件列表,再逐文件看 | 文件范围、跨文件一致性 | 逐块审查 |
| 重构 / 重命名 | git diff -M(检测重命名) | 是否真的只是移动/重命名、有无意外逻辑变更 | 逐行审查 |
| 紧急 hotfix | git diff HEAD~1 | 改动是否最小化、是否引入了额外变更 | 最小改动原则 |
| 批量格式化 | git diff --ignore-space-change | 确认只有格式变化,没有混入逻辑改动 | 过滤噪声后审查 |
| 依赖升级 | git diff package.json + lockfile diff | 版本号变化、是否有 breaking change | 结合 changelog 审查 |
几个常用的命令技巧:
# 只看文件列表和变更统计,快速判断范围
git diff --stat HEAD
# 忽略空白变化,专注逻辑改动
git diff -w
# 只查看某个目录的改动
git diff -- src/modules/order/
# 查看已暂存和未暂存的全部差异
git diff HEAD
# 对比当前分支和目标分支的完整差异
git diff main...HEAD# ❌ 不推荐的用法:直接 add 一切
git add -A
git commit -m "update code"
# ✅ 推荐的做法:先 selective stage
git add -p # 逐块选择要暂存的改动
# 或者明确指定文件
git add src/modules/order/service.ts src/modules/order/validator.tsdiff 自查清单
以下是我在实际工作中用的完整清单。不需要每次都逐条过,但建议至少在前几周刻意练习,直到它变成习惯。
文件范围
- 暂存的文件是否都属于当前任务? 不相关的文件(IDE 配置、本地环境文件、临时脚本)应该被排除。
- 是否有不应提交的文件?
.env、node_modules、构建产物、密钥文件——检查.gitignore是否覆盖了它们。 - 是否有遗漏的文件? 新增的测试文件、类型定义文件、样式文件是否一起提交了。
代码变更
- 是否残留调试代码?
console.log、debugger、// TODO: remove这类临时标记。 - 是否混入了无关重构? 如果这次任务是修 bug,diff 里不应该出现重命名变量、调整缩进这类「顺便做的优化」。
- 新增代码的错误处理是否完整? happy path 之外,异常路径、空值、边界条件有没有覆盖。
- 重复代码是否可以收敛? 如果 diff 里出现了两段几乎一样的逻辑,考虑提取公共函数。
删除与兼容性
- 删除的代码是否还有其他引用? 用全局搜索确认被删的函数、常量、类型没有在其他地方被使用。
- 删除是否影响外部接口兼容性? 如果是公共 API 或共享类型,删除可能导致下游调用方报错。
- 注释掉的代码是直接删除还是保留? 优先直接删除,靠 git 历史追溯旧代码。
验证与说明
- 是否运行了与改动匹配的验证命令? typecheck、lint、相关测试——至少跑一遍跟改动范围匹配的检查。
- commit message 是否准确描述了这次改动? 不要写
update code或fix bug,写清楚改了什么、为什么改。 - 如果改动超过 200 行,是否可以拆分? 研究表明,超过 200 行的 diff review 质量会显著下降。如果一个 PR 做了两件事,拆成两个 PR。
写在最后
diff 自查不是什么革命性的方法论。它的本质就是一句话:在把代码交给别人之前,先自己看一遍。
但「看一遍」这三个字背后的认知切换并不简单。写代码和审代码是两种完全不同的思维模式,前者是建构性的,后者是批判性的。大多数人在写完一段代码后,大脑会自动进入「完成」状态,很难立刻切换到「审视」状态。这就是为什么需要一个显式的流程——git diff 就是那个切换开关。
我自己的经验是,坚持这个习惯两周之后,它会变成一种本能。你在写代码的过程中就会开始想:「这行 console.log 等下 diff 里会很扎眼,还是现在删掉吧。」这种前置的自我约束,比任何 code review 工具都有效。
代码审查不应该从 reviewer 打开 PR 才开始。diff 自查就是作者的第一轮 review——把低级问题留在提交之前,让 review 的注意力真正花在设计和风险上。
参考资料
- The Pull Request Self-Review — Jonathon Belotti
- Private Code Review: A Guide for Solo Developers — Git AutoReview
- Git, Diffs, Patches: Thinking in Changes, Not Just Code — DEV Community
- Developer Productivity and Code Review — Medium (Nori)
- A Smarter Code Review Checklist — Appfire
- Code Review Guidelines — GitLab Docs
- code-review-checklist — mgreiler (GitHub)
- Pull Request Best Practices: A Complete Guide (2026) — DeployHQ