# riff refines a diff so you see the changed words, not the whole line

> riff is a Rust wrapper around diff that highlights which parts of a line actually changed, hyperlinks file names and hunk headers back to the source, and detects conflict markers. It plugs into git as a pager or a diff filter.

**walles/riff** — A syntax highter for diffs, clarifying which parts of lines have changed

- Repository: https://github.com/walles/riff
- Stars: 530 · Forks: 10
- Language: Rust
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/walles-riff

## Git already colours whole lines, riff colours the changed words

Git marks an added or removed line in its entirety. That is fine when a line is short, and close to useless when one token changes in the middle of a long statement. riff is a filter that sits between diff and the terminal and refines that output.

The description is a syntax highlighter for diffs, clarifying which parts of lines have changed. Unchanged parts of changed lines are shown in yellow, so a removed line reads as the sentence with the altered words picked out, and the surrounding words stay legible. The project is called Riff, the Refining Diff, and the crate is riffdiff.

The mechanism is visible in the implementation notes. The main program identifies different kinds of lines by prefix and colours them accordingly, using the same colour scheme as git, so the diff headers and the added and removed markers look the way you expect. It strips all colour from the input before handling it, which is what allows you to point git's own pager settings at riff without also having to turn git's colour off. The initial + and - on added and removed lines are not highlighted, since the colour is being spent on the content instead.

The actual refinement uses the diffus crate, and the project notes refining by word rather than by character. So the unit of comparison is a word, not a byte.

## The pager chain tries your PAGER, then moor, then less

Like git, riff sends its output to a pager, and the order it tries them in is fixed and documented. First whatever is specified in the $PAGER environment variable, then moor because the documentation says it is nice, then less because it is ubiquitous.

The pager selection follows the algorithm described under core.pager in git help config, and the implementation notes say that if stdout is a terminal the output is piped to a pager using that algorithm. There is also deliberate care about the failure mode that would otherwise bite you: a fork loop prevention effort if people set $PAGER to riff, which is a sensible thing to think about when your tool's whole job is to sit between a producer and a pager.

The other thing riff does with your pager is worth noticing. File names and hunk headers are hyperlinked to the relevant source code lines where possible. The url crate is in the dependency list, which is what makes that possible, and the git-version crate is there so the tool can report its own version.

Hyperlinks in a terminal are a nice touch, but they depend on the terminal supporting them, and the terminal support for OSC 8 style links is still uneven. If your pager or terminal does not handle them, the fallback is plain text, which is the sensible design, but it does mean the feature may be invisible to you on a given machine.

## Plugging into git, and the interactive.diffFilter hook

The simplest way to use riff is as a pipe:

```bash
git diff | riff
```bash

The more useful way is to make it git's pager, so every diff you invoke is refined without changing your habits:

```bash
git config --global pager.diff riff
git config --global pager.show riff
git config --global pager.log riff
git config --global interactive.diffFilter "riff --color=on"
```bash

After that, all future git diff, git show and git log --patch output is refined. The four settings cover the three pager slots, for diff, show and log, and then the separate interactive.diffFilter, which is what handles the add and patch mode that git runs through an external filter rather than through a pager. The --color=on argument there is not decoration: in interactive mode the output is not going to a terminal, so colour has to be forced on, otherwise the refinement would be invisible.

riff can also stand in for diff itself:

```bash
riff file1.txt file2.txt
```bash

That is worth noting because it means riff is useful outside git entirely, for anyone with two versions of a file and a diff command line.

The file and directory names here are the exact ones to set: pager.diff, pager.show and pager.log for the pager slots, and interactive.diffFilter for the interactive filter slot.

## Configuration is a single RIFF environment variable of raw options

riff has no configuration file. It is configured by setting the RIFF environment variable to one or more space separated command line options, so anything you could pass on the command line you can put in the environment.

The documented example is RIFF=--unchanged-style=yellow, described as giving nicer visualization of unchanged line parts. That is the option the project itself reaches for, and it points at how the tool thinks about the problem: the interesting signal in a refined diff is the changed part, and the unchanged remainder is the context that tells you what the change means.

Two release notes give you the history of that option. Version 3.6.0 changed the default unchanged style to Yellow, and version 3.6.1 stopped hyperlinking the no-color output, and version 3.6.2 updated the file header hyperlink target. Those three releases in a row are all small, targeted changes to the presentation layer rather than to the diffing logic, which tells you the core has been stable for a while and that recent work has been about how the output looks.

Since the configuration is passed as raw options, it also means the full command line surface is available to you, including the help and version flags, and riff prints help and bails if stdin is a terminal, since there would be nothing to refine in that case.

## Conflict markers and merge commits get their own highlighting

Beyond refining ordinary diffs, riff can highlight conflict markers created by git, and it highlights git merge commits. Both are shown in the project's screenshots with the file names screenshot-diff2-conflict.png and screenshot-git-merge.png.

