Skip to content

feat!: rewrite the Node server in Rust - #355

Merged
pavelzw merged 52 commits into
mainfrom
rust-rewrite
Oct 22, 2026
Merged

pavelzw merged 52 commits into
mainfrom
rust-rewrite

Conversation

@pavelzw

@pavelzw pavelzw commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

closes #351

Rewrites diffle's server and CLI in Rust (crates/diffle: axum on tokio). The React client is unchanged, and src/shared/protocol.ts stays the one client/server contract: a vitest snapshot exports it to JSON Schema (crates/diffle/protocol.generated.json), and build.rs generates the Rust types from it with typify, JSDoc included as doc comments.

What changed

  • Server: src/server, src/cli and their tests are replaced by crates/diffle.
    • Git still runs through the git CLI with the same argv; paths are canonicalized through dunce, so Windows gets no \\?\ prefixes.
    • Watching uses notify, GitHub requests use reqwest, and language servers use our own JSON-RPC framing. Language servers start without a shell (shlex splits the command line, Windows resolves it on PATH, a busy executable is retried briefly).
    • The client is embedded with rust-embed.
    • Status lines go through anstream with anstyle styles, so NO_COLOR and a piped stderr stay plain.
  • Review model: a Review is a range (old/new, with a base, a pull request or a view) or a single commit. A range's RangeView shows the whole range, one commit, the uncommitted changes, or an interdiff between two iterations and one of its pairs. POST /api/review enters a review, PUT /api/view changes the view, and the Snapshot carries what was resolved: the hashes, follows, commentKey, and the interdiff. Every review has a comment key, so threads and viewed marks load in interdiffs too.
  • CLI: diffle config prints the config path on stderr and the JSON on stdout, nothing else. diffle completion --shell <shell> takes one of clap_complete's shells.
  • Tests: 330 Rust tests port the TS server tests against real temporary repositories. LSP tests drive the existing test/lsp/fake-lsp.mjs, and the prompt and review payloads are pinned with insta.
  • e2e: the runner and diffle run natively, the browser is always Playwright's Linux Chromium in a container (compose.yml), so screenshots match CI on every host. The suite drives the release binary; DIFFLE_BIN overrides it.
  • pixi locks the toolchain (Rust, Node, git, gh, lefthook, zizmor, cargo-about) and puts real language servers on PATH. Its tasks are the entry points, locally and in CI: build-client, build-release, test-node, test-rust, e2e, lefthook-run, smoke, package, npm-packages, licenses, diffle. npm scripts cover the client alone; cargo owns the server.
  • CI: one pixi run per job, coloured logs, build (<os>) jobs that package and prove each release binary (archive, checksums, and the installers against a served release, under both PowerShells on Windows), an npm-packages job that assembles and smoke-tests the npm packages, and a lockfile job that checks package-lock.json and THIRD_PARTY_NOTICES.md are current.
  • npm: @moritzwilksch/diffle is a launcher with one optionalDependencies entry per platform package; scripts/npm-packages.ts assembles them from the release archives. Publishing uses npm trusted publishing (OIDC), no token.
  • Third-party notices: THIRD_PARTY_NOTICES.md is generated (pixi run licenses): the fonts, the packages vite bundled into the client, and the crates the binary links (cargo-about). An unaccepted licence fails the build.
  • Nix: crane builds the crate; unwrapped is the bare binary, default wraps it with git and gh on PATH, and withLsps composes the language-server variants.
  • Commit gate: lefthook runs through pixi and also runs zizmor over the workflows; the quality gate counts .rs files, minus their trailing #[cfg(test)] module. .gitattributes marks the lockfiles and generated files.
  • Docs: the README covers the pixi entry points, and the demo videos are re-recorded from this build.

💥 Breaking

  • The review API. ModeSpec and the mode endpoints are replaced by Review, RangeView, POST /api/review and PUT /api/view.
  • diffle config only prints; the set/unset subcommands are gone. diffle completion requires --shell.
  • --dev is gone. A debug build reads dist/client from disk, so npm run build plus a page reload is the dev loop.
  • Old comment stores: comments.json files older than format version 3 are no longer migrated.
  • Help output uses clap's layout; the wording is unchanged.
  • Entry points: npm run dev, npm run check, npm run install-hooks and the e2e Docker scripts are replaced by the pixi tasks above.
  • npm installs a launcher plus a platform package instead of the TS sources.

Verification

  • CI is green on every job on the final commit: tests, Rust tests, e2e, lint, lockfile, nix, the five release builds with their installers, and the npm packages.
  • The crate builds on Rust 1.89, the declared minimum.

Follow-ups

  • The conda-forge feedstock must build from the Rust crate.
  • A darwin-x64 build, if Intel Macs come up.
  • The TreeRef enum for resolved sides, and explicit re-export lists in protocol/mod.rs.

The Rust server in crates/diffle now serves every review; the TypeScript
server, CLI and their tests are gone. Release binaries are built with cargo,
the e2e harness and its Docker image build and run the release binary, and
the Nix flake builds the client and the binary in two stages.

BREAKING CHANGE: npm publishing is paused until the binaries ship as
per-platform npm packages; install with the installers, pixi, or Nix.
`--dev` is gone (a debug build reads dist/client from disk), and comment
stores older than format version 3 are no longer migrated.
@github-actions github-actions Bot added breaking enhancement New feature or request labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

