git-stk
Git-native stacked branch workflow helper with GitHub, GitLab, and Gitea support.
git-stk keeps stacks as ordinary Git branches. Stack parent metadata is stored locally in .git/config as
branch.<name>.stkParent, and GitHub/Gitea PR bases or GitLab MR target branches can be used to reconstruct that metadata.

Reporting issues
Planned work and known issues are tracked in GitHub issues.
Feel free to report bugs, feedback, feature requests, or ask questions there; just be polite 😉
Install
Install using the official install script (except for PowerShell), which downloads the pre-built binary:
|
Or, with Homebrew on macOS:
With a Rust toolchain, cargo install git-stk --locked builds from source, or cargo binstall git-stk
fetches the pre-built binary without compiling.
On native Windows, use the PowerShell installer instead:
powershell -ExecutionPolicy Bypass -c "irm https://github.com/lararosekelley/git-stk/releases/latest/download/git-stk-installer.ps1 | iex"
The shell command runs install.sh - a thin wrapper around the
cargo-dist-generated git-stk-installer.sh; PowerShell
fetches the matching git-stk-installer.ps1. Every release attaches the pre-built binaries, both
installers, and per-file .sha256 checksums to
GitHub Releases, so you can download and verify a
binary directly instead of piping to a shell:
# Linux x86_64, for example
The Linux builds are static (musl), so they run anywhere - glibc, Alpine, or an older distro.
Prerequisites: review commands drive the GitHub (gh), GitLab (glab), or Gitea/Forgejo (tea) CLI,
so install the one you use and sign in (gh auth login / glab auth login / tea login add). git-stk
needs git 2.38 or newer (for rebase --update-refs). The local stack commands work without any CLI.
Then install the man page and wire up shell completions (idempotent; prompts before touching your shell rc):
Upgrade an installer-managed copy with:
To remove git-stk, git stk uninstall reverses setup and the installer: it strips the completion line it
added to your shell rc, deletes the man page, and removes the config/receipt directory (--dry-run to
preview, -y to skip the prompt). It prints how to remove the binary itself rather than deleting it - a
running program can't reliably delete its own executable, and a cargo install / Homebrew copy should go
through cargo uninstall git-stk or brew uninstall git-stk. Per-repo stk.* config and branch metadata
are left untouched.
Quickstart
# ...commit work...
# ...commit work...
New to stacking? git stk guide runs the whole loop offline in a disposable sandbox - nothing real is
touched and no account is needed. The tours: intro (create a stack, submit, restack, land it),
conflicts (resolve and continue an interrupted restack), repair (rebuild lost stack metadata),
absorb (fold review fixes into the commits that introduced them), adopt (adopt a hand-made branch, or
move one to a new parent), undo (reverse the last stack-rewriting command), and github (GitHub's own
stacked pull requests: the workflow and the gotchas). git config stk.provider demo turns any scratch repo
into the same offline playground.
Shell Completions
git stk setup configures these automatically. Completions are dynamic: the shell asks the binary for
candidates at completion time, so subcommands, flags, and even branch names complete (git stk up <TAB>
offers only the current branch's stack children). The installed binary prints its own registration script,
so completions stay in sync across upgrades:
# bash: add to ~/.bashrc (the guard keeps shell startup quiet if git-stk is removed)
&&
# zsh: write to a directory on your fpath
# PowerShell: add to $PROFILE (git stk setup does this for you on Windows)
if (Get-Command git-stk -ErrorAction SilentlyContinue) { git stk completions powershell | Out-String | Invoke-Expression }
git stk setup detects bash, zsh, and fish from $SHELL (covering Git Bash and WSL), and falls back to
PowerShell on native Windows by wiring $PROFILE. Elvish is also supported via git stk completions elvish. The bash and zsh output includes a wrapper so git's own completion can complete git stk <TAB>
in addition to git-stk <TAB>. -y/--yes skips the shell-rc confirmation for non-interactive setup;
--refresh only re-renders the man page and never touches your shell rc - it is what upgrade runs with
the freshly installed binary.
--wrapper additionally defines the stk shell function whose up/down/top/bottom
cd into the worktree holding the branch, and teaches your shell to complete stk the same way it
completes git-stk. It is opt-in because it defines a new command name: setup skips it, telling you why,
if an stk executable is on your PATH or your rc file already defines one. bash and zsh only - fish
needs different syntax. Re-running with --wrapper after a plain setup merges it into the existing
block rather than appending a second one, and git stk uninstall removes the whole block either way.
Worktrees
git-stk understands linked worktrees. list and status name the worktree holding each branch, and the
commands that rewrite history refuse up front - before touching anything - rather than failing partway
through, because git will not rebase, delete, or check out a branch another worktree has checked out.
That last one makes navigation awkward in a worktree-per-branch layout: moving up the stack is a cd, not
a checkout, and a program cannot change its parent shell's directory. So the navigation commands take
--from-path, which prints where to go and lets the shell do the moving:
git stk setup --wrapper writes this for you (see Shell Completions); by hand it is:
# bash/zsh: add to ~/.bashrc or ~/.zshrc
stk up then follows the branch wherever it lives - into another worktree when that is where it is
checked out, or an ordinary checkout when it is here (printing ., so your current directory is left
alone). The switch is still reported, on stderr, so stdout stays a single usable path. Every other
command falls through to git stk untouched, so stk works as the only name you need.
Capture the path before the cd rather than cd "$(...)" directly: a navigation that fails prints
nothing on stdout, and cd "" would add its own cd: null directory complaint on top of the error
git-stk already gave you.
Install For Development
After installation, Git can use the binary as a sub-command:
Commands
Git's own narration (rebase progress, switch advice, push chatter) is captured and shown only when a
git command fails; pass -v/--verbose to any command to stream it through instead, along with the
occasional detail git-stk holds back because it would repeat on every run - such as a host that cannot
answer for GitHub's stacked pull requests. Output is colored
when the terminal supports it; pipes and NO_COLOR turn it off. Every command
that takes --dry-run also accepts -n as a short alias.
# build the stack
# move around
# keep it stacked
# review and land
# setup and housekeeping
Every flag, and the behavior behind each command, lives in docs/COMMANDS.md.
Configuration
All settings live under [stk] in git config, so the tool's footprint stays separated from git's own.
Everything is optional; defaults shown below:
[stk]
; Review provider: github, gitlab, gitea, or demo (offline playground).
; Default: auto-detect from the remote URL.
provider = github
; Self-hosted GitLab host to detect as GitLab alongside gitlab.com (a bare
; host or a full URL). `glab` picks up the host from the remote itself.
; Default: gitlab.com only.
gitlabHost = gitlab.example.com
; Self-hosted Gitea/Forgejo host to detect as Gitea alongside gitea.com and
; codeberg.org (a bare host or a full URL). `tea` picks up the host itself.
; Default: gitea.com and codeberg.org only.
giteaHost = gitea.example.com
; Remote used for provider detection and pushes. Default: origin.
remote = origin
; Pass --update-refs to git rebase during restack. Default: false.
updateRefs = true
; Fetch the trunk from the remote before restacking, so branches rebase
; onto its latest tip. Default: false.
fetchBeforeRestack = true
; Force-push (with lease) rebased branches after restack (also the restack
; step inside sync and merge). Default: false.
pushOnRestack = true
; Push branches (-u --force-with-lease) before submitting reviews. Default: false.
pushOnSubmit = true
; Bare `submit` submits the whole stack instead of one branch. Default: false.
submitStack = true
; `sync` and `cleanup` also clean up branches whose review was closed
; without merging, not just merged ones. Default: false.
cleanClosed = true
; Strategy for `merge`: squash, rebase, or merge. Default: squash.
mergeStrategy = squash
; `merge --all` waits for each review's checks before merging it. Default: false.
mergeWait = true
; Seconds `merge --wait` polls a review's checks before giving up. 0 waits
; indefinitely. Default: 1800 (30m).
checkTimeout = 1800
; Open new reviews as drafts. Default: false.
submitDraft = true
; Seed a new review's body from the repo's PR/MR template (GitHub/Gitea
; PULL_REQUEST_TEMPLATE, GitLab's Default.md) instead of replacing it.
; Default: true.
usePrTemplate = false
; `absorb` also folds unstaged tracked edits, not just staged ones. Default: false.
absorbIncludeUnstaged = true
; Skip the once-a-day check for a newer release. Default: false.
noUpdateCheck = true
; Register submitted stacks with GitHub's native stacked pull requests, so
; their layers get GitHub's stack map. Only registering is gated: git-stk
; reads and follows a stack whoever created it, and `git stk unstack`
; dissolves one without the setting - undoing a registration must not need
; the setting that made it. GitHub only, and in public preview there.
; Default: false.
githubStacks = true
; Where `new --worktree` puts a branch's worktree. Default: a
; <repo>-worktrees directory beside the repo.
worktreeDir = ~/code/myrepo-worktrees
The tool also manages per-branch metadata: branch.<name>.stkParent (the stack parent),
branch.<name>.stkBase (the recorded fork point), branch.<name>.stkFloor (marking a branch as a
stack's base - see below), and - for branches made with new --worktree - branch.<name>.stkWorktree,
recording that git-stk created that worktree and so may remove it. These are written by new, adopt,
rename, sync, restack, cleanup, and repair; you normally never touch them by hand.
A stack does not have to sit on the trunk. Root one on any branch - a release line, say - and git-stk
records that branch as the stack's base (stkFloor). A base is not part of the stack: it is never
submitted, pushed, rebased, merged, or deleted, so a shared branch cannot be pulled into a stack and
rewritten. The marker outranks a stkParent too, so a base that picks one up elsewhere - metadata from an
older git-stk, say - is still left alone. The marker is what keeps that true after the branches above it land,
at which point nothing about the shape says it is a base any more. git stk detach <branch> clears it.
Branches are the real state; the metadata is just annotation. If it is ever lost or stale, git stk repair
rebuilds it from review bases (when the provider CLI - gh/glab/tea - is available) and branch
ancestry, and verifies recorded fork points. Anything it cannot resolve safely is reported for a manual
git stk adopt <branch> --parent <parent>. Where GitHub records a stack of its own, repair prefers that
to both - the ordering was stated rather than inferred - until the layer below has landed, at which point
the platform has already retargeted the review while the listing goes on naming the merged branch, so the
review base becomes the fresher answer. That does not need stk.githubStacks: the setting governs whether
git-stk registers a stack, not whether it reads one.
Working across machines? The parent map - and which branch a stack sits on - rides along on a shared ref
(refs/stk/metadata), published automatically whenever git-stk pushes branches
(submit/restack/sync with push). On another clone,
git stk repair --from-remote fetches that ref, pulls down any of its branches you do not have yet, and
rebuilds the local metadata - no platform or open PRs required. (Local-only commits you have not pushed
still can't transfer, of course.)
While a stack-rewriting command runs (submit, merge, sync, restack, absorb, and friends) it holds
a lock at .git/stk-lock so a second git-stk run cannot rewrite the stack at the same time; read-only
commands are never blocked. If a run is killed mid-operation the lock can linger, but the next
stack-rewriting command reclaims it automatically once it sees the holding process is gone (on Windows,
remove .git/stk-lock by hand).
Inspect everything stk reads or wrote with:
Providers
Provider detection uses stk.provider first, then stk.remote, then origin. GitHub support shells out
to gh, GitLab support shells out to glab, and Gitea to tea. Authenticate those CLIs before using provider commands.
Generated Assets
Shell completions and a man page can be generated with:
Generated files are written under target/generated.
Project Tasks
License
Copyright (c) 2026 Lara Kelley. MIT License. See LICENSE.