DEV Community

Aniket Bhattacharyea
Aniket Bhattacharyea

Posted on

How to Stack Pull Requests on GitHub (Without Losing Your Mind)

You just finished building your feature. You open a pull request. Then you wait. And wait. And wait some more.

Meanwhile, the next part of your project is ready to build, but it depends on the code you just submitted. Do you start working anyway and risk a rebasing nightmare? Or do you context switch to something completely different and lose your flow?

This is the "waiting game" that kills productivity. You've probably tried both approaches. Starting the next feature while the first is still under review leads to merge conflicts when changes come back. Context switching to unrelated work destroys your momentum and makes you lose the mental model you built up.

The typical escape hatch is cramming everything into one massive 1,000-line PR. Ship it all at once, avoid branch dependencies entirely. But reviewers hate this. Big PRs take forever to review. Bugs hide in the noise. And if something needs to change, you're back to square one.

There's a better way. Stacked pull requests let you break work into small, reviewable chunks without the Git gymnastics. And the Graphite CLI (gt) makes it stupid simple.

This isn't theory. Companies like Airbnb and Meta have used stacking for years. Now it's available for the rest of us, without building custom tooling.

Why stacking matters

When you stack PRs, you create a chain: PR 1 builds the foundation, PR 2 builds on PR 1, and PR 3 builds on PR 2. Each PR is small and focused. Reviewers can approve them independently. You keep coding without getting blocked.

Think of it like chapters in a book. Each chapter makes sense on its own, but they build toward a larger story. Your reviewer reads chapter 1 (the API), approves it, then moves to chapter 2 (the UI). They're not overwhelmed by 50 pages at once.

The benefits compound:

  • Faster reviews: A 200-line PR gets reviewed in minutes. A 2,000-line PR sits for days.
  • Better feedback: Reviewers spot issues when the context is fresh and focused.
  • Easier debugging: When something breaks, you know exactly which small change caused it.
  • No blocking: You keep building while waiting for reviews.

The problem? Managing this in vanilla Git is painful. Rebasing changes down the stack, keeping base branches in sync, and avoiding merge conflicts requires constant mental overhead. You spend more time managing branches than writing code.

Graphite automates all of it. The entire workflow becomes three commands: create, submit, restack.

Setup in 60 seconds

Install the Graphite CLI using either Homebrew or npm:

brew installation

brew install withgraphite/tap/graphite
gt --version
Enter fullscreen mode Exit fullscreen mode

npm installation

npm install -g @withgraphite/graphite-cli@stable
gt --version
Enter fullscreen mode Exit fullscreen mode

Sign in to https://app.graphite.com/activate with your GitHub account. You might be prompted to complete the setup if you haven’t done so already. There, you can create a token, and Graphite will provide you with an authentication command to run:

gt auth --token <your_cli_auth_token>
Enter fullscreen mode Exit fullscreen mode

Run this command, which will log you in.

Initialize Graphite in your repo (make sure this repo exists in GitHub, and you chose it for syncing during Graphite setup):

# Creates local config, doesn't modify your actual repo
gt init
Enter fullscreen mode Exit fullscreen mode

That's it. Graphite works alongside Git. You can still use git status, git add, and other standard Git commands. The gt commands just handle the branching logic for you.

Building the first layer: backend API

Let's build a simple todo app. Start with the API endpoints.

Make sure you’re on the main branch:

gt checkout main
Enter fullscreen mode Exit fullscreen mode

Now write your code. Here's a minimal Express API:

// server.js
const express = require('express');
const app = express();

// In-memory storage for todos
let todos = [];

// GET endpoint to fetch all todos
app.get('/api/todos', (req, res) => {
  res.json(todos);
});

// POST endpoint to create a new todo
app.post('/api/todos', express.json(), (req, res) => {
  const todo = { id: Date.now(), text: req.body.text };
  todos.push(todo);
  res.status(201).json(todo);
});

app.listen(3000, () => console.log('Server running on port 3000'));
Enter fullscreen mode Exit fullscreen mode

Now run the following command, which will create a new branch named feat/api and add a commit:

gt create feat/api --all \
      --message "Add API endpoints"
Enter fullscreen mode Exit fullscreen mode

The API is done, but not merged yet. Time to stack the frontend on top.

Stacking the frontend: the magic part

