Developing a Web App to Generate Private yet Shareable Diffs

TL;DR

I built a web app to generate private yet shareable diffs without backend.

Every now and then I need to do a quick diff —compare two pieces of text/code for differences— outside a Git environment.

There are websites where you can paste the two texts, and you get a diff. Are the texts being sent to a server, though? I don’t like that.

To avoid using these sites, I use a snippet to start vim in diff mode with a bash alias. It (mostly) works, but I often want to share the diff. A screenshot is not convenient for the receiver; they can’t copy or edit it.

Recently I learnt browsers can handle big URLs. At a minimum, they’re supposed to support 8,000 characters. In practice:

After seeing a notes app that stores everything on the URL (HN discussion), I came up with the idea for a web app that generates diffs locally with the ability to share them by encoding everything in the URL.

I used Claude and Gemini to code it. I didn’t want any dependencies: I trusted vanilla JavaScript, HTML, and CSS would be enough.

The main trick was to store the entire two texts, compressed with deflate-raw (see spec), in the URL. That and having an algorithm to generate the diff.

Diff algorithms

It wasn’t my goal, but I ended up learning about diff algorithms.

For a snippet like this:

fn load_config() {
    // ...
}

fn initialize_logger() {
    // ...
}

fn start_server() {
    // ...
}

If you change the order so as to define start_server() first, would you expect this diff?

Side-by-side Myers diff of reordered Rust functions. initialize_logger and start_server are marked as deleted on the left; load_config and initialize_logger as added on the right. The identical body lines are matched across different functions, making it look like functions were renamed rather than moved. Side-by-side Myers diff of reordered Rust functions. initialize_logger and start_server are marked as deleted on the left; load_config and initialize_logger as added on the right. The identical body lines are matched across different functions, making it look like functions were renamed rather than moved.

or this?

Side-by-side Patience diff of the same reordering. Only start_server is shown as removed from the bottom and added at the top. load_config and initialize_logger remain matched in place, clearly showing a block move. Side-by-side Patience diff of the same reordering. Only start_server is shown as removed from the bottom and added at the top. load_config and initialize_logger remain matched in place, clearly showing a block move.

The first is the result of the Myers diff algorithm, by Eugene W. Myers. The second diff uses Patience Diff, by Bram Cohen. Myers tries to find the smallest set of line removals/additions that changes one file into another. Patience diff tries to make diffs more human-readable by avoiding confusing matches.

Less is less

I tried to get even shorter URLs than deflate-raw was providing when storing full “Original” and “Modified” inputs. Deduplicating common lines helped, but it gave me less than a 5% decrease in URL length. I dropped this change in favour of simplicity.

Cool stuff

LLMs + me = feature creep. That said, I’m glad I added:

The mini-map

Inspired by VSCode. Useful for navigating through the diff. At first I had buttons to jump to next/previous change; this is way more convenient.

More

  • It uses service workers to avoid locking the browser on large diffs.
  • Can be installed as PWA for full offline functionality.
  • Keyboard shortcuts for faster input/editing, and vim-like navigation (move up/down the diff with j and k).
  • Includes “Ignore” options for whitespace differences and quote styles.
  • It’s not necessary to build the app, but I use node to run tests on the code. The tests verify the patches generated by the tool are (1) valid and when applied to the original text (2) generate the expected result.

I was worried about the practicality of long URLs. I tested them in Slack, and noticed they get visually truncated, but the content is preserved. Plus, you can always link a specific word instead of pasting the raw URL.

If you want to play around with it, visit diff.osc.garden. The source code is on GitHub. I named it kawari for “change” in Japanese.


Extra: more URL-abusing projects

A few cool projects storing data in the URL, from the Hacker News discussion: