OmniDiff:Rust 编写的开源内容感知 Git difftool 发布
Show HN: OmniDiff – Opensource, super fast, content aware Git difftool, in Rust
OmniDiff 发布,这是一个用 Rust 编写的开源内容感知 Git difftool,支持 25 种语言的 tree-sitter 结构化 diff,并能按内容比对图片、压缩包、字体、光标和 PDF。
快速、稳健、准确的内容感知差异比较。
- 稳健:可对 99.95% 的代码变更进行差异比较
- 内容感知:98.8% 的变更使用内容专用算法进行差异比较
| 内容 | 快速:低于 100ms | 准确:完美 | 准确:近乎完美 |
|---|---|---|---|
| 代码 | 98% | 70% | 92% |
| 图片 | 96% | 69% | 91% |
| 光标 | 100% | 85% | 100% |
| 字体 | 95% | 82% | 100% |
| 归档文件 | 88% | 87% | 87% |
| 消息目录 | 100% | 83% | 83% |
| 57% | 100% | 100% |
速度在 T14s、Intel Core i7-1370P、32 GB RAM 上测得(见快速)。PDF 的评测测试用例少于十个(见准确)。
在浏览器中查看:二十个真实代码变更,从命令行工具录制,并在 Unix diff 和 OmniDiff 中并排比较,以及内容示例:一个 OpenDocument 文件、一个编译后的翻译目录,以及图片(静态和动态)。
提供多种浅色和深色主题:
文件按类型进行差异比较。一个重命名了一个框的 PDF 图表,以及一个重绘了二十个图标的网页字体:
安装
从源码构建
cargo install --locked omnidiff
此命令从源码构建 OmniDiff,并使用其测试时所依赖的版本。你需要在 PATH 上有一个 C 编译器,以及一个 Rust 工具链,rustc 1.92 或更高版本。构建过程会从 C 编译每一个 tree-sitter 语法。由于这一点以及 lto = "fat" 发布配置,第一次 cargo install 需要几分钟。
预编译二进制文件
适用于 Linux、macOS(Intel 和 Apple Silicon)以及 Windows 的预编译二进制文件附在每个 GitHub release 中。
Homebrew
macOS 和 Linux:
brew install ivankovic/omnidiff/omnidiff
Debian 和 Ubuntu
amd64 和 arm64 的 .deb 软件包可在已签名的 apt 仓库中获取:
sudo install -d -m 0755 /etc/apt/keyrings curl -fsSL https://ivankovic.github.io/omnidiff/apt/omnidiff-archive-keyring.gpg \ | sudo tee /etc/apt/keyrings/omnidiff-archive-keyring.gpg > /dev/null echo "deb [signed-by=/etc/apt/keyrings/omnidiff-archive-keyring.gpg] \ https://ivankovic.github.io/omnidiff/apt stable main" \ | sudo tee /etc/apt/sources.list.d/omnidiff.list > /dev/null sudo apt update && sudo apt install omnidiff
Nix 和 NixOS
在 NixOS 上,或在任何安装了 Nix 的地方:
nix run github:ivankovic/omnidiff
如果你的 Nix 配置中未启用 flakes,请在 nix 之后添加 --extra-experimental-features 'nix-command flakes'。
Arch 和 Gentoo
Arch 和 Gentoo 的配方位于 packaging/:PKGBUILD 使用 makepkg -si 在本地构建,Gentoo ebuild 已准备好用于 overlay。这两个发行版不堪重负,其仓库对新软件包的开放和关闭是零星的。
编辑器集成
VS Code 尚不允许扩展替换其内置的差异视图。该 API 已在上游实现但尚未发布;扩展将在其发布后采用。
- VS Code - omnidiff-vscode。在扩展视图中搜索 OmniDiff,或
code --install-extension ivankovic.omnidiff。也已在 Open VSX 上提供,适用于 VSCodium、Cursor 和 Windsurf。 - Neovim - omnidiff.nvim。
使用 OmniDiff
交互式 TUI
omnidiff # open an empty viewer; press o to pick each file
omnidiff BEFORE AFTER # open directly into the diff of two files
在查看器中按 ? 可查看完整的键位绑定列表。
当终端通过 COLORTERM=truecolor 声明支持时,TUI 使用 24 位色,否则使用最接近的 256 色(macOS Terminal.app,或大多数通过 ssh 连接的终端,它们不会转发 COLORTERM)。如果你的终端支持 24 位色但未声明,请运行 export COLORTERM=truecolor。
在浏览器中
要在浏览器中审查变更,请参见 codereview。
注意:codereview 处于 v0.0.0;使用风险自负。
无头 / 批处理模式
omnidiff --headless BEFORE AFTER 或其同义词 --batch 会以纯文本形式打印差异(可选颜色),而不是打开 TUI。可用于脚本或 CI。只要 stdout 不是终端,无头模式也会自动启动,例如通过管道传给 less 或重定向到文件时。因此,omnidiff BEFORE AFTER | less 无需该标志即可工作。
OmniDiff 会折叠长段未更改的行。它在每个变更的两侧保留 3 行上下文(可用 --context N 覆盖),与 diff -u 的约定相同。当最近的封闭函数、类或结构体行在其他情况下不可见时,OmniDiff 还会在每个差异块前加上该行。这显示了大型文件深处变更的位置。
默认开启颜色;传入 --color never,或设置 NO_COLOR=1,可禁用 ANSI 颜色,例如当你将输出重定向到文件时——--color always 即使在 NO_COLOR 下也会强制启用。
OmniDiff 成功时退出码为 0,出错时为 2。用于脚本时,传入 --exit-code 可在文件不同时额外得到 1。这遵循 diff(1) 约定。--exit-code 是可选启用而非默认,原因与 git diff 即使在文件不同时也以 0 退出相同:OmniDiff 通常的非交互调用方是将其作为显示工具驱动的版本控制系统,它们会把非零退出码理解为“工具失败”——jj 会对每个文件发出警告,而 git difftool 配合 difftool.trustExitCode=true 会中止整个 diff。(7 参数形式的 GIT_EXTERNAL_DIFF 即使配合 --exit-code 也保持 0,因为 git 在那里将非零退出视为致命错误。)
JSON 输出
omnidiff --mode json BEFORE AFTER 将 diff 输出为一个 JSON 对象。其字段在 src/tui/json_output.schema.json(一个 JSON Schema)中描述。
Git 集成
OmniDiff 是一个 git difftool 后端。运行交互式设置向导,它会询问是全局配置还是仅针对当前仓库配置:
omnidiff git configure
手动配置
git config difftool.omnidiff.cmd 'omnidiff "$LOCAL" "$REMOTE"'
git difftool --tool=omnidiff
运行 git config diff.tool omnidiff 使普通 git difftool 默认使用 OmniDiff,无需 --tool。如果你不希望 git 在每个文件前询问“view diff ... [Y/n]?”,请运行 git config difftool.prompt false。
用法
git difftool 打开交互式 TUI。git diff 和 git log -p 从不打开。 git diff 将其输出通过 git 的分页器管道传输,而全屏 TUI 无法绘制到管道上,因此无论终端或 GIT_EXTERNAL_DIFF 配置如何,OmniDiff 在那里总是回退到纯文本(见上文“无头 / 批处理模式”)。如果你想从 git 获得交互式查看器,请使用 git difftool,而不是 git diff。如果 git difftool 在 SSH 上仍无法交互式打开,请使用 ssh -t 重新连接——会话需要分配一个伪终端;tmux 窗格总是有一个。
OmniDiff 也可通过 GIT_EXTERNAL_DIFF 直接与 git diff 和 git log -p 配合使用。此路径无需 difftool 配置:
GIT_EXTERNAL_DIFF=omnidiff git diff
图片、压缩包、字体以及其余支持的内容会得到一份关于其中变更的简短报告。任何其他二进制文件会得到一行 Binary file <path> differs 提示而非 diff,这与 git 和 diff(1) 为它们打印的替代内容相同。两者都不会阻塞 git diff 的其余部分:退出码非零的外部 diff 会使 git 放弃整个运行,因此 OmniDiff 将无法显示的文件报告为成功的空 diff,而非失败。
Jujutsu (jj) 集成
jj 即使在共置仓库中也不读取 git 的 difftool/diff.external 设置,因此它需要自己的配置。运行设置向导:
omnidiff jj configure
手动配置
jj config set --user merge-tools.omnidiff.program omnidiff
jj config set --user merge-tools.omnidiff.diff-args '["$left","$right"]'
jj config set --user merge-tools.omnidiff.diff-invocation-mode file-by-file
这会注册 jj diff --tool omnidiff。要使其也成为普通 jj diff 的默认值:
jj config set --user ui.diff-formatter omnidiff
使用 --repo 代替 --user 以仅配置当前仓库。
diff-invocation-mode = "file-by-file" 是必需的。 jj 的默认设置会向 diff 工具传递两棵目录树;OmniDiff 比较两个文件,因此没有此设置时每次调用都会失败。有了它,jj 会一次传递一对变更文件,保留每个文件的真实路径和扩展名,因此语言检测与在 git 下完全相同。
用法
jj diff 在分页器下运行其格式化程序,因此 OmniDiff 在那里以其非交互式文本模式渲染——与 git diff 得到的输出相同。jj 没有 git difftool 的交互式逐文件查看器的等价物(其终端附加钩子 ui.diff-editor 用于 jj diffedit/jj split,它们编辑右侧并读回——这不是只读查看器应该声称能做的),因此要在 jj 仓库上使用全屏 TUI,请直接运行 omnidiff BEFORE AFTER。
支持的内容
语言从文件扩展名检测。扩展名未知的文件按纯文本逐行 diff,因此不会拒绝任何内容。
25 种语言使用 tree-sitter 语法解析并进行结构化 diff:
| 语言 | 文件扩展名 |
|---|---|
| C | .c, .h |
| C# | .cs |
| C++ | .cc, .cpp, .cxx, .hpp, .hh, .hxx |
| CSS | .css, .scss |
| Go | .go |
| HTML | .html, .htm |
| Java | .java |
| JavaScript | .js, .mjs, .cjs, .jsx |
| JSON | .json |
| Kotlin | .kt |
| Lua | .lua |
| Perl | .pl, .pm |
| PHP | .php |
| Python | .py, .pyi, .pyw |
| R | .r |
| Ruby | .rb |
| Rust | .rs |
| Scala | .scala |
| Shell (Bash) | .bash, .sh |
| Swift | .swift |
| TSX | .tsx |
| TypeScript | .ts, .mts, .cts |
| Vimscript | .vim |
| XML | .xml, .xht, .xhtml, .svg, .glif, .plist, .ui, .qrc, .glade, .xib, .storyboard, .xaml, .vcxproj, .filters, .csproj, .fsproj, .vbproj, .props, .targets, .nuspec, .resx, .wxs, .iml, .manifest, .config, .policy, .xsd, .xsl, .xslt, .xlf, .xliff, .kml, .gpx, .rss, .atom, .graphml, .dae |
| YAML | .yaml, .yml |
可按扩展名识别,但因未编译对应语法而按纯文本进行差异比较:Bazel(.bazel)、Dart(.dart)、Emacs Lisp(.el)、Markdown(.md、.markdown)、Protocol Buffers(.proto)、SQL(.sql)。
非文本文件按其字节识别,绝不依据文件名,并按其所含内容进行差异比较:
| 内容 | 格式 | 差异比较方式 |
|---|---|---|
| 图片 | PNG、JPEG、GIF、WebP、BMP、ICO、TIFF | 逐像素比较,如同人眼对比:变化了多少、变化在哪里;动画则逐帧比较 |
| 归档文件 | zip(以及 jar、Office 文档、EPUB)、tar、gzip、xz、bzip2 | 逐文件比较,每个文件按其自身类型比较,包括归档文件中的归档文件;Java class 文件则按其字段和方法的列表比较 |
| 字体 | TrueType、OpenType、WOFF、WOFF2、EOT | 逐字形比较,每个字形单独绘制,并比较字体的名称和度量 |
| 光标 | Windows .cur 和 .ani、X 光标、Hyprland .hlc |
逐尺寸、逐帧比较 |
| 消息目录 | gettext .mo、Qt .qm |
逐条消息比较 |
| 文档 | 逐页比较,每页单独绘制 | |
| 其他编码的文本 | 带字节顺序标记的 UTF-16 和 UTF-32 | 按文本处理,与其他文件相同 |
只打开发生变化的成员,因此一个仅有两个字形被编辑的字体只会显示这两个字形。每处变化还会按读者能看到的程度命名:不可见(仅字节不同)、无法察觉、伪影(压缩或重采样留下的痕迹)、已编辑、重绘或替换——对于文本,空白、格式和重写分别对应第二、第三和第五种。
TUI 将图片显示为图片:在支持 kitty、sixel 或 iTerm2 协议的终端中使用终端自身的图形能力,在其他终端中则使用 Unicode 半块字符。b 可切换到半块字符并切回,OMNIDIFF_GRAPHICS=halfblocks(或 kitty、sixel、iterm2)可覆盖自动检测;在 tmux 内,由窗格前方的终端决定。归档文件、字体或任何其他容器会在所选成员旁列出发生变化的成员,g 可一次显示所有发生变化的字形或图片,Enter 可在完整差异视图中打开文本成员。
其他任何二进制内容都会显示一行 Binary file <path> differs 提示。
许可证
版权所有 (C) 2026 Marko Ivankovic
本程序是自由软件:你可以根据自由软件基金会发布的 GNU Affero 通用公共许可证第 3 版条款重新分发和/或修改它。
许可证全文请参见 LICENSE 文件。
无法使用 AGPL 软件?
可通过 GitHub Sponsors 按月订阅获取商业许可证。它涵盖你的组织对 omnidiff 的内部使用,且无需承担 AGPL 的源代码披露义务。条款见 LICENSE-COMMERCIAL。请选择将商业许可证列为权益的档位。如需开具发票或其他安排,请通过 marko@ivankovic.me 联系我。
指导原则
快速
OmniDiff 的目标是:
- 中位差异比较耗时不超过 100ms。
- 第 99 百分位差异比较耗时不超过 1000ms。
两项都满足。在 src/test/data/diffs/ 中的 2,302 个测试用例上,于 2026-10-31 在一台笔记本电脑上测量,该电脑为 Intel Core i7-1370P(6 个性能核和 8 个能效核,20 线程),32 GB 内存:p50 4.2ms,p90 35.1ms,p99 177ms,最慢 482ms。100ms 是第 97.6 百分位——55 个测试用例耗时超过该值,且没有一个超过一秒。
时间属于测量它们的机器,因此每台机器保留自己的数据(见 research/data/machines.csv):测量研究数据的 Intel Xeon E3-1275 v5 服务器每个测试用例耗时约为其 2.5 倍。运行时间会被报告,但从不作为门槛:make check-quality 在每次推送时将该分布与机器自身提交的基线进行对比,并在运行时间超过基线两倍时发出警告。
其他内容按从同一语料库中采样的配对计时,包含解码——解包归档、绘制字体的字形或 PDF 的页面——于 2026-10-17 在同一台笔记本电脑上测量:
| 内容 | 配对 | p50 | p90 | p99 | 低于 100ms |
|---|---|---|---|---|---|
| 图片 | 230 | 1.8ms | 41ms | 526ms | 96.1% |
| 光标 | 22 | 0.6ms | 1.3ms | 15ms | 100% |
| 字体 | 100 | 6.4ms | 57ms | 826ms | 95% |
| 归档 | 100 | 2.6ms | 156ms | 1,938ms | 88% |
| 消息目录 | 81 | 0.4ms | 1.9ms | 4.7ms | 100% |
| 100 | 69ms | 310ms | 985ms | 57% |
归档未达到第 99 百分位目标:最慢的包含数千个更改的成员。PDF 满足两个目标,但只有 57% 的耗时低于 100ms,因为 PDF 会绘制每个更改的页面。make benchmark-content 测量这一点并写入 research/data/performance/content_speed/<machine>.csv。
稳健
OmniDiff 的目标是处理 100% 的所有提交。
完整测试数据集包含约 7,500 个开源 git 仓库的 git 提交历史,以主分支上的可用内容为准。该仓库列表来自 Gentoo Linux 发行版。可在 list_of_repositories.csv 中找到。
在这些仓库各自最近 50 次提交中每个修改过的代码文件上测量——25 种语言中 442,530 个可读的前后配对,在 120 秒预算和每进程 6 GB 内存上限下进行无大小限制的差异比较——442,322 个(99.95%)成功完成。96 个配对超出预算,112 个超出内存上限;后者是二十个文件,其中十九个是生成或嵌入的——tree-sitter 解析器表、代码生成、压缩包、作为 C 数组的 PNG——以及一个 40,000 行单头 C++ 库的一次提交。给定 24 GB 和 300 秒,最初触及上限的 28 个文件中有 11 个完成。该运行、其测试框架以及每个未完成的配对都记录在 research/data/performance/PROVENANCE.md 中。
除代码外,在同一窗口内每个更改的文件上——7,352 个仓库中的 2,018,010 个更改,包括图片、字体和归档——98.8% 按文本或其内容进行差异比较(若每个仓库最多计数 1,000 次则为 98.2%,以免少数巨型仓库决定结果;完全覆盖 6,481 个仓库)。剩余的主要是传统编码的文本、编译对象、音频以及用 MicroType Express 压缩的 EOT 字体。该普查记录在 research/data/corpus_stats/PROVENANCE.md 中。
准确
OmniDiff 必须与人类自身对更改的解读一致,以 src/test/data/diffs/ 中手工编写的真实映射为基准进行测量:
- 90% 的测试用例零字节不匹配。
- 99% 的测试用例至多 1% 字节不匹配。
两者均尚未满足。2,302 个测试用例中有 1,419 个带有手工绘制的真实标注——两个文件的每个字节都标注了人类所说的发生情况——OmniDiff 自身的突出显示在两种突出显示预设下(--full,保留括号、分隔符和前导空白;--minimal,丢弃它们)逐字节与其比较:
| 预设 | 零字节不匹配 | 至多 1% 不匹配 | 整个语料库的不匹配字节 |
|---|---|---|---|
--full |
851 (60.0%) | 1,241 (87.5%) | 73.3M 中的 99,157(0.14%) |
--minimal |
987 (69.6%) | 1,299 (91.5%) | 73.3M 中的 51,740(0.07%) |
全语料库的比率远低于 1%,因为每个文件的大部分内容都没有变化,而且没有人会搞错这一点;真正重要的是每个测试用例的数字,因为读者是一次一个 diff 地遇到这些错误的。剩下的大部分问题并不在匹配环节:渲染人类自己的节点映射,仍然在 91% 的 --full 字节和 91% 的 --minimal 字节上与绘制结果不一致,所以高亮规则比匹配器承担了更多差距。
make update-painting-attribution 对此进行测量,并为每个 fixture 和预设向 research/data/quality/painting_attribution.csv 写入一行,表格正是据此统计的;如果任何 fixture 变差,make check-painting-attribution 就会失败。
其他内容是被评判而非被绘制的:由人类指出每处变化的程度(不可见、难以察觉、有伪影、已编辑、重绘或替换,见支持的内容),然后将 OmniDiff 的判定与他们的判定进行比较。当每个判定都匹配时,测试用例就是完美的;当每个判定最多相差一个级别时,就是接近完美的:
| 内容 | 评判 | 完美 | 接近完美 |
|---|---|---|---|
| 图片 | 130 | 90 (69%) | 118 (91%) |
| 光标 | 13 | 11 (85%) | 13 (100%) |
| 字体 | 11 | 9 (82%) | 11 (100%) |
| 归档 | 23 | 20 (87%) | 20 (87%) |
| 消息目录 | 12 | 10 (83%) | 10 (83%) |
| 4 | 4 (100%) | 4 (100%) |
make benchmark-content 也会统计这些。
AI 政策
本项目使用了大量 AI 辅助,目前是 Claude Code。大多数提交都会通过 Co-Authored-By 尾注和指向产生该提交的会话的链接来披露这一点。本项目不隐瞒这一事实。本项目不将 AI 辅助视为一种次等的软件开发方式。
目前不接受贡献,以保持较高的开发速度(见CONTRIBUTING.md)。当情况改变时,AI 辅助的贡献将与其他贡献一样受欢迎,并以同样的方式披露:提交工作的人有责任理解它并为其负责。
面向开发者,无论是否人类
有关技术概览、代码质量和测试期望、项目结构以及 CI 在每次推送和 PR 时检查的内容,请参见CONTRIBUTING.md。AGENTS.md 还有额外的 AI 代理特定约定。
来源:Hacker News · github.com



