đŻ What a âStacked PRâ Is (and Why Youâll Want One)
A stacked pull request (sometimes called a stacked PR, stacked diff, or dependent PR) is a series of PRs that build on top of each other, each one containing a small, logicallyâisolated change.
main âââş A âââş B âââş C
â â â
â â ââ PRâC (depends on B)
â ââ PRâB (depends on A)
ââ PRâA (directly on main)
-
A is based on
main. - B is based on A (its head).
- C is based on B, etc.
When you eventually merge the stack in order (A â B â C), each change lands cleanly, and reviewers can focus on one cohesive piece at a time.
Why Stack PRs?
| Problem | Stacked PR Solution |
|---|---|
| Huge, monolithic PRs that are hard to review & cause long CI times | Break the work into biteâsize PRs (e.g., âfeature flagâ, âdata modelâ, âUIâ) |
| Interâdependent changes (e.g., a new API + its consumer) | Each dependent change lives in its own PR, but they still get tested together because they are built on top of each other |
| Rebasing on main constantly drags in unrelated changes | Only the bottom PR needs to be rebased onto main; the rest stay on top of it |
| Need to ship part of a larger change early | Merge the first PR in the stack; the rest stay pending until theyâre ready |
| CI resources | Only the bottom PR runs the full suite against main; higher PRs can run a lighter subset because they already passed lowerâlevel tests |
đŚ The Landscape of Tools (as ofâŻ2026)
| Tool / Service | Key Features | Installation / Setup | Typical Workflow |
|---|---|---|---|
ghstack (GitHub CLI plugin) |
- Creates stacked PRs automatically from a series of commits. - Handles baseâbranch updates, resolves merge conflicts, and can reâstack after rebases. - Works with GitHub's GraphQL API, so you get âdependent PRâ links in the UI. |
pip install ghstack (or brew install ghstack).Requires a personal access token with repo scope. |
bash git checkout -b feature/stacked\n# create many commits âŚ\nghstack push\n# later, after rebasing on main\nghstack rebase
. |
| GitTown (aka git-town) | - git town ship can ship a stack of dependent branches in order.
- Not GitHubâspecific, works with any remote. | brew install git-town / cargo install git-town. |
bash git town new featureA\n# commit âŚ\ngit town new featureB\n# work on B âŚ\ngit town sync
|
| gstack (openâsource script) | - Very lightweight Bash script that creates a series of PRs from sequential commits.
- Good for CIâonly pipelines. | curl -L https://raw.githubusercontent.com/âŚ/gstack.sh | bash. |
bash gstack create
. |
| GitHubâs âDraft PRâ + âDepends onâ labels | - No external tool needed; you manually create PRs and add a depends-on:<PR#> label (or a comment).
- GitHub UI now shows a âThis PR depends on #123â banner (rolled out in early 2026). | No install. Just enable the âPull request dependenciesâ preview in your org settings. | Create PRâA â PRâB â PRâC, add depends-on: #A comment on B, etc. |
| pullrequest.io (SaaS) | - Managed service that visualises stacks, autoâupdates bases, and adds âstackâstatusâ checks.
- Works with public & private repos. | Sign up, link GitHub repo, generate a machineâuser token. | UIâdriven: select commits â âCreate stackâ. |
| Gerrit (if youâre on a hybrid workflow) | - Has native support for âdependent changesâ.
- Works great if you already use Gerrit for code review. | Already part of Gerrit install. | Use git review -d <change-id> etc. |
Bottomâline: If you just need a quick, ânoâinstallâ solution, the builtâin draftâPR +
depends-onlabel works fine. If you want to automate the entire stack lifecycle (create, rebase, update, merge),ghstackis the most mature and GitHubânative option in 2026.
đ ď¸ StepâbyâStep Guide Using ghstack (the most popular choice)
Below is a complete workflow you can copyâpaste into a terminal. It assumes you have:
- GitâŻ2.40+ (or newer)
- GitHub CLI (
gh) installed and authenticated - PythonâŻ3.9+ (for
ghstack)
1ď¸âŁ Install the tools
# GitHub CLI (if you donât have it)
brew install gh # macOS
# or: sudo apt install gh # Ubuntu
# ghstack (Python package)
pip install --user ghstack
# make sure ~/.local/bin is on your $PATH
2ď¸âŁ Prepare a branch and make a series of commits
# Start from the latest main
git checkout main
git pull origin main
# Create a âstackâ branch (the base of the stack)
git checkout -b feature/stacked
# 1ď¸âŁ First logical change (e.g., add a new API)
# -------------------------------------------------
echo "def hello(): return 'world'" > hello.py
git add hello.py
git commit -m "feat: add hello() helper"
# 2ď¸âŁ Second logical change that depends on the first
# -------------------------------------------------
cat >> hello.py <<'EOF'
def greet(name):
return f"Hello, {name}! " + hello()
EOF
git add hello.py
git commit -m "feat: add greet() that uses hello()"
# 3ď¸âŁ Third logical change (e.g., tests)
# -------------------------------------------------
mkdir -p tests
cat > tests/test_hello.py <<'EOF'
import unittest
from hello import greet
class TestHello(unittest.TestCase):
def test_greet(self):
self.assertIn("Hello, Alice!", greet("Alice"))
EOF
git add tests/
git commit -m "test: add unit tests for greet()"
Now you have three commits that you want to turn into three stacked PRs.
3ď¸âŁ Push the stack to GitHub
# ghstack will create a separate remote branch for every commit
ghstack push
What happens under the hood:
| Commit | Remote branch created | PR title (autoâderived) | Base branch |
|---|---|---|---|
| 1ď¸âŁ | ghstack/1 |
feat: add hello() helper |
main |
| 2ď¸âŁ | ghstack/2 |
feat: add greet() that uses hello() |
ghstack/1 |
| 3ď¸âŁ | ghstack/3 |
test: add unit tests for greet() |
ghstack/2 |
Each PR appears in GitHub with the âdepends on #XYZâ banner (thanks to the new UI feature). The PR descriptions contain a tiny block:
--- ghstack metadata ---
base: ghstack/2
stack: [ghstack/1, ghstack/2, ghstack/3]
--- end ---
4ď¸âŁ Review & Iterate
- Reviewers see each PR separately.
- CI runs on each branch; GHâActions can be configured to run only the diff against its base (fast) because the lower PR already passed the full suite.
If you need to amend the second commit:
git checkout ghstack/2 # switch to the branch created for commit #2
# Make change, amend commit
git commit --amend -m "feat: improve greet() implementation"
# Push the new version (ghstack takes care of the dependent PRs)
ghstack push
ghstack will automatically reâstack all downstream PRs (3, 4, âŚ) on top of the updated commit.
5ď¸âŁ Rebase the whole stack onto a newer main
Suppose main has moved forward and you want to keep your stack upâtoâdate:
git checkout main
git pull # get latest
ghstack rebase
Result:
-
ghstack/1is rebased onto the newmain. -
ghstack/2andghstack/3are automatically rebased onto the newghstack/1. - All open PRs get updated automatically â no manual
git push -fgymnastics.
6ď¸âŁ Merging the stack
When every PR is approved:
# Merge bottomâmost PR first (or let GitHub autoâmerge with âRequire linear historyâ)
gh pr merge ghstack/1 --merge
gh pr merge ghstack/2 --merge
gh pr merge ghstack/3 --merge
Because each PRâs base is the previous PR, merging in order results in a perfectly linear history on main.
If you enable GitHubâs âautoâmerge when all checks passâ on each PR, the merges will happen automatically as soon as the lower PR merges.
7ď¸âŁ Clean up
# Delete the temporary remote branches
ghstack delete-branches
Youâre left with a clean main branch plus the series of logical commits merged in order.
đ Alternative Workflows (NoâTool, âManual Stackâ)
If you prefer not to install anything, you can still get a stacked workflow using plain Git:
# 1ď¸âŁ Create the base branch (on main)
git checkout -b feature/a main
# commit A ⌠push and open PRâA
# 2ď¸âŁ Create B *on top of* A
git checkout -b feature/b feature/a
# commit B ⌠push and open PRâB
# In PRâB comment: âDepends on #PR-Aâ
# 3ď¸âŁ Create C on top of B
git checkout -b feature/c feature/b
# commit C ⌠push and open PRâC
# Comment: âDepends on #PR-Bâ
Key manual steps to remember:
| Action | How to do it manually |
|---|---|
| Keep the base upâtoâdate |
git fetch origin && git rebase origin/main on the bottom branch, then git checkout feature/b && git rebase feature/a, etc. |
| Close a PR when you want to drop a change | Delete the branch (git push origin --delete feature/b) and remove the dependent PRs (or git rebase them onto the new base). |
| Show the dependency in GitHub UI | Add a comment Depends on #123 (GitHub renders a banner). You can also add a label depends-on:123. |
Pros: No extra tooling, works everywhere.
Cons: You have to reâbase manually, risk of diverging PR bases, and you wonât get the nice âreâstackâ automation that ghstack offers.
đ§Š Integrating Stacked PRs with CI/CD
1ď¸âŁ Split CI Jobs
-
Fullâsuite job runs on the bottom PR (the one that targets
main). -
Incremental job runs on higher PRs, using
git diffagainst its base to only test the new changes.
GitHub Actions example (.github/workflows/ci.yml):
name: CI
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for diffing
- name: Find base commit
id: base
run: |
# GH provides GITHUB_BASE_REF (the branch PR is based on)
echo "base=$(git merge-base HEAD origin/${{ github.base_ref }})" >> $GITHUB_OUTPUT
- name: Run incremental tests
run: |
# Example with pytest: run only changed files
git diff --name-only ${{ steps.base.outputs.base }} | grep '\.py$' | xargs -r pytest
Higher PRs get a quick feedback loop; the bottom PR still validates the full integration test suite.
2ď¸âŁ Guardrails (Branch Protection)
- Require âdependents merged firstâ: In your repoâs Branch protection rules enable the optional âRequire status checks to pass before mergingâ and add a custom check that verifies âAll dependent PRs are mergedâ.
- Enforce linear history: Turn on âRequire linear historyââthis guarantees that merges happen in order and never create merge commits that would break the stack.
đ BestâPractice Checklist
| â | Practice | Why it matters |
|---|---|---|
| 1ď¸âŁ | One logical change per PR (e.g., âadd APIâ, âadd consumerâ, âadd testsâ) | Keeps review size small and makes stacking natural. |
| 2ď¸âŁ |
Name branches feature/stackâNâ<description> (or let ghstack generate them). |
Makes it obvious which branch belongs to which stack level. |
| 3ď¸âŁ |
Keep the bottom PR always rebased onto main. |
Guarantees that merging the stack never introduces merge conflicts. |
| 4ď¸âŁ |
Add explicit âDepends on #XYZâ comments or labels (if youâre not using ghstack). |
Human reviewers and bots can see the order at a glance. |
| 5ď¸âŁ | Enable GitHubâs âPull request dependenciesâ preview (Org â Settings â Features). | Gives the UI banner and a builtâin dependency graph. |
| 6ď¸âŁ | Run incremental CI on topâofâstack PRs. | Faster feedback for downstream changes. |
| 7ď¸âŁ | Never forceâpush the bottom PR without reâstacking downstream. | If you do, downstream PRs will diverge and CI will fail. |
| 8ď¸âŁ | When a stack is ready to ship, merge bottomâup (or enable autoâmerge with âMerge when the head branch is upâtoâdateâ). | Guarantees linear history and avoids âmerge commitâ noise. |
| 9ď¸âŁ |
Delete temporary ghstack/* branches after the stack lands. |
Keeps the repo tidy and avoids accidental pushes. |
| đ | Document the stack in the PR description (e.g., âStack: #12 â #13 â #14â). | Future maintainers know the intent and can locate related changes quickly. |
đŚ Example: RealâWorld Scenario (Feature Flag Rollout)
Suppose you need to:
- Add a feature flag in the config library.
- Guard a new endpoint behind that flag.
- Add integration tests for the endpoint.
- Deploy a monitoring dashboard.
You can create four stacked PRs:
| PR | Description | Branch (ghstack) | Base |
|---|---|---|---|
| #101 | feat: add ânewâfeatureâ flag |
ghstack/1 |
main |
| #102 | feat: new endpoint (guarded by flag) |
ghstack/2 |
ghstack/1 |
| #103 | test: integration tests for new endpoint |
ghstack/3 |
ghstack/2 |
| #104 | chore: dashboard for new feature |
ghstack/4 |
ghstack/3 |
Team workflow:
- DayâŻ1: Open PRâŻ#101, get flag reviewed.
-
DayâŻ2: Merge #101, rebase the rest automatically (
ghstack rebase). - DayâŻ3: Open #102, review the endpoint.
- DayâŻ4: Merge #102 â #103 now runs against the updated code.
- DayâŻ5: Merge #103 â #104, then ship the whole stack in one go if you want the dashboard to go live only after tests pass.
If a stakeholder decides to skip the dashboard for now, you simply close #104âno need to rewrite history.
đ§ Quick Reference Cheat Sheet
| Command | What it does |
|---|---|
ghstack push |
Creates a remote branch for each local commit and opens a PR for each. |
ghstack rebase |
Rebases the entire stack onto the current main (or any other base you specify). |
ghstack status |
Shows the current stack layout (base â ... â HEAD). |
ghstack delete-branches |
Deletes the temporary ghstack/* branches after the stack is merged. |
git checkout -b feature/X <base> |
Manual way to start a new stacked branch off <base>. |
git rebase <new-base> |
Rebase the bottom branch onto a newer base; then reâbase the children manually (git checkout B && git rebase A). |
gh pr merge <num> --merge |
Merge a specific PR (use --squash or --rebase if you prefer those strategies). |
| `gh pr comment --body "Depends on |
Top comments (0)