codediff 0.0.14

Fast, robust, syntax-aware code diffing using tree-sitter ASTs
Documentation
[package]
name = "codediff"
version = "0.0.14"
edition = "2024"
rust-version = "1.88"
description = "Fast, robust, syntax-aware code diffing using tree-sitter ASTs"
license = "AGPL-3.0-or-later"
repository = "https://github.com/ivankovic/codediff"
readme = "README.md"
keywords = ["diff", "ast", "tree-sitter", "cli", "git"]
categories = ["command-line-utilities", "development-tools", "text-processing"]
exclude = [
    "research/**",
    # Checkout-only: the `stats`- and `test-fixtures`-gated [[bin]] targets below (ten total)
    # cannot be built from the published/packaged crate once their source is excluded here
    # (confirmed via `cargo package` + a build against the extracted tarball with `--features
    # stats`: cargo reports no such bin target at all). That's intentional - these are
    # dataset-analysis/dev-fixture-curation tools, not part of the published product (see README's
    # Installation section) - but nothing else signals this boundary, so `cargo install codediff
    # --features stats` fails with a confusing error rather than an explanatory one. Noted here for
    # whoever next touches this.
    "src/bin/**",
    "src/test/data/**",
    # Excluded for the same reason, one step removed: tests/benchmark_other_e2e.rs spawns two of
    # those excluded bins through `CARGO_BIN_EXE_*`, so a packaged crate would carry a [[test]]
    # target that cannot compile - `cargo package` keeps tests/ and the manifest stanza by default
    # even when the binaries the test names are gone (confirmed by unpacking the .crate).
    "tests/**",
    # Distribution recipes, not crate content: the ebuild alone is ~300 lines of dependency names.
    # Both are excluded together on purpose - flake.nix imports packaging/nix/package.nix, so
    # shipping one without the other would leave a flake that cannot evaluate.
    "packaging/**",
    "flake.nix",
]

# No [profile.release] section existed before this (2026-07-25, speed-goal investigation) - Cargo's
# own defaults (codegen-units = 16, lto = false) leave real, free performance on the table for a
# CPU-bound algorithmic tool like this one. `lto = "fat"` lets the optimizer see across crate
# boundaries (tree-sitter, the APTED engine's own module split); `codegen-units = 1` trades slower
# release builds for better cross-function inlining within this crate. Pure compiler-flag change -
# zero source code touched, output is bit-for-bit the same algorithm, so this carries none of the
# correctness risk a concurrency or algorithmic change would.
[profile.release]
lto = "fat"
codegen-units = 1

# Debian packaging, driven by `cargo deb` (cargo-deb), not by the Debian archive's own toolchain.
# That distinction is deliberate and worth stating: a package in Debian proper would need every one
# of this crate's 293 dependencies packaged as `librust-*-dev` first, including 24 tree-sitter
# grammar crates, almost none of which are in Debian. That path is not reachable. What this
# produces is an unofficial .deb, built by the release workflow and attached to the GitHub release,
# which installs the same files the Arch/Gentoo/Nix recipes under packaging/ do.
#
# `cargo deb` is not vendored here; install it with `cargo install cargo-deb`. See packaging/README.md.
[package.metadata.deb]
maintainer = "Marko Ivankovic <marko@ivankovic.me>"
copyright = "2026 Marko Ivankovic"
license-file = ["LICENSE", "0"]
extended-description = """
CodeDiff parses both sides of a diff with tree-sitter and matches their syntax \
trees, so a change is reported as what it structurally is - an insertion, \
deletion, update, or move - rather than as whichever lines happened to align. \
It runs as an interactive terminal UI, a plain-text batch formatter, or a JSON \
emitter for editor integrations, and works as a git difftool backend and a \
Jujutsu (jj) diff formatter."""
section = "devel"
priority = "optional"
# `$auto` lets cargo-deb read the built binary's actual ELF dependencies (libc, libgcc) rather than
# guessing. Every tree-sitter grammar is compiled in statically, so there is nothing else to name.
depends = "$auto"
assets = [
    ["target/release/codediff", "usr/bin/", "755"],
    ["README.md", "usr/share/doc/codediff/README.md", "644"],
    # Generated before packaging by `codediff util man` / `codediff util completions` - see the
    # `deb` job in .github/workflows/release.yml and packaging/README.md. cargo-deb copies files
    # from disk and cannot run the binary itself, so these must already exist or the build fails
    # loudly (which is the intent: a silently man-page-less .deb is worse).
    ["target/dist/codediff.1", "usr/share/man/man1/", "644"],
    ["target/dist/codediff.bash", "usr/share/bash-completion/completions/codediff", "644"],
    ["target/dist/_codediff", "usr/share/zsh/vendor-completions/", "644"],
    ["target/dist/codediff.fish", "usr/share/fish/vendor_completions.d/", "644"],
]

