divot
Fast diff/patch engine, in Rust, with Node.js and Python bindings.
Why
jsdiff (138M weekly downloads) is
the dominant diff library on npm, and its own docs admit a real
performance compromise: linked-list diagonal tracking instead of
optimized array-based Myers diff. divot's core is built on
imara-diff — the diff
engine that powers gitoxide in production — which doesn't make that
trade-off, and additionally implements the Histogram algorithm (git's
modern default: faster and more human-readable for code than plain
Myers).
Benefits
- ~4x faster than jsdiff for a single diff, ~11x for batch workloads (many files at once) — both measured through the real compiled native addon, not the underlying Rust library in isolation. See Performance for the full, honest breakdown, including where the numbers came down from bigger ones once FFI cost was accounted for.
jsdiff-shaped API (diffLines/diffWords/diffChars, the same{value, added, removed, count}result shape) — switching fromdiffis close to a drop-in change, not a rewrite.- Batch diffing with built-in parallelism (
diffLinesMany) — a capability jsdiff structurally can't offer (it's single-threaded with no batch API at all). Useful for linting a whole PR or diffing every file in a directory in one call. - Real correctness guarantees, not just speed: every diff function
is tested to reconstruct the original
before/aftertext exactly from its output, including multibyte UTF-8 — not just "looks right on an example." - Built on
imara-diff, the same diff engine trusted in production bygitoxide— not a from-scratch reimplementation of something security/correctness-sensitive.
Installation
Rust: published on crates.io.
cargo add divot
use divot::{line_diff, Algorithm};
let changes = line_diff("abc\ndef\n", "abc\nDEF\n", Algorithm::Histogram);
for c in &changes {
let sign = if c.added { "+" } else if c.removed { "-" } else { " " };
print!("{sign}{}", c.value);
}
Node: not yet published to npm. Build from source:
git clone https://github.com/hsputra/divot.git
cd divot
npm install
npm run build # compiles the native addon via napi-rs
This produces index.js/index.d.ts (already committed) plus a
platform-specific divot.<platform>.node binary. require("./index.js")
(or require("/path/to/divot"), which resolves the same way via
package.json's main field) from there.
Python: not yet published to PyPI. Build from source:
git clone https://github.com/hsputra/divot.git
cd divot
pip install maturin
maturin build --release
pip install target/wheels/divot-0.1.0-*.whl
import divot
changes = divot.diff_lines("abc\ndef\n", "abc\nDEF\n")
for c in changes:
sign = "+" if c.added else "-" if c.removed else " "
print(sign + c.value, end="")
How to use (Node)
The Rust API is line_diff/word_diff/char_diff/unified_diff
(see the crate docs) — same shape as the JS
functions below, just snake_case.
// Once published: require("divot"). Until then, per Installation above,
// require the path to your local build instead:
const { diffLines, diffWords, diffChars, unifiedDiff, diffLinesMany } = require("./index.js");
diffLines(before, after)
const before = "function greet(name) {\n console.log('Hello ' + name);\n}\n";
const after = "function greet(name) {\n console.log(`Hello ${name}!`);\n}\n";
for (const part of diffLines(before, after)) {
const prefix = part.added ? "+" : part.removed ? "-" : " ";
console.log(prefix + part.value.trimEnd());
}
// function greet(name) {
// - console.log('Hello ' + name);
// + console.log(`Hello ${name}!`);
// }
diffWords(before, after) / diffChars(before, after)
Same shape, finer granularity:
diffWords("the quick brown fox", "the slow brown fox");
// [
// { value: "the ", added: false, removed: false, count: 2 },
// { value: "quick", added: false, removed: true, count: 1 },
// { value: "slow", added: true, removed: false, count: 1 },
// { value: " brown fox", added: false, removed: false, count: 4 },
// ]
unifiedDiff(before, after)
Real git diff/diff -u-style patch text:
console.log(unifiedDiff(before, after));
// @@ -1,3 +1,3 @@
// function greet(name) {
// - console.log('Hello ' + name);
// + console.log(`Hello ${name}!`);
// }
diffLinesMany(pairs) — batch, parallel across CPU cores
For diffing many files at once (the realistic shape of CI/lint/AI-agent
workloads), not a loop of individual diffLines calls:
const results = diffLinesMany([
{ before: fileAContentOld, after: fileAContentNew },
{ before: fileBContentOld, after: fileBContentNew },
// ...
]);
// results[i] is exactly what diffLines(pairs[i].before, pairs[i].after)
// would return -- diffLinesMany just does all of them across a Rayon
// thread pool instead of one at a time.
Only reaches for this when you actually have many pairs — for a single
diff, plain diffLines is the right call; see
Performance for why.
Performance
Real, measured, reproducible — not projected. 270 before/after pairs
extracted from jQuery's actual git history (src/event.js, css.js,
core.js, manipulation.js, selector.js, ajax.js, effects.js,
deferred.js), diffed with the real diff npm package (jsdiff v9.0.0)
and with divot, same corpus, both warmed up first.
| Time/pair | vs. jsdiff | |
|---|---|---|
jsdiff v9.0.0 (diffLines) |
118.96µs | — |
| divot, via the real npm binding | 29.9µs | ~4.0x |
| divot, pure Rust (no FFI) | ~19.8µs | ~5.9x |
~4.0x is the number that matters — it's what the compiled npm binding actually delivers (not yet published, but built and measured exactly as it will ship — see Installation), measured through the real compiled native addon, not the underlying Rust library in isolation. The pure-Rust figure is shown for transparency about where FFI (UTF-16→UTF-8 conversion in, JS object construction out) costs real time, not to imply it's what you'll observe from JS.
Full methodology, raw CSV data, and the chart-generation script are in
plots/.
Batch diffing: a real capability jsdiff can't offer
diffLinesMany() diffs many pairs at once across all available CPU
cores (via Rayon internally) — not an algorithm improvement over
imara-diff, a different capability entirely: jsdiff is single-threaded
with no built-in batch API, so this isn't something it can match by
being faster, only by the caller manually parallelizing across worker
threads/processes themselves.
| Time/pair | vs. jsdiff (looped) | |
|---|---|---|
| jsdiff v9.0.0, called in a loop | ~118.4µs | — |
diffLinesMany, real npm binding, 18 cores |
~10.5µs | ~11x |
Verified this is genuinely about parallelism and not some batching
trick: forcing a single Rayon thread reproduces the non-batched ~19.8µs
pure-Rust number exactly. The multiplier scales with available cores
— it's a batch-throughput number, not a claim that any single diff got
11x faster. Useful for realistic bulk workloads (lint a whole PR, diff
every file in a directory), less useful for a one-off single diff, where
diffLines() is the right call.
Status
Early. Implemented and tested: line/word/char diffing, unified diff
output, npm binding (diffLines/diffWords/diffChars/unifiedDiff/
diffLinesMany, real native addon, real tests via Node's built-in test
runner), Python binding (diff_lines/diff_words/diff_chars/
unified_diff/diff_lines_many, real built wheel, real tests via
pytest). No CLI yet.
Not yet implemented: a fuzzy/atomic patch-application layer (the planned differentiator for LLM-coding-agent workflows — see the project's technical spec for detail on the failure modes it targets). Published on crates.io; not yet on npm or PyPI (both bindings are built and tested, just not yet published — see Installation above).
Credit
Built on imara-diff
(Apache-2.0) by Pascal Kuthe — the diff computation itself is that
crate's work; this project adds word/char tokenization (not provided
upstream), a jsdiff-compatible result shape, batch/parallel diffing,
and (planned) the patch-application layer.
License
Dual-licensed under MIT or Apache-2.0, at your option.
Metadata
Release files for divot 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| divot-0.1.0.tar.gz | 27.5 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| divot-0.1.0-cp38-abi3-win_amd64.whl | CPython 3.8 | abi3 | Windows x86-64 | Details |
| divot-0.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.8 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| divot-0.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.8 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| divot-0.1.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl | CPython 3.8 | abi3 | macOS 10.12+ x86-64, macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64) | Details |
Total release size: 1.6 MB
Release files / divot-0.1.0.tar.gz
| Download URL | divot-0.1.0.tar.gz |
|---|---|
| Size | 27.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6661c8d9335972395dfc039443fe29f9061f220ecc144ea16d9c3fcc0b6b00bc
|
|
BLAKE2b-256 checksum How to use checksums |
f9c989371d4261d97cf4027a9fe6bdf0e79c5e7964c07f018f2c5213e96b1b30
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.14.1
|
Release files / divot-0.1.0-cp38-abi3-win_amd64.whl
| Download URL | divot-0.1.0-cp38-abi3-win_amd64.whl |
|---|---|
| Size | 208.4 kB |
| Tags | CPython 3.8 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
0d91b605dbdd76e7a837b937d1819bb317d94c2ac49df5c32bb9f3ef8270c11b
|
|
BLAKE2b-256 checksum How to use checksums |
b44846391523957dee0462551b2e6a2de400b5dcbcfc5719e297c438acab5e90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.14.1
|
Release files / divot-0.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | divot-0.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 377.9 kB |
| Tags | CPython 3.8 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b2732ce5d14cda5b5f32e5b34e2e51fa1a375b1c8bc2fbfa287458c9994845c1
|
|
BLAKE2b-256 checksum How to use checksums |
a51814fccf726644d6a0c4d4209f9511cb9d0ddd6450d557c0cc2ae4c7c90727
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.14.1
|
Release files / divot-0.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | divot-0.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 370.7 kB |
| Tags | CPython 3.8 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
3c2c4f779bce7453f46098e7a357aa57dc77579bbb63042f052e4974684a6a3e
|
|
BLAKE2b-256 checksum How to use checksums |
42f283c702c039cee62dcf8db4ccb4180d2f293a28b9a69b1b1ff7d5b3183b96
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.14.1
|
Release files / divot-0.1.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | divot-0.1.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
|---|---|
| Size | 641.6 kB |
| Tags | CPython 3.8 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
335bf99f3f9af60853c9127dbea0671b7a61cde3c77729b7e1ba7360841b8542
|
|
BLAKE2b-256 checksum How to use checksums |
c359502cc71e2361d269e32d336b2161b6c0a39ea0e7be1ba9d12afacbac227f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.14.1
|