🛠️ 第9章 场景一:接手陌生代码库
📄 一句话概括

三个数字:改了几个文件、增删多少行、切成几段(hunk)。 最小变更不是「改得少」,是「目标改动之外一行都没有」。 同一个 bug,三种改法:最少九行,顺手重构四十三行,其中三十四行与 bug 无关。

📝 本章和前面几章的关系

上一篇讲改之前。这一篇讲改完之后,别人怎么看。 为什么「审查友好」值得单独成篇? 因为 AI 改代码的成本降下来之后,瓶颈从「写」移到了「看」。 一个人一天能看多少 diff 是有上限的,而这个上限决定了团队能并行多少事。 Demo 同一个 bug 修三遍:顺手重构、只修这个、修加抽函数。 三版都用真 git diff 取数字,再按「每行二十秒」估审查成本。

一、改动的三个数字

三个数字都能从 git diff --numstat 和 -U0 直接拿到。

数字 从哪拿 说明什么
文件数 numstat 的行数 审查要切换几次上下文
增删行 numstat 两列之和 要读多少行
hunk 数 -U0 里 @@ 的个数 改动散不散

第三个数字最容易被忽略,但它最能说明问题。 同样改二十行,集中在两段和散在十段,审查成本差好几倍—— 因为每开一段,审查者都要重新建立「这段在干什么」的上下文。 Demo 里 A 版的文件数是八、hunk 数是十三,行数四十三。 三个数一起看,你就知道这份 diff 大概要看多久。 单看行数会漏掉「散」这个维度。 这三个数是给人用的,不是给机器用的。 它们回答的问题是「这份改动值不值得现在看」—— 如果文件数远大于改动意图,答案通常是「先拆开再说」。

二、一次只做一件事

「最小变更」容易被理解成「改得越少越好」,这是错的。 改得少但没解决问题,等于零。 它真正的意思是:目标改动之外,一行都不多。 Demo 的 A 版是最好的反例。 它没有一行是错的——函数改名、加类型注解、把配置抽成类,每一件都是好事。 但它把这个 diff 从「这个 bug 修好了吗」变成了「这四十三行里哪几行是在修 bug」。 审查者要重新做一遍定位,而那本来是你该做的事。 所以「顺手」这两个字是成本转移。 你顺手省了五分钟,审查者多花二十分钟——而且他很可能漏掉真正的 bug,因为它在噪音里。 正确做法是把它拆成两个提交: 先一个只修 bug 的(九行),再一个只做重构的(三十四行)。 第一个能被快速确认,第二个可以慢慢看。 两个提交加起来还是四十三行,但审查成本差一个量级。

三、提交信息怎么写

提交信息是 diff 的目录。 好的提交信息让审查者能只读它、不看 diff,就判断「要不要细看」。

写法 审查者拿到什么
fix bug 什么都没有
fix: 空名字返回 400 知道改了什么
标题加两行正文 知道改了什么、为什么、怎么验

第二行正文讲「为什么」,第三行讲「怎么验」。 这两句是给未来的:半年后有人 git blame 到这一行,他要的不是「改了什么」(diff 里有), 是「当时为什么这么改」。 AI 写的提交信息常见毛病是「复述 diff」。 「修改 handle 函数,增加空字符串判断」——这句等于没说。 有用的版本会说清「check 只校类型不校空串,空串要进 save 才炸」。 这是 diff 里看不出来的因果。 所以给 Agent 的任务里要写清「提交信息里必须有为什么」。 不然它默认只复述改了什么——因为它看得见 diff,看不见你的推理过程。

四、动手 Demo:最小变更的三个数字

同一个 bug——空名字导致 KeyError——用三种改法各修一遍,每版都真跑 git diff 取文件数、 增删行、hunk 数。 再按「每行二十秒」的粗模型估审查成本。 这个模型是粗的,但三版之间的相对关系不粗。

def stat(s):
    nf = add = dele = 0
    for ln in s.split("\n"):
        p = ln.split("\t")
        if len(p) == 3 and p[0].isdigit():
            nf += 1
            add += int(p[0])
            dele += int(p[1])
    return nf, add, dele

def hunks(s):
    return len([x for x in s.split("\n") if x.startswith("@@")])

实跑输出:

--------------------------------------------------------------------
最小变更的三个数字
--------------------------------------------------------------------

  同一个 bug,三种改法,各自跑一遍 git diff 取真数字。
  bug 是:handle({"name": "  "}) 抛 KeyError。check 只校类型不校空串,空名字被送进 save 才炸。

  ① 先复现
  --------------------------------------------------------------------
  退出码 1,最后一行:KeyError: 'name'
  ! 这就是 500 的来源:一个只由空格组成的名字,在 store 那层才被发现。
