omnidiff

ivankovic活跃维护

基于 AST 的代码差异对比工具,能快速、准确、稳健地比较代码改动,减少纯文本 diff 的噪音,适合代码审查和代码质量检查。

Fast, robust, accurate diffing

v0.4.0发布于 2026-11-08 18:38 UTC1,314star +92 / 7 天36fork同步于 2026-11-10 17:51 UTC

怎么上手

由 README 提炼,执行前请确认
cargo install --locked omnidiff
brew install ivankovic/omnidiff/omnidiff
硬件要求按 README 说明

README 未说明显存或 GPU 要求;从源码构建需要 C 编译器和 rustc 1.92 以上,官方提供 Linux、macOS(Intel 与 Apple Silicon)、Windows 预编译二进制

支持 Apple Silicon
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

按内容类型智能比对的 diff 工具,代码按语法结构、图片按像素、字体按字形给出精确差异

基于 AST 的代码差异对比工具,能快速、准确、稳健地比较代码改动,减少纯文本 diff 的噪音,适合代码审查和代码质量检查。

核心能力

  • 用 tree-sitter 语法解析 25 种语言,按结构比对代码改动
  • 图片按像素逐点比对,动图逐帧报告变化位置
  • 压缩包逐文件递归比对,内层压缩包也展开
  • 字体按字形比对,列出每个被改动的字符
  • 支持 git 外部 diff 与 jj 集成,可作 difftool 使用

容易踩的坑