[dependencies]
anyhow = "1.0.100"
clap = { version = "4.5.51", features = ["derive"] }
# Both generate packaging artifacts (shell completions, a man page) for `codediff util
# completions`/`codediff util man`, and are used only by src/main.rs - so they ride the `tui`
# feature alongside the rest of the product binary's deps rather than being unconditional, keeping
# a `default-features = false` library consumer free of them.
clap_complete = { version = "4.6.9", optional = true }
# 0.2.x, not 0.3.x, deliberately. clap_mangen 0.3 requires clap >= 4.6.6; depending on it would
# raise this crate's *effective* clap floor from the declared 4.5.51 to 4.6.6 without the manifest
# ever saying so (cargo just unifies the two requirements silently). 0.2.33 accepts clap 4.0+, so
# it imposes no floor of its own - a man-page generator is not a reason to constrain the CLI
# framework every consumer of this crate resolves against.
clap_mangen = { version = "0.2.33", optional = true }
# TUI-only, but also used directly by src/bin/human_solver/'s own small debug UI - gated by the
# `tui` feature (see [features] below), not unconditional: a pure library consumer of
# `codediff::diff`/`codediff::code` has no use for it.
confy = { version = "2.0.0", optional = true }
# Only used by src/stats/ (feature-gated) and the stats-gated src/bin/ tools that need concurrent
# filesystem walks (file_stats.rs, commit_stats.rs) - gated the same way as git2/rusqlite below.
crossbeam-channel = { version = "0.5.15", optional = true }
# Used by both the stats-gated dev tools (materialize_test_diffs.rs, sample_code_pairs.rs,
# sample_test_diffs.rs, benchmark_diff_pairs.rs) and the three that only need test fixtures
# (human_solver.rs, benchmark_optimal_solutions.rs, benchmark_other.rs) - gated by `test-fixtures`,
# which `stats` also includes (see [features] below), so either feature alone is sufficient.
csv = { version = "1.4.0", optional = true }
git2 = { version = "0.20.3", optional = true }
# Only used by src/stats.rs itself - gated the same way as the rest of the stats-only deps.
indoc = { version = "2.0.7", optional = true }
# Only used by src/bin/file_stats.rs (stats-gated).
libc = { version = "0.2", optional = true }
# Only used by src/bin/file_stats.rs and commit_stats.rs (both stats-gated).
num_cpus = { version = "1.17.0", optional = true }
# Only used by src/stats/sampling.rs (feature-gated) and the stats-gated sample_*.rs dev tools.
rand = { version = "0.8", optional = true }
# Only used by src/stats.rs and the fixture-loading helpers under src/test/ (`test-fixtures`);
# also a dev-dependency below because those helpers compile under a plain `cargo test` too.
regex = { version = "1.12.2", optional = true }
# Terminal cell widths for `text_range::ScreenColumn`. Already in the lockfile via ratatui, so
# declaring it directly adds no new dependency - it makes the existing one honest, since the
# source->screen column conversion needs it outside the TUI too (headless rendering).
unicode-width = "0.1.14"
# Bundled/C-compiled, and (like git2 just above) only used by two of the stats-gated src/bin/
# tools (commit_stats.rs, file_stats.rs) - gated the same way for the same reason: the main
# codediff binary/library has no use for it, so it shouldn't be a mandatory build-time cost for
# every `cargo install`/library consumer.
rusqlite = { version = "0.37.0", features = ["bundled"], optional = true }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0"
# Only used by tests and the `test-fixtures` helpers - gated like regex above, for the same reason.
tempfile = { version = "3.10.1", optional = true }
# TUI-only (the event stream) - gated by `tui`.
futures = { version = "0.3", optional = true }
# TUI-only, but also used directly by src/bin/commit_stats.rs for concurrent I/O - gated by `tui`,
# same reasoning as confy above.
tokio = { version = "1.0", features = ["full"], optional = true }
tree-sitter = "0.25"
# TUI-only (see `tui` feature below) - also used directly by src/bin/human_solver/'s own small
# debug UI, same as confy/crossterm's siblings above.
ratatui = { version = "0.26.3", features = ["serde", "macros"], optional = true }
crossterm = { version = "0.27.0", features = ["serde", "event-stream"], optional = true }
tree-sitter-bash = "0.25"
tree-sitter-c = "0.24"
tree-sitter-c-sharp = "0.23"
tree-sitter-cpp = "0.23"
tree-sitter-css = "0.25"
tree-sitter-go = "0.25"
tree-sitter-html = "0.23"
tree-sitter-java = "0.23"
tree-sitter-javascript = "0.25"
tree-sitter-json = "0.24"
tree-sitter-kotlin-ng = "1.1"
tree-sitter-lua = "0.2"
tree-sitter-php = "0.24"
tree-sitter-python = "0.25"
tree-sitter-r = "1.2"
tree-sitter-ruby = "0.23.1"
tree-sitter-rust = "0.24"
tree-sitter-scala = "0.24"
tree-sitter-swift = "0.7"
tree-sitter-typescript = "0.23"
tree-sitter-vim = "0.4"
tree-sitter-xml = "0.7.0"
tree-sitter-yaml = "0.7"
# Only used by src/stats/filesystem.rs - gated by `stats`.
walkdir = { version = "2", optional = true }
metrohash = "1.0.7"
rustc-hash = "2.1"
# All three TUI-only (src/tui.rs sets up the subscriber, src/tui/app.rs logs) - gated by `tui`.
tracing = { version = "0.1.44", optional = true }
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "serde"], optional = true }
tracing-error = { version = "0.2.1", optional = true }
strum = { version = "0.28.0", features = ["derive"] }
syntect = { version = "5.2", features = ["default-fancy"], optional = true }
two-face = { version = "0.5.2", default-features = false, features = ["syntect-fancy"], optional = true }

# Unix only, and only for `libc::raise(SIGTSTP)` in `tui::app::suspend` - the Ctrl-Z half of
# job control, which has no Windows equivalent. Already present in Cargo.lock as a transitive
# dependency of tokio and crossterm, so naming it here does not add a crate to the tree (and so
# leaves packaging/gentoo's generated CRATES= block untouched).
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[dev-dependencies]
# The same two crates the `test-fixtures` feature enables, for the `#[cfg(test)]` builds that
# compile src/test/ without that feature (see lib.rs's gate on `mod test`).
regex = "1.12.2"
tempfile = "3.10.1"

[features]
# `tui` on by default so a bare `cargo build`/`cargo install codediff` keeps producing the working
# `codediff` TUI binary, exactly as before this feature existed - a pure library consumer can still
# opt out entirely via `default-features = false`, which was not possible at all before (every one
# of these deps used to be unconditional).
default = ["tui"]
# Everything the TUI (src/tui/, src/main.rs) needs. Also required by `test-fixtures`/`stats` below,
# since a couple of the dev-tool bins those gate use some of these deps directly for their own
# unrelated purposes (human_solver.rs has its own small ratatui-based debug UI; commit_stats.rs
# uses tokio for concurrent I/O) - simpler to have every dev-tool feature imply `tui` than to track
# exactly which of these 6 deps each individual bin happens to need.
tui = [
    "dep:clap_complete",
    "dep:clap_mangen",
    "dep:ratatui",
    "dep:crossterm",
    "dep:tokio",
    "dep:syntect",
    "dep:two-face",
    "dep:confy",
    "dep:futures",
    "dep:tracing",
    "dep:tracing-subscriber",
    "dep:tracing-error",
]
# Gates `codediff::test` (fixture-loading helpers whose data files under src/test/data/ are
# excluded from the published crate - see this file's own `exclude` list - so the module itself
# must not be part of the crate's default public surface either, or a real dependent would see
# API that's guaranteed to fail at runtime). Needed by every src/bin/ dev tool, not just the three
# that don't otherwise need `stats` (human_solver.rs, benchmark_optimal_solutions.rs,
# benchmark_other.rs) - `stats` includes it below so the other six don't need to list it twice.
test-fixtures = ["dep:csv", "dep:regex", "dep:tempfile", "tui"]
# Pulls in git2 (and its openssl/libssh2 build-time deps), rusqlite (a bundled, C-compiled SQLite),
# and the rest of the git-history stats/sampling tools' dependencies for the src/bin/ tools gated
# the same way. Not needed by the main codediff binary or a library consumer, so it's opt-in to
# keep `cargo install codediff` and library consumers free of that dependency chain by default.
stats = [
    "dep:git2",
    "dep:rusqlite",
    "dep:crossbeam-channel",
    "dep:libc",
    "dep:num_cpus",
    "dep:rand",
    "dep:indoc",
    "dep:walkdir",
    "test-fixtures",
]
# The browser front end: `codediff-web` (src/web_main.rs, src/web/) serves the same diff the TUI
# draws, to a page in a local browser. Implies `tui` because it reuses that module's diff-session
# assembly (`tui::app::compute_diff_with_options`), config persistence (`tui::theme`) and syntect
# highlighting (`tui::widgets::code_viewer`) rather than duplicating them. It adds no dependency
# of its own: the HTTP/1.1 subset a single-page app on localhost needs is small enough to sit on
# the tokio `tui` already pulls in (src/web/http.rs says why that beats a server crate here), so
# Cargo.lock - and with it packaging/gentoo's generated CRATES= block - is unchanged by it.
# Off by default: `cargo install codediff` keeps producing exactly the one binary it always has.
web = ["tui"]

