Skip to main content

divot

crates.io docs.rs Downloads License

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 from diff is 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/after text 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 by gitoxide — 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.

Per-diff latency: divot vs jsdiff, log-log scatter across 270 real pairs Per-diff speedup: divot vs jsdiff across 270 real pairs, mean marked

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.

Batch/parallel diffing throughput vs thread count, divot vs jsdiff single-threaded reference line

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)

Source distribution for divot 0.1.0
File Size Uploaded
divot-0.1.0.tar.gz 27.5 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for divot 0.1.0
File
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page