Overview

Stacked PRs (also called “stacked diffs”) allow you to split large features into smaller, reviewable chunks while maintaining dependencies between them. This guide covers how to efficiently manage stacked PRs, especially when changes are requested on earlier PRs in the stack.

Table of Contents


What are Stacked PRs?

Stacked PRs are a series of pull requests where each PR builds on top of the previous one:

main ← PR1 (branch-1) ← PR2 (branch-2) ← PR3 (branch-3)

Example scenario:

  • PR1: Refactor authentication service
  • PR2: Add new OAuth provider (depends on PR1)
  • PR3: Add UI for new OAuth provider (depends on PR2)

The Problem

When a reviewer requests changes on PR1, you face a cascade of updates:

# Without proper tools, you'd need to:
1. Make changes on branch-1
2. Manually cherry-pick or rebase branch-2 onto updated branch-1
3. Manually cherry-pick or rebase branch-3 onto updated branch-2
4. Force push all branches
5. Hope you didn't introduce conflicts or mistakes

This is tedious, error-prone, and time-consuming.


Prerequisites

Install Git Absorb

macOS:

brew install git-absorb

Linux:

cargo install git-absorb

From source:

git clone https://github.com/tummychow/git-absorb.git
cd git-absorb
cargo build --release
cp target/release/git-absorb /usr/local/bin/

Verify Installation

git absorb --version

Setting Up Git Absorb

# Make git-absorb more aggressive in matching changes
git config --global absorb.maxStack 50
 
# Enable auto-staging after absorb
git config --global absorb.autoStageIfNothingStaged true

Understanding Git Absorb

Git absorb automatically distributes your uncommitted changes to the correct commits in your history by matching:

  • File names
  • Line proximity
  • Context similarity

It’s like git commit --fixup but fully automated.


Basic Workflow

1. Create Your Stacked PRs

# Start from main
git checkout main
git pull origin main
 
# Create first branch/PR
git checkout -b feature/auth-refactor
# ... make changes ...
git add .
git commit -m "refactor: modernize auth service"
git push origin feature/auth-refactor
# Create PR1 on GitHub
 
# Create second branch based on first
git checkout -b feature/add-oauth
# ... make changes ...
git add .
git commit -m "feat: add Google OAuth provider"
git push origin feature/add-oauth
# Create PR2 on GitHub (base: feature/auth-refactor)
 
# Create third branch based on second
git checkout -b feature/oauth-ui
# ... make changes ...
git add .
git commit -m "feat: add OAuth UI components"
git push origin feature/oauth-ui
# Create PR3 on GitHub (base: feature/add-oauth)

Key Point: When creating PR2, set the base branch to feature/auth-refactor instead of main. Same for PR3 → feature/add-oauth.

2. Track Your Stack

Keep a note of your stack structure:

main
└── feature/auth-refactor (PR1)
    └── feature/add-oauth (PR2)
        └── feature/oauth-ui (PR3)

Handling Review Changes

Scenario: Reviewer Requests Changes on PR1

This is where git-absorb shines!

# 1. Checkout the base branch (PR1)
git checkout feature/auth-refactor
 
# 2. Make your review changes directly to the files
# Don't commit yet! Just edit the files.
vim src/auth/service.py  # make requested changes
 
# 3. Stage the changes
git add src/auth/service.py
 
# 4. Run git absorb
git absorb --and-rebase
 
# 🎉 Git absorb automatically:
#   - Finds the right commit to apply changes to
#   - Updates the commit
#   - Rebases everything on top

What just happened?

  • Git absorb looked at your staged changes
  • Matched them to the appropriate commit in feature/auth-refactor
  • Automatically amended that commit
  • Rebased all subsequent commits cleanly

Method 2: Absorb with Force Push to All Branches

# After git absorb on feature/auth-refactor
git push origin feature/auth-refactor --force-with-lease
 
# Now update downstream branches
git checkout feature/add-oauth
git rebase feature/auth-refactor
git push origin feature/add-oauth --force-with-lease
 
git checkout feature/oauth-ui
git rebase feature/add-oauth
git push origin feature/oauth-ui --force-with-lease

Method 3: Absorb Across Multiple Commits

If you need to fix multiple commits:

git checkout feature/auth-refactor
 
# Make all your changes
vim src/auth/service.py
vim src/auth/middleware.py
vim tests/auth_test.py
 
# Stage everything
git add .
 
# Absorb will distribute changes to the right commits
git absorb --and-rebase --base main
 
# Verify the changes
git log --oneline -10

Advanced Scenarios

Scenario A: Changes Needed on Middle PR (PR2)

# 1. Checkout PR2's branch
git checkout feature/add-oauth
 
# 2. Make changes
vim src/oauth/google.py
 
# 3. Stage and absorb
git add .
git absorb --and-rebase --base feature/auth-refactor
 
# 4. Update downstream PR3
git checkout feature/oauth-ui
git rebase feature/add-oauth
git push origin feature/oauth-ui --force-with-lease
 
# 5. Push PR2
git checkout feature/add-oauth
git push origin feature/add-oauth --force-with-lease

Scenario B: Interactive Absorb (Preview Changes)

# Use --dry-run to see what absorb will do
git absorb --dry-run
 
# Review and then apply
git absorb --and-rebase

Scenario C: Absorb Fails - Manual Fixup

If git-absorb can’t determine the right commit:

# 1. Create a fixup commit manually
git add .
git commit --fixup=<commit-hash>
 
# 2. Rebase with autosquash
git rebase -i --autosquash main
 
# 3. Update downstream branches
git checkout feature/add-oauth
git rebase feature/auth-refactor
# ... continue for other branches

Scenario D: Full Stack Rebase Script

Create a script to rebase your entire stack:

#!/bin/bash
# save as: rebase-stack.sh
 
set -e
 
echo "Rebasing feature/auth-refactor onto main..."
git checkout feature/auth-refactor
git rebase main
git push origin feature/auth-refactor --force-with-lease
 
echo "Rebasing feature/add-oauth onto feature/auth-refactor..."
git checkout feature/add-oauth
git rebase feature/auth-refactor
git push origin feature/add-oauth --force-with-lease
 
echo "Rebasing feature/oauth-ui onto feature/add-oauth..."
git checkout feature/oauth-ui
git rebase feature/add-oauth
git push origin feature/oauth-ui --force-with-lease
 
echo "✅ Stack rebased successfully!"

Usage:

chmod +x rebase-stack.sh
./rebase-stack.sh

Best Practices

1. Keep Commits Atomic

Each commit should be a single logical change:

# Good
git commit -m "refactor: extract auth validation logic"
git commit -m "feat: add OAuth provider interface"
git commit -m "feat: implement Google OAuth provider"
 
# Bad
git commit -m "WIP auth changes"

2. Use Descriptive Branch Names

# Good
feature/JIRA-123-auth-refactor
feature/JIRA-124-add-oauth
feature/JIRA-125-oauth-ui
 
# Bad
feature/stuff
feature/auth-2

3. Document Your Stack

In each PR description, mention the stack:

## Stack
- PR1: #123 (feature/auth-refactor) ← **This PR**
- PR2: #124 (feature/add-oauth)
- PR3: #125 (feature/oauth-ui)
 
## Dependencies
This PR depends on: None (base PR)
Dependent PRs: #124, #125

4. Use --force-with-lease Instead of --force

# Safer - fails if remote has commits you don't have
git push origin feature/branch --force-with-lease
 
# Dangerous - can overwrite other's work
git push origin feature/branch --force

5. Test Each PR Independently

# Checkout and test PR1
git checkout feature/auth-refactor
npm test
 
# Checkout and test PR2
git checkout feature/add-oauth
npm test
 
# Checkout and test PR3
git checkout feature/oauth-ui
npm test

6. Regular Stack Maintenance

Weekly maintenance script:

#!/bin/bash
# update-stack.sh
 
git checkout main
git pull origin main
 
for branch in feature/auth-refactor feature/add-oauth feature/oauth-ui; do
    echo "Updating $branch..."
    git checkout $branch
    git rebase main
    git push origin $branch --force-with-lease
done
 
echo "✅ Stack updated with latest main"

Troubleshooting

Problem: Git Absorb Can’t Find Matching Commit

Symptoms:

No commits available to absorb into.

Solution:

# Ensure you have commits to absorb into
git log --oneline -5
 
# Specify base explicitly
git absorb --base main
 
# Or create fixup manually
git commit --fixup=<commit-hash>
git rebase -i --autosquash main

Problem: Merge Conflicts During Rebase

Symptoms:

CONFLICT (content): Merge conflict in src/auth/service.py

Solution:

# 1. Resolve conflicts
vim src/auth/service.py  # fix conflicts
 
# 2. Stage resolved files
git add src/auth/service.py
 
# 3. Continue rebase
git rebase --continue
 
# 4. If it's too messy, abort and try different approach
git rebase --abort

Problem: Accidentally Absorbed to Wrong Commit

Solution:

# Find the commit before absorb
git reflog
 
# Reset to before absorb
git reset --hard HEAD@{1}
 
# Try again with more specific base
git absorb --base <specific-commit>

Problem: Lost Track of Stack Order

Solution: Create a stack map file:

# .stack-map
main feature/auth-refactor (PR #123)

  feature/add-oauth (PR #124)

  feature/oauth-ui (PR #125)

Or use git log to visualize:

git log --oneline --graph --all --decorate

Quick Reference

Essential Commands

# Create stacked branch
git checkout -b new-branch base-branch
 
# Absorb changes
git add .
git absorb --and-rebase
 
# Update downstream branch
git checkout downstream-branch
git rebase upstream-branch
git push origin downstream-branch --force-with-lease
 
# Preview absorb
git absorb --dry-run
 
# Absorb with specific base
git absorb --base main

Git Absorb Flags

FlagDescription
--and-rebaseAutomatically rebase after absorbing
--base <ref>Specify base commit for absorption range
--dry-runPreview what would be absorbed
--forceForce absorption even with warnings
-v / --verboseShow detailed output

Alternative Tools

While git-absorb is powerful, you might also consider:

Graphite (CLI)

# Install
npm install -g @withgraphite/graphite-cli
 
# Stack management
gt stack create
gt stack sync

Sapling

Facebook’s source control system with built-in stack support.

Git Tower

GUI tool with stack visualization (commercial).


Example: Complete Workflow

Let’s walk through a real example:

# 1. Start fresh
git checkout main
git pull origin main
 
# 2. Create base PR
git checkout -b refactor/user-model
# ... work on user model refactor ...
git add .
git commit -m "refactor: extract user validation to service"
git push origin refactor/user-model
# Create PR #100 (base: main)
 
# 3. Create dependent PR
git checkout -b feature/add-user-roles
# ... add role system ...
git add .
git commit -m "feat: add role-based access control"
git push origin feature/add-user-roles
# Create PR #101 (base: refactor/user-model)
 
# 4. Create another dependent PR
git checkout -b feature/admin-dashboard
# ... build admin UI ...
git add .
git commit -m "feat: add admin role management UI"
git push origin feature/admin-dashboard
# Create PR #102 (base: feature/add-user-roles)
 
# 5. Review feedback on PR #100 - need to add input sanitization
git checkout refactor/user-model
vim src/services/user-validation.js  # add sanitization
git add .
 
# 6. Absorb and propagate
git absorb --and-rebase --base main
git push origin refactor/user-model --force-with-lease
 
# 7. Update downstream PRs
git checkout feature/add-user-roles
git rebase refactor/user-model
git push origin feature/add-user-roles --force-with-lease
 
git checkout feature/admin-dashboard
git rebase feature/add-user-roles
git push origin feature/admin-dashboard --force-with-lease
 
# ✅ All PRs updated with review changes!

Conclusion

Stacked PRs with git-absorb transform a tedious workflow into an efficient one. Key takeaways:

  1. Always use --force-with-lease for safety
  2. git-absorb automates fixup commits - use it liberally
  3. Document your stack in PR descriptions
  4. Test each layer independently
  5. Maintain atomic commits for easier absorption

The initial setup takes practice, but once mastered, you’ll ship features faster with better code review quality.


Additional Resources


Tags: git workflow pr-management git-absorb stacked-diffs

Linked Map of Contexts