Reviewing large pull requests commit by commit Jump to heading
A two-thousand-line pull request is hard to review as one diff. Renames, mechanical refactoring, new tests and the actual behaviour change are interleaved, so the reviewer either skims or spends an afternoon reconstructing the author’s reasoning. Often the author already did the work in sensible steps — “extract interface”, “move callers”, “change behaviour”, “delete old path” — and the steps are sitting in the branch’s commits, invisible in the combined diff. Reviewing commit by commit uses that structure: each commit is small, has one purpose and a message explaining it, and mechanical commits can be checked quickly while attention goes to the ones that change behaviour. This page covers asking for a reviewable series, stepping through it on the forge and locally, re-reviewing after force-pushes, and keeping the structure when the change merges, within code review workflow engineering.
When to use this approach Jump to heading
- A pull request is too large to review as one diff but cannot sensibly be split into several pull requests.
- The change mixes mechanical edits (renames, moves, formatting) with behaviour changes.
- Reviewers keep missing the important lines inside large refactorings.
- Splitting into separate pull requests is preferred where possible; see splitting a branch into reviewable pull requests.
Step 1 — Ask for a reviewable commit series Jump to heading
A series is reviewable when each commit does one thing, builds and passes tests, and has a message saying what and why. Mechanical changes go in their own commits, separate from behaviour changes. Put the expectation in the pull request template.
<!-- .github/pull_request_template.md (excerpt) -->
### For large changes
Please structure commits so they can be reviewed one at a time:
- one purpose per commit; mechanical changes (renames, moves, formatting) separate
- each commit builds and passes tests
- say in the description which commits need the closest review Step 2 — Step through commits on the forge Jump to heading
Forges let you view one commit at a time inside the pull request. Use it, and comment on the commit’s diff so feedback is attached to the step it concerns.
gh pr view 1234 --json commits --jq '.commits[] | "\(.oid[0:7]) \(.messageHeadline)"'
# Open the "Commits" tab, then use "next commit" to walk the series Review the description first to learn which commits matter, then walk the series in order. Mechanical commits get a quick check that they really are mechanical; behaviour commits get full attention.
Step 3 — Step through commits locally Jump to heading
For a long series, local review is faster: your editor, your tools, and the ability to build and test each step.
gh pr checkout 1234
base=$(git merge-base origin/main HEAD)
git log --reverse --format='%h %s' "$base..HEAD"
for c in $(git rev-list --reverse "$base..HEAD"); do
git show --stat --format='%n=== %h %s%n%n%b' "$c"
printf 'show full diff? [y/N] '; read -r a </dev/tty
[ "$a" = y ] && git show --format= "$c"
done Step 4 — Verify mechanical commits mechanically Jump to heading
A commit claiming to be a pure rename or formatting change can be checked rather than read. Rename detection, word diffs and ignoring whitespace show whether anything else changed.
git show -M --stat abc1234 # renames shown as R100 when content is identical
git show -w --format= def5678 | head # formatting-only commit: empty with -w
git show --color-words --format= 9876fed # word-level view of a rename refactoring If a mechanical commit shows changes under these views, ask the author to move them into a separate commit.
Step 5 — Re-review after a force-push Jump to heading
Authors often address feedback by amending individual commits and force-pushing. Re-reading the whole series wastes the first review. Compare the old and new series with range-diff to see exactly what changed in each commit.
git range-diff old-base..old-tip new-base..new-tip
# Or with the forge's before/after commit IDs from the force-push event:
git range-diff 1a2b3c4...9f8e7d6 The technique is covered in reviewing a force-push with git range-diff.
Step 6 — Keep the series when merging Jump to heading
Squash merging collapses a carefully structured series into one commit, losing the structure that made review possible — and that later helps git bisect and git blame. For pull requests reviewed commit by commit, merge with a merge commit or rebase merge.
gh pr merge 1234 --rebase # keeps each commit, linear history
gh pr merge 1234 --merge # keeps each commit under a merge commit If the repository squash-merges by default, allow other methods and choose per pull request. The trade-offs are in merge vs rebase decision matrix.
Step 7 — Check each commit builds Jump to heading
If commits will survive into main, each should build, or git bisect will stumble on broken steps. Run a quick check over the series in CI.
base=$(git merge-base origin/main HEAD)
git rebase --exec 'make build' "$base" # stops at the first commit that fails to build Running the full suite per commit is expensive; a build or fast unit run catches most broken steps.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Isn’t a reviewable series a lot of extra work for authors? Jump to heading
Less than it looks. Interactive rebase lets authors reorder, split and reword after the fact, and the structure usually helps them think through the change. It is worth it for large changes, not for small ones.
What if a later commit undoes part of an earlier one? Jump to heading
That is noise for reviewers. Ask the author to fold the fix into the earlier commit with a fixup and autosquash, so each commit shows its final form.
Should every pull request be reviewed this way? Jump to heading
No. Small pull requests are reviewed as one diff. Commit-by-commit review is for changes that cannot be split into separate pull requests but are too large to read at once.
Related Jump to heading
- Code Review Workflow Engineering — the parent topic.
- Stacked Pull Requests Without a Dedicated Tool — splitting into separate pull requests instead.
- Keeping Generated Files Out of Review Diffs — less noise in every commit.
- Bisecting Across Merges with First-Parent — how preserved series help bisect.