代码考古指南:如何调查一行祖传代码
周一早上,测试同事指着屏幕说:“这个 if 不能删。”
我问为什么。
他说:“不知道,反正删了会炸。”
于是我们两个对着这行代码看了三分钟。它长这样:
1if (user.id === 0) {
2 return null;
3}注释写着:// 历史原因,勿动。
“历史原因”四个字,堪称程序员留给后人的甲骨文。你没法反驳,也没法理解,只能选择敬畏。但敬畏不能当饭吃,线上问题还得修。于是,我决定进行一次代码考古。
第一铲:先别删,先 blame
考古的第一原则:不要先动手,先看地层。
1git blame -L 118,124 -- src/order.tsgit blame 会告诉你每一行最后由谁、在哪次提交里改过。注意是“最后修改”,不是“最初作者”。一个人可能只是顺手调了个缩进,就替真正的“肇事者”背了锅。
如果文件改过名,记得加 --follow:
1git log --follow -p -- src/order.ts这条命令会把重命名之前的历史也翻出来。输出可能很长,可以配合 head 或分页器:
1git log --follow -p -- src/order.ts | head -n 80看到可疑提交后,先别急着下结论,把当时的上下文一起看:
1git show a1b2c3d --stat
2git show a1b2c3d -- src/order.ts--stat 看这次提交动了哪些文件,后面的路径限定只看这个文件。很多时候,答案不在代码里,而在提交信息里。比如那句经典的:
fix: 修复用户 ID 为 0 时订单丢失
好,至少知道这行代码不是凭空长出来的。
第二铲:用 pickaxe 找“这句话是什么时候出现的”
git blame 只能看到“最后改这一行的人”。如果这行代码被格式化工具整体重排过,blame 会指向一次毫无意义的“style: format code”。
这时候要用 git log -S,也就是常说的 pickaxe:
1git log -S "user.id === 0" --oneline -- src/order.ts它会找出新增或删除了这个字符串的提交。注意,是字符串出现次数的变化,不是“某行包含它”。所以重命名变量、改缩进都不影响它。
如果你不确定具体写法,可以用正则版本 -G:
1git log -G "user\.id\s*===?\s*0" --oneline -- src/order.ts-S 适合找“这句话什么时候来的”,-G 适合找“这类写法什么时候变过”。两者都不是万能的,但比盯着屏幕发呆强。
第三铲:追踪一行的“生平”
有时候你想知道某一行从生到死的完整经历,可以用 -L:
1git log -L 120,124:src/order.ts这会追踪第 120 到 124 行的历史,包括它们被移动、修改、删除的过程。行号会随着历史自动调整,比手动来回切提交舒服得多。
不过要提醒一句:-L 在大型仓库里可能比较慢,最好加上路径限定,别让 Git 把整个仓库翻个底朝天。
考古发现图鉴
挖了半小时,我大概总结出几类“遗迹”。它们有共同特征,也有相对固定的处理方式:
| 遗迹类型 | 典型特征 | 处理建议 |
|---|---|---|
| TODO 化石 | // TODO: 临时方案,下周删 | 查 issue;没有 issue 就补一个,别让它继续风化 |
| 注释打架 | 注释说“不能为 0”,代码判断 === 0 | 以测试和实际行为为准,先补测试再改 |
| 被注释掉的代码 | 一大段 /* 旧逻辑 */ | 有 Git 就别留;版本控制比你更会保存历史 |
| 魔法数字 | if (retry > 3) | 提成常量,并在注释里写清“3 从哪来” |
| 防御性废话 | if (true)、return; 后面还有代码 | 先跑测试,再删;别让“看起来没用”骗了你 |
| 幽灵配置 | 代码里写死 timeout: 5000,配置文件里也有 | 统一来源,删掉另一份,不然迟早对不上 |
其中最危险的,不是 TODO,也不是魔法数字,而是注释是错的。
错的注释比没有注释更可怕。没有注释,你至少会去读代码;有了错的注释,你会带着错误的前提读代码,然后得出一个非常自信的错误结论。
最危险的一铲:bisect 找“案发时间”
如果问题不是“这行代码为什么存在”,而是“这个行为从哪个版本开始不对”,那就该请出 git bisect 了。
先准备一个能自动判断“好/坏”的脚本,比如 ./check.sh,返回 0 表示正常,非 0 表示有问题。然后:
1git bisect start
2git bisect bad HEAD
3git bisect good v1.2.0
4git bisect run ./check.shGit 会自动在好版本和坏版本之间二分,跑完告诉你第一个坏提交。最后别忘了:
1git bisect resetbisect 的关键不是命令,而是那个判断脚本。脚本必须稳定、快速、可重复。如果脚本本身时好时坏,你得到的不是案发时间,而是更多悬案。
考古礼仪
挖到提交之后,还有几条不成文的规矩。
第一,不要在群里直接 @ 三年前的人。
尤其是已经离职的同事。你真正想问的往往不是“你当时怎么想的”,而是“现在能不能改”。前者没人记得,后者才需要答案。
第二,删代码前先补测试。
“看起来没用”是一种非常危险的直觉。先写一个测试锁定当前行为,再改代码。测试挂了,说明你挖到了真东西;测试过了,说明你可以放心地送走这段遗迹。
第三,给未来自己留张字条。
注释应该写“为什么”,而不是“是什么”。下面这种注释,等于没写:
1// 如果用户 ID 为 0 则返回 null
2if (user.id === 0) {
3 return null;
4}代码已经说了“是什么”。真正有价值的是:
1// 历史订单在迁移时把游客 ID 写成了 0,
2// 这里返回 null 是为了让上层走“无主订单”分支。
3// 等数据迁移脚本 v3 全量跑完即可删除。见 ISSUE-1842。
4if (user.id === 0) {
5 return null;
6}前者是复读机,后者是考古现场的路标。
结尾
那天下午,我们终于查清了:三年前的一次数据迁移把游客订单的用户 ID 写成了 0,而当时的修复方式是在查询入口直接返回 null。迁移早就跑完了,但这行代码一直留着,像一块没人敢动的碑。
我们补了测试,清了历史数据,删掉了那行 if。然后我在新代码里郑重地写下:
1// 临时方案,等下周接口上线后删除。写完我笑了一下。
因为我知道,三年后,会有另一个我,拿着 git blame 来找我。
而那时的我,大概率也已经不记得为什么了。