Git 也该有“懂业务”的对比了:一篇博客把冷门 diff driver 讲明白了

Git 世界里,最容易被低估的能力之一,就是“比较差异”。
大多数开发者每天都在用 git diff,看代码改了哪几行、删了哪个函数、加了什么配置。可一旦文件内容不再是那种适合逐行阅读的纯文本,事情就会立刻变味。比如 OpenAPI 规范、依赖元数据、自动生成的 JSON 清单,甚至某些半结构化配置文件——你明明知道它们变了,而且可能变得很重要,但 Git 给你的输出,往往像在拿放大镜看沙尘暴。
最近,英国软件工程师 Jamie Tanna 发布了一篇博客,专门解释如何构建一个 git diff 的外部 diff driver。文章不长,技术味很浓,却戳中了一个长期存在的空白:Git 当然支持这件事,但相关文档既分散又不显眼,很多开发者知道 textconv,却未必意识到,自己其实可以把 git diff 变成一个“更懂业务语义”的比较器。
Git 不缺 diff,缺的是“语义层”的 diff
Jamie Tanna 写这篇文章的起点很实在:他在实现 renovate-packagedata-diff 时,发现网上关于如何给 git diff 接入外部命令的资料并不充分。后来又受到 Andrew Nesbitt 关于 Git Diff Drivers 的文章启发,再加上这周研究如何用 oasdiff 比较 OpenAPI 规范,于是干脆把这个坑填了。
这类问题之所以越来越常见,和软件工程的现实变化有关。今天的代码仓库里,真正需要审查的早就不只是 .go、.js 或 .py 文件。越来越多关键变更来自自动化工具:依赖机器人更新版本,CI 生成产物,平台导出 API 描述,基础设施工具更新配置。这些文件通常“技术上是文本”,但“阅读体验上接近二进制”。你能 diff,但你不想看。
Git 过去给出的经典解法是 textconv:把不适合直接比较的内容,先转换成更友好的文本再 diff。这招对很多场景够用,比如把二进制文件转成元信息,把复杂格式转成普通文本。但 Jamie 强调,有些场景不是“先转文本”就能解决的。开发者想要的不是更好看的原文,而是更有含义的输出,比如一份 API 变更日志,直接告诉你哪个接口新增了、哪个字段被标记为废弃、哪个响应结构有破坏性变化。
说白了,这不是格式问题,而是解释权问题。你不是想看文件怎么变,你是想知道系统行为怎么变。
原来 git diff 给外部工具传了 7 个参数
Jamie 这篇文章里最“涨知识”的部分,是把 git diff 调用外部 diff driver 时的参数机制讲清楚了。
很多人会下意识以为,外部工具只需要接收两个路径:旧文件和新文件。确实,很多命令行工具本身也是这么设计的——tool before after,简单粗暴。但 Git 没这么省事。它会传 7 个参数,除了仓库内文件名,还包括比较前后的临时文件路径、对应 SHA-1、以及文件模式。
这听上去有点繁琐,甚至有点“Git 式”的古典主义:明明是比较文件,为什么连权限模式都一起带上?可这恰恰说明 Git 的 diff 不是简单文本对比器,而是版本对象系统的一部分。它不仅关心内容,也关心对象身份和状态。对外部工具来说,这些附加信息并非负担,而是能力边界的延展。你可以识别一个文件是新建、删除还是修改,也可以基于 SHA 做缓存,避免反复计算昂贵的差异。
Jamie 还特别提到,当文件新增或删除时,Git 会用 /dev/null 表示不存在的一侧,而无关参数则用 . 填充。这种细节如果没人点破,开发者很容易踩坑。很多“看上去脚本能跑”的实现,一旦碰到文件新增删除就会翻车。也正因为如此,这篇博客的价值不在于发明了什么新机制,而在于把一套埋在文档深处的老能力,重新翻译成今天开发者能直接上手的实践知识。
用 oasdiff 比 OpenAPI:这才是机器生成文件该有的阅读方式
文章里最具代表性的例子,是把 oasdiff 包装成一个 Git diff driver,用来比较 OpenAPI 规范。
这个思路非常漂亮。OpenAPI 文件通常很长,JSON 或 YAML 结构层级多、字段密集、顺序变化还可能干扰阅读。你用普通 git diff 去看,经常会陷入“文件确实变了,但我还得自己脑补影响”的状态。而 oasdiff 这样的专用工具,输出的是更接近产品和平台团队关心的结果:新增了哪些接口,哪些参数变了,是否有 breaking changes。
Jamie 给出的 bash 脚本也很克制:如果旧文件是 /dev/null,说明文件被新增;如果新文件是 /dev/null,说明文件被删除;否则直接调用 oasdiff changelog 去生成可读变更日志,并强制保留彩色输出。它不复杂,甚至有点朴素,但恰恰说明这类能力真正有价值的地方,不在于宏大的工程,而在于“轻量整合”。
从记者视角看,这背后折射的是一个越来越明显的趋势:开发工具链正在从“通用文本工具”走向“领域感知工具”。我们已经习惯 IDE 会理解语法、CI 会理解测试、扫描器会理解依赖风险。那代码审查和 diff 工具,为什么还要停留在逐行比对层面?如果一份 API 描述文件变化的最终结果是“支付接口变成了必填字段更多、兼容性更差”,那最好的 diff 就应该把这句话说出来,而不是扔给你几十行 YAML 改动自己悟。
冷门能力被重新发现,恰好踩中了今天的软件协作痛点
这件事为什么偏偏在当下值得关注?因为软件仓库里的“人写代码占比”正在下降,“工具写文件占比”正在上升。
依赖升级机器人、IaC 工具、代码生成器、API 管理平台、AI 编码助手,都在持续制造变更。未来工程师花在“理解机器产出的改动”上的时间,很可能不比写代码少。这个时候,diff 工具如果还停留在传统文本模型,团队协作成本就会越来越高:审查者疲劳,关键变更漏看,PR 里塞满无意义噪声,最后谁都不想认真 review。
这也是 Git 生态一个有趣的矛盾。它底层极其强大,很多年以前就把扩展点留好了;但真正被广泛使用的,往往还是最表层的功能。相比之下,像 GitHub、GitLab 这些平台虽然在网页端不断增强代码审查体验,却很难深入到每一种私有格式和业务语义里。真正知道“这个文件该怎么读”的,通常还是团队自己。因此,diff driver 这类看似老派的本地扩展机制,反而可能重新变得重要。
当然,它也有现实问题。外部 diff driver 带来的最大好处是上下文更懂你,最大风险也是上下文过于定制。一个仓库里如果塞满各种自定义 diff 逻辑,新同事、外包协作者或开源贡献者未必能马上复现同样的体验。你在本地看到的是结构化 changelog,别人可能看到的是原始文本噪声。工具链越聪明,一致性管理就越重要。
Jamie 在文末顺手提到两个尚未处理的问题,我觉得都很关键。一个是权限变化没有纳入输出,这在某些仓库里可能是安全问题;另一个是可以利用 SHA-1 做 diff 结果缓存。别小看这个建议——当外部比较器开始分析大型规范文件、依赖图甚至生成报告时,性能会迅速从“无关紧要”变成“团队体验的瓶颈”。
Git 的下一步,不一定是更复杂,而是更会“翻译”
如果把这篇博客放到更大的行业背景里看,它其实对应着一个很朴素的问题:我们是不是该重新定义“代码差异”了?
过去,差异就是字符差异;后来,差异变成语法差异;再往后,差异可能要进入语义差异和影响差异。一个配置文件从 false 变成 true,在文本上只是五个字符的变化,在业务上却可能意味着某个服务暴露到了公网。一个 API schema 改了一行字段约束,在系统层面可能意味着移动端版本全部失配。真正重要的,不是改了哪一行,而是这次改动会让谁受影响。
从这个角度说,Jamie Tanna 这篇文章的意义,不只是教你写一个 Bash 包装脚本。它更像是在提醒开发者:Git 的可扩展性并没有过时,只是长期被忽略了。与其抱怨自动生成文件越来越难审,不如让工具开始替你做第一层解释。
未来我很看好这类“领域化 diff”的扩展继续增长。OpenAPI 只是个开始,Terraform plan、Kubernetes 清单、SBOM、数据库 schema、设计 token,甚至 AI prompt 配置,都可能成为下一个适合接入专用 diff driver 的对象。到那时,一个优秀的代码审查系统,可能不再只是显示红绿两色,而是能用你所在业务的语言,直接告诉你:这次改动究竟意味着什么。
而这,才是开发工具最迷人的进化方向——不是变得更花哨,而是变得更懂人。