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?
- The Problem
- Prerequisites
- Setting Up Git Absorb
- Basic Workflow
- Handling Review Changes
- Advanced Scenarios
- Best Practices
- Troubleshooting
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 mistakesThis is tedious, error-prone, and time-consuming.
Prerequisites
Install Git Absorb
macOS:
brew install git-absorbLinux:
cargo install git-absorbFrom 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 --versionSetting Up Git Absorb
Configure Git (Optional but Recommended)
# Make git-absorb more aggressive in matching changes
git config --global absorb.maxStack 50
# Enable auto-staging after absorb
git config --global absorb.autoStageIfNothingStaged trueUnderstanding 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!
Method 1: Using Git Absorb (Recommended)
# 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 topWhat 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-leaseMethod 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 -10Advanced 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-leaseScenario 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-rebaseScenario 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 branchesScenario 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.shBest 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-23. 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, #1254. 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 --force5. 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 test6. 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 mainProblem: 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 --abortProblem: 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 --decorateQuick 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 mainGit Absorb Flags
| Flag | Description |
|---|---|
--and-rebase | Automatically rebase after absorbing |
--base <ref> | Specify base commit for absorption range |
--dry-run | Preview what would be absorbed |
--force | Force absorption even with warnings |
-v / --verbose | Show 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 syncSapling
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:
- Always use
--force-with-leasefor safety - git-absorb automates fixup commits - use it liberally
- Document your stack in PR descriptions
- Test each layer independently
- 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