Code Review Workflow Engineering Jump to heading
Every team that complains about slow reviews has tried asking people to review faster, and it has never worked for longer than a fortnight. The reason is that most review delay is structural: changes arrive too large to read in one sitting, dependent work is blocked behind a queue of its own making, and half the diff is generated output nobody can meaningfully approve. Those are properties of how the repository is organised, and they respond to engineering in a way that exhortation does not. This part of Git Workflow Architecture & Branching Strategies covers the Git-level techniques that make review fast.
Prerequisites Jump to heading
Where the Time Actually Goes Jump to heading
Before changing anything, measure. The intuition that reviewers are slow is usually wrong; the time is typically spent waiting for a reviewer to start, and then waiting for the author to respond to the first round of comments.
# Hours from opened to first review, and from first review to merge
gh pr list --state merged --limit 100 \
--json number,createdAt,mergedAt,reviews --jq '
.[] | select(.reviews | length > 0) |
{pr: .number,
to_first_review: (((.reviews[0].submittedAt | fromdate) - (.createdAt | fromdate)) / 3600 | floor),
to_merge: (((.mergedAt | fromdate) - (.reviews[0].submittedAt | fromdate)) / 3600 | floor)}' Step 1 β Make Changes Arrive Reviewable Jump to heading
A change that can be read in fifteen minutes gets read today. One that needs an hour gets scheduled, and scheduling is where the twenty-one hours come from. The lever is not discipline but tooling: splitting has to be easy enough to be the default.
# Where are the natural seams in the current branch?
git log --oneline --stat origin/main..HEAD | head -30
# Which files move together, suggesting an independent step?
git log --format='---' --name-only origin/main..HEAD \
| awk '/^---/{if(n)print s;s="";n=0;next}{s=s" "$0;n++}' | sort | uniq -c | sort -rn | head The full procedure is in splitting a branch into reviewable pull requests, and the mechanics of keeping several dependent changes in flight at once are in stacked pull requests without a dedicated tool.
Step 2 β Remove the Noise From the Diff Jump to heading
Generated files inflate every measurement and every reading. Marking them changes what the review interface shows and what any size rule counts.
cat >> .gitattributes <<'ATTR'
*.lock linguist-generated=true
**/generated/** linguist-generated=true -diff
*.pb.go linguist-generated=true -diff
*.snap linguist-generated=true
ATTR # Verification: the attribute is applied where you expect
git check-attr linguist-generated -- package-lock.json src/api/generated/client.ts # And the effect on a typical change
git diff --stat origin/main...HEAD
git diff --stat origin/main...HEAD -- ':!**/generated/**' ':!*.lock' Step 3 β Give the Reviewer the Commits, Not Just the Diff Jump to heading
A branch whose commits each do one thing can be reviewed commit by commit, which is dramatically easier than reading a combined diff. That is a property the author creates, and --fixup plus autosquash is how it survives review feedback.
# Address feedback without adding "address review comments" commits
git commit --fixup "$(git log --format=%H -1 --grep 'add retry')"
git rebase -i --autosquash origin/main # Verification: the history still reads as a sequence of intentions
git log --oneline origin/main..HEAD The workflow is covered in using fixup commits and autosquash during review, and the safety rules for rewriting a branch others may have pulled are in safe git rebase -i for shared branches.
Step 4 β Handle Force-Pushes Without Losing the Review Jump to heading
Rewriting a branch during review is normal and destroys the reviewerβs place unless they have a way to see what changed between versions.
# Before rewriting, record where the branch was
git branch review/v1 HEAD
# After the rewrite, show how the commits themselves changed
git range-diff review/v1...HEAD # Verification: the range-diff should show only the intended difference
git range-diff origin/main review/v1 HEAD | head -30 This single command is the difference between a reviewer re-reading a whole branch and reading three changed lines β the detail is in reviewing a force-push with git range-diff.
Step 5 β Set Expectations Explicitly Jump to heading
Most review latency variance comes from nobody knowing when a review is expected. A written expectation, with a measurement behind it, removes the guessing.
# Changes currently waiting longer than the agreed window
gh pr list --state open --json number,title,createdAt,reviews \
--jq '.[] | select(.reviews | length == 0)
| select(((now - (.createdAt | fromdate)) / 3600) > 24)
| "\((now - (.createdAt | fromdate)) / 3600 | floor)h #\(.number) \(.title)"' Making It Stick Without Turning It Into Process Jump to heading
The failure mode of a review initiative is that it becomes a checklist nobody believes in. Three things keep these techniques alive once the initial enthusiasm passes.
The first is that the measurement stays visible. A weekly number β median hours to first review β is enough, and it should be published where the team already looks rather than in a dashboard someone has to remember to open. When the number moves in the wrong direction, the conversation is about what changed rather than about whether anyone is trying, which is a far more productive place to start.
The second is that the tooling carries the effort. Splitting a branch is a technique, and techniques that require twelve commands get used once. Wrapping the common paths in a script, committing it to the repository and mentioning it in the contributing guide converts a good intention into the path of least resistance. The same applies to the generated-file attributes: they belong in the repository, reviewed like code, not in a wiki page describing what people should do.
The third is that expectations are explicit and modest. A team that agrees on βa first look within one working dayβ and mostly hits it has a functioning review culture; one that agrees on two hours and misses it constantly has a number everyone has learned to ignore. Set the expectation from the measurement rather than from ambition, and tighten it when the structural work has actually made it achievable.
What none of this addresses is review depth, and it is worth saying so plainly. Faster review is not better review, and every technique here makes approval cheaper as well as quicker. The counterweight is to watch the proportion of changes approved without a single comment: if that rises while latency falls, the process has been optimised into a rubber stamp. The intended outcome is that reviewers spend the same attention on smaller, clearer changes β not less attention on the same ones.
Configuration Reference Jump to heading
| Setting or command | Effect | When to use |
|---|---|---|
linguist-generated=true | Collapses a file in review | Every generated artefact |
-diff attribute | Treats a file as binary in diffs | Files nobody should read line by line |
git range-diff A...B | Compares two versions of a branch | After any force-push during review |
git commit --fixup <sha> | Marks a change as belonging to an earlier commit | Addressing review feedback |
rebase --autosquash | Folds fixups into their targets | Before the final push |
git log --oneline base..HEAD | Shows the branch as a sequence | Checking readability before opening |
| Draft pull requests | Signals work in progress | Sharing early without requesting review |
Troubleshooting Jump to heading
| Symptom | Likely cause | Fix |
|---|---|---|
| Reviews start a day late | No expectation set, or no routing | Assign owners automatically and publish the target |
| Diffs are dominated by generated output | Files not marked | Add linguist-generated attributes |
| Reviewers ask to see it as separate changes | The branch does several things | Split it; keep the parts stacked |
| Review restarts after every push | No range-diff, so the reviewer re-reads | Record the pre-rewrite tip and use range-diff |
| Dependent work is blocked for days | Everything queued behind one review | Stack the branches and review in parallel |
| Approvals arrive with no comments | Changes too large to read | Reduce size before anything else |
Frequently Asked Questions Jump to heading
Is it worth splitting a change that is already written? Jump to heading
Usually yes when it exceeds what a reviewer can read at once, and the split is cheaper than it looks if the commits are already sensible: cherry-picking a prefix onto a new branch takes a couple of minutes. The cost is one-off; the benefit repeats for every reviewer.
Does stacking make merging more complicated? Jump to heading
It adds one rule β merge from the bottom of the stack β and removes the alternative, which is one enormous change or a week of serialised waiting. Most of the perceived complexity comes from rebasing the stack after the base merges, which is one command per branch.
Should reviewers read commit by commit or the whole diff? Jump to heading
Whichever the author has made feasible. A branch with three well-shaped commits is best read in sequence; a branch with fourteen commits including three βfix typoβ entries is best read as a diff. That asymmetry is the argument for authors doing the tidying.
How do we keep review quality while making it faster? Jump to heading
Watch comment density and the silent-approval rate alongside latency. The structural changes here are designed to make changes easier to understand, not easier to wave through β if approvals get faster and quieter at the same time, something has gone wrong and the numbers will say so.
What about pair programming β does it replace review? Jump to heading
It replaces the reading, not the record. Work produced in a pair has already had a second set of eyes on every line, which is the expensive part of review, so a lightweight approval afterwards is usually proportionate. What it does not produce is the artefact: a pull request with a description, a diff and an approval is what an auditor, a future bisect, or a colleague three years from now will find. Keep the record even when the review itself was live, and say in the description that the change was paired so nobody wonders why the approval arrived in four minutes.
A second caveat is rotation. Pairing concentrates knowledge in two people rather than spreading it across the team the way review does, so teams that pair heavily usually still route a proportion of changes to someone outside the pair β not to catch defects, but to keep more than two people familiar with each area. That is a staffing decision rather than a Git one, but it shows up in the same measurements: a repository where the same two names appear on every change in an area has a bus-factor problem that review latency figures will not reveal.
Related Jump to heading
- Stacked Pull Requests Without a Dedicated Tool β keeping dependent work moving in parallel.
- Splitting a Branch Into Reviewable Pull Requests β turning one large branch into a readable sequence.
- Reviewing a Force-Push With git range-diff β resuming a review after a rewrite.
- Keeping Generated Files Out of Review Diffs β making the diff show only what a person decides.
- Measuring Review Latency From Git History β the numbers that tell you whether any of this worked.