Skip to content
FLAVIO COPES
flaviocopes.com
2026

GitHub Stacked PRs: a practical guide to gh stack

By

Learn GitHub Stacked PRs with gh stack: design focused layers, submit the chain, handle review feedback, rebase safely, and merge.

~~~

GitHub Stacked PRs let you split one large change into several small pull requests that depend on each other.

Each pull request stays focused. Reviewers see one layer at a time. You can still merge the whole feature as one operation.

This solves a problem I often see with large features: the code is ready, but the pull request is too large to review well.

Instead of opening one 2,000-line pull request, you might open 3 smaller ones:

The pull requests form a chain. GitHub understands that chain, shows it in the UI, runs checks for every layer, and helps keep all the branches up to date.

In this tutorial we’ll build one stack from start to finish. We’ll also change a branch in the middle, resolve a rebase conflict, and see what happens when we merge only part of the stack.

GitHub Stacked PRs is in private preview as I write this. Your repository must have the feature enabled. You can join the waitlist, and you should expect some commands or details to change before the final release.

The problem with one large pull request

Imagine we’re adding user search to an application.

The feature needs 3 parts:

  1. types and data access
  2. an API endpoint
  3. a search interface

We could put everything on one branch:

main
  └── user-search

That gives us one large pull request.

The reviewer must understand the data layer, API, and interface at the same time. A small concern about the data model can invalidate comments already made on the UI.

We could also create 3 independent branches from main:

main
  ├── user-model
  ├── search-api
  └── search-ui

But this does not model reality. The API needs the data model, and the UI needs the API.

The branches are not independent.

A stack represents that dependency:

main
  └── user-model
        └── search-api
              └── search-ui

The first branch starts from main. The second starts from the first. The third starts from the second.

This is the central idea behind stacked pull requests.

A stack is both cumulative and divided

There are two useful ways to look at a stack.

Git sees cumulative branches. The search-ui branch contains the model, API, and UI because it descends from both lower branches.

The pull request interface shows divided changes:

PR #1: main       → user-model
PR #2: user-model → search-api
PR #3: search-api → search-ui

PR #2 shows only what changed between user-model and search-api. It does not show the model again.

PR #3 shows only the UI layer.

This gives the reviewer small diffs without pretending the changes are unrelated.

What GitHub adds to the old stacked-branch workflow

Developers have stacked branches for years. You can do it with regular Git commands and ordinary pull requests.

GitHub’s feature adds a first-class Stack object around those PRs.

That gives us a few important things:

The branches and pull requests are still normal Git branches and PRs. The extra stack relationship tells GitHub how they belong together.

This difference matters. A chain of PR base branches is the Git structure. The Stack object is the GitHub feature built around it.

Before you start

You need:

If you are new to the basics, read my Git guide and first GitHub pull request tutorial first.

Check that GitHub CLI is authenticated:

gh auth status

Then install the official extension:

gh extension install github/gh-stack

You can use gh stack for every command. The extension can also create a shorter gs command:

gh stack alias

I’ll use the full command in this tutorial. It makes each example easier to recognize.

Create the first layer

Start from an updated main branch:

git switch main
git pull --ff-only

Now initialize a stack and name its first branch:

gh stack init user-model

This creates and checks out user-model. It also records the new stack in .git/gh-stack.

That file lives inside .git, so it is local metadata. It is not committed to your repository.

The command also enables Git’s rerere feature. rerere means “reuse recorded resolution.” If the same conflict appears during several cascading rebases, Git can remember how you resolved it.

Let’s pretend we now add the user model and its tests. Commit the first focused change:

git add src/users.ts tests/users.test.ts
git commit -m "Add user search model"

The bottom of our stack now looks like this:

main
  └── user-model

This branch should make sense on its own. A reviewer can check the model without thinking about HTTP or interface code.

Add the API layer

The API depends on the model, so it belongs above user-model.

Add a branch to the top of the stack:

gh stack add search-api

gh stack add creates the branch at the current HEAD and checks it out.

Add the API code and commit it:

git add src/api/search.ts tests/search-api.test.ts
git commit -m "Add user search endpoint"

The stack now has two layers:

main
  └── user-model
        └── search-api

Notice that search-api contains the model commits too. It must contain them because the endpoint uses the model.

Its pull request will not repeat them. The PR base will be user-model, so the diff only contains the API changes.

Add the interface layer

Let’s add one more branch:

gh stack add search-ui

Then add and commit the interface:

git add src/components/UserSearch.ts tests/user-search.test.ts
git commit -m "Add user search interface"

We now have the full chain:

main
  └── user-model
        └── search-api
              └── search-ui

This is a good stack because the dependency arrows all point down.

The UI depends on the API. The API depends on the model. Nothing in user-model depends on a branch above it.

Inspect the stack before publishing it

Use gh stack view to see the current stack:

gh stack view

You can also ask for a compact list:

gh stack view --short

Or get JSON for a script:

gh stack view --json

I also like to inspect each layer with regular Git before submitting:

git diff main..user-model
git diff user-model..search-api
git diff search-api..search-ui

