Code Review Tools and Practices: From GitHub PRs to Stacked Diffs
Code Review Tools and Practices: From GitHub PRs to Stacked Diffs
Code review is the highest-leverage quality practice in software engineering. It catches bugs, spreads knowledge, and improves code quality -- but only when done well. Bad code review is a bottleneck that slows teams to a crawl. This guide covers the tools that make review efficient and the practices that make it effective.
The State of Code Review Tools
| Tool | Type | Best For | Cost |
|---|---|---|---|
| GitHub Pull Requests | Built-in | Most teams already using GitHub | Free with GitHub |
| Graphite | Stacked PRs | Teams that want stacked diffs on GitHub | Free tier + paid |
| Reviewable | Enhanced review | Detailed review tracking | Free for open source |
| GitLab Merge Requests | Built-in | GitLab users | Free with GitLab |
| Gerrit | Change-based | Large open-source projects | Free (OSS) |
| Phabricator (Phorge) | Differential | Companies wanting stacked diffs | Free (OSS, archived) |
| Codestream | In-editor review | Teams wanting to review in IDE | Free |
GitHub Pull Requests: The Baseline
GitHub PRs are the default code review tool for most teams. Out of the box they're decent, but a few configurations and habits make them significantly better.
PR Templates
<!-- .github/pull_request_template.md -->
## Summary
<!-- What does this PR do and why? -->
## Changes
<!-- Key changes, organized by area -->
-
## Testing
<!-- How was this tested? -->
- [ ] Unit tests added/updated
- [ ] Manual testing completed
- [ ] Edge cases considered
## Screenshots
<!-- If applicable, add screenshots or screen recordings -->
## Notes for Reviewers
<!-- Anything the reviewer should pay attention to -->
Branch Protection Rules
# Settings -> Branches -> Branch protection rules for "main"
Required reviews: 1 # (or 2 for critical repos)
Dismiss stale PR reviews: Yes
Require review from code owners: Yes
Require status checks before merging:
- tests
- lint
- build
Require branches to be up to date: Yes
Require linear history: Yes # (forces rebase or squash merging)
CODEOWNERS File
Automatically assign reviewers based on file paths:
# .github/CODEOWNERS
# Default owners for everything
* @engineering-team
# Frontend
/src/components/ @frontend-team
/src/pages/ @frontend-team
*.css @frontend-team
*.tsx @frontend-team
# Backend
/src/api/ @backend-team
/src/services/ @backend-team
# Infrastructure
/terraform/ @platform-team
/docker/ @platform-team
/.github/ @platform-team
# Database migrations require DBA review
/prisma/migrations/ @dba-team @backend-team
# Security-sensitive files
/src/auth/ @security-team
/src/middleware/auth* @security-team
GitHub CLI for PR Workflows
# Create a PR
gh pr create --title "Add user authentication" --body "Implements JWT-based auth"
# View PR details
gh pr view 42
# Check out a PR locally
gh pr checkout 42
# Review a PR
gh pr review 42 --approve
gh pr review 42 --request-changes --body "Need to handle the null case in line 45"
gh pr review 42 --comment --body "Looks good, minor nit on naming"
# Merge a PR
gh pr merge 42 --squash --delete-branch
# List PRs awaiting your review
gh pr list --search "review-requested:@me"
# List your open PRs
gh pr list --author @me
Useful GitHub PR Search Queries
# PRs waiting for your review
review-requested:@me is:open
# PRs you need to follow up on
involves:@me is:open review:changes_requested
# Old PRs that might be stale
is:open sort:updated-asc created:<2026-01-01
# PRs that touch database migrations
is:open path:prisma/migrations
# Large PRs that need extra attention
is:open size:>500
Stacked PRs: The Better Workflow
The biggest problem with GitHub PRs is that they encourage large, monolithic changes. You build a feature, submit one big PR, wait for review, and nothing can proceed in parallel. Stacked PRs solve this by breaking work into small, dependent PRs that can be reviewed independently.
The Problem with Large PRs
# A single PR with 40 files changed and 800 lines:
- Reviewers skim (or rubber-stamp)
- Feedback is delayed because review takes an hour
- Merge conflicts accumulate
- Bug probability increases with PR size
# The same work as 4 stacked PRs of 200 lines each:
- Each PR is focused and reviewable in 15 minutes
- Earlier PRs can be approved while later ones are still in progress
- Conflicts are smaller and easier to resolve
- Bugs are caught at a finer granularity
Graphite: Stacked PRs for GitHub
Graphite is the most polished tool for stacked PRs on GitHub. It manages the branch dependencies, rebases automatically when parent PRs are updated, and provides a dashboard that makes the stack visible.
# Install Graphite CLI
npm install -g @withgraphite/graphite-cli
gt auth # Authenticate with GitHub
# Start a stack from main
gt checkout main
# Create first PR in the stack
gt branch create add-user-model
# ... make changes ...
gt commit create -m "Add User model and migration"
gt stack submit # Creates PR for this branch
# Build on top
gt branch create add-user-api
# ... make changes ...
gt commit create -m "Add user CRUD API endpoints"
gt stack submit # Creates PR, marks dependency on previous PR
# Build on top again
gt branch create add-user-ui
# ... make changes ...
gt commit create -m "Add user management UI"
gt stack submit # Creates PR, marks dependency
# View the stack
gt log
# main
# ├── add-user-model (PR #42 - approved)
# │ ├── add-user-api (PR #43 - in review)
# │ │ └── add-user-ui (PR #44 - in review)
# When the first PR is approved, merge from the bottom
gt stack submit --merge # Merges approved PRs from the bottom up
# After parent PR is merged, rebase children
gt stack restack
Graphite Dashboard
Graphite provides a web dashboard that shows:
- All your stacks and their review status
- Which PRs are blocking others
- Merge queue status
- Team-level review metrics (time to first review, time to merge)
Manual Stacked PRs (Without Graphite)
If you don't want to use Graphite, you can manage stacked PRs manually:
# Create the first branch
git checkout -b feature/step-1 main
# ... make changes, commit ...
git push -u origin feature/step-1
gh pr create --base main --title "Step 1: Add data model"
# Create the second branch ON TOP of the first
git checkout -b feature/step-2 feature/step-1
# ... make changes, commit ...
git push -u origin feature/step-2
gh pr create --base feature/step-1 --title "Step 2: Add API endpoints"
# Create the third branch ON TOP of the second
git checkout -b feature/step-3 feature/step-2
# ... make changes, commit ...
git push -u origin feature/step-3
gh pr create --base feature/step-2 --title "Step 3: Add UI"
The manual approach works but requires careful rebasing when parent PRs change. Graphite automates this entirely.
When Stacked PRs Make Sense
- Feature development: Break a feature into data model, API, UI layers
- Refactoring: Extract, then rename, then clean up -- each as a separate PR
- Dependencies: PR 1 adds a utility, PR 2 uses it in several places
- Reviews are slow: Smaller PRs get reviewed faster, unblocking subsequent work
When Stacked PRs Don't Make Sense
- Small changes: A 50-line fix doesn't need to be stacked
- Solo projects: No reviewer to unblock
- Tightly coupled changes: Sometimes 400 lines really are one atomic change
AI-Assisted Code Review
AI tools can catch common issues before human reviewers see them, saving reviewer time for higher-level feedback.
GitHub Copilot Code Review
# GitHub Copilot can review PRs automatically
# Enable in repository settings -> Copilot -> Code review
# It comments on PRs with suggestions for:
# - Bug risks
# - Performance issues
# - Security vulnerabilities
# - Code style improvements
AI Review Tools Comparison
| Tool | What It Does | Integration | Cost |
|---|---|---|---|
| GitHub Copilot Review | AI comments on PRs | GitHub native | Part of Copilot subscription |
| CodeRabbit | Detailed AI review comments | GitHub/GitLab | Free for open source |
| Sourcery | Refactoring suggestions | GitHub/GitLab | Free tier available |
| Codium PR-Agent | AI review bot | GitHub/GitLab/Bitbucket | Free (OSS) |
What AI Review Is Good At
- Spotting null/undefined handling issues
- Identifying missing error handling
- Flagging potential race conditions
- Suggesting more idiomatic code
- Catching typos in strings and comments
- Identifying security anti-patterns (SQL injection, XSS)
What AI Review Is Bad At
- Understanding business logic correctness
- Evaluating architectural decisions
- Assessing user experience implications
- Judging whether an abstraction is appropriate
- Understanding team conventions not captured in linting rules
Use AI review as a first pass to catch low-hanging fruit, then have humans focus on design, architecture, and business logic.
Code Review Best Practices
For Authors
1. Keep PRs small. Under 400 lines of non-test code. If it's bigger, split it.
2. Write a good PR description. Explain why, not just what. Reviewers can see the diff -- they need context.
# BAD PR description
"Update user service"
# GOOD PR description
## Why
Users are seeing intermittent 500 errors when updating their profile.
The root cause is a race condition in the cache invalidation logic --
when two updates happen within the cache TTL, the second update reads
stale data and produces an invalid state.
## What
- Add optimistic locking to the user update path
- Invalidate cache synchronously instead of async
- Add a regression test that reproduces the race condition
## Testing
- Added test that runs 100 concurrent updates and verifies consistency
- Tested manually by rapidly clicking "Save" on the profile page
3. Self-review before requesting review. Go through the diff yourself. You'll catch obvious issues.
4. Respond to every comment. Even if it's just "Done" or "Good point, fixed." Don't leave reviewers wondering if you saw their feedback.
5. Annotate complex changes. Use PR comments to explain tricky sections:
// In the PR, add a comment on a complex section:
"This looks weird but it's necessary because the payment gateway
returns amounts in cents for USD but in base units for JPY (no cents).
See their docs: https://docs.stripe.com/currencies#zero-decimal"
For Reviewers
1. Review within 24 hours. Code review is the most common team bottleneck. Prioritize it.
2. Distinguish blocking from non-blocking feedback:
# Blocking (must fix before merge)
"Bug: This will throw a NullPointerException when `user.address` is null,
which happens for new users who haven't set an address yet."
# Non-blocking (suggestion, author decides)
"Nit: Consider renaming `data` to `userProfile` for clarity.
Not blocking."
# Question (need more context)
"Question: Why do we need this cache here? The database query
takes <5ms according to our metrics."
3. Review the design, not just the code. Ask:
- Is this the right approach to the problem?
- Does this align with existing patterns in the codebase?
- Are there edge cases not covered?
- Will this be easy to modify in six months?
4. Don't bikeshed. If two approaches are roughly equivalent, approve and move on. Save your review energy for things that matter.
5. Praise good code. "This is a clean abstraction" or "Nice test coverage" takes 5 seconds and makes authors want to keep writing good code.
Team-Level Practices
Rotate reviewers. Don't let the same person review everything. Spread knowledge across the team.
Set SLOs for review time. "All PRs get a first review within 4 business hours" is a reasonable target. Track it.
Review metrics that matter:
| Metric | Target | Why |
|---|---|---|
| Time to first review | < 4 hours | Unblock authors quickly |
| Time to merge | < 24 hours | Prevent stale PRs |
| PR size | < 400 lines | Keep reviews meaningful |
| Review rounds | < 3 | Indicates clear communication |
| Reviewer coverage | All team members | Spread knowledge |
Advanced: Merge Queues
Merge queues solve the "but it passed CI on my branch" problem. When multiple PRs merge around the same time, the combined result might break even if each PR passes individually.
GitHub Merge Queue
# Enable in repository settings -> Branch protection -> Merge queue
# Configuration options:
# - Required checks: Which CI checks must pass
# - Group size: How many PRs to batch together
# - Maximum wait time: How long to wait before running the queue
When enabled, clicking "Merge" adds the PR to the queue instead of merging immediately. The queue:
- Rebases the PR onto the latest main
- Runs CI on the rebased code
- If CI passes, merges
- If CI fails, removes the PR from the queue and notifies the author
Graphite Merge Queue
Graphite's merge queue is particularly good for stacked PRs -- it merges the entire stack atomically and handles the rebasing.
Review Checklist Templates
Create checklists for different types of changes:
<!-- .github/REVIEW_CHECKLIST.md -->
### General
- [ ] Code is readable and well-structured
- [ ] PR description explains the "why"
- [ ] No unnecessary changes included
### API Changes
- [ ] Backward compatible (or version bumped)
- [ ] Error responses follow our standard format
- [ ] Input validation is present
- [ ] Rate limiting considered
### Database Changes
- [ ] Migration is reversible
- [ ] Large table changes use batched updates
- [ ] Indexes added for new query patterns
- [ ] No N+1 query patterns introduced
### Security
- [ ] No hardcoded secrets
- [ ] Input is sanitized
- [ ] Authorization checks are present
- [ ] Sensitive data is not logged
### Testing
- [ ] Happy path covered
- [ ] Error cases covered
- [ ] Edge cases considered
- [ ] Tests are deterministic (no flakiness)
Measuring Code Review Effectiveness
Track these metrics to understand if your review process is healthy:
# Using GitHub CLI to gather PR metrics
# Average time from PR open to first review
gh pr list --state merged --limit 50 --json createdAt,reviews \
--jq '.[] | {created: .createdAt, firstReview: .reviews[0].submittedAt}'
# Average PR size
gh pr list --state merged --limit 50 --json additions,deletions \
--jq '.[] | .additions + .deletions' | awk '{sum+=$1} END {print "avg:", sum/NR}'
Healthy team metrics:
- First review < 4 hours: Authors aren't blocked for long
- Merge < 24 hours: PRs don't go stale
- < 400 lines per PR: Reviews are meaningful
- 2-3 review rounds max: Communication is clear
- < 5% post-merge bugs: Reviews are catching issues
Summary
Code review is a skill that improves with deliberate practice and good tooling. Use GitHub's built-in features (templates, CODEOWNERS, branch protection) as a foundation. Add stacked PRs with Graphite when large features need to be broken down. Layer AI review tools to catch mechanical issues before humans review. Most importantly, invest in review culture: small PRs, fast turnaround, constructive feedback, and a shared commitment to code quality. The tools support the process, but the process is what makes code review valuable.