# diffsitter: an AST difftool that ignores reformatting

> diffsitter diffs the tree-sitter parse tree of a file instead of its text, so whitespace and line-break churn disappear. It is a Rust CLI that is still self-described as a work in progress.

**afnanenayet/diffsitter** — A tree-sitter based AST difftool to get meaningful semantic diffs

- Repository: https://github.com/afnanenayet/diffsitter
- Stars: 2,404 · Forks: 53
- Language: Rust
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/afnanenayet-diffsitter

## The problem diffsitter picks out of the pile

Text diffs are literal. Move a function to the bottom of a file, reindent a block, or split a call across lines, and the diff reports a large change even when the program means exactly the same thing. Reviewers then spend their attention on formatting noise and miss the one line that alters behaviour. diffsitter attacks that directly: it parses both files with tree-sitter and computes the diff over the abstract syntax tree, so the comparison happens between structural nodes rather than between characters. The README states the goal plainly: it creates semantically meaningful diffs that ignore formatting differences like spacing. The audience is anyone who reads diffs for a living in a supported language, and who has ever had a whitespace-only commit hide a real edit. It is a CLI, not a service, and it is not a general-purpose binary differ: the languages it handles are exactly the languages for which tree-sitter grammars exist and are wired in.

## What the AST diff actually compares

The pipeline is short. diffsitter reads two files, selects a tree-sitter grammar for the language, parses each file into a syntax tree, and diffs those trees. The output is a hunk list keyed by line numbers from the original files, which is why the README example prints entries such as 9:, 11: and 1: against the source rather than a unified-diff header. The worked example in the README makes the trade-off visible. One file has fn main() { let x = 1; } and an empty fn add_one; the other spreads main across a dozen lines and renames the second function. GNU diff reports the whole block as changed. diffsitter reports the added brace, the added fn addition(), the removed let x = 1; and the removed fn add_one, and it does not report the reformatting of main at all. That is the point, and it is also the risk: a change that only moves code can vanish from the output. Node filtering is the second lever. The config exposes include_kinds and exclude_kinds under input-processing, where the value is the tree-sitter node kind, and exclude_kinds takes precedence over include_kinds. The README is explicit that this applies to leaf nodes only, and that recursive exclusion is a maybe. That is a real ceiling on how precisely you can shape the diff.

## Installing diffsitter and running a first diff

The README lists several routes. Cargo builds from source and is the route that always exists; published binaries come from GitHub Actions for each tagged release, with a nightly tag as well. Homebrew users can tap afnanenayet/tap, Arch users have diffsitter-bin on the AUR via @samhh, and Alpine ships a diffsitter package from v3.16 onward with grammars packaged separately as tree-sitter-* packages or the tree-sitter-grammars virtual package. The cargo command is:

```bash
cargo install diffsitter --bin diffsitter
```

There is a second binary, diffsitter_completions, installed the same way with --bin diffsitter_completions, for generating completion files. Once installed, the simplest useful invocation is to point it at two files in a supported language:

```bash
diffsitter test_data/short/rust/a.rs test_data/short/rust/b.rs
```

What you should see is a hunk list headed by the two paths and a separator line, with each hunk labelled by a line number and prefixed + or - for added and removed nodes. Before you rely on defaults, print the config the tool would use:

```bash
diffsitter dump-default-config
```

The README says diffsitter looks for config at ${XDG_HOME:-$HOME}/.config/diffsitter/config.json5 on macOS and Linux and in the standard directory on Windows, and that the sample lives at assets/sample_config.json5. To override the path, use the --config flag or set the DIFFSITTER_CONFIG environment variable. Node filtering is written in JSON5, in a block like this:

```json5
"input-processing": {
  "exclude_kinds": ["string_content"],
  "include_kinds": ["method_definition"]
}
```

For Docker, the repository ships a Dockerfile that builds with a Rust base image and copies the binary into debian:bullseye-slim, with the documented commands docker build -t diffsitter . followed by docker run -it --rm --name diffsitter-interactive diffsitter. Note that the Dockerfile in the repository pins rust:1.67 in the builder stage while Cargo.toml declares rust-version 1.85.1, so the container path and the source path are not obviously built against the same toolchain. Verify that before you standardise on the image.

## Where diffsitter is the wrong tool