从源码构建会编译全部 tree-sitter 语法并启用 lto=fat,首次安装需要几分钟;git diff 走管道时只会输出纯文本,要交互式 TUI 得用 git difftool 或直接 omnidiff BEFORE AFTER;jj 集成必须设置 diff-invocation-mode 为 file-by-file,否则每次调用都会失败

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v0.4.0
  1. v0.4.0最新正式版2026-11-08 · 2026-11-08 18:38 UTC

    One settings menu, two panels from 160 columns, text you can select and copy, and a viewer that stays idle when nothing changes.

    Added

    • Drag the mouse in a panel to select text, and let go to copy it to the clipboard (#71). The selection stays in that panel's code: the line numbers, the other panel and the minimap are never included. Dragging above or below the panel scrolls it. The copy is sent as an OSC 52 escape code, so it works on Wayland and over SSH with no clipboard tool. In tmux this needs set-clipboard on. Shift-drag still uses the terminal's own selection.
    • One settings menu with three tabs, switched with Tab and Shift-Tab: Highlighting, Layout and Colors. c opens it on Colors and M on Highlighting. Changes take effect and are saved immediately; Enter and Esc only close the menu.
    • The Highlighting tab names each option in plain words and shows a live before/after example in the language of the open file (25 languages and plain text). It marks the text the selected option changes, and says when an option has no effect.
    • The width from which two panels are shown side by side can be set: on the Layout tab, as wide_columns in the config file, or with OMNIDIFF_WIDE_COLUMNS for one run (#70).

    Changed

    • Two panels are shown from 160 columns, down from 200 (#70).
    • The theme editor no longer reverts on Esc; only a hex color being typed can still be cancelled.
    • The node highlight (H) and syntax highlighting (S) are rows in the settings menu too; the keys still work.

    Fixed

    • An idle viewer no longer uses CPU (#72). It used to redraw 60 times a second whether or not anything had changed, which kept a GPU terminal repainting. Now it draws only after input or a change, and is silent after the first frame. Idle CPU went from 2-22% to about 0.5%. Each frame is also faster on files with many changes: 14 ms to 0.8 ms for 20,000 changes.
    • As git's external diff, omnidiff --headless titles each side with the file's path instead of git's temporary file. A renamed file shows its old and new paths, and an added or deleted file shows /dev/null on the missing side (#59).
    • The project site, showcase and apt repository are published again: the Pages build had been failing on every push to main.

    Library

    • headless::run takes an extra titles argument; pass None to keep the file paths as titles.
    • Action: ThemeSelected, ThemePreviewed, SyntaxThemePreviewed and RenderOptionsAccepted are replaced by ThemeChanged, SyntaxThemeChanged, SyntaxHighlightingChanged, NodeHighlightChanged, PanelLayoutChanged, WideColumnsChanged, SettingsClosed and CopyToClipboard.
    • SINGLE_PANEL_THRESHOLD is now 160. New: diff::text::OPTION_INFO, tui::clipboard, tui::positional::display_names, tui::theme::WIDE_COLUMNS_ENV.
  2. v0.3.02026-11-01 · 2026-11-01 10:58 UTC
    查看更新说明

    Code held in strings is diffed as code, class lists class by class, the highlighting follows the hand-painted ground truth more closely, and big diffs are a fifth to two fifths faster.

    Added

    • Code of another language held in a string is diffed as a file of its own: TSX in a JSON "content" next to a "path" naming a .tsx file, shell in a YAML run:/script: block, CSS and HTML in css/styled.*/html template literals, C header lists in shell '...' strings, and YAML in an Ansible module's DOCUMENTATION, EXAMPLES and RETURN. The string is read as its language means it - escapes resolved, a YAML block's indentation and a C# raw string's closing indentation removed, ${...} and ${{ ... }} interpolations held in place - and each edit inside it is highlighted where it is, instead of one span from the first change to the last. A changed string with no language of its own is diffed line by line, or word by word when it is one line holding several separate edits.
    • CSS class lists are diffed class by class: a JSX className/class value, or a string argument of cn, clsx, classnames, cx, twMerge, twJoin, cva or tw. A class is added, removed or moved whole, never split at its :, - or /.
    • A new highlighting option, "Whitespace between differently painted ranges" in the M panel: the blank between two highlighted ranges on one row is highlighted too, so one edit does not read as two. On in --full, off in --minimal.

    Changed

    • Highlighting follows the hand-painted ground truth more closely: --full leaves a token that an edit beside it only pushed down a row unhighlighted, --minimal keeps inserted and deleted punctuation, and a one-sided change inside a node shows as an insertion or deletion rather than an update. Over the 1,419 hand-painted test cases, 987 (69.6%) are now highlighted exactly as a human would under --minimal and 851 under --full.
    • Matching: a statement whose value moved into a new statement stays paired with it; a changed function, type or class inside a large flat container is found by its name in Rust, Go, Python, Kotlin, JavaScript/TypeScript, Scala, PHP and C#; a Java built-in type replaced by another keeps its declaration; if-chain clauses that swapped places, members of JS/TS classes, JSON pairs, #defines and identical twins in a list are matched by what they are rather than where.
    • Faster: a diff takes 22% less time on average and 23% less at the 99th percentile; on big files, from the command line, 22% to 43% less. The two sides are parsed on two threads, the syntax trees' metadata is built with fewer allocations, and the tree edit distance prices its table without string comparisons.
    • Library: ASTDiff has a public embedded field (the embedded layers, diff::embedded) and RenderOptions an interior_whitespace field, so a struct literal of either needs updating. New: Code::parse_only, code::build_sides.

    Security

    • zip 9, for RUSTSEC-2026-0367, -0368 and -0369, and libz-sys 1.1.31, for RUSTSEC-2026-0418.
  3. v0.2.12026-10-12 · 2026-10-12 15:45 UTC
    查看更新说明

    A large number of improvements to non-code diffs, and adding more robust tests for non-code content.

    Added

    • Pictures (PNG, JPEG, GIF, WebP, BMP, ICO, TIFF) are diffed instead of reported as "Binary file differs": what each side is (format, size, color type), how much of it changed and where, as rectangles of changed pixels. Pixels compare perceptually, so re-encoding noise is not a change. Headless and git diff print this as a short report and --mode json adds a picture object. The TUI shows the two pictures with the changed regions outlined, and t cycles to a difference, blend and swipe view; it draws real pixels where the terminal speaks the kitty, sixel or iTerm2 graphics protocol and Unicode half blocks everywhere else.
    • Animated GIF, PNG and WebP pictures are compared frame by frame, the frames aligned the way lines are: the report says which frames changed, which were added or removed, and whether the same frames now show for different times. In the TUI, , and . step through the frames and space plays the animation.
    • + and - zoom into the same part of both pictures, up to 32x, and Shift+h/j/k/l pans: for a large screenshot whose change is a few pixels.
    • Inside tmux, the TUI draws pictures with the graphics protocol of the terminal in front of the pane, as tmux knows it, rather than whichever attached client answered the query: a kitty on a desk no longer makes a phone's terminal attached to the same session show crossed-out boxes. b switches every picture to half blocks and back, and OMNIDIFF_GRAPHICS=halfblocks (or kitty, sixel, iterm2) overrides the detection.
    • Archives, fonts, cursor themes, message catalogs and PDFs are diffed by what they hold, member by member: a zip's or tar's files (jars, Office documents and EPUBs are zips; gzip, xz and bzip2 streams are opened too), a font's glyphs and names (TrueType, OpenType, WOFF, WOFF2, EOT), a cursor's sizes and frames (Windows .cur and .ani, X cursors, Hyprland .hlc), a gettext .mo or Qt .qm catalog's messages, a PDF's pages. Members match by name and only the changed ones are opened: each is diffed by what it is - a picture as a picture, text by lines, a nested archive as an archive. The report lists the members that changed, were added or were removed; --mode json adds a content object, which a picture pair carries too. In the TUI the members are listed beside the selected one, a shows the unchanged ones as well, g shows every changed glyph or picture at once, and Enter opens an archive inside an archive. A text member is diffed by OmniDiff in its own language, its long lines wrapped and cut to the changes (J/K step through them), and Enter opens it in the full diff view; a Java class file in a jar is diffed as a listing of its fields, its methods and what each method calls, reads and loads.
    • What happened to a picture or a member is said in one of six levels, by how hard it is to see and what the after side is: invisible (only bytes differ), imperceptible (seen only once highlighted), artifacts (what compression, resampling or a formatter leaves behind), edited, redrawn (the same thing made anew) and replaced; with tags for a picture resized, cropped, retimed or given frames, and for a container given members. Pictures of one aspect ratio and different sizes are compared at the smaller size; --mode json reports, per comparison, the pixels that differ at all and the clearly changed ones beside those that changed.
    • Text in UTF-16 or UTF-32 (announced by a byte order mark) is read as text and diffed like any other file instead of reported as binary; --mode json names its encoding.

    Changed

    • XML formats with an extension of their own are diffed with the XML grammar instead of line by line: .svg, .plist, Qt .ui/.qrc, MSBuild .vcxproj/.csproj/.props/.targets, .xib, .storyboard, .xaml, .resx, `.xs
  4. v0.2.02026-10-01 · 2026-10-01 20:18 UTC
    查看更新说明

    CodeDiff is now OmniDiff. The old name is a registered trademark, so everything was renamed.

    Changed

    • Renamed from CodeDiff to OmniDiff. The crate, binary, packages, editor extensions and repositories are all omnidiff now: install omnidiff (cargo install --locked omnidiff, the omnidiff deb from https://ivankovic.github.io/omnidiff/apt, brew install ivankovic/omnidiff/omnidiff, the ivankovic.omnidiff VS Code extension, omnidiff.nvim), then rerun omnidiff git configure and omnidiff jj configure, since a configuration that names codediff points at a binary that no longer updates. The apt repository moved with the GitHub Pages site: replace ivankovic.github.io/codediff/apt and the codediff-archive-keyring.gpg key file in your apt sources with the omnidiff ones (see the README). The codediff crate and the codediff packages stay available for now and are retired in a later release.
    • The config moved to .omnidiff.toml, $OMNIDIFF_CONFIG and ~/.config/omnidiff/config.toml. For this release the old names are still read, and ~/.config/codediff/config.toml is moved to the new place the first time it is needed.
    • Code copied to several places is shown as such: when a statement or declaration of some size appears once before and twice after (or the reverse) in two branches of one if, every copy paints as a move of the original instead of one being matched and the rest inserted.
    • A comment of three or more words that moves inside a function, class or other construct that survives the change is shown as moved, instead of deleted and re-inserted.
    • A run of comments or small statements that changes length is aligned by similarity, so the entries that stayed are matched instead of the whole run being deleted and re-inserted.
    • A leaf renamed across two different constructs (one identifier of a replaced expression reused in the new one) is shown as deleted and inserted rather than as an in-place update.
    • Library: ASTDiff records N:M groups next to its one-partner node maps (add_group), and its node maps can no longer disagree about a pair (is_valid checks it).
  5. v0.1.12026-09-27 · 2026-09-27 10:13 UTC
    查看更新说明

    Responding to initial user reports for v0.1.0.

    Note the removal of "--web" and sending users to CodeReview instead.

    Changed

    • Tabs display at tab stops, in the TUI and in headless output, instead of as one space.
    • The TUI uses the nearest 256 colors when the terminal does not advertise 24-bit color (macOS Terminal.app).
    • --minimal, --full, --whole-updates and --paint-reindent-moves apply in the TUI too, for that run only.
    • --review rejects a BEFORE/AFTER pair and --mode, and --headless rejects --mode, instead of ignoring one flag.
    • codediff git configure writes the resolved path of the running binary to diff.external.
    • change N/M counts changes in the order n walks them.
    • The render-options badge names what differs from --full.
    • Panel titles name languages as people write them (C++, Lua, Markdown). The JSON language values are unchanged.
    • Library: the diff::solve_* pass modules are crate-private, ASTDiff::is_valid has no after parameter, and NodeCache takes a lifetime (see Fixed). Breaking for library users, against the policy above; the library API was never meant to be relied on (see the crate docs).

    Fixed

    • The TUI cursor lands on the right column on lines with non-ASCII text.
    • Ctrl-C quits from every screen, and Ctrl-E scrolls instead of opening $EDITOR.
    • The theme dialog opens on the syntax theme in use, and accepting a preset keeps the saved Custom palette.
    • Home and End move the cursor, not only the view.
    • $VISUAL/$EDITOR may carry arguments (code -w, emacsclient -t).
    • Omitting --whole-updates no longer switches a saved "Whole-pair updates" setting off.
    • The TUI exits when its input stream closes.
    • A block whose children were all deleted no longer takes its whole subtree with it: code matched inside it (a body moved from an if into an unless) is shown as matched, not deleted and inserted.
    • Library: NodeCache borrows the Code it was built from, so a cache that outlives its Code no longer compiles; before, safe code could read freed memory through it.

    Removed

    • The browser front end (codediff-web) and the web Cargo feature. Breaking for anyone who installed with cargo install codediff --features web; for reviewing changes in a browser, use codereview. The GitHub Pages showcase is unaffected.

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    OmniDiff

    CI Latest release docs.rs coverage License: AGPL v3

    Fast, robust, accurate content-aware diffing.

    • Robust: can diff 99.95% of code changes
    • Content-aware: 98.8% of changes diffed using content-specific algorithms
    ContentFast: under 100msAccurate: perfectAccurate: near-perfect
    Code98%69%92%
    Pictures96%69%91%
    Cursors100%85%100%
    Fonts93%82%100%
    Archives87%87%87%
    Message catalogs100%83%83%
    PDFs55%100%100%

    Speed measured on a T14s, Intel Core i7-1370P with 32 GB RAM (see Fast). PDFs have fewer than ten judged test cases (see Accurate).

    See it in the browser: twenty real code changes, recorded from the command-line tool and compared side by side in Unix diff and in OmniDiff, and content examples: an OpenDocument file, a compiled translation catalog, and pictures, still and animated.

    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.

    Various light and dark themes are available:

    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

    Files are diffed based on type. A PDF diagram with one box renamed, and a web font with twenty icons redrawn:

    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.

    Installation

    From source

    cargo install --locked omnidiff
    

    This command builds OmniDiff from source, with the dependency versions it was tested with. You need a C compiler on PATH and a Rust toolchain, rustc 1.92 or later. The build compiles every tree-sitter grammar from C. The first cargo install takes a few minutes, because of this and the lto = "fat" release profile.

    Prebuilt binaries

    Prebuilt binaries for Linux, macOS (Intel and Apple Silicon), and Windows are attached to every GitHub release.

    Homebrew

    macOS and Linux:

    brew install ivankovic/omnidiff/omnidiff
    

    Debian and Ubuntu

    .deb packages for amd64 and arm64 are available in a signed apt repository:

    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 and NixOS

    On NixOS, or anywhere with Nix installed:

    nix run github:ivankovic/omnidiff
    

    If flakes are not enabled in your Nix configuration, add --extra-experimental-features 'nix-command flakes' after nix.

    Arch and Gentoo

    Recipes for Arch and Gentoo live in packaging/: the PKGBUILD builds locally with makepkg -si, and the Gentoo ebuild is ready for an overlay. The two distros are overwhelmed and their repositories open and close to new packages sporadically.

    Editor integration

    VS Code does not yet let an extension replace its built-in diff view. The API is implemented upstream but unreleased; the extension will adopt it when it ships.

    • VS Code - omnidiff-vscode. Search for OmniDiff in the Extensions view, or code --install-extension ivankovic.omnidiff. Also on Open VSX for VSCodium, Cursor and Windsurf.
    • Neovim - omnidiff.nvim.

    Using OmniDiff

    The interactive TUI

    omnidiff                  # open an empty viewer; press o to pick each file
    omnidiff BEFORE AFTER     # open directly into the diff of two files
    

    Press ? in the viewer for the full list of keybindings.

    The TUI uses 24-bit color when the terminal advertises it with COLORTERM=truecolor, and the nearest 256 colors otherwise (macOS Terminal.app, or most terminals over ssh, which does not forward COLORTERM). If your terminal supports 24-bit color but does not advertise it, run export COLORTERM=truecolor.

    In a browser

    For reviewing changes in a browser, see codereview.

    Note: codereview is at v0.0.0; use it at your own risk.

    Headless / batch mode

    omnidiff --headless BEFORE AFTER, or its synonym --batch, prints the diff as plain text, with optional color, instead of opening the TUI. Use this for scripts or CI. Headless mode also starts automatically whenever stdout is not a terminal, for example when piped into less or redirected to a file. Because of this, omnidiff BEFORE AFTER | less works without the flag.

    OmniDiff collapses long runs of unchanged lines. It keeps 3 lines of context on each side of a change (override with --context N), the same convention as diff -u. OmniDiff also prefixes each hunk with the nearest enclosing function, class, or struct line, when that line is not otherwise visible. This shows the location of a change deep inside a large file.

    Colors are on by default; pass --color never, or set NO_COLOR=1, to disable ANSI colors, for example when you redirect output to a file - --color always forces them even under NO_COLOR.

    OmniDiff exits 0 on success and 2 on error. For scripting, pass --exit-code to additionally get 1 when the files differ. This follows the diff(1) convention. --exit-code is opt-in rather than the default for the same reason git diff exits 0 even when files differ: OmniDiff's usual non-interactive callers are version control systems driving it as a display tool, and they read a non-zero exit as "the tool failed" - jj warns on every file, and git difftool with difftool.trustExitCode=true aborts the whole diff. (The 7-argument GIT_EXTERNAL_DIFF form stays at 0 even with --exit-code, since git treats a non-zero exit there as fatal.)

    JSON output

    omnidiff --mode json BEFORE AFTER prints the diff as one JSON object. Its fields are described in src/tui/json_output.schema.json, a JSON Schema.

    Git integration

    OmniDiff is a git difftool backend. Run the interactive setup wizard, which asks whether to configure it globally or for the current repository only:

    omnidiff git configure
    

    Configure by hand

    git config difftool.omnidiff.cmd 'omnidiff "$LOCAL" "$REMOTE"'
    git difftool --tool=omnidiff
    

    Run git config diff.tool omnidiff to make plain git difftool use OmniDiff by default, without needing --tool. If you do not want git to ask "view diff ... [Y/n]?" before every file, run git config difftool.prompt false.

    Usage

    git difftool opens the interactive TUI. git diff and git log -p never do. git diff pipes its output through git's pager, and a full-screen TUI cannot draw onto a pipe, so OmniDiff always falls back to plain text there regardless of terminal or GIT_EXTERNAL_DIFF config (see "Headless / batch mode" above). If you want the interactive viewer from git, use git difftool, not git diff. If git difftool still doesn't open interactively over SSH, reconnect with ssh -t — the session needs an allocated pseudo-terminal; tmux panes always have one.

    OmniDiff also works directly with git diff and git log -p, through GIT_EXTERNAL_DIFF. This path needs no difftool config:

    GIT_EXTERNAL_DIFF=omnidiff git diff
    

    Pictures, archives, fonts and the rest of the supported content get a short report of what changed in them. Any other binary file gets a one-line Binary file <path> differs notice instead of a diff, the same stand-in git and diff(1) print for them. Neither ever blocks the rest of a git diff: an external diff that exits non-zero makes git abandon the entire run, so OmniDiff reports an unshowable file as a successful diff of nothing rather than as a failure.

    Jujutsu (jj) integration

    jj does not read git's difftool/diff.external settings, even in a colocated repo, so it needs its own configuration. Run the setup wizard:

    omnidiff jj configure
    

    Configure by hand

    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
    

    That registers jj diff --tool omnidiff. To make it the default for plain jj diff as well:

    jj config set --user ui.diff-formatter omnidiff
    

    Use --repo in place of --user to configure the current repository only.

    diff-invocation-mode = "file-by-file" is required. jj's default hands a diff tool two directory trees; OmniDiff diffs two files, so without this setting every invocation fails. With it, jj passes one changed file pair at a time, keeping each file's real path and extension, so language detection works exactly as it does under git.

    Usage

    jj diff runs its formatter under a pager, so OmniDiff renders in its non-interactive text mode there - the same output git diff gets. jj has no equivalent of git difftool's interactive per-file viewer (its terminal-attached hook, ui.diff-editor, is for jj diffedit/jj split, which edit the right-hand side and read it back - not something a read-only viewer should claim to do), so for the full-screen TUI on a jj repo, run omnidiff BEFORE AFTER directly.

    Supported content

    The language is detected from the file extension. A file with an unknown extension is diffed as plain text, line by line, so nothing is refused.

    25 languages are parsed with a tree-sitter grammar and diffed structurally:

    LanguageFile extensions
    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

    Recognised by extension but diffed as plain text, since no grammar is compiled in: Bazel (.bazel), Dart (.dart), Emacs Lisp (.el), Markdown (.md, .markdown), Protocol Buffers (.proto), SQL (.sql).

    Files that are not text are recognised by their bytes, never by their names, and diffed by what they hold:

    ContentFormatsDiffed
    PicturesPNG, JPEG, GIF, WebP, BMP, ICO, TIFFpixel by pixel, as the eye compares them: how much changed and where; animations frame by frame
    Archiveszip (and so jar, Office documents, EPUB), tar, gzip, xz, bzip2file by file, each by what it is, archives inside archives included; Java class files as a listing of their fields and methods
    FontsTrueType, OpenType, WOFF, WOFF2, EOTglyph by glyph, each drawn, and the font's names and metrics
    CursorsWindows .cur and .ani, X cursors, Hyprland .hlcsize by size and frame by frame
    Message catalogsgettext .mo, Qt .qmmessage by message
    DocumentsPDFpage by page, each page drawn
    Text in other encodingsUTF-16 and UTF-32 with a byte order markas text, like any other file

    Only the members that changed are opened, so a font with two edited glyphs shows two glyphs. Each change is also named by how much of it a reader would see: invisible (only the bytes differ), imperceptible, artifacts (what compression or resampling leaves behind), edited, redrawn or replaced - for text, whitespace, formatting and rewritten stand in for the second, third and fifth.

    The TUI shows pictures as pictures: in the terminal's own graphics where it speaks the kitty, sixel or iTerm2 protocol, and in Unicode half blocks everywhere else. b switches to half blocks and back, and OMNIDIFF_GRAPHICS=halfblocks (or kitty, sixel, iterm2) overrides the detection; inside tmux, the terminal in front of the pane decides. An archive, a font or any other container lists its changed members beside the one selected, g shows every changed glyph or picture at once, and Enter opens a text member in the full diff view.

    Anything else binary gets the one-line Binary file <path> differs notice.

    License

    Copyright (C) 2026 Marko Ivankovic

    This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.

    See the LICENSE file for the full text of the License.

    Cannot use AGPL software?

    A commercial license is available as a monthly subscription through GitHub Sponsors. It covers internal use of omnidiff by your organisation without the source-disclosure obligations of the AGPL. The terms are in LICENSE-COMMERCIAL. Pick the tier that names the commercial license as a benefit. For invoicing or other arrangements, contact me at [email protected].

    Guiding principles

    Fast

    OmniDiff's goal is:

    • A median diff in 100ms or less.
    • A 99th-percentile diff in 1000ms or less.

    Both are met. Over the 2,302 fixtures in src/test/data/diffs/, measured 2026-11-07 on a laptop, an Intel Core i7-1370P (6 performance and 8 efficiency cores, 20 threads) with 32 GB RAM: p50 4.4ms, p90 35.7ms, p99 181ms, slowest 507ms. 100ms is the 97.5th percentile — 57 fixtures take longer than that, and none takes longer than a second.

    Times belong to the machine that measured them, so each machine keeps its own (see research/data/machines.csv): the Intel Xeon E3-1275 v5 server that measured the research figures takes about 2.5 times as long per fixture. Runtime is reported, never gated: make check-quality prints this distribution against the machine's own committed baseline on every push, and warns when the runtime is more than twice the baseline's.

    Other content is timed over pairs sampled from the same corpus, decoding included - unpacking an archive, drawing a font's glyphs or a PDF's pages - measured 2026-11-07 on the same laptop:

    ContentPairsp50p90p99Under 100ms
    Pictures2302.1ms43ms589ms96.1%
    Cursors220.6ms1.3ms18ms100%
    Fonts1008.4ms60ms1,109ms93%
    Archives1003.3ms160ms1,878ms87%
    Message catalogs810.4ms2.1ms5.1ms100%
    PDFs10089ms473ms1,330ms55%

    Each figure is the median of three runs. Archives, fonts and PDFs miss the 99th-percentile goal: the slowest archives hold thousands of changed members, and a font or a PDF draws every changed glyph or page. Only 55% of PDFs take under 100ms. make benchmark-content measures this and writes research/data/performance/content_speed/<machine>.csv.

    Robust

    OmniDiff's goal is to process 100% of all commits.

    The full test dataset holds the git commit history of about 7,500 open-source git repositories, as available on the main branch. This list of repositories comes from the Gentoo Linux distribution. Find it in list_of_repositories.csv.

    Measured over every modified code file in the most recent 50 commits of each of those repositories - 442,530 readable before/after pairs in 25 languages, diffed with no size cap under a 120-second budget and a 6 GB memory cap per process - 442,322 (99.95%) completed successfully. 96 pairs ran past the budget and 112 past the memory cap; the latter are twenty files, nineteen of them generated or embedded - tree-sitter parser tables, codegen, minified bundles, a PNG as a C array - and one commit of a 40,000-line single-header C++ library. Given 24 GB and 300 s, 11 of the 28 files that first hit the cap complete. The run, its harness and every pair that did not complete are documented in research/data/performance/PROVENANCE.md.

    Beyond code, over every file changed in the same window - 2,018,010 changes in 7,352 repositories, pictures, fonts and archives included - 98.8% are diffed as text or by their content (98.2% counting each repository at most 1,000 times, so that a few giant ones do not decide it; 6,481 repositories entirely). What is left is mostly text in legacy encodings, compiled objects, audio, and EOT fonts compressed with MicroType Express. The census is documented in research/data/corpus_stats/PROVENANCE.md.

    Accurate

    OmniDiff must match a human's own reading of a change, measured against the hand-authored ground-truth mappings in src/test/data/diffs/:

    • 90% of test cases with zero mismatched bytes.
    • 99% of test cases with at most 1% of bytes mismatched.

    Neither is met yet. 1,479 of the 2,302 fixtures carry a hand-painted ground truth - every byte of both files labelled with what a human says happened to it - and OmniDiff's own highlighting is compared against it byte by byte, under each of its two highlighting presets (--full, which keeps brackets, separators and leading whitespace, and --minimal, which drops them):

    presetzero mismatched bytesat most 1% mismatchedmismatched bytes, whole corpus
    --full873 (59.0%)1,291 (87.3%)112,321 of 75.9M (0.15%)
    --minimal1,021 (69.0%)1,354 (91.5%)55,930 of 75.9M (0.07%)

    The whole-corpus rate is far below 1% because most of each file is unchanged and nobody gets that wrong; the per-test-case numbers are the ones that count, since a reader meets the mistakes one diff at a time. Most of what is left is not in the matching: rendering the human's own node mapping still disagrees with the painting on 92% of the --full bytes and 92% of the --minimal ones, so the highlighting rules own the gap more than the matcher does.

    make update-painting-attribution measures this and writes one row per fixture and preset to research/data/quality/painting_attribution.csv, which the table is counted from; make check-painting-attribution fails if any fixture gets worse.

    Other content is judged rather than painted: a human names how much each change is (invisible, imperceptible, artifacts, edited, redrawn or replaced, see supported content) and OmniDiff's verdict is compared with theirs. A test case is perfect when every verdict matches, and near-perfect when every one is at most one level off:

    ContentJudgedPerfectNear-perfect
    Pictures13191 (69%)119 (91%)
    Cursors1311 (85%)13 (100%)
    Fonts119 (82%)11 (100%)
    Archives2320 (87%)20 (87%)
    Message catalogs1210 (83%)10 (83%)
    PDFs44 (100%)4 (100%)

    make benchmark-content counts these too.

    AI policy

    This project uses substantial AI assistance, currently Claude Code. Most commits disclose this with a Co-Authored-By trailer and a link to the session that produced them. This project does not hide that fact. This project does not treat AI assistance as a lesser way to write software.

    Contributions are not accepted at this time, to keep the development speed high (see CONTRIBUTING.md). When that changes, AI-assisted contributions will be as welcome as any other, disclosed the same way: whoever submits the work is responsible for understanding it and standing behind it.

    For Developers, human or otherwise

    See CONTRIBUTING.md for the technology overview, code-quality and testing expectations, project structure, and what CI checks on every push and PR. AGENTS.md has additional AI-agent-specific conventions.