| 改法 | 文件 | 增 | 删 | hunk | 非目标占比 |
|---|---:|---:|---:|---:|---:|
| A 顺手重构 | 8 | 33 | 10 | 13 | 79% |
| B 只修这个 bug | 2 | 7 | 2 | 2 | 0% |
| C 修 bug 加抽函数 | 4 | 14 | 4 | 5 | 50% |

  三个数字就是这三列:改了几个文件、增删多少行、切成几段(hunk)。
  ! B 那组是基准——为了修这个 bug,最少要动 9 行。A 动了 43 行,其中 34 行跟这个 bug 无关。
  A 版没有一行是错的,但它把「这个 bug 修好了吗」变成了「这些改动里哪几行是在修 bug」。
  审查者要重新做一遍定位。

  ② 审查者要花多少时间
  --------------------------------------------------------------------
| 改法 | 要读行数 | 粗估分钟 | 主要成本 |
|---|---:|---:|---|
| A 顺手重构 | 43 | 14 | 找哪几行在修 bug |
| B 只修这个 bug | 9 | 3 | 确认两处改动 |
| C 修 bug 加抽函数 | 18 | 6 | 多读一个文件 |

  按每行二十秒的粗模型算。真实情况更糟:改动散在 8 个文件里,来回切换的成本比读数本身还高。

  ③ 提交信息怎么写
  --------------------------------------------------------------------
| 写法 | 审查者能拿到什么 |
|---|---|
| fix bug | 什么都没有 |
| fix: 空名字返回 400 | 知道改了什么 |
| fix: 加两行正文 | 知道改了什么、为什么、怎么验 |


  [提交信息示例]
  | fix: 空名字返回 400 而不是 500
  | 
  | check() 只校类型,空串要进 save 才炸。
  | 改为先 strip,空则返回 400。
  | 顺带补一条测试 test_blank_name。
  第三行那种写法,让后来人能只读提交信息就决定「要不要看 diff」。

三组数摆在一起:最少九行,顺手重构四十三行,半顺手十八行。 非目标占比从零升到七成九——这就是「顺手」的代价被量化之后的样子。 重点在「B 是基准」这件事上。 为了修这个 bug,最少要动九行,这是任务本身的下限。 超出这个下限的部分,全是审查者要多承担的。 所以「最小变更」有一个可算的定义:贴着九行往下砍。 A 版那张表里,文件数是八、hunk 数是十三。 这两个数比行数更能说明「散」——十三段改动散在八个文件里,审查者要来回切十三次上下文。 最后那张提交信息表。 第三行那种写法(标题加两行正文)不是格式好看,是让审查者能「只读正文就决定要不要看 diff」。 这件事省下来的时间,可能比把 diff 写干净还多。

30 秒实验

  1. 翻一个你最近的提交,数三个数:文件、行、hunk。 文件数超过三而改动意图只有一件事的,就是可以拆的。
  2. 找一个「顺手改」的提交,试着把它拆成两个: 一个只做目标改动,一个只做顺手的事。看第二个还能不能独立成立。
  3. 把「每行二十秒」换成你自己的实际速度,算一下你上周审查花了多少分钟。 这个数就是「顺手改」向你收的费。

常见误区

  1. 「最小变更就是改得越少越好。」 改得少但没解决问题等于零。 它的意思是「目标改动之外一行都不多」。
  2. 「顺手重构是提高代码质量,应该鼓励。」 重构没错,混在 bug 修复里错。 拆成两个提交,质量照样提高,审查成本降一个量级。
  3. 「AI 生成的提交信息都差不多,改不改无所谓。」 差很多。 默认它只会复述 diff,而「为什么」是 diff 里没有的那部分。
自测三问(点开对答案) 1. 三个数字分别说明什么? —— **文件数说要切换几次上下文,增删行说要读多少,hunk 数说改动散不散。** 2. 为什么说「顺手」是成本转移? —— **你省五分钟,审查者多花二十分钟,还可能漏掉真正的改动。** **更糟的是它把定位工作推给了别人。** 3. 「最小变更」的可算定义是什么? —— **贴着任务本身的下限砍。** **Demo 里那个下限是九行,超出部分全是额外成本。**

最后一篇讲最难的场景:遗留项目。它多一条铁律——先补验证,再补文档。

相关笔记