git-stack
Lightweight CLI + TUI for stacked branches / PRs in plain git.
Parents are not only encoded in branch names. Resolution order:
- Open PR base (shared with the team — draft PRs count)
- Local config
branch.<name>.gitstack-parent
- Dot-depth name (
feature.ui → feature) — zero-config fallback
- Trunk (default branch)
You can reparent in or out of a stack without renaming.
Install
curl -fsSL https://raw.githubusercontent.com/benwyrosdick/git-stack/main/scripts/install.sh | bash
From source:
go install ./cmd/git-stack
# or
go install github.com/benwyrosdick/git-stack/cmd/git-stack@latest
Usage
git-stack # interactive TUI
git-stack ls [root]
git-stack parent [branch] [-v] # -v shows source: pr|local|name|trunk
git-stack create <name> [--from <start>]
git-stack restack [branch] [--push] [--onto-trunk] [--no-fetch] [--descendants]
git-stack reparent <branch> <new-parent> [--from <old>] [--push] [--no-fetch]
git-stack sync [root] [--push] [--onto-trunk] [--dry-run] [--no-fetch]
git-stack pr [branch] [--draft]
git-stack track <branch> --parent <parent> # metadata only
git-stack untrack [branch]
git-stack adopt [root] # write local parents from current resolution
git-stack upgrade [--check] [--force] # install a newer release if available
Global flags: --offline (no gh), --refresh (force PR parent map reload).
git-stack upgrade checks GitHub Releases for a newer version and replaces the running binary in place. Use --check to only report status; --force reinstalls even when already on the latest.
Free names (recommended for reparenting)
git-stack create api-work
git-stack create ui-work --from api-work # records local parent
git push -u origin HEAD && git-stack pr --draft # PR base = team-shared parent
git-stack reparent ui-work other-api # move stacks without rename
Dot names (still work)
main
└─ wms-batching # parent: main
└─ wms-batching.ui # parent: wms-batching
restack vs sync
| Direction |
Command |
Does |
| Back |
restack |
Put me on my parent tip |
| Back + trunk |
restack --onto-trunk |
Restack chain onto trunk path |
| Back + kids |
restack --descendants |
Restack me, then children (or prompt y/N) |
| Forward |
sync |
Fix root on parent, then descendants |
| Forward + trunk |
sync --onto-trunk |
Include trunk |
--dry-run on sync prints the plan only (no branch rewrites).
Trunk is never absorbed unless --onto-trunk.
TUI keys
| Key |
Action |
j/k |
Move |
enter |
Checkout |
r / R |
Restack / restack --onto-trunk |
s / S |
Sync / sync --onto-trunk |
d |
Safe delete local branch (git branch -d; confirms with y) |
D |
Force-delete local branch (git branch -D; confirms with y) |
p |
Push (--force-with-lease) |
P |
Open/retarget PR |
c |
Create child (suffix prompt) |
f |
Fetch origin |
F |
Pull selected (FF/update without switching checkout) |
y |
Copy branch name to clipboard |
Y |
Copy commit SHA to clipboard |
v |
Open PR in browser (gh pr view --web) |
C |
Toggle conflict mode: rollback (abort) ↔ resolve (leave conflicts) |
ctrl+r |
Refresh list + PR parents |
? |
Help |
q |
Quit |
How parents are shared
- Draft or ready PR base is the team-visible stack parent (
gh pr list bulk-loaded, cached under .git/git-stack/).
- Local config covers branches without a PR yet and offline work.
- Opening/retargeting a PR with
git-stack pr or reparent keeps base in sync.
git-stack pr / TUI P writes a ## PR Stack section (with PR links) at the top of the PR body between <!-- git-stack --> markers so it can be updated without clobbering the rest of the description.
Safety
- Tracked dirty worktree / rebase-in-progress refused (untracked OK)
- Diverged local/origin is needs-push (force-with-lease) — stack ops still use the local tip
- Push opt-in via
--push (--force-with-lease)
- Restack cutoff prefers
git merge-base --fork-point
- After restack, optional prompt to restack descendants (
--descendants to auto-yes)
- Squash-absorbed children (patches already on parent) reset to parent tip instead of conflict-rebasing
Development
go test ./...
go build -o git-stack ./cmd/git-stack
License
MIT