Stacked branches on top of plain git, in a single self-contained Go binary. You stitch your branches into one clean, reviewable chain.
Big changes review better as a series of small, dependent branches — one reviewable step each. Keeping such a chain healthy by hand (rebasing everything above every edit) is the painful part. Stitch automates exactly that, and nothing else: it's plain git underneath, with no server, no account, and all state inside your repo's own .git.
- Stack-aware branching —
st createstacks a branch on the current one;st modifyamends a branch and automatically rebases everything above it. - Safe restacks — each rebase is a precise
git rebase --ontofrom a recorded base revision, so commits are never duplicated or dropped; conflicts pause cleanly and resume withst continue. - Stacked pull requests —
st submitpushes the stack and opens one PR per branch, each showing only its own diff, with an auto-maintained stack map in every description. - Squash-merge-aware sync —
st syncdeletes branches whose PRs merged (even squash merges) and re-parents the rest. - Drop-in for git — any command
stdoesn't recognise passes through to git, sost status,st diff,st rebase -iall just work. - Graphite import — already have stacks in Graphite?
st init --from-graphitebrings them along, non-destructively.
Homebrew (macOS):
brew install onancelabs/tap/stitchOr with Go 1.21+:
go install github.com/onancelabs/stitch/cmd/st@latest
# or, from a checkout:
go build -o st ./cmd/st # produces ./st
sudo mv st /usr/local/bin/ # put it on your PATH
# or, with the Makefile:
make build && sudo mv st /usr/local/bin/git is required at runtime either way. Pre-built binaries for macOS, Linux, and Windows are also attached to each GitHub release.
Shell completions come for free via cobra — e.g. st completion zsh.
cd your-repo
st init # detects trunk (main/master)
# build a 3-branch stack, one commit each
echo "..." > a.txt
st create add-model -a -m "Add model"
echo "..." > b.txt
st create add-api -a -m "Add API on top of model"
echo "..." > c.txt
st create add-ui -a -m "Add UI on top of API"
st log # see the stack
# fix something lower in the stack; everything above is rebased automatically
st down # move toward trunk
# ...edit files...
st modify -am "fix" # amends this branch, restacks its children
st submit # push the stack, open a PR per branch
st sync # later: drop merged branches, restackThe branch name in create may come before or after the flags — both st create my-branch -a -m "msg" and st create -a -m "msg" my-branch work, and short flags bundle git-style (st modify -am "msg").
| Command | Alias | What it does |
|---|---|---|
init [--trunk <name>] [--from-graphite] |
Configure the trunk branch (auto-detects main/master), or import an existing Graphite stack. |
|
create <branch> [-m msg] [-a] |
c |
Create a branch stacked on the current one, committing staged changes (-a stages everything first). |
modify [-m msg] [-a] [-c] |
m |
Amend the current branch's commit (or -c for a new commit), then restack everything above it. |
restack [--all] |
r |
Rebase the current stack so each branch sits on its parent. --all does every tracked branch. |
continue |
Resume a restack after you resolve a conflict. | |
abort |
Cancel an in-progress restack and return to where you started. | |
up [<child>] |
u |
Check out a child branch (prompts when there are several). |
down |
d |
Check out the parent branch (toward trunk). |
track [-p <parent>] |
Start tracking the current (ordinary git) branch; parent defaults to trunk. | |
untrack [<branch>] [--thread] [--all] |
Stop tracking a branch (re-parenting children), the current thread (--thread), or every tracked branch (--all). Git branches stay intact. |
|
log |
ls |
Show the stack as a tree, top of stack first, flagging branches that need a restack. |
sync [--no-fetch] |
Fetch, fast-forward trunk, delete merged branches (squash-merge aware), and restack the survivors. | |
submit [-r/--ready] |
s |
Push the current stack and open/update one PR per branch (base = parent). Drafts by default; -r opens for review. |
auth [--token | --status | --logout] |
Sign in to GitHub via the OAuth device flow (default), or store a PAT with --token; kept in the OS keychain. |
Run st <command> -h for per-command flags. Any command not in this table is forwarded straight to git with the same arguments and exit code.
A "stack" is a chain (or tree) of branches where each branch is one reviewable change and remembers two things: its parent branch and the parent revision it was based on. From those two facts everything else — restacking after an edit, navigating up and down, syncing onto a moved trunk — falls out of ordinary git plumbing. (Stitch's own word for a single stack is a thread — you'll see it in commands like st untrack --thread.)
When you amend a commit, its SHA changes, so every branch above it is now based on a commit that no longer exists. restack walks the stack top-down (parents before children) and, for each branch, runs:
git rebase --onto <parent's new tip> <parent's OLD tip> <branch>
The "old tip" is the parentRev Stitch recorded for that branch. Passing it as the rebase base is what guarantees exactly the branch's own commits are replayed — no parent commits get duplicated, none get dropped. After a branch is replayed, Stitch updates its parentRev to the parent's new tip so the next branch up the stack lines up correctly.
If a rebase hits a conflict, the remaining plan is saved to .git/stitch/restack.json and the run stops. Resolve the files, git add them, then:
st continue # finishes the current branch and resumes the rest
st abort # or bail out entirely and return to your start branchPer-branch metadata is stored as a git blob pointed to by the ref refs/stitch/<branch>. This is durable (survives git gc), invisible to your working tree, never treated as a real branch, and not pushed by default. The trunk name is stored in git config stitch.trunk. Removing the tool leaves your branches untouched; to wipe its state:
git for-each-ref --format='%(refname)' refs/stitch/ | xargs -n1 git update-ref -d
git config --unset stitch.trunkst submit turns your local stack into a set of stacked pull requests.
Sign in once — the resulting token is kept in your OS keychain:
st auth # OAuth device flow: copy the one-time code, confirm in your browser
st auth --token <tok> # or store a personal access token directly
st auth --status # check it's set; st auth --logout to removeThe device flow needs an OAuth App client id (embedded at release time, or set STITCH_GITHUB_CLIENT_ID). A PAT instead needs pull-request and contents write access — fine-grained with Pull requests: read/write and Contents: read/write, or a classic token with the repo scope. st also reads GITHUB_TOKEN / GH_TOKEN from the environment, which is handy for CI.
Then, from any branch in your stack:
st submit # draft PRs; st submit -r opens them for reviewFor each branch in the current stack, bottom-up, submit:
- pushes it with
--force-with-lease(restacks rewrite history, so a plain push would be rejected; the lease keeps the force push safe); - opens or updates a PR whose base is the branch's parent — so each PR shows only its own diff — recording the PR number in the branch metadata;
- writes a stack map into each PR description, inside a managed block so re-submitting updates it instead of piling on comments.
When it finishes, st submit opens the top PR in your browser (pass --no-open to skip).
After PRs merge, st sync asks GitHub the state of each one. Merged PRs — including squash merges, which git branch --merged cannot detect — have their local branch deleted and their children re-parented onto the nearest surviving ancestor before the stack restacks. With no token available, sync still does the local half (fetch, fast-forward trunk, restack).
If you have existing stacks in Graphite, import them in place:
st init --from-graphiteThis reads each branch's parent, base revision, and PR number, and writes Stitch's equivalent (refs/stitch/… and stitch.trunk), leaving the original data untouched so you can switch back at any time. Branches that no longer exist locally are skipped, and a missing base revision falls back to a live git merge-base.
It reads whichever store is present, in order:
- the SQLite database (
.git/.graphite_metadata.db) via thesqlite3CLI — the authoritative, always-current source; - if
sqlite3isn't installed, the JSON snapshot (.git/.gt/snapshots/) — no external tool required; - the legacy
refs/branch-metadatablobs used by older versions.
So you rarely need anything extra: sqlite3 ships with macOS, and if it's absent the JSON-snapshot fallback covers it. Only if none of the three are readable does it print an OS-specific hint to install sqlite3.
- No shell, ever. git is always invoked via
exec.Command("git", args...)with a separate argument vector — neversh -cand never string interpolation — so branch names, commit messages, and SHAs cannot inject shell commands. - Argument-injection guarded. User-supplied branch names are validated (rejected if empty or starting with
-) so they can't be misread by git as options. SHAs handed torebase --ontocome fromgit rev-parse, not user input. - Tokens stay in the OS keychain.
st authstores your GitHub token via the system keychain (or readsGITHUB_TOKEN/GH_TOKEN); it is never written to the repo or printed back out. All GitHub calls are confined tointernal/forge. - History-rewriting commands are gated.
restack/sync/modifyrefuse to run with a dirty working tree, while another rebase is in progress, or while a restack is paused;modifyalso refuses to touch trunk or to amend a branch that has no commit of its own;createrolls back the new branch if its commit is aborted. Because rewrites go throughgit rebase, the previous tips remain recoverable viagit reflog. - State is confined to
.git. The only files written are inside the repo's own git directory (.git/stitch/), resolved throughgit rev-parse --git-path; no user-controlled paths are ever written.
go test ./... # the suite drives real throwaway git repositories
go vet ./...
make dist # cross-compile dist/st-<os>-<arch> for macOS, Linux, Windows
go build -ldflags "-X main.version=1.2.3" -o st ./cmd/st # version injectionReleases are automated: pushing a vX.Y.Z tag runs GoReleaser via GitHub Actions, which builds the binaries, attaches them to a GitHub release, and updates the Homebrew tap (onancelabs/homebrew-tap).
The restack engine is exercised end-to-end against real git (tree-shaped stacks, conflict-then-continue, moved trunks). The submit/sync orchestration is tested offline against a fake forge plus a local bare origin, and the OAuth device flow against a fake GitHub server. The only untested paths are the real GitHub API round-trips behind the Forge interface.
Dependencies are three small, well-known libraries: cobra/pflag (CLI), go-github (PR API), and go-keyring (token storage). Everything else is the standard library.
More forges behind the Forge interface (GitLab, Bitbucket); reviewer/label/title management on submit; GitHub Enterprise base URLs; an undo built on reflog snapshots.
Stacked-branch workflows have a healthy ecosystem of tools — Graphite, ghstack, git-spr, av, git-branchless, and others — each with its own take. Stitch is an independent, from-scratch implementation of the workflow, built to stay small: plain git underneath, local-only state, one binary.
Business Source License 1.1. In short: you can use Stitch freely — personally and inside your organization, commercial or not — and read and modify the source. What you can't do is offer Stitch itself to third parties as a commercial product or service. Each version converts to the Apache 2.0 license four years after its release. For other arrangements, see the contact in the LICENSE file.