跳到正文
Hacker News· FabCH·· 9 小时前AI 评分58

OmniDiff:Rust 编写的开源内容感知 Git difftool 发布

Show HN: OmniDiff – Opensource, super fast, content aware Git difftool, in Rust

AI 导读

OmniDiff 发布,这是一个用 Rust 编写的开源内容感知 Git difftool,支持 25 种语言的 tree-sitter 结构化 diff,并能按内容比对图片、压缩包、字体、光标和 PDF。

正文 · AI 翻译

CI Latest release docs.rs coverage License: AGPL v3

快速、稳健、准确的内容感知差异比较。

  • 稳健:可对 99.95% 的代码变更进行差异比较
  • 内容感知:98.8% 的变更使用内容专用算法进行差异比较
内容 快速:低于 100ms 准确:完美 准确:近乎完美
代码 98% 70% 92%
图片 96% 69% 91%
光标 100% 85% 100%
字体 95% 82% 100%
归档文件 88% 87% 87%
消息目录 100% 83% 83%
PDF 57% 100% 100%

速度在 T14s、Intel Core i7-1370P、32 GB RAM 上测得(见快速)。PDF 的评测测试用例少于十个(见准确)。

在浏览器中查看:二十个真实代码变更,从命令行工具录制,并在 Unix diff 和 OmniDiff 中并排比较,以及内容示例:一个 OpenDocument 文件、一个编译后的翻译目录,以及图片(静态和动态)。

An animation of one Python refactoring painted two ways. A vertical bar sweeps left to right and back across a two-pane diff. On one side of the bar, GNU diff marks whole lines as deleted and inserted; on the other, OmniDiff paints only the parts that changed - sum(numbers) and len(numbers) rather than the whole assignment, and numbers shown as moved rather than rewritten.

提供多种浅色和深色主题:

A screenshot of OmniDiff's two-panel terminal UI in a light theme, showing the same Python refactoring, with the changed right-hand sides highlighted rather than whole lines

文件按类型进行差异比较。一个重命名了一个框的 PDF 图表,以及一个重绘了二十个图标的网页字体:

An animation in three parts. First, git diff on a PDF prints only "Binary files a/CommunicationModel_en.pdf and b/CommunicationModel_en.pdf differ". Then OmniDiff, as git's external diff, reports that the metadata and page 1 changed and where. Then OmniDiff's viewer lists the changed members and shows page 1 before and after side by side, the renamed box outlined on both, and then the difference alone.

An animation of a Font Awesome web font. git diff prints only "Binary files differ"; OmniDiff reports 21 changed members and lists each changed glyph; its viewer shows the font's name table with the version and style changed, one glyph before and after, and then all twenty changed icons in a grid, each change outlined.

安装

从源码构建

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 逐条消息比较
文档 PDF 逐页比较,每页单独绘制
其他编码的文本 带字节顺序标记的 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%
PDF 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%)
PDF 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