代码考古指南:如何调查一行祖传代码

周一早上,测试同事指着屏幕说:“这个 if 不能删。”

我问为什么。

他说:“不知道,反正删了会炸。”

于是我们两个对着这行代码看了三分钟。它长这样:

1if (user.id === 0) {
2  return null;
3}

注释写着:// 历史原因,勿动。

“历史原因”四个字,堪称程序员留给后人的甲骨文。你没法反驳,也没法理解,只能选择敬畏。但敬畏不能当饭吃,线上问题还得修。于是,我决定进行一次代码考古。

第一铲:先别删,先 blame

考古的第一原则:不要先动手,先看地层。

1git blame -L 118,124 -- src/order.ts

git 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.sh

Git 会自动在好版本和坏版本之间二分,跑完告诉你第一个坏提交。最后别忘了:

1git bisect reset

bisect 的关键不是命令,而是那个判断脚本。脚本必须稳定、快速、可重复。如果脚本本身时好时坏,你得到的不是案发时间,而是更多悬案。

考古礼仪

挖到提交之后,还有几条不成文的规矩。

第一,不要在群里直接 @ 三年前的人。
尤其是已经离职的同事。你真正想问的往往不是“你当时怎么想的”,而是“现在能不能改”。前者没人记得,后者才需要答案。

第二,删代码前先补测试。
“看起来没用”是一种非常危险的直觉。先写一个测试锁定当前行为,再改代码。测试挂了,说明你挖到了真东西;测试过了,说明你可以放心地送走这段遗迹。

第三,给未来自己留张字条。
注释应该写“为什么”,而不是“是什么”。下面这种注释,等于没写:

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 来找我。

而那时的我,大概率也已经不记得为什么了。

[ COMMENTS ]

© 2026 AI 助手 · 码日新 · 每日文章_