Turn local commits into a GitHub stack of pull requests. Each commit becomes its own PR, stacked on top of the previous one. Reviewers see small, focused diffs. You keep working without waiting for merges.
feat/user-auth
○ feat(db): add user schema ──► #101
○ feat(api): add auth endpoints ──► #102
○ feat(ui): add login page ──► #103
npm install -g bstackInstall and authenticate the GitHub CLI, plus the gh-stack extension:
gh auth login
gh extension install github/gh-stackbstack drives gh-stack to link the PRs into a native GitHub stack, so GitHub shows the stack structure right on the PRs.
bstack pushes one remote branch per commit and opens one PR for each. The first PR targets your base branch, and every PR after it targets the branch before it, so the PRs form a stack.
bstack remembers the PRs it opened between runs. When you run bstack again, it compares against what it remembers and only touches what changed.
Create a branch from main, commit one reviewable change per commit, then run bstack:
git switch -c feat/user-auth main
git commit -am "feat(db): add user schema"
git commit -am "feat(api): add auth endpoints"
git commit -am "feat(ui): add login page"
bstack feat/user-auth
+ ○ feat(db): add user schema ──► #101 created
+ ○ feat(api): add auth endpoints ──► #102 created
+ ○ feat(ui): add login page ──► #103 createdbstack pushes dedicated remote branches and opens one PR per commit. Add --dry-run to preview first.
Commit on top of the stack, then run bstack again:
git commit -am "feat(api): add rate limiting"
bstack ○ feat(ui): add login page ──► #103
+ ○ feat(api): add rate limiting ──► #104 createdExisting PRs are untouched. bstack only opens what's new.
Amend the latest commit, then sync:
git commit --amend && bstackFor an older commit, use interactive rebase:
git rebase -i main # mark the commit as 'edit'
git commit --amend && git rebase --continue
bstack ○ feat(db): add user schema ──► #101 unchanged
○ feat(api): add auth endpoints ──► #102 unchanged
○ feat(ui): add login page ──► #103 updatedbstack force-pushes the rewritten branches and updates the affected PRs. Editing an older commit also updates every PR above it.
Reorder commits with interactive rebase, then run bstack:
git rebase -i main # swap lines to reorder
bstack- ○ feat(db): add user schema ──► #101
- ○ feat(api): add auth endpoints ──► #102
+ ○ feat(api): add auth endpoints ──► #102
+ ○ feat(db): add user schema ──► #101
○ feat(ui): add login page ──► #103PR numbers follow their commits. bstack rebuilds the stack in the new order and re-points the PR bases.
Use fixup in interactive rebase to fold a commit into its parent, then sync:
git rebase -i main # mark a commit as 'fixup'
bstack ○ feat(db): add user schema ──► #101 updated
- ○ fixup! add user schema
○ feat(api): add auth endpoints ──► #102
○ feat(ui): add login page ──► #103The fixup folds into the parent PR. The stack contracts, and the parent PR is updated in place.
Delete a commit from the stack with interactive rebase, then sync:
git rebase -i main # mark a commit as 'drop'
bstack ○ feat(db): add user schema ──► #101
- ○ feat(api): add auth endpoints ──► #102 closed
○ feat(ui): add login page ──► #103 updatedThe dropped PR closes. The PRs above it are rebased onto their new parents.
When a lower PR merges, rebase your branch onto the updated base and sync:
git rebase main
bstack- ○ feat(db): add user schema ──► #101 merged
○ feat(api): add auth endpoints ──► #102
○ feat(ui): add login page ──► #103
+ ○ feat(ui): add logout ──► #104 createdThe merged PR leaves the stack. Surviving PRs keep their numbers, and new commits append to the stack.
Jump to any stack by PR number or URL:
bstack checkout 123
bstack checkout https://github.com/owner/repo/pull/123Self-update through the package manager that installed it (update works the same):
bstack upgrade| Flag | Description |
|---|---|
--base <branch> |
Stack base branch (default: repo default branch) |
--remote <name> |
Git remote to push to (default: remote.pushDefault or origin) |
--draft |
Create PRs as drafts instead of ready-for-review |
--dry-run |
Preview what bstack would do without pushing anything |
--verbose |
Print every git/gh command before it runs |
--same-base |
Refuse checkout if it would change the current merge base |
- One commit = one PR. Don't push the bstack branches or open PRs manually. bstack owns them.
-
No merge commits. When
mainmoves, rebase your branch onto it (git rebase main) instead of merging. -
Run
bstackafter every change. It's idempotent. Running it twice changes nothing.