The README opens with a disclaimer that diffsitter is very much a work in progress and nowhere close to production ready. Take that at face value. Three consequences follow. First, language coverage is bounded by tree-sitter grammars, so a diff of a config format, a template language or a data file outside the supported list will not get AST treatment. Second, the diff is lossy by design: because formatting is ignored, a change that only relocates code may not appear, and if your review depends on seeing that a block moved, diffsitter will not tell you. Third, node filtering only reaches leaf nodes, so you cannot currently ask it to drop or keep an entire subtree. The output format is also its own thing rather than unified diff, which matters if you expect to pipe it into tooling that parses --- and +++ headers. The README does not document a rollback or undo path, because there is nothing to roll back: diffsitter reads files and prints a report, it does not write. If you need a patch you can apply, this is not that.

## How it sits next to Difftastic and structural diff tools

The obvious comparison is Difftastic, which also parses code and diffs syntax trees. The difference in approach is granularity and presentation. Difftastic is built around a side-by-side terminal view and word-level highlighting inside the structural diff; diffsitter prints a hunk list keyed to line numbers in the original files, with terminal-aware formatting and optional logging for timing and debugging. diffsitter also exposes explicit node-kind filtering through include_kinds and exclude_kinds, which Difftastic does not present in the same way. If you want a pager-style reading experience, Difftastic is closer to that; if you want a filterable report you can shape through a config file, diffsitter's model is more direct. Both inherit the same ceiling: the parse must succeed, and the language must have a grammar. Neither replaces git diff for non-code files.

## Maintenance, licensing and what upgrading costs

The repository is not archived, and the last push was on 2026-09-28. Releases show a nightly tag dated the same day, v0.9.0 from 2025-04-27 and v0.8.4 from 2024-08-15, so the tagged cadence is roughly annual while nightly tracks main. That pattern tells you something practical: if you want fixes between tags, you are on nightly, and nightly is not a stability promise. The crate is MIT licensed, which is permissive and imposes no source-disclosure obligation on your own code; the tree-sitter grammars bundled under grammars/ may carry their own licences, and the README does not summarise them, so check the grammar directories if you redistribute a build. Upgrade cost is dominated by two coupling points. The tree-sitter dependency is pinned at 0.26.3 in Cargo.toml, and node kinds come from the grammar, so a grammar bump can change the kind names your include_kinds and exclude_kinds reference. The sample config is validated by the crate's tests according to the README, which is a useful signal but also means the config schema moves with the code. Re-run dump-default-config after every upgrade and diff it against your own file.

## Conclusion

Adopt diffsitter if you review code where reformatting keeps drowning the real change, and you are comfortable with a tool whose own README calls it nowhere close to production ready. Do not adopt it as a drop-in git diff replacement for arbitrary file types, and do not assume it will parse a language outside its published list. Before wiring it into anything, run diffsitter dump-default-config to read the config it will actually use, then point --config or DIFFSITTER_CONFIG at a file you control and confirm the include_kinds and exclude_kinds values match the node kinds your grammars emit.

## FAQ

### What does diffsitter do that git diff does not?

It diffs the tree-sitter parse tree of two files rather than their text, so formatting differences such as spacing and line breaks are ignored. The README's Rust example shows GNU diff flagging a reformatted main function while diffsitter does not.

### What is the point of tree-sitter in diffsitter?

tree-sitter supplies the parsers diffsitter uses to turn source code into an AST. The README states that the languages diffsitter supports are restricted to the languages supported by tree-sitter.

### How do I install diffsitter?

The README lists cargo install diffsitter --bin diffsitter, a Homebrew tap at afnanenayet/tap, the diffsitter-bin AUR package, an Alpine package, published GitHub release binaries, and a Docker build.

### Which languages does diffsitter support?

The README lists Bash, C#, C++, CSS, Go, Java, OCaml, PHP, Python, Ruby, Rust, Typescript/TSX and HCL. Anything outside that list depends on whether a tree-sitter grammar is available and wired in.

### Where does diffsitter look for its config file?

It looks at ${XDG_HOME:-$HOME}/.config/diffsitter/config.json5 on macOS and Linux and the standard Windows directory. You can override the path with the --config flag or the DIFFSITTER_CONFIG environment variable, and dump-default-config prints the defaults.

### Can diffsitter filter which nodes appear in the diff?

Yes, through include_kinds and exclude_kinds under input-processing in the config, using tree-sitter node kinds. The README notes that exclude_kinds takes precedence and that filtering currently applies to leaf nodes only.

## Sources

- [afnanenayet/diffsitter on GitHub](https://github.com/afnanenayet/diffsitter)
- [Issues](https://github.com/afnanenayet/diffsitter/issues)
- [License: MIT](https://github.com/afnanenayet/diffsitter/blob/main/LICENSE)
- [README](https://github.com/afnanenayet/diffsitter/blob/main/README.md)
- [Releases](https://github.com/afnanenayet/diffsitter/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/afnanenayet-diffsitter
