Isolated multi-project git diff/patch generator and applier — usable as a CLI (npx git-patcher) or as a local web UI (React + shadcn/ui).
Core (CLI + UI + API)
- Project isolation — every registered repo gets its own storage namespace under
~/.git-patcher/projects/<id>/(patch history, metadata). Nothing from one project ever leaks into another, even if two repos share a folder name. - Diff/patch generation — generate a unified diff from the whole working tree, staged-only, unstaged-only, or a hand-picked set of files (including untracked/new files), against any base ref.
- Patch application — apply any generated or saved patch to a project, with:
--checkdry-run (verify it applies cleanly without touching files)--reverse(undo an already-applied patch)- automatic 3-way merge fallback on conflicts, with a per-file pre-flight divergence check
- Patch history — save named patches per project, list them, view their contents, re-apply them later, or delete them. Patches can be exported/imported as portable, checksummed, optionally GPG-signed JSON bundles, or shared as a GitHub gist.
- Commit history & blame — paginated
git log, commit-to-commit diffing, andgit blameat HEAD. - Stash — create, list, diff, apply/pop, and drop stashes.
- Hunk-level index staging — stage/unstage a patch's changes directly against the index (a
git add -pequivalent), reusing the same hunk machinery as "save as patch". - Amend/fixup — apply a patch straight into the last commit instead of leaving it uncommitted, with a message override and a guard against amending a commit that's already pushed upstream.
- Patch dependency ordering — mark a saved patch as depending on another; check whether dependencies are satisfied, and topologically sort a batch into a safe apply order (detecting cycles).
- Webhook notifications — configure a per-project URL that gets a POST whenever a patch is applied. Best-effort/fire-and-forget: a broken webhook never blocks an apply.
- GPG patch signing — optionally detached-sign a bundle's checksum on export, and verify the signature on import. Signing is additive: unsigned and older-format bundles still import normally.
Web UI + API only (no CLI command yet — cross-project or visually-oriented features that don't map as cleanly onto a single-repo terminal invocation)
- Blame-aware conflict resolution — a toggleable
git blamegutter inside the visual conflict resolver. - Multi-repo patches — generate and save one logical patch spanning several registered projects at once, and apply it to all of them together.
- Cross-project search — search saved-patch names, descriptions, tags, and touched file paths across every registered project at once.
- Diff-of-diffs — compare two saved patches against each other to see how a fix evolved between iterations.
- Rule-based commit message suggestion — infers a Conventional-Commits-style subject line from a patch's changed files and stats. Fully offline/deterministic — no LLM call, no network dependency.
- Line-ending /
.gitattributesawareness — advisory (non-blocking) warnings when a patch's added lines mix CRLF/LF, or conflict with a.gitattributestext/eol rule, or introduce CRLF where no rule exists at all.
Interface
- Web UI — project switcher, a directory browser to register new projects, a checkbox file picker, a syntax-highlighted diff viewer, and one-click apply/save/download, plus everything above.
- CLI — everything in "Core" above is scriptable:
project,status,diff,apply,amend,patches,stash,log,log-diff,blame,stage,watch,ui.
npx git-patcher project add /path/to/your/repo --name "My Project"
npx git-patcher status
npx git-patcher diff --all --save my-patch
npx git-patcher patches list
npx git-patcher apply my-patch
npx git-patcher apply my-patch --reverse # undo itEvery command accepts an optional project argument (an id or a directory path) so you don't have to cd into the repo first:
npx git-patcher status /path/to/other/reponpx git-patcher uiThis starts a local server (default port 5177) and opens your browser. From there you can register projects, pick files, preview diffs, and apply/save patches visually.
git-patcher project add [dir] [--name <name>] Register a repo as an isolated project
git-patcher project list List all registered projects
git-patcher project rename <idOrDir> <newName> Rename a project
git-patcher project remove <idOrDir> [--keep-storage]
git-patcher project webhook <idOrDir> [url] Set (or, with no url, clear) the apply-notification webhook
git-patcher status [idOrDir] Show working tree status
git-patcher diff [idOrDir]
--all | --staged | --unstaged Scope of the diff
-f, --files <files...> Only these files
-i, --interactive Pick files interactively (checkbox prompt)
-o, --output <file> Write patch to a file
--save [name] Save into the project's patch history
-c, --context <lines> Context lines (default 3)
git-patcher watch [idOrDir] Continuously regenerate a live patch file on change
-o, --output <file> Patch file to keep up to date (default live.patch)
-c, --context <lines> Context lines (default 3)
-b, --base <ref> Diff against this ref instead of HEAD
-d, --debounce <ms> Debounce window (default 400)
git-patcher apply <patchFileOrId> [idOrDir]
--check Dry run, don't modify files
--reverse Reverse-apply (undo)
--no-3way Disable 3-way merge fallback
git-patcher amend <patchFileOrId> [idOrDir] Apply a patch and fold it into the last commit
-m, --message <message> Replace the commit message (default: keep existing)
--force Allow amending a commit that looks already pushed
git-patcher stage <patchFile> [idOrDir] Stage a patch file's changes against the index
--unstage Unstage instead of stage
git-patcher log [idOrDir] Show commit history
-n, --limit <count> Number of commits to show (default 20)
-s, --skip <count> Pagination offset (default 0)
-f, --file <path> Only commits touching this file
git-patcher log-diff <shaA> <shaB> [idOrDir] Diff between two commits (shaA = before, shaB = after)
-c, --context <lines> Context lines (default 3)
git-patcher blame <file> [idOrDir] Show who last touched each line, at HEAD
--from <line> --to <line> Restrict to a line range
git-patcher stash list [idOrDir]
git-patcher stash push [idOrDir] Stash the working tree (or selected files)
-m, --message <message>
--staged Stash only unstaged changes (keep the index)
-f, --files <files...> Stash only these files
--no-untracked Do not include untracked files
git-patcher stash show <index> [idOrDir] Print a stash's diff
git-patcher stash apply <index> [idOrDir] [--pop] Apply a stash (optionally drop it after)
git-patcher stash pop <index> [idOrDir] Apply and drop a stash
git-patcher stash drop <index> [idOrDir] Delete a stash without applying it
git-patcher patches list [idOrDir]
git-patcher patches show <patchId> [idOrDir]
git-patcher patches rm <patchId> [idOrDir]
git-patcher patches depends-on <patchId> [ids...] [--project <idOrDir>] [--clear]
Mark (or clear) a patch's declared dependencies
git-patcher patches check-deps <patchId> [idOrDir] Check whether declared dependencies are applied
git-patcher patches apply-order <patchIds...> [--project <idOrDir>]
Order patch ids so dependencies come first
git-patcher patches export <patchId> [idOrDir] Export a saved patch as a portable JSON bundle
-o, --output <file> Write to a file instead of stdout
--sign GPG-sign the bundle checksum (needs gpg on PATH)
--key-id <keyId> GPG key to sign with (defaults to your default key)
git-patcher patches import <bundleFile> [idOrDir] Import a bundle, verifying checksum + signature
git-patcher ui [--port <port>] [--no-open] Launch the web UI
<patchFileOrId> in apply accepts either a path to a .patch file on disk or a saved patch's id from patches list.
Each registered project is keyed by a SHA-256 hash of its resolved absolute path. That hash becomes a folder name under ~/.git-patcher/projects/<hash>/, containing:
~/.git-patcher/
registry.json # id -> { name, path, webhookUrl, timestamps }
projects/
<hash>/
meta.json
history.json # saved patch metadata (name, scope, stats, dependsOn, applied status)
patches/
<patchId>.patch # the actual patch text
multi-repo-patches/
index.json # multi-repo patch metadata (spans several project ids at once)
<id>-<projectId>.patch # one patch file per project in the bundle
Removing a project deletes its storage folder (unless --keep-storage is passed). The actual git repository on disk is never touched by registration/removal — only the tracking metadata is. Multi-repo patches live outside any single project's folder, since they intentionally span more than one.
Patch-bundle signing shells out to a gpg binary on PATH — via --sign/--key-id on patches export in the CLI, or ?sign=true&keyId= on the bundle-export route in the API/UI. It's entirely optional: git-patcher patches export works without it (just produces an unsigned bundle), GET /api/system/gpg-available reports whether signing is usable, and nothing else in the tool depends on it.
packages/
core/ Pure Node engine: project registry, git diff/apply, patch manager (no UI deps)
server/ Express API wrapping core, serves the built UI in production
cli/ Commander-based CLI; vendors core+server+built UI for standalone npm publishing
ui/ React 19 + Vite + Tailwind v4 + shadcn-style components
npm install # installs all workspaces
npm run dev # runs the API server (5177) and Vite dev server (5173) concurrentlyThe Vite dev server proxies /api/* to http://localhost:5177, so the UI talks to a live server with HMR.
npm run bundle # builds the UI, then vendors core+server+ui/dist into packages/cli/vendor
cd packages/cli
npm publish # `prepack` re-runs the bundle step automaticallyThe published git-patcher package is fully self-contained — it does not depend on @git-patcher/core or @git-patcher/server as npm packages; those are vendored directly into packages/cli/vendor at pack time so npx git-patcher works with zero workspace resolution.
The local server exposes a REST API under /api, consumed by the web UI. This is the full surface as it exists today.
GET/POST/PATCH/DELETE /api/projects[/:id]
PATCH /api/projects/:id/webhook { webhookUrl } — set/clear the apply-notification webhook
GET /api/projects/:id/status
GET /api/projects/:id/refs
POST /api/projects/:id/diff { scope, filePaths, contextLines, base, save, name }
POST /api/projects/:id/apply { patchText, check, reverse, threeWay }
POST /api/projects/:id/preflight { patchText } — per-file divergence check
GET /api/projects/:id/saved-raw?...
GET/DELETE /api/projects/:id/saved[/:patchId]
PATCH /api/projects/:id/saved/:patchId { name, description, tags, dependsOn }
POST /api/projects/:id/saved/:patchId/apply { check, reverse, threeWay } → { result, dependencyWarning }
GET /api/projects/:id/saved/:patchId/base-compatibility
GET /api/projects/:id/saved/:patchId/bundle?sign=true&keyId=<id> — export a signed/unsigned JSON bundle
POST /api/projects/:id/import-bundle { bundle } → { saved, verifiedSignature }
GET /api/projects/:id/log?skip=&limit=&filePath=
GET /api/projects/:id/log/:shaA/diff/:shaB?contextLines=
GET /api/projects/:id/blame?filePath=&from=&to=
GET/POST /api/projects/:id/stash { message, scope, filePaths, includeUntracked }
GET /api/projects/:id/stash/:index/diff
POST /api/projects/:id/stash/:index/apply { pop }
DELETE /api/projects/:id/stash/:index
POST /api/projects/:id/stage-hunks { patchText }
POST /api/projects/:id/unstage-hunks { patchText }
POST /api/projects/:id/amend { patchText, message, force }
GET /api/projects/:id/saved/:patchId/dependencies
POST /api/projects/:id/saved/apply-order { patchIds } → { order } (400 on a cycle)
GET /api/projects/:id/saved/:patchIdA/diff-against/:patchIdB
POST /api/projects/:id/suggest-message { patchId } or { stats } → { suggestion } (rule-based, no LLM)
POST /api/projects/:id/line-ending-check { patchText } → { warnings } (advisory only)
POST /api/multi-repo/diff { requests: [{ projectId, scope, filePaths, contextLines, base }] }
POST/GET /api/multi-repo[/:id]
POST /api/multi-repo/:id/apply { check, reverse, threeWay } → { results }
DELETE /api/multi-repo/:id
GET /api/search?q=<query> — searches every registered project's saved-patch history
GET /api/system/gpg-available — whether a usable gpg binary is on PATH
GET /api/fs/browse?path=<dir> — directory listing for the "add project" folder picker
The conflict-resolution and GitHub gist-export routes existed before this reference was written and are unchanged; see packages/server/src/routes/*.js for the full source of truth if a route above looks stale — this list is best-effort documentation, not a generated spec.
MIT