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 (PR status + ahead/behind once submitted)
# 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 s' for short)
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 [--remote] |
ls |
Show the stack as a tree, top of stack first, with PR number/state, ahead/behind vs origin, and restack flags. --remote refreshes PR states from the code host. |
sync [--no-fetch] |
Fetch, fast-forward trunk, delete merged branches (squash-merge aware), and restack the survivors. | |
submit [-r/--ready] [--title <t>] |
s |
Push the current stack and open/update one PR per branch (base = parent). Titles/descriptions come from each branch's first commit; --title overrides for the current branch. 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 review
st s --no-open # same, short form, without opening the browserFor 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. New PRs are titled and described from the branch's first commit (the one that started the change);
st submit --title <t>overrides the title for the current branch's PR, retitling it if it already exists; - posts a stitch-managed comment on each PR with the thread map (the stack of PRs, current one marked), editing it in place on every submit so it never piles up. The comment is authored by your own GitHub account — it notifies reviewers once when first created, and later updates are silent.
Your PR description is entirely yours — stitch no longer writes into it (and on the first submit after upgrading, it removes any stack block it had previously added there). The thread map lives in the managed comment, which stitch only ever rewrites between its <!-- stitch:start --> and <!-- stitch:end --> markers.
When it finishes, st submit opens the top PR in your browser (pass --no-open to skip). PR numbers and states land in st log, so you rarely need gh pr list.
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.