# The one integration test in this project: `benchmark_other`'s harness end to end, driven through
# a stand-in tool (`fake_diff_tool`) whose answers are known in advance. It has to live in tests/
# rather than beside the binary's own unit tests because `CARGO_BIN_EXE_<name>` - the only way to
# get a built binary's path without shelling out to cargo - is set for integration tests only.
[[test]]
name = "benchmark_other_e2e"
path = "tests/benchmark_other_e2e.rs"
# Both binaries it spawns are gated this way, and so is the `csv` crate it reads results with, so
# a bare `cargo test` skips this target instead of failing to build.
required-features = ["test-fixtures"]

[[bin]]
name = "codediff"
path = "src/main.rs"
required-features = ["tui"]

[[bin]]
name = "codediff-web"
path = "src/web_main.rs"
required-features = ["web"]

[[bin]]
name = "ascii_visualizer"
path = "src/bin/ascii_visualizer.rs"
# Debug/dev tool only (a plain-text AST dump), no test-fixture or stats deps of its own - reuses
# `test-fixtures` as the general "checkout-only dev tool" gate rather than inventing a feature
# just for this one binary.
required-features = ["test-fixtures"]

[[bin]]
name = "human_solver"
path = "src/bin/human_solver/main.rs"
required-features = ["test-fixtures"]