Here's where it gets interesting. The API isn't approved or merged, but you're ready to build the UI:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Todo App</title>
</head>
<body>
  <h1>My Todos</h1>
  <input id="todo-input" type="text" placeholder="Add a todo" />
  <button onclick="addTodo()">Add</button>
  <ul id="todo-list"></ul>

  <script>
    // Fetch and display all todos on page load
    async function loadTodos() {
      const response = await fetch('/api/todos');
      const todos = await response.json();
      const list = document.getElementById('todo-list');
      list.innerHTML = todos.map(t => `<li>${t.text}</li>`).join('');
    }

    // Add a new todo via POST request
    async function addTodo() {
      const input = document.getElementById('todo-input');
      await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: input.value })
      });
      input.value = '';
      loadTodos(); // Refresh the list
    }

    loadTodos(); // Initialize on page load
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Run this command, which creates a feat/ui branch:

gt create feat/ui --all \
      --message "Add UI"
Enter fullscreen mode Exit fullscreen mode

Graphite automatically sets feat/api as the parent. You're now working on code that depends on unmerged changes.

You now have two branches, stacked on top of each other, both ready to review. You can check the structure with:

$ gt log short

◉  feat/ui
◯  feat/api
◯  main
Enter fullscreen mode Exit fullscreen mode

Syncing to GitHub: one command

Push everything to GitHub and create PRs:

gt submit --stack
Enter fullscreen mode Exit fullscreen mode

Graphite does three things automatically:

  1. Detects both branches in your stack
  2. Creates two separate PRs on GitHub
  3. Sets the base branch correctly so diffs show only the new changes

On running the command, you’ll be taken to a webpage, where you can add a description for the PRs, and publish them.

On GitHub, the PR for the UI is based on the feat/api branch, not main. This means reviewers see only the UI changes, not the API code.

Handling review feedback: the restack

Your reviewer asks you to rename a variable in the API. In a normal Git workflow, this breaks everything. You'd need to manually rebase the UI branch and pray you don't hit conflicts.

With Graphite, it's automatic.

Check out the API branch:

gt checkout feat/api
Enter fullscreen mode Exit fullscreen mode

Make the change:

// Update variable name in server.js
let todoList = []; // was 'todos'
Enter fullscreen mode Exit fullscreen mode

Commit the changes:

gt modify -a
Enter fullscreen mode Exit fullscreen mode

This will amend the last commit to add the new changes. If you prefer to create a separate commit, you can use this command instead:

gt modify --commit \
    --all \
    --message "Responded to reviewer feedback"
Enter fullscreen mode Exit fullscreen mode

Graphite detects the change and automatically restacks (rebases) feat/ui on top of the updated feat/api code. No conflict resolution needed. No manual rebasing. It just works.

Check your stack again:

gt log short
Enter fullscreen mode Exit fullscreen mode

Both branches are in sync. The UI branch has the latest API changes.

Resubmit the PR:

gt submit --stack --update-only
Enter fullscreen mode Exit fullscreen mode

Pro tips for power users

Visualize your stack. Use gt log short anytime to see where you are:

gt log short
Enter fullscreen mode Exit fullscreen mode

This shows your branch hierarchy at a glance. It's like git log, but focused on your stack structure instead of commit history.

Sync frequently. Start each work session with:

gt sync
Enter fullscreen mode Exit fullscreen mode

This pulls the latest changes from main, detects any merged PRs, and prompts you to delete local branches that are no longer needed. It keeps your stack clean.

You're not locked in. Merge PRs directly on GitHub if you want. Graphite figures it out. You can mix and match workflows. Some team members can use Graphite while others stick with vanilla Git.

Your teammates don't need Graphite. They review PRs on GitHub like normal. Graphite only changes your local workflow, not theirs. The PRs look identical to standard GitHub PRs.

Navigate with commands. Use gt up and gt down to move between branches in your stack:

# Move to the parent branch
gt down

# Move to the child branch  
gt up
Enter fullscreen mode Exit fullscreen mode

This is faster than typing out branch names, especially when you're deep in a stack.

Split changes later. If you realize mid-branch that you should split the work, use gt split:

gt split
Enter fullscreen mode Exit fullscreen mode

Graphite will guide you through splitting commits into separate branches. Useful when you realize a "quick fix" turned into a bigger change.

Amend instead of adding commits. Instead of creating multiple commits on one PR, amend your work:

gt modify -a
Enter fullscreen mode Exit fullscreen mode

This keeps each branch as a single, clean commit. When you need to address review feedback, amend again. The history stays linear and readable.

Why this matters

Stacking lets you code at the speed of thought, not the speed of CI. Small PRs get reviewed faster. Fewer bugs slip through. You stay in flow instead of waiting around.

GitHub's review process is powerful, but it wasn't built for this workflow. Graphite fills the gap. It's like adding power steering to your development process.

Resources

Have you tried stacking before? Drop a comment and let me know if you prefer big PRs or small stacks.

Top comments (0)