The conflict marker case is a genuinely different job from diff refinement, and it is where riff earns a place in a merge workflow. When git stops with a conflict, the file on disk contains the conflict markers and the resolution has not happened yet. Refining that text is not about comparing two versions, it is about drawing your attention to the parts of the file that are contested, which is the same visual problem with a different input.

The implementation notes give a hint of the added complexity. The main program identifies blocks of lines that have been replaced by another block of lines, and it makes the Refiner not highlight anything if either the old or the new is empty, so an insertion is never presented as a replacement. It also does not highlight anything if there are too many differences between the sections, on the reasoning that if it is a replacement rather than a change then highlighting it would be misleading.

That heuristic is the interesting design decision in the whole project. A refinement that highlights everything is noise, and the project is willing to give up highlighting in the ambiguous cases rather than produce a misleading picture. The same caution appears in the TODO list, which asks to find out how the LCS algorithm scales and improve the heuristic for when not to call it.

## The TODO list tells you what riff does not do yet

The README keeps an unusually candid TODO section, and it is more useful to an evaluator than the feature list.

The most substantive open item is moved blocks. The plan is to detect moved blocks and use a number as a prefix for both the add and the remove part of the move, and to highlight any changes just like for other changes. Until that exists, a refactor that moves a block of code shows up as a deletion and an insertion with no link between them.

There is also a known constraint about the number of lines. The project has a TODO to add a test for never changing the number of lines in the input, with the note that doing so messes up git add -p behavior. That tells you something important about the risk profile of using riff as a pager: if riff changed the line count, the interactive add and patch mode would break, because git maps hunks back to the original file by line number. The current design keeps the line count intact and encodes the refinement in colour instead, which is the safe choice for a tool that is going to sit in your pager path.

Other open items are smaller: making sure the output of git show --stat is highlighted properly, and passing three files on the command line through to diff3 and highlighting the result. A --help change is also listed, to only print installing-into-$PATH help if the tool is not already being executed from inside the $PATH.

## A Rust binary with thin dependencies and an MIT licence

The manifest describes a Rust 2018 edition package named riffdiff at version 3.6.2, with a separate [[bin]] entry that sets the binary name to riff, because the package name riffdiff was already taken on crates.io. The description is a diff filter highlighting changed line parts, and the categories field says command-line-utilities.

The dependency list is short and tells you what the tool actually does. The similar crate at version 2.6.0 is the diffing library, and it brings with it the diffus component used to refine hunks. regex handles pattern matching over diff lines, url and git-version support the hyperlinks and the version reporting, and clap with the derive feature handles the argument parsing. bytecount and num_cpus are there for sizing work, and threadpool and itertools suggest the refining is parallelised across hunks.

The release profile is tuned deliberately, with lto set to thin. The manifest documents the measured trade-off in comments: lto off gives 67.5ms, thin gives 40ms and fat gives 38.5ms, and the choice is thin because it is fast at building while still providing good performance. The release profile also enables debug = 1, meaning line number information only, so a panic still gives you a usable stack trace without shipping full debug symbols.

The licence is MIT, and the repository carries an MIT LICENSE file. The last push was on 2026-08-15, with version 3.6.2 tagged the same day, and version 3.6.1 dated 2025-11-17. For a tool that sits in a pager path, that release history suggests a slow, deliberate update cadence rather than rapid churn.

## Conclusion

riff suits a reviewer who wants word-level detail inside a line diff and works in git or with plain diff output on the terminal. It does not suit a pipeline that needs byte-identical diff text, since riff reformats hunks, and its own TODO notes that work on moved blocks is not done yet. Before adopting, run git log -p | cargo run -- to see the highlighting on your own history, and set RIFF=--unchanged-style=yellow if you want the unchanged parts of a changed line to stand out more.

## FAQ

### How do I install riff?

Install it with `brew install riff`, `paru -S riffdiff-bin` from the AUR, or `cargo install riffdiff`. A manual install is also possible by downloading the binary for your platform from the releases page and moving it to /usr/local/bin/riff.

### How do I make git use riff automatically?

Set pager.diff, pager.show and pager.log to riff in your git config, and set interactive.diffFilter to "riff --color=on" so that the interactive add mode keeps its colouring. The README gives the four commands verbatim.

### What does the RIFF environment variable do?

It configures riff with one or more space separated command line options. The documented example is RIFF=--unchanged-style=yellow, which gives a clearer visualization of the unchanged parts of changed lines.

### Does riff work with plain diff output, outside git?

Yes. It reads from stdin and writes to stdout, so `git diff | riff` works, and it can also be used as a direct replacement for diff, for example `riff file1.txt file2.txt`.

## Sources

- [Issues](https://github.com/walles/riff/issues)
- [License: MIT](https://github.com/walles/riff/blob/master/LICENSE)
- [README](https://github.com/walles/riff/blob/master/README.md)
- [Releases](https://github.com/walles/riff/releases)
- [walles/riff on GitHub](https://github.com/walles/riff)

---

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