在日常开发中,Git 默认的文本对比足以满足大多数源码文件的需求。但当项目包含结构化文档(如 JSON、YAML、XML)或二进制资产(如图片、PDF)时,开发者往往希望看到更具可读性的差异,甚至希望把外部工具的解析结果直接交给 Git 展示。Git 提供的 diff driver(亦称自定义 diff 驱动)正是解决这一痛点的低层 API。通过 diff driver,我们可以把任意文件的 “人类可读” 表示交给 Git,从而在 git diffgit log -p 甚至 IDE 的 diff 视图中获得统一的对比体验。

1. 什么是 diff driver

diff driver 本质上是 Git 配置文件中的一组指令,告知 Git 在比较特定类型的文件时使用哪个外部程序(command)或转换过滤器(textconv)。它由两个核心概念组成:

  • 注册:在 git config 中声明一个以 diff.<name> 开头的配置块,提供 command(外部 diff 程序)或 textconv(把文件转为可比较的文本)路径。
  • 绑定:在 .gitattributes 文件里通过 diff=<name> 属性把某个文件名模式(比如 *.json)映射到已经注册的 driver。

这样,当 Git 遇到匹配的文件时,会先调用对应的转换或外部程序,再把结果交给内部的 diff 引擎生成统一的差异报告。

2. 注册自定义 diff driver

在任意层级(全局 --global、仓库局部 --local)执行 git config 即可完成注册。最常见的两种方式是:

2.1 使用 textconv(文本转换)

# 将 JSON 文件pretty‑print后进行比较
git config diff.json.textconv "jq -S ."

上述命令会在当前仓库的 .git/config 中生成:

[diff "json"]
    textconv = jq -S .

jq -S . 读取文件内容,输出键排序后的 JSON 文本。Git 会把原始文件的压缩形式转换为易读的展开形式,再进行 diff。

2.2 使用 command(完整外部 diff 程序)

如果你希望自行实现差异生成逻辑(比如调用 diff -u 之外的自定义算法),可以指定 command

git config diff.custom.command "/path/to/my-diff-script"

此时 Git 会把两个文件路径作为前两个参数传递给脚本,脚本的标准输出即为差异结果。脚本的签名应为:

my-diff-script old-file new-file

3. 把 driver 绑定到文件类型

仅注册 driver 不会自动生效,需要在仓库根目录的 .gitattributes(或子目录的 .git/info/attributes)中添加映射:

*.json      diff=json
*.xml       diff=xml
*.png       diff=image
  • *.json diff=json:告诉 Git 遇到 JSON 文件时使用名为 json 的 driver(即前一步注册的 diff.json)。
  • *.png diff=image:示例中假设我们对图像文件使用自定义 driver。

提示.gitattributes 必须纳入版本库,以便团队成员共享文件类型的 diff 行为;而 diff driver 的实现(脚本或工具)则需要每位成员自行安装或配置。

4. 触发方式

  • 属性触发:只要文件匹配 .gitattributes 中的 diff=…,执行 git diffgit log -pgit show 等命令时会自动调用对应的 driver。
  • 手动触发:使用 git diff --ext-diff 可强制使用所有已注册的属性驱动,即使没有在 .gitattributes 中声明也能生效。

5. 处理二进制文件与特殊文件类型

5.1 使用 binary 属性

Git 默认会把未声明的文件视作文本。若要彻底禁止比较二进制文件,可以在 .gitattributes 中直接标记为二进制:

*.png binary

这样 git diff 只会提示 “Binary files a/foo.png and b/foo.png differ”,不再尝试显示内容。

5.2 使用 textconv 转换二进制为可读文本

如果你想在 diff 中看到二进制的结构化信息(例如 PNG 的尺寸、颜色模式),可以写一个小型脚本把元数据打印出来,然后将其注册为 textconv:

# image-meta 脚本示例(依赖 ImageMagick 的 identify)
#!/usr/bin/env bash
identify -verbose "$1" | grep -E '^(Image:|Geometry:|Colorspace:|Depth:)'

# 注册
git config diff.image.textconv "/path/to/image-meta"

这样当比较两张 PNG 时,Git 会把元数据差异呈现出来,而不是直接报 “Binary files differ”。

5.3 使用外部 command 处理特殊格式

对于像 PDF、Office 文档等只能抽取文本的文件,可以使用 pandocpdftotext 等工具:

git config diff.pdf.command "pandoc -t plain"

Git 会先调用 pandoc 把 PDF 转为纯文本,再进行 diff。

6. 配置 diff 算法与行为

diff driver 本身不限制使用的算法,但可以在全局或 driver 级别覆盖一些行为:

[diff "json"]
    textconv = jq -S .
    algorithm = histogram
    wsErrorHighlighting = all
  • algorithm:可取 myersminimalpatiencehistogram,影响差异块的选择策略。
  • wsErrorHighlighting:开启空格错误高亮,适合代码审查场景。

如果想对所有文本文件统一配置,也可以在 ~/.gitconfig[diff] 段落设置全局选项。

7. 完整示例:JSON 文件的可读 diff

  1. 编写转换脚本~/bin/json-pretty.sh):
#!/usr/bin/env bash
# 将 JSON 规范化并排序键
jq -S . "$1"

记得 chmod +x ~/bin/json-pretty.sh

  1. 注册 driver
git config diff.json.textconv "~/bin/json-pretty.sh"
  1. 在仓库中绑定
echo '*.json diff=json' >> .gitattributes
git add .gitattributes
  1. 验证
git diff HEAD~1 -- *.json

你会看到原本紧凑的一行 JSON 被展开为多行、键排序后的可读形式。

同理,如果想对图片、PDF、YAML 等实现类似可读 diff,只需把对应的转换工具路径放入 textconv,并在 .gitattributes 中声明即可。

8. 常见坑与最佳实践

  • 路径必须可执行:无论是 textconv 还是 command,都必须具备执行权限;Windows 环境下注意使用 .bat.exe 或 WSL 路径。
  • 使用绝对路径或 $PATH 内工具:相对路径在不同机器上可能失效,推荐把脚本放在项目根目录的 scripts/ 子目录并使用 $PWD 变量。
  • 本地 vs 全局配置:若团队成员需要统一的 diff 行为,最好在项目的 CONTRIBUTING.md 中说明安装步骤,或把脚本随仓库一起发布(放在 .git/hooks/ 之外的目录),让每位开发者自行配置。
  • 返回值与错误处理:自定义脚本返回非零状态码会导致 diff 失败,建议捕获异常并输出空文本,以免阻断 git diff
  • 调试技巧:使用 GIT_TRACE=1 git diff 可以看到实际的驱动调用参数,帮助定位问题。

9. 小结

自定义 diff driver 为 Git 打开了处理结构化文档和二进制资产的钥匙。通过 git config 注册 diff.<name>.textconvdiff.<name>.command,再在 .gitattributes 中把特定文件模式映射到该 driver,即可在 git diff 系列命令中得到统一且可读的差异展示。结合 algorithmwsErrorHighlighting 等细粒度选项,还能进一步调优差异块的生成策略。掌握以上注册、绑定与调参技巧后,你完全可以为项目中的任何文件类型构建专属的 diff pipeline。详细参数可参阅 Git 官方文档 diff-config(中文手册亦可查询)。