@hannanel100/worktree-diff-viewer
v0.7.0
Published
Local web UI that lists every worktree of a git repository and shows what each worktree's branch changed against a base branch of your choice.
Maintainers
Readme
Worktree Diff Viewer
A local developer tool that gives an overview of every worktree of one git
repository and shows the diff each worktree's branch introduced against a
base branch of your choosing. Runs entirely on your machine: a small Node
server bound to 127.0.0.1 serving a static React UI, no account, no network,
no runtime dependencies.
$ wtdiff
Repository : C:/work/my-repo
Worktree : C:/work/my-repo
Worktrees : 7
Base : dev
Serving at http://127.0.0.1:4747/?base=devWhat it shows
Overview – one row per worktree (the main checkout counts too):
- branch, or
detached @ sha - commits ahead of / behind the base
- merge status: merged (every commit is in the base, or the branch's changes are already there via a squash/rebase merge), unmerged, and for unmerged branches the files that would conflict with the base
- files changed and lines added/removed since the branch forked from the base
- a separate uncommitted badge: edits sitting in the worktree directory that are not part of any commit. They are never folded into the diff; the badge opens them on their own screen.
The table can be sorted by any column (click a header, shift-click for a second sort key), filtered by branch or path text, by merge status and by uncommitted state, and rearranged by dragging column headers or hiding columns from the Columns menu. Sorting, order, visibility and filters are remembered in the browser.
Diff – click a row to open one worktree:
- the list of commits the branch has that the base lacks
- a file list with status (A/M/D/R), filter box,
j/kkeyboard navigation, in a drawer you can collapse (toolbar button orf) to give the diff the whole width, and resize by dragging its edge (double-click the edge to fit the longest file name);j/kkeep working while it is closed - side-by-side or unified rendering with syntax highlighting (diff2html)
- "All files" mode, and a
.patchdownload of the whole diff
The diff is always merge-base(base, HEAD)..HEAD, i.e. what the branch
changed since it diverged. Commits the base gained after the fork do not show
up as "changes" in the worktree.
Per-commit view – click a commit in the Commits panel to see only what
that commit changed (against its first parent; a merge commit is shown against
the branch it was merged into, a root commit as all-new files). The file list,
the diff pane, "All files" and the .patch download then cover just that
commit, and the header shows its sha, subject, author and age next to the
branch facts. The Whole branch entry at the top of the panel (or the link in
the header) returns to the full diff; ] and [ step down and up the list.
The URL carries the selection (&commit=<sha>), so it can be shared.
Uncommitted changes – the same screen for what sits in a worktree's
directory but not in any commit: index and working tree against HEAD, with
untracked files shown as new files. Each file is labelled staged, staged +
unstaged, untracked or conflict. Reachable from the Overview badge and from
the Diff header; a Refresh button re-reads the working tree.
Install
Requires git on PATH and Node 18.17+ to run (20.9+ to develop).
From npm, once published (see below):
npm i -g @hannanel100/worktree-diff-viewer # or: npx @hannanel100/worktree-diff-viewerFrom source:
git clone <this repo> worktree-diff-viewer
cd worktree-diff-viewer
npm install
npm run build # static UI export (out/) + bundled server (dist/server.mjs)
npm link # makes the `wtdiff` command available globallyUsage
Run it from the main checkout or from inside any worktree of the repository you want to look at. Git's worktree list is shared, so every worktree shows up either way.
wtdiff # overview of all worktrees vs the default branch
wtdiff develop # ... vs develop
wtdiff --here # jump straight to this worktree's diff
wtdiff -b origin/main --here
wtdiff --port 5000 --no-open # pick a port, do not launch the browser
wtdiff --cwd ../other-repo # inspect a repository you are not inside ofThe default base is whatever origin/HEAD points to, falling back to
main, master, develop, dev, trunk. You can type any branch, tag or
commit sha into the Base box in the UI and press Compare.
Press Ctrl+C in the terminal to stop the server.
Security notes
- The server binds to
127.0.0.1only. Do not expose it; it reads your repository on request. - All git invocations use argument arrays (no shell). Refs are validated syntactically and then resolved with git before use; anything that looks like an option or a range is rejected.
- Worktree paths in requests must match an entry of
git worktree list. - Only
GETis served. Nothing changes refs, the index or any working tree. The one write isgit merge-tree --write-tree(used for the merge status), which stores small loose tree objects in.git/objects; git's normal garbage collection removes them. - Static files are served only from the export directory; paths that escape it are refused.
How it is put together
Next.js is the development and build tool, not a runtime dependency:
next devserves the React UI and mounts the API throughapp/api/*/route.dev.ts(the.dev.tsextension is only recognised in development, seenext.config.ts).next buildproduces a static export of the UI inout/.scripts/build-server.mjsbundlesserver/main.tsand thelib/it imports with esbuild into a single dependency-freedist/server.mjs.bin/wtdiff.jsstarts that server, which answers/api/*through the very same handler functionsnext devuses (lib/handlers.ts) and servesout/for everything else.
npm run dev # Next.js dev server; inspects the current directory
WTDIFF_CWD=/path/to/repo npm run dev
node bin/wtdiff.js --dev --cwd <some-repo> # same, through the CLI
npm test # vitest: parsers, RepoService, handlers, production server
npm run typecheck
npm run build # out/ + dist/server.mjsLayout:
bin/wtdiff.js CLI: arguments, starts dist/server.mjs (or `next dev`), opens the browser
bin/open.js cross-platform "open URL in default browser"
server/main.ts startServer(): repository check, listen with port fallback
server/http.ts request listener: /api/* via shared handlers, static export otherwise
lib/handlers.ts the API as (Request) => Response functions, one per route
lib/api.ts jsonRoute() helper and error-to-status mapping
lib/git.ts git wrappers + parsers (worktree list, raw/numstat, status v2, merge-tree)
lib/repo-service.ts RepoService: summary / diff / file / patch / merge status, caching
lib/context.ts one RepoService per process, bound to WTDIFF_CWD
lib/client/ browser helpers: fetch, URL builders, formatting, localStorage
app/ Next.js app: layout, pages, dev-only API routes, styles
components/ Overview, OverviewTable (TanStack), ChangeSetView (file drawer + diff pane) used by
DiffView (committed) and UncommittedView, DiffPane (diff2html), TopBar, ...
scripts/build-server.mjs esbuild bundle of the server
tests/ fixture builder + parser, service, handler and server testsAPI (all GET, JSON unless noted):
| Route | Params | Returns |
| ------------------ | -------------------------------------- | -------------------------------------------- |
| /api/repo | – | worktrees, default base, repo root |
| /api/branches | refresh? | local and remote branches (cached 60 s) |
| /api/summary | worktree, base | ahead/behind, merge status, file/line totals |
| /api/uncommitted | worktree | counts of changed and untracked files |
| /api/diff | worktree, base | summary + commits + file list |
| /api/file | worktree, base, path, oldPath? | unified diff of one file |
| /api/patch | worktree, base | whole diff as text/x-patch download |
| /api/commit | worktree, commit | one commit vs its first parent: metadata, parents, file list |
| /api/commit-file | worktree, commit, path, oldPath? | unified diff of one file in that commit |
| /api/commit-patch | worktree, commit | that commit's diff as a .patch |
| /api/uncommitted-files | worktree | uncommitted files with area and line counts |
| /api/uncommitted-file | worktree, path, oldPath? | working-tree diff of one file vs HEAD |
| /api/uncommitted-patch | worktree | all uncommitted changes as a .patch |
Only paths that git status itself reports can be diffed on the uncommitted
screen; untracked files are read from disk to build a new-file diff, so no
other file in the worktree is ever readable through the API.
worktree is a path exactly as git worktree list prints it (or the row id
from /api/repo); commit is a full sha (any ref git resolves to a commit is
accepted). Spawning git is the dominant cost (hundreds of ms per process on
Windows), so the server caches the worktree list and resolved refs for a few
seconds and merge-bases and commit metadata for the life of the process, and
avoids %(refname:short), which takes seconds on repositories with thousands
of refs.
Roadmap
Ideas for later versions, roughly in order of usefulness:
- Whitespace and context controls – ignore whitespace, widen context, expand collapsed lines.
- Review aids – mark files as viewed, collapse viewed files, remember position per worktree/base.
- Compare two worktrees directly, not only worktree vs base branch.
- Multiple repositories in one overview (pass several
--cwdroots). - Live refresh – watch
.gitfor new commits and update the overview. - Open in editor links (
vscode://file/...).
Publishing to npm
The package ships only bin/, dist/server.mjs and the static out/; it has
no runtime dependencies, so npx starts it in seconds.
- Pick the final name in
package.json, e.g.@hannanel100/worktree-diff-viewer(scoped packages can be published privately, or to GitHub Packages / Verdaccio). - Bump the version and run
npm publish(add--access publicfor a public scoped package, or setpublishConfig.registryfor a private one).prepublishOnlybuilds and tests first. - Teammates install with
npm i -g @hannanel100/worktree-diff-vieweror run ad hoc withnpx @hannanel100/worktree-diff-viewer.
Without a registry, npm i -g git+ssh://[email protected]/<org>/worktree-diff-viewer.git
also works if a "prepare": "npm run build" script is added, at the cost of
building on each teammate's machine.
License
MIT
