提交前先看 git diff:把 review 留给真正的问题

0阅读9分钟

一次真实的「翻车」

去年我在一个小团队做后端开发。某天赶需求,花了两小时写完一个用户权限校验模块,本地测试全过,顺手 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 自查的完整流程

把上面的经验系统化之后,我整理出一个三阶段流程:

流程图画布 · 115%
Mermaid 流程图加载中...

这个流程的核心思路是「从外到内」:先看宏观的文件范围,再看微观的代码变更,最后做验证对齐。每一步都在过滤不同类型的问题,层层递进。

自查 vs. 不自查:代价对比

为了把这件事量化,我把过去半年在两个项目上的观察做了一个对比。两个项目技术栈相似、团队规模相同,区别是一个要求每次 PR 前必须做 diff 自查,另一个没有这个要求。

维度有 diff 自查流程无 diff 自查流程
PR 首轮评论中的低级问题数平均 0.5 条/PR平均 3.2 条/PR
reviewer 花在格式/调试代码上的时间< 2 分钟/PR8-15 分钟/PR
因「无关改动」被打回重提的比例约 3%约 18%
提交后发现遗漏文件的比例约 5%约 22%
开发者自评「对 PR 质量的信心」8.2/105.7/10
从 PR 提交到合并的平均周期4.5 小时11 小时

数据来源:两个内部项目(6 人团队,2025.12 - 2026.05),指标取月度均值。样本量有限,但趋势很明显——diff 自查的收益不是「锦上添花」,而是实实在在减少返工。

不同场景下的 diff 查看策略

不是所有 diff 都需要同样深度的审查。根据改动类型,我会调整自查的颗粒度:

改动类型查看命令重点关注自查深度
单文件小修改git diff逻辑正确性、边界条件快速扫一遍
多文件功能开发git diff --stat 先看文件列表,再逐文件看文件范围、跨文件一致性逐块审查
重构 / 重命名git diff -M(检测重命名)是否真的只是移动/重命名、有无意外逻辑变更逐行审查
紧急 hotfixgit 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.ts

diff 自查清单

以下是我在实际工作中用的完整清单。不需要每次都逐条过,但建议至少在前几周刻意练习,直到它变成习惯。

文件范围

  1. 暂存的文件是否都属于当前任务? 不相关的文件(IDE 配置、本地环境文件、临时脚本)应该被排除。
  2. 是否有不应提交的文件? .envnode_modules、构建产物、密钥文件——检查 .gitignore 是否覆盖了它们。
  3. 是否有遗漏的文件? 新增的测试文件、类型定义文件、样式文件是否一起提交了。

代码变更

  1. 是否残留调试代码? console.logdebugger// TODO: remove 这类临时标记。
  2. 是否混入了无关重构? 如果这次任务是修 bug,diff 里不应该出现重命名变量、调整缩进这类「顺便做的优化」。
  3. 新增代码的错误处理是否完整? happy path 之外,异常路径、空值、边界条件有没有覆盖。
  4. 重复代码是否可以收敛? 如果 diff 里出现了两段几乎一样的逻辑,考虑提取公共函数。

删除与兼容性

  1. 删除的代码是否还有其他引用? 用全局搜索确认被删的函数、常量、类型没有在其他地方被使用。
  2. 删除是否影响外部接口兼容性? 如果是公共 API 或共享类型,删除可能导致下游调用方报错。
  3. 注释掉的代码是直接删除还是保留? 优先直接删除,靠 git 历史追溯旧代码。

验证与说明

  1. 是否运行了与改动匹配的验证命令? typecheck、lint、相关测试——至少跑一遍跟改动范围匹配的检查。
  2. commit message 是否准确描述了这次改动? 不要写 update codefix bug,写清楚改了什么、为什么改。
  3. 如果改动超过 200 行,是否可以拆分? 研究表明,超过 200 行的 diff review 质量会显著下降。如果一个 PR 做了两件事,拆成两个 PR。

写在最后

diff 自查不是什么革命性的方法论。它的本质就是一句话:在把代码交给别人之前,先自己看一遍。

但「看一遍」这三个字背后的认知切换并不简单。写代码和审代码是两种完全不同的思维模式,前者是建构性的,后者是批判性的。大多数人在写完一段代码后,大脑会自动进入「完成」状态,很难立刻切换到「审视」状态。这就是为什么需要一个显式的流程——git diff 就是那个切换开关。

我自己的经验是,坚持这个习惯两周之后,它会变成一种本能。你在写代码的过程中就会开始想:「这行 console.log 等下 diff 里会很扎眼,还是现在删掉吧。」这种前置的自我约束,比任何 code review 工具都有效。

代码审查不应该从 reviewer 打开 PR 才开始。diff 自查就是作者的第一轮 review——把低级问题留在提交之前,让 review 的注意力真正花在设计和风险上。


参考资料

评论 0

0 / 1000