stack-pr: one pull request per commit, stacked in dependency order
A tool for working with stacked PRs on github.
At a glance
- What is it?
- stack-pr is a command line tool from Modular that turns a branch of commits into a stack of GitHub pull requests, one per commit, each based on the one below it, and it can update, rebase and land the stack as it moves. It drives the GitHub CLI rather than the API directly, and it stores stack metadata inside the commit messages.
- Who is it for?
- stack-pr suits a contributor whose change has decomposed into several reviewable commits and who wants each commit to become its own pull request instead of one large diff, because submit pushes a stack of branches and cross-links the pull requests for you. It does not suit a team on GitHub flow with its own stack tooling, and it does not suit a workflow where the base branch is not called main without changing the options.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 102 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem is one pull request carrying three unrelated commits
stack-pr is a command line tool that helps you create multiple GitHub pull requests at once, with a stacked order of dependencies.
The README explains the problem with a concrete example. Suppose you have a change A and a change B that depends on A, and you would like both reviewed. Without stacked pull requests you would have to create two pull requests, A and A+B. The second would be difficult to review because it includes all the changes at once.
With stacked pull requests the first pull request contains only change A, and the second contains only change B. The README makes the payoff explicit: one can group related changes together, making them easier to review.
That is the entire thesis, and it is a review-cost argument rather than a branching argument. The number of pull requests goes up, the size of each goes down, and the diff a reviewer has to read at any one point is one logical change rather than a mixture.
The dependency ordering is what makes it work rather than just splitting a diff. Each pull request is based on the branch of the one below it, so the stack has to be landed in order, bottom to top. The tool models that with a branch number in each branch name and with metadata embedded in the commit messages that records the stack position.
The tool is from Modular, published under a custom licence that GitHub cannot classify, and the pyproject metadata lists it as a console utility with Development Status 5, Production/Stable. The Python requirement is 3.9 or newer, with classifiers through 3.13, and the console entry point is stack-pr = stack_pr.cli:main.
The primary language is Python, the default branch is main, and the last push was on 2026-07-14 with the most recent release being 0.1.7 from 2026-02-18, after 0.1.6 in December 2025 and 0.1.5 in November 2025.
Five commands, and only one of them changes anything on GitHub
The command set is small and the division of responsibility is unusually clean.
The submit command, whose alias is export, creates a new stack of pull requests from the given set of commits. The README's own gloss on it is the clearest description of the whole tool: push my local changes to the corresponding remote branches and update the corresponding pull requests, or create new pull requests if they do not exist yet.
The view command inspects the given set of commits and finds the linked pull requests. This command does not push any changes anywhere and does not change any commits. It can be used to examine what other commands did or will do.
The abandon command removes all stack metadata from the given set of commits, deletes the corresponding local and remote branches, and closes the pull requests.
The land command merges the bottom-most pull request in the current stack and rebases the rest of the stack on the latest main.
The config command sets configuration values in the config file, which is .stack-pr.cfg by default. The format is the same as git config, taking a setting in the form section.key=value.
So view is the read-only command, submit and land write to GitHub, abandon tears down, and config is local. The README is explicit that view is always safe to run and it recommends doing so often, as a pro-tip, because it helps you understand the current state of your stack and catch any potential issues early.
That is a sensible division for a tool that pushes branches and creates pull requests. You can inspect the plan before it becomes three pull requests on someone else's review queue.
The typical loop is compact.
while not ready to merge:
make local changes
commit to local git repo or amend existing commits
create or update the stack with `stack-pr.py submit`
merge changes with `stack-pr.py land`And you can use view at any point to examine the current state, and abandon to drop the stack.
The stack lives in branch names and commit messages
Under the hood the tool creates and maintains branches named $USERNAME/stack/$BRANCH_NUM, and the name pattern can be customised with the --branch-name-template option. It also embeds stack metadata into commit messages.
Then comes the warning, which is the most important line in the README for anyone about to try this: you are not supposed to work with those branches or edit that metadata manually. Instead of pushing to these branches you should use submit, instead of deleting them you should use abandon, and instead of merging them you should use land.
That is the central design decision. The stack is not a set of ordinary branches that you drive with git. It is a structure the tool owns, and the metadata it writes into your commit messages is what lets it reconstruct the stack on a later run, find the existing pull requests, and know which commit belongs at which position.
The consequence is that stack-pr and plain git do not mix well on the same branch. If you push to a stack branch yourself, or amend a commit message and drop the metadata, the tool loses track of the stack. And because the metadata lives in the commit messages, an interactive rebase that rewrites those messages will break it, which is why the recommended update flow is to amend the corresponding commit and run submit again rather than to rebase by hand.
The commit range is the other parameter worth understanding. The tool looks at commits in the range BASE..HEAD and creates a stack of pull requests to apply these commits to TARGET. By default BASE is main as a local branch, HEAD is the git revision HEAD, and TARGET is main on remote, meaning origin/main. Those can be changed with the options -B, -H and -T, and they accept standard git notation, so one can use -B HEAD~2 to create a stack from the last two commits.
So the defaults assume a fork-and-branch workflow where main exists locally and on the remote, and the range main..HEAD is the commits you have not yet landed. The tool then creates one pull request per commit in that range, each targeting the branch of the previous one.
Installing needs the gh CLI, and the range defaults are worth checking
The dependency list is short and one item is not optional in practice. You need gh installed, for example brew install gh on macOS, and you need to run gh auth login with SSH.
The tool therefore authenticates through the GitHub CLI rather than managing a token of its own. That has an immediate consequence: the tool inherits whatever gh is configured for, so your access is whatever that account can do, and there is no separate credential for stack-pr to rotate.
The installation itself is one command through pipx.
pipx install stack-prOr manually, by cloning the repository and installing from the source tree.
pipx install .The basic workflow starts with a feature branch off main.
git checkout main
git pull
git checkout -b my-featureThen you make your changes and create multiple commits, one commit per pull request you want to create, with ordinary git commit calls. The commit messages are ordinary too, which matters because stack-pr adds its own metadata alongside your message rather than replacing it.
Before pushing anything, you check what the tool has decided to include.
stack-pr view # Always safe to run, helps catch issues earlyAnd then you create or update the stack.
stack-pr submitThe README notes that export is an alias for submit, which is a leftover from an earlier naming of the operation.
One detail in the documentation is worth repeating because it is the sort of thing that only bites once. By default stack-pr will look at commits in the main..HEAD range and will create a pull request for every commit in that range. The example given shows a branch with three commits on top of origin/main, and the tool considers the top three commits as the changes it will turn into a stack.
For a quick look at what that range contains without involving the tool, the README suggests a shell alias.
alias githist='git log --abbrev-commit --oneline $(git merge-base origin/main HEAD)^..HEAD'The merge-base formulation is what makes it equivalent to the range the tool uses, and it is a useful thing to have in your shell regardless of whether you adopt the tool.
Landing is bottom-up, and the rebase is the tool's job
When the pull requests are ready, the README gives two options and they differ in who does the work.
Option A is the stack-pr land command. It merges the bottom-most pull request in the stack and automatically rebases your remaining pull requests. You can run stack-pr land again to merge the next one once CI passes.
Option B is to merge the bottom-most pull request through the GitHub web interface, and then, after the merge, on your local machine, pull the merged changes and resubmit the stack so the remaining pull requests rebase.
git checkout my-feature
git pull origin main # Get the merged changes
stack-pr submit # Resubmit the stack to rebase remaining PRsThe difference in practice is who owns the rebase. With land, the tool does it and you run one command. With the web interface, you do it yourself and then tell the tool to redo it.
Either way, the same thing happens to the pull requests. When a pull request in the middle of the stack is merged, every pull request above it has to be rebased onto the new main, which means their branches move and their diffs are recomputed. Doing that by hand across three pull requests is the tedious part that stacked pull requests are supposed to remove, and both options push that work back into the tool.
The same rebasing need shows up earlier in the lifecycle, when main moves under your stack for reasons unrelated to your own merges.
git checkout my-feature
git pull origin main # Get the latest main
git rebase main # Rebase your commits on top of main
stack-pr submit # Resubmit to update all PRsSo the pattern repeats: rebase locally, then resubmit so the remote branches and pull requests catch up.
To update a single pull request in the middle of the stack, the flow is to amend the corresponding commit, run stack-pr view to verify the changes, and run stack-pr submit again.
The repository layout matches a small, focused tool: src/ for the package, tests/ for the test suite, a pyproject.toml built with the pdm backend with the version taken from the SCM, and a pdm.lock. The dev extra pulls in pytest, pytest-mock, mypy and ruff. There is also a pixi configuration with conda-forge as the channel and platforms for macOS on both architectures plus Linux on x86-64 and aarch64, so the project supports a pixi-based environment as well as pipx.
Where stack-pr is the wrong tool
The first wrong-tool case is a team that already has a stack workflow. If you use GitHub flow, or a hosted tool that creates stacked pull requests, or an internal tool that encodes the same relationships in branch naming, then stack-pr adds a second source of truth. The stack would be described twice, in your branch names and in the commit metadata, and the two would eventually disagree.
The second case is a repository whose base branch is not called main. The defaults are BASE as the local branch main, HEAD as the git revision HEAD, and TARGET as origin/main, so a project using master or trunk needs the -B, -H and -T options on every invocation, or a config entry. That is workable but it is friction on every command, and a forgotten option is a stack built against the wrong base.
The third case is a workflow where one pull request is the right unit. If a change is genuinely one reviewable unit, splitting it into three pull requests that must be landed in order makes the review harder, not easier, because the reviewer now has to track three diffs and an ordering constraint. The tool does not decide which commits are separately reviewable; you do, by making one commit per pull request.
The fourth case is a team that does not want stack metadata inside commit messages. The metadata lives in the commit messages by design, so every commit in the stack carries it. If your project has commit message conventions enforced by a hook or a CI check, or if you rewrite messages with tooling that will not preserve the metadata, the tool loses the stack.
There is a dependency question to settle as well. The tool drives gh, which means your GitHub access is whatever gh is authenticated for, and the README's dependency list is described as non-comprehensive. Anything else the tool calls at runtime, beyond gh and git, is not documented, so a team with a restricted network or a locked-down CI environment should check that before adopting rather than after.
On licence, the project metadata records a custom licence that GitHub cannot classify, while the pyproject classifiers list Apache Software License and the licence field points at the LICENSE file. The dependency on the gh CLI means you are also accepting whatever terms the GitHub CLI carries, and if your policy reviews the licences of everything in your developer toolchain, stack-pr, gh and git all belong in that review.
Editorial conclusion
stack-pr suits a contributor whose change has decomposed into several reviewable commits and who wants each commit to become its own pull request instead of one large diff, because submit pushes a stack of branches and cross-links the pull requests for you. It does not suit a team on GitHub flow with its own stack tooling, and it does not suit a workflow where the base branch is not called main without changing the options. Before adopting, install gh and run gh auth login with SSH, then run stack-pr view first to confirm which commits the tool has decided to include, since the default range is main..HEAD and a wrong range silently publishes the wrong stack.
Frequently asked questions
What is stack-pr?
stack-pr is a command line tool from Modular that helps you create multiple GitHub pull requests at once, with a stacked order of dependencies. It turns a branch of commits into one pull request per commit, each based on the one below it, so each diff stays small enough to review.
How do I install stack-pr?
Install gh first, for example with brew install gh on macOS, and run gh auth login with SSH. Then install the tool with pipx install stack-pr, or clone the repository and run pipx install . from the source tree.
Which commands does stack-pr have?
Five main commands: submit, with export as an alias, creates or updates the stack of pull requests; view inspects commits and their linked pull requests without pushing anything; abandon removes stack metadata, deletes the branches and closes the pull requests; land merges the bottom-most pull request and rebases the rest; and config sets values in the .stack-pr.cfg file.
Where does stack-pr keep the stack information?
In branch names of the form $USERNAME/stack/$BRANCH_NUM, with the pattern customisable through the --branch-name-template option, and in stack metadata embedded in the commit messages. The README warns not to push to those branches, delete them or edit the metadata by hand, and to use submit, abandon and land instead.
Which commits does stack-pr turn into pull requests?
The commits in the range BASE..HEAD, applied to TARGET. By default BASE is the local branch main, HEAD is the git revision HEAD, and TARGET is origin/main, and these can be changed with the -B, -H and -T options using standard git notation, so -B HEAD~2 selects the last two commits.
How do I land a stack with stack-pr?
Run stack-pr land. It merges the bottom-most pull request in the stack and automatically rebases the remaining ones on the latest main, and you can run it again to merge the next pull request once CI passes. The alternative is to merge through the GitHub web interface and then resubmit the stack locally so the rest rebase.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/modular-stack-pr)Was this useful?