šÆ 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 (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.