This pull request changes committed snapshots. View the snapshot report to compare them side by side.

@pavelzw
pavelzw marked this pull request as draft October 21, 2026 00:22
…ee off the queue

Diff-content search reads patches from the snapshotter's cache instead of
re-running git diff per file and query; the LSP routes check snapshot
membership through Session::has_side; the Linux inotify walk runs on
spawn_blocking so the session queue keeps serving; the config path uses
std::env::home_dir like the LSP modules.
…container

The browser is the only part that must be the same everywhere for the
screenshots to match, so only it stays in Docker: compose.yml serves
Playwright's Linux Chromium over WebSocket, test/e2e/browser-server.ts
starts and stops it, and the runner connects with the loopback exposed
so each natively run diffle stays on 127.0.0.1. CI runs the same way.
The zoom regression test zooms the root element instead of launching a
local Chromium with a profile, which a remote browser cannot take.
A Review names what is reviewed and nothing resolved: a range between two
revisions carrying the slice on screen (the whole range, one commit, the
uncommitted changes, or an interdiff against an iteration or a named range,
narrowed to one pair), or one commit against its parent. The Snapshot carries
the hashes, what the review follows and where its comments live. Entering a
review (POST /api/review) and showing a slice (PUT /api/view) replace
POST /api/mode; the view resolver and the comment keys replace the three
transition builders, and the liveness and comment-key fields stored on the
old ModeSpec are derived. Comment keys keep their strings, so comments.json
needs no migration.

💥 Breaking: the API's mode request and the snapshot's mode field are gone;
clients send EnterRequest and RangeView and read Snapshot.review, follows and
commentKey.
…form

scripts/npm-packages.mjs assembles them from the release archives: the
platform packages carry the binary with os and cpu set, the launcher in npm/
depends on all of them optionally and runs the matching one. CI assembles
and smoke-tests them after the binaries; Publish uploads them with
provenance, platform packages first, skipping versions already published.
npm run build builds the client alone; the dev and full build scripts that
wrapped cargo are gone, and every reference names cargo run or cargo build.
A base checkout from before the port still builds its client under the old
script name.
The committed prompt fixture had been reformatted by the pre-commit
formatter, which flattens blockquotes in Markdown; insta's .snap files are
left alone, and cargo insta review replaces UPDATE_SNAPSHOTS.
diffle config prints the file path on stderr and the settings on stdout;
its subcommands are gone, so language-server overrides and GitHub hosts are
edited in the file. diffle completion takes a required --shell from clap's
own Shell values and prints clap's script unchanged.

💥 Breaking: diffle config set-lsp, unset-lsp, set-context, set-follow-refs,
add-auto-viewed and remove-auto-viewed are removed.
Both crates were already in the tree through clap; anstream strips the
styles off a terminal and honours NO_COLOR and CLICOLOR_FORCE.
An interdiff answered the threads and viewed-mark routes with 400, and the
client fetches both on every view switch, so entering a range-diff or
comparing two iterations never took effect. Every review has a comment key
again, the interdiff keys as before the review model; only creating threads
stays refused there.
The binary is built unwrapped by crane with cached dependency artifacts;
each package is a symlinkJoin wrapping it with git, gh and language servers
on PATH, and takes more through the withLsps passthru.
octo-sts Bot and others added 3 commits October 21, 2026 15:46
clap took the name from the executable, so Windows printed diffle.exe and
the help snapshot differed there.
…Script

Node 24 strips the types itself, so the workflows keep running them with
plain node on a bare checkout, and the hand-written declaration file goes.
A slash inside a join keeps the slash on Windows, while the code reports
what it found on disk; three project tests compared the two.
A slash inside a joined component stays a slash on Windows, so tsserver was
handed a mixed path. CI's Rust job now runs every test binary before
failing, so one run reports all platform differences.
@pavelzw
pavelzw marked this pull request as ready for review October 21, 2026 19:58
A Windows checkout carried the carriage returns into the schema's
descriptions, so the committed schema mismatched there.
The reusable workflows reported their jobs as binaries / build (...) and
npm-packages / assemble, which branch protection could not name. Their steps
are composite actions now, run by jobs named build and npm-packages in CI
and Publish alike.
On Linux a program written moments ago can still be open for writing in a
process mid-fork; the window closes on its own, as the process tests showed
on an arm64 runner.
@pavelzw pavelzw changed the title feat!: replace the Node server with the Rust binary feat!: rewrite the Node server in Rust Oct 21, 2026
pixi run e2e builds the release binary the browser suite drives instead of
the suite building it in its global setup; release, smoke, package and
npm-packages are tasks too, and the environment locks for Windows ARM so
every runner can use it.
Each job is a checkout, the pixi environment, a cargo cache where it helps,
and one task. The composite actions are gone: packaging the binary and
installing it from a served release is a script behind pixi run package.
The composite actions were deleted a commit early; the jobs now spell out
their few steps. The tests point git at a global config that does not exist
instead of NUL, which conda-forge's git on Windows refuses.
@pavelzw
pavelzw merged commit 843921f into main Oct 22, 2026
23 checks passed
@pavelzw
pavelzw deleted the rust-rewrite branch October 22, 2026 00:00
@octo-sts octo-sts Bot mentioned this pull request Oct 22, 2026
@pavelzw pavelzw mentioned this pull request Oct 22, 2026
6 of 7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

migrate server to rust

1 participant