[[bin]]
name = "benchmark_optimal_solutions"
path = "src/bin/benchmark_optimal_solutions.rs"
required-features = ["test-fixtures"]

[[bin]]
name = "benchmark_other"
path = "src/bin/benchmark_other.rs"
required-features = ["test-fixtures"]

[[bin]]
name = "generate_mapping_site"
path = "src/bin/generate_mapping_site.rs"
required-features = ["test-fixtures"]

[[bin]]
# Research-only: scores codediff against the Alikhanifard & Tsantalis AST node-mapping oracle
# (see research/external/README.md). Needs `csv`, hence the gate; touches nothing in the product.
name = "benchmark_astdiff_oracle"
path = "src/bin/benchmark_astdiff_oracle.rs"
required-features = ["test-fixtures"]

[[bin]]
# Not a diff tool: a stand-in that answers predictably (nothing changed / everything changed /
# a per-line hash / a crash) so `benchmark_other`'s harness can be scored against arithmetic
# instead of against a real tool whose correct answer nobody knows. Speaks difftastic's JSON, so
# `DIFFT_BIN` points at it and a real adapter is what runs. See tests/benchmark_other_e2e.rs.
name = "fake_diff_tool"
path = "src/bin/fake_diff_tool.rs"
# No fixture dependency of its own - gated like ascii_visualizer's entry, reusing `test-fixtures`
# as the general "checkout-only dev tool" gate so it stays out of the published crate.
required-features = ["test-fixtures"]

[[bin]]
name = "diff_inventory"
path = "src/bin/diff_inventory.rs"
required-features = ["test-fixtures"]

[[bin]]
name = "analyze_human_mappings"
path = "src/bin/analyze_human_mappings.rs"
required-features = ["test-fixtures"]

[[bin]]
name = "apted_only_worker"
path = "src/bin/apted_only_worker.rs"
# Single-pair worker, spawned as a subprocess by apted_only_benchmark (stats-gated, below) -
# doesn't itself need git2/csv, but gated the same way as ascii_visualizer's own bin entry: reuse
# test-fixtures as the general "checkout-only dev tool" gate rather than inventing a feature for
# one binary with no fixture/stats dependency of its own.
required-features = ["test-fixtures"]

[[bin]]
name = "apted_only_benchmark"
path = "src/bin/apted_only_benchmark.rs"
# Needs git2 (repo/blob access, same as benchmark_diff_pairs) and csv - both gated by `stats`.
required-features = ["stats"]

[[bin]]
name = "commit_stats"
path = "src/bin/commit_stats.rs"
required-features = ["stats"]

[[bin]]
name = "materialize_test_diffs"
path = "src/bin/materialize_test_diffs.rs"
required-features = ["stats"]

[[bin]]
name = "benchmark_diff_pairs"
path = "src/bin/benchmark_diff_pairs.rs"
required-features = ["stats"]

[[bin]]
name = "file_stats"
path = "src/bin/file_stats.rs"
required-features = ["stats"]

[[bin]]
name = "reclassify_tips"
path = "src/bin/reclassify_tips.rs"
required-features = ["stats"]

[[bin]]
name = "sample_test_diffs"
path = "src/bin/sample_test_diffs.rs"
required-features = ["stats"]

[[bin]]
name = "sample_code_pairs"
path = "src/bin/sample_code_pairs.rs"
required-features = ["stats"]

[[bench]]
name = "diff_code_benchmark"
harness = false
# Loads its inputs through `codediff::test::helper`, which is gated the same way (see `test-fixtures`
# above), so a bare `cargo bench` skips this target instead of failing to build it.
required-features = ["test-fixtures"]

[dev-dependencies.criterion]
version = "0.5"
features = ["html_reports"]