
Each commit becomes a pull request. Stop juggling branches.
git spr manages stacked pull requests on GitHub so you don't have to. Write commits on a single branch, and spr turns each one into its own pull request -- kept in sync, correctly ordered, and ready to merge.

Why stacked PRs?
- Small PRs get reviewed faster. A 50-line change gets meaningful feedback; a 500-line change gets "looks good."
- No more branch gymnastics. Stop creating
feature-part-1, feature-part-2, rebasing one onto the other, and resolving conflicts between them.
- Ship incrementally. Land the database migration today, the API tomorrow, the UI the day after -- each reviewed and merged independently.
- Works with native GitHub. No extra services, no custom merge bots. Just pull requests and branches, managed for you.
Quick Start
Install via brew, nix, or download a binary:
brew install ejoffe/tap/spr # macOS/Linux
nix profile install github:ejoffe/spr # Nix
Then use it like normal git -- just replace git push + manual PR creation with git spr update:
git commit -m "Add user authentication"
git commit -m "Add login page"
git commit -m "Add session management"
git spr update # creates 3 pull requests, stacked in order
git spr status # show status of your stack
git spr merge # merge everything that's ready
That's it. Each commit is a PR. Amend a commit and run git spr update again to sync changes.
Commands
| Command |
Aliases |
Description |
git spr update |
u, up |
Create and update pull requests for commits in the stack |
git spr status |
s, st |
Show status of open pull requests |
git spr merge |
|
Merge all mergeable pull requests |
git spr amend |
a |
Amend a commit in the stack |
git spr edit |
e |
Edit a commit in the stack (interactive rebase) |
git spr sync |
|
Synchronize local stack with remote |
git spr check |
|
Run pre-merge checks (configured by mergeCheck) |
git spr version |
|
Show version info |
Global flags: --detail (show status bit headers), --verbose (log git commands and GitHub API calls), --debug, --profile
Installation
Brew
brew tap ejoffe/homebrew-tap
brew install ejoffe/tap/spr
Nix
nix profile install github:ejoffe/spr
Or run without installing:
nix run github:ejoffe/spr
Apt
echo "deb [trusted=yes] https://apt.fury.io/inigolabs/ /" | sudo tee /etc/apt/sources.list.d/inigolabs.list
sudo apt update
sudo apt install spr
Binary
Download pre-compiled binaries from the releases page.
From source
make bin # requires goreleaser; binaries output to dist/
Usage Guide
Workflow
Commit your changes to a branch as you normally do. Every commit becomes a pull request.
git add feature_1.go
git commit -m "Feature 1"
git add feature_2.go
git commit -m "Feature 2"
git add feature_3.go
git commit -m "Feature 3"
git spr update
The commit subject becomes the PR title; the commit body becomes the PR description. There's no need to create branches or call git push -- git spr update handles everything.
Work in progress: Prefix a commit message with WIP to skip PR creation for that commit. Remove the prefix when you're ready.
Updating pull requests
Run git spr update to sync your entire stack. New commits get new PRs; amended commits update existing PRs automatically.
> git spr update
[⌛❌✅❌] 60: Feature 3
[✅✅✅✅] 59: Feature 2
[✅✅✅✅] 58: Feature 1
| Flag |
Alias |
Description |
--count |
-c |
Update a specific number of PRs from the bottom of the stack |
--reviewer |
-r |
Add reviewers to newly created pull requests |
--no-rebase |
--nr |
Disable rebasing (also supports SPR_NOREBASE env var) |
Amending commits
Stage your changes, then use git spr amend to pick which commit to amend:
> git add feature_2.go
> git spr amend
3 : 5cba235d : Feature 3
2 : 4dc2c5b2 : Feature 2
1 : 9d1b8193 : Feature 1
Commit to amend [1-3]: 2
Use --update (-u) to automatically run git spr update after amending.
Editing commits
Use git spr edit to start an interactive rebase session on a specific commit:
> git spr edit
3 : 5cba235d : Feature 3
2 : 4dc2c5b2 : Feature 2
1 : 9d1b8193 : Feature 1
Commit to edit [1-3]: 2
Finish with git spr edit --done (add -u to also update). Cancel with git spr edit --abort.
Syncing
Use git spr sync to pull remote changes into your local stack. Useful after PRs have been merged or updated on GitHub.
Merging
Use git spr merge instead of the GitHub UI to merge in the correct order:
> git spr merge
MERGED #58 Feature 1
MERGED #59 Feature 2
MERGED #60 Feature 3
[✅❌✅✅] 61: Feature 4
spr finds the top mergeable PR in the stack, combines all commits up to it into a single PR, merges it, and closes the intermediate PRs. This avoids triggering redundant CI runs.
Use --count N to merge only the bottom N pull requests.
Merge status bits
Each PR shows four status bits:
[✅❌✅✅] 61: Feature 4
│ │ │ └─ stack: all PRs below are ready
│ │ └──── conflicts: no merge conflicts
│ └─────── approval: PR is approved
└────────── checks: CI checks pass
| Bit |
⌛ |
❌ |
✅ |
➖ |
| Checks |
pending |
failed |
pass |
not required |
| Approval |
-- |
not approved |
approved |
not required |
| Conflicts |
-- |
has conflicts |
no conflicts |
-- |
| Stack |
-- |
blocked below |
all clear |
-- |
Configure check and approval requirements with requireChecks, requiredChecks, and requireApproval in .spr.yml. When requiredChecks lists specific check names, only those checks are evaluated -- all others are ignored. This is useful when optional checks (e.g. linters, deploy previews) would otherwise cause the status to show as failed.
Starting a new stack
Create a new branch from the latest pushed state:
git checkout -b new_stack @{push}
Configuration
Configuration is created automatically on first run. Repository config lives in .spr.yml at the repo root; user config lives in ~/.spr.yml.
Repository configuration (.spr.yml)
| Setting |
Type |
Default |
Description |
requireChecks |
bool |
true |
Require checks to pass in order to merge |
requiredChecks |
list |
|
List of check names that must pass. When set, only these checks are evaluated; all others are ignored |
requireApproval |
bool |
true |
Require PR approval in order to merge |
githubRepoOwner |
str |
|
GitHub owner (auto-detected from git remote) |
githubRepoName |
str |
|
GitHub repository name (auto-detected from git remote) |
githubRemote |
str |
origin |
Git remote name to use |
githubBranch |
str |
main |
Target branch for pull requests |
githubHost |
str |
github.com |
GitHub host (update for GitHub Enterprise) |
mergeMethod |
str |
rebase |
Merge method: rebase, squash, or merge |
mergeQueue |
bool |
false |
Use GitHub merge queue |
prTemplateType |
str |
stack |
PR template: stack, basic, why_what, or custom |
prTemplatePath |
str |
|
Path to custom PR template file (auto-sets type to custom) |
prTemplateInsertStart |
str |
|
Marker in custom template for commit body insertion start |
prTemplateInsertEnd |
str |
|
Marker in custom template for commit body insertion end |
mergeCheck |
str |
|
Command to run with git spr check before merging |
forceFetchTags |
bool |
false |
Fetch tags during git spr update |
showPrTitlesInStack |
bool |
false |
Show PR titles in stack description within PR body |
branchPushIndividually |
bool |
false |
Push branches one at a time instead of atomically |
defaultReviewers |
list |
|
Reviewers to add to every new pull request |
Example .spr.yml:
requireChecks: true
requiredChecks:
- "ci/test"
- "ci/build"
requireApproval: true
mergeMethod: squash
defaultReviewers:
- teammate
User configuration (~/.spr.yml)
| Setting |
Type |
Default |
Description |
showPRLink |
bool |
true |
Show full pull request URL |
shortPRLink |
bool |
false |
Show clickable PR-<number> instead of full URL |
showCommitID |
bool |
false |
Show first 8 characters of commit hash |
logGitCommands |
bool |
false |
Log git commands to stdout |
logGitHubCalls |
bool |
false |
Log GitHub API calls to stdout |
statusBitsHeader |
bool |
true |
Show status bit type headers |
statusBitsEmojis |
bool |
true |
Use emoji status bits |
createDraftPRs |
bool |
false |
Create new PRs as drafts |
preserveTitleAndBody |
bool |
false |
Don't overwrite PR title and body on update |
noRebase |
bool |
false |
Skip rebasing on git spr update |
deleteMergedBranches |
bool |
false |
Delete branches after PRs are merged |
branchPrefix |
str |
spr |
Prefix for spr-managed branch names |
How it compares
spr is similar to Graphite, ghstack, and Gerrit's stacked review model -- but works purely with GitHub's native pull requests. No extra service, no custom merge bot, no lock-in.
Contributing
Found a bug? Open an issue. Pull requests are welcome.
If you find spr useful, a star helps others discover it.
License
MIT License