Back to Blog

AI loves big PRs, your engineers don’t

githubpull-requestcode-reviewaigit

Big PRs were already a code reviewer’s nightmare. AI made the problem nuclear.

Any developer who has worked in the industry for more than a second has experienced the dreaded 1000+ line PR that is sent to them for code review. The explosive cocktail of “Hey, can you review my PR?” and opening it up to find the following can make you shiver and want to go home crying.

A 1000+ line PR already made code review miserable before AI. Reviewers will often skim them instead of read in depth. That risks missing important change amongst many boilerplate ones. Eventually these PRs get approved because nobody wants to be the reason the release slips another day.

With AI, these big PRs are being generated multiple times a day. The only way code reviewers can keep up with that volume is to use AI themselves to review the code. AI writing code, AI reviewing code. At that point, what’s the point of code review at all. Maybe we’ll get there where all code is written and reviewed without humans in-the-loop, but we’re not there yet.

If you still want a human-involved review process, there is hope.

The obvious fix has a catch

The fix everyone reaches for is smaller PRs: split the migration, the API, and the frontend work into three branches instead of one. That’s good advice. But there’s a catch: these branches depend on each other, and Git doesn’t manage that relationship for you.

The snippets below show the relationships setup correcly amongst the branches that build on top of one another. The developer checks out each one individually, works on them from the bottom (db-migration) to the top (frontend).

main
 └─ feature/db-migration
     └─ feature/api-routes
         └─ feature/frontend
git checkout -b feature/db-migration main
# ...commit migration work...
git push -u origin feature/db-migration

...

git checkout -b feature/api-routes feature/db-migration
# ...commit API work...
git push -u origin feature/api-routes

...

git checkout -b feature/frontend feature/api-routes
# ...commit frontend work...
git push -u origin feature/frontend

They post the first PR for db-migration when ready and continue work on the other branchs in parallel. Then the db-migration PR receives review feedback and they need to update it. Uh oh! Now each of the feature branches built on top of it needs to be rebased. That pain needs to be repeated each time a branch lower in the dependency tree gets an update.

main
 └─ feature/db-migration   (updated)
     └─ feature/api-routes  (stale — needs rebase)
         └─ feature/frontend (stale — needs rebase)
git checkout feature/db-migration
# address feedback, commit
git push

# feature/api-routes is now stale
git checkout feature/api-routes
git rebase feature/db-migration
# resolve any conflicts the migration fix introduced
git push --force-with-lease
# and now feature/frontend is stale too
git checkout feature/frontend
git rebase feature/api-routes
git push --force-with-lease

That’s two rebases and two force-pushes for one round of feedback on the bottom branch. A second round, or a fourth branch in the stack, and the synchronization work costs more time than the review saved. This is usually where people quit and go back to one big PR.

Stacked PRs provide some relief

GitHub recently launched Stacked PRs (along with a CLI called gh-stack). It’s in private preview so you still need to sign-up for the waitlist, but there are exciting things on the way.

gh extension install github/gh-stack

gh stack init feature/db-migration
# ...commit migration work...

gh stack add feature/api-routes
# ...commit API work...

gh stack add feature/frontend
# ...commit frontend work...

gh stack push # push all branches to the remote at once
gh stack submit # open PRs for all of them on GH and link them
main
 └─ feature/db-migration    PR #1 → main
     └─ feature/api-routes   PR #2 → feature/db-migration
         └─ feature/frontend  PR #3 → feature/api-routes

gh stack submit opens all three PRs at once, each one targeting the branch below it. When the migration PR gets feedback, the fix is one command instead of two rebases:

git checkout feature/db-migration
# address feedback, commit

gh stack push

gh stack push rebases feature/api-routes and feature/frontend on top of the updated migration branch and pushes the whole chain. You never touch rebase yourself.

Rules still apply against main, not the branch below it

The rebasing is just convenience. The reason to prefer this over a hand-rolled chain is how merge rules get evaluated. The gh stack docs state that branch protection rules are enforced against the final target branch (not just the direct base), and CI runs for every PR in the stack as if they were targeting the final branch.

A hand-rolled stack doesn’t give you that. feature/frontend can merge as long as it satisfies whatever state feature/api-routes happens to be in, and if feature/api-routes hasn't passed its own required checks yet, you can merge code that was only ever compared against a branch that isn't done. gh stack holds every branch in the chain to the same bar as main: same required status checks, same required reviews, no matter where the branch sits in the stack.

That matters more now that an agent can generate the entire stack in one sitting. Every layer still gets checked against your real rules instead of a looser check against a branch still in flight.

Takeaway

Splitting big PRs into smaller ones has always been good advice. People stop doing it because of 1) synchronization cost 2) cognitive overload 3) laziness or 4) they love to watch the world burn :). gh stack reduces that cost and keeps every branch honest against main's actual rules. If you're not in the preview yet, I encourage you to check it out! Your fellow engineers will thank you.

Want help scaling your ops?

I help engineering teams build operational excellence, from observability to incident response.

Book a Discovery Call