This is a useful test. If the last command shows changes to the data model, some work may live on the wrong branch.

Create the pull requests

Submit the stack:

gh stack submit

This command pushes all branches, creates or updates the pull requests, and links them as a Stack on GitHub.

It also gives every PR the correct direct base:

user-model → main
search-api → user-model
search-ui  → search-api

The command opens an editor for the PRs. You can accept the generated titles, write clearer ones, and toggle each new PR between ready and draft.

For a non-interactive submission, use generated titles:

gh stack submit --auto

New PRs created by --auto are drafts by default. Add --open when you want them ready for review:

gh stack submit --auto --open

If you only want to push the branches, without creating or updating PRs, run:

gh stack push

The distinction is useful:

How reviewers should read a stack

A stack tells a story from the bottom up.

For our example, review in this order:

  1. user-model
  2. search-api
  3. search-ui

The lower layers establish concepts used above them. Starting from the UI would force the reviewer to guess how the API and model work.

GitHub shows a stack map at the top of every PR. The reviewer can jump between layers without returning to the repository’s PR list.

Each review is still independent. A reviewer can approve the model, request changes on the API, and leave the UI untouched.

My advice is to write each PR description for its own layer. Mention the larger feature, but explain exactly what that PR adds and what it deliberately leaves for the next one.

Change a branch in the middle of the stack

This is where stacked PRs usually become tedious without tooling.

Suppose a reviewer asks us to change the API while we’re working on search-ui.

Move down one branch:

gh stack down

You can also select the branch by name:

gh stack checkout search-api

Make the requested change where it belongs:

git add src/api/search.ts
git commit -m "Validate empty search queries"

Now search-ui is still based on the old tip of search-api. The history is no longer one clean chain.

Rebase every branch above the current one:

gh stack rebase --upstack

Then push the rewritten branches:

gh stack push

gh stack push uses --force-with-lease and an atomic push. The lease stops you from overwriting unexpected remote work, and the atomic push means all branch references update or none do.

This is the daily rhythm of stacked development:

change the correct layer

rebase the layers above it

push the updated stack

Do not put an API fix on the UI branch just to avoid a rebase. That makes the UI diff harder to understand and defeats the point of the stack.

Rebase the entire stack on main

While we work, other changes keep landing on main.

Update the entire chain with:

gh stack rebase

This performs a cascading rebase. GitHub starts at the trunk and works upward:

  1. rebase user-model onto the latest main
  2. rebase search-api onto the new user-model
  3. rebase search-ui onto the new search-api

Then push:

gh stack push

You can also run the all-in-one command:

gh stack sync

sync fetches remote changes, updates the trunk, rebases the stack, pushes it, updates PR state, and reconciles the local stack with GitHub.

Use rebase when you expect to resolve conflicts interactively. Use sync for the routine case where you want everything brought up to date.

Resolve a cascading rebase conflict

A conflict in a lower layer can affect every layer above it.

Run the rebase as usual:

gh stack rebase

If Git stops, open the reported files and resolve the conflict markers.

Stage the resolved files:

git add src/users.ts

Then continue:

gh stack rebase --continue

The command continues through the remaining branches.

If the rebase is going in the wrong direction, abort it:

gh stack rebase --abort

The extension restores all branches to their pre-rebase state.

This is also where the automatically enabled rerere option helps. A resolution recorded on one branch can be reused when a similar conflict appears higher in the chain.

Rebase locally or on GitHub

GitHub also provides a Rebase Stack button in the pull request UI.

The server rebases every unmerged branch and force-pushes the result. This is convenient when the rebase is clean.

There are two reasons to prefer the CLI:

The server-side rebase is unavailable when conflicts need manual resolution. It also keeps the original author but sets the person who clicked the button as committer, and those new commits are not signed.

If your rules require signed commits, rebase locally.

Merge the whole stack

Once every layer is approved and passing its checks, merge the top PR.

GitHub merges that PR and every unmerged PR below it, from bottom to top.

For our stack, merging search-ui lands all 3 PRs:

user-model → search-api → search-ui

GitHub supports all 3 merge methods:

The squash behavior is particularly useful. A stack of 3 PRs becomes 3 focused commits on main, not one giant squash commit.

GitHub describes a stack merge as one atomic operation. There is a small operational nuance: its FAQ says an unexpected failure partway through can leave the lower PRs already merged while the failed PR and higher PRs remain open. Pre-merge checks reduce this risk, but there is no rollback of PRs that already landed.

Merge only part of the stack

You do not have to merge everything.

Suppose the model and API are ready, but the UI needs more work. Merge the search-api PR.

GitHub lands:

user-model → search-api

The search-ui PR stays open.

GitHub then retargets the remaining PR to main and rebases its unique commits onto the updated trunk.

You cannot merge a middle PR while leaving a lower PR unmerged. A PR always brings every unmerged layer below it.

This rule follows the dependencies. The API cannot land without the model it uses.

After a partial merge, update your local stack:

gh stack sync --prune

This syncs the remaining branches and removes local branches for merged PRs.

Once the entire stack is merged, it is complete. You cannot extend that finished Stack object. If you add new branches locally and submit them, the CLI starts a new stack from the trunk.

How squash merges avoid fake conflicts

Squash merging normally creates a problem for dependent branches.

Imagine the bottom PR contains commits A and B. The next branch contains A, B, C, and D.

After a squash merge, main contains a new commit S1 instead of A and B:

before: main ← A ← B ← C ← D
after:  main ← S1

A naive rebase might try to replay A and B even though their changes are already inside S1. This can produce conflicts that are not real code conflicts. They exist because commit identities changed.

GitHub’s stack rebase finds the unique commits and uses git rebase --onto to replay only C and D on top of S1.

The remaining branch becomes:

main ← S1 ← C' ← D'

This detail is one of the strongest reasons to use stack-aware tooling. The easy part is creating several branches. The hard part is maintaining their meaning after merges rewrite history.

GitHub Actions and the CI multiplication problem

GitHub evaluates every PR as if it targeted the stack’s final base, usually main.

A workflow configured for pull requests into main runs for every PR in the stack. Required checks and branch protection apply to every layer too.

This closes a common hole in manual stacks, where only the bottom PR triggers the right checks.

It can also multiply CI usage. A stack of 5 PRs might run the same expensive integration suite 5 times.

GitHub exposes stack metadata through github.event.pull_request.stack. We can run an expensive job only for standalone PRs or the top PR, which contains the complete feature:

jobs:
  integration:
    if: >-
      github.event.pull_request.stack == null ||
      github.event.pull_request.stack.position == github.event.pull_request.stack.size
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration

Keep fast checks on every layer. They give reviewers confidence in each focused change.

Reserve the most expensive end-to-end work for the top PR when that tradeoff makes sense for your project.

One subtle detail: GitHub evaluates CODEOWNERS from the stack base. If PR #1 changes CODEOWNERS, that new file does not control reviews for PR #2 yet because the change has not landed on the final base.

Bring existing branches into a stack

You do not have to start with gh stack init.

If you already have dependent branches, list them from bottom to top:

gh stack init user-model search-api search-ui

Then submit them:

gh stack submit

Existing PRs are detected and linked when possible.

If you prefer another local stacking tool, keep using it. GitHub supports standard branches and PRs, so tools such as Jujutsu, Sapling, and git-town can manage the local chain.

Use gh stack link when you only want to create the relationship on GitHub:

gh stack link user-model search-api search-ui

This does not create local gh stack tracking. It pushes branches, creates or finds their PRs, fixes the base branch chain, and links the PRs on GitHub.

Restructure a stack

Sometimes the first plan is wrong.

Maybe the API PR is too large. Maybe two tiny branches should be one. Maybe a layer belongs lower in the dependency chain.

Open the interactive editor:

gh stack modify

The interface can:

The working tree must be clean, and the stack must have a linear history.

After applying the change, update GitHub:

gh stack submit

Restructuring is powerful, but use it early. Reordering layers after detailed reviews have started creates unnecessary work for reviewers.

Use stacks with AI coding agents

GitHub publishes an agent skill for gh-stack:

gh skill install github/gh-stack

You can also install it with the Skills CLI:

npx skills add github/gh-stack

The skill teaches a compatible coding agent how to plan layers, create branches, move through the stack, and rebase after mid-stack changes.

This is a good fit for agent work. An agent can produce a large diff quickly, but a human reviewer still needs understandable boundaries.

The hard part is not asking the agent to “split this into 5 PRs.” The hard part is choosing a dependency order where every layer tells a coherent story.

Review that plan before the agent starts moving commits.

How to design a good stack

A good stack is not one PR per commit.

Create a new layer when the concern, dependency, or reviewer changes.

Good boundaries include:

Keep one effort in one stack. A search UI and an unrelated billing fix should not share a chain.

I aim for every layer to answer one sentence:

This PR adds ___ so the next layer can ___.

If that sentence needs 3 “and” clauses, the layer is probably too broad.

Also avoid very deep stacks. Small PRs help review, but every layer adds CI runs, branches, rebases, and review state. Three clear PRs are usually better than 12 microscopic ones.

When not to use a stack

Use a regular pull request when the change is already small and independent.

Do not create a stack when:

Stacks are most useful for dependent work that is too large for one review but too connected for independent PRs.

The commands I would remember

You do not need to memorize the entire CLI.

These commands cover the normal workflow:

gh stack init user-model   # start the first layer
gh stack add search-api    # add a layer on top
gh stack submit            # push and create/update PRs
gh stack view              # inspect the stack
gh stack up                # move away from trunk
gh stack down              # move toward trunk
gh stack rebase            # cascade changes through the stack
gh stack push              # push every branch
gh stack sync              # fetch, rebase, push, and sync state
gh stack modify            # restructure the stack

The mental model matters more than the commands:

foundations at the bottom
dependencies above them
review from bottom to top
change code in the layer where it belongs
rebase everything above that layer

Once that feels natural, gh stack removes most of the branch bookkeeping.

You can find the latest commands and preview status in the official GitHub Stacked PRs documentation and the github/gh-stack repository.

Tagged: Git · All topics
~~~

Related posts about git: