Stop Submitting Giant PRs Nobody Wants to Review
You know that feeling when you open a PR and it's 47 files changed, 3,000 lines? The reviewer sighs, skims it, leaves a comment like "looks good," and merges it. No real feedback. Nothing caught. That's not code review—that's rubber-stamping.
Stacked PRs are a way out of that trap.
The Basic Idea
Instead of one massive PR, you break the work into a chain. Each branch builds on the previous one, and each PR targets its parent branch rather than master directly.
master
└── branch1 (PR #1 → master)
└── branch2 (PR #2 → branch1)
└── branch3 (PR #3 → branch2)
PR #1 is small, focused, and reviewable in ten minutes. PR #2 only shows the diff on top of PR #1—again, small. Reviewers can actually engage with the code.
The tradeoff is that you now have to manage a dependency chain. That's the part people find annoying, and it's where most explanations fall short.
Two Ways to Do This
There's a raw git approach and a tool-assisted approach. Both work. Pick based on how much you want to manage manually.
Pure Git
Setting up the stack is straightforward:
# Start from master
git checkout master && git pull
git checkout -b branch1
# ... do the work ...
git add . && git commit -m "Add feature part 1"
git push -u origin branch1
# Open PR #1 targeting master
git checkout -b branch2 # branches from branch1
# ... more work ...
git push -u origin branch2
# Open PR #2 targeting branch1
git checkout -b branch3 # branches from branch2
# ... more work ...
git push -u origin branch3
# Open PR #3 targeting branch2
The happy path is when PR #1 gets approved and merged with no changes. After squash-merging into master, branch2 and branch3 are now based on a commit that no longer exists in master's history. You need to rebase.
Here's where Git 2.38 added something genuinely useful—the --update-refs flag:
git checkout branch2
git rebase master --update-refs
git push --force
git checkout branch3
git push --force # branch3 was already updated by --update-refs
Without --update-refs, you'd rebase branch2, then manually checkout branch3 and rebase that onto branch2. With it, rebasing branch2 automatically moves branch3's pointer too. You still need to force-push each branch, but you skip the manual rebase steps.
Also don't forget: after the merge, go into the GitHub UI and update PR #2's base branch from branch1 to master. GitHub won't figure that out on its own.
The unhappy path—when review feedback comes in—is simpler than it sounds:
git checkout branch1
# make the requested changes
git add . && git commit -m "Address review feedback"
git push
# Now update the children
git checkout branch2
git rebase branch1 --update-refs
git push --force
git checkout branch3
git push --force
Conflict resolution is normal rebase conflict resolution. Nothing special.
Git Town
Git Town automates the bookkeeping.
brew install git-town
The key difference is that Git Town tracks the parent-child relationships between branches. You don't have to remember that branch3 depends on branch2 which depends on branch1—it knows.
Setup:
git checkout master
git town append branch1
# make changes
git town propose # opens a PR
git checkout branch1
git town append branch2
# make changes
git town propose
git checkout branch2
git town append branch3
# make changes
git town propose
append is like checkout -b but it also registers the parent in Git Town's config.
Happy path with Git Town:
git checkout branch1
git town ship
That's it. ship squash-merges the PR, updates PR #2's base branch in GitHub, rebases branch2 onto master, and cascades updates to branch3. The stuff that takes four manual steps with pure git is one command here.
Unhappy path:
git checkout branch1
# make the changes
git add . && git commit -m "Address review feedback"
git town sync --stack
sync --stack propagates the changes down through all dependent branches.
Conflict resolution works similarly to pure git—fix the conflict, git add ., then git town continue instead of git rebase --continue.
Which Should You Use?
Pure git works fine if your stack is shallow (two or three levels) and you merge frequently enough that you're not constantly juggling rebases. The --update-refs flag covers most of the pain.
Git Town is worth the setup if you work in deeper stacks, ship slower, or just don't want to think about the dependency graph. The ship command alone saves enough time to justify installing it.
Either way, the actual workflow—small focused changes, incremental review, merge when ready—is the same. The tooling is just about how much of the grunt work you want to handle yourself.
Comments
Post a Comment