Code Review on GitHub

Code review on GitHub is the process of having another developer inspect the changes in a pull request before they get merged — reading the diff, leaving comments on specific lines, and formally approving or requesting changes. It is where most teams catch bugs, share knowledge, and enforce conventions before code reaches main. GitHub turns this into a structured workflow rather than a free-form chat: a reviewer submits a review, which GitHub stores as a first-class object tied to the exact commit that was reviewed.

Overview: How Code Review Fits Into the Pull Request Workflow

A pull request (PR) asks GitHub to merge a source branch (the "head") into a target branch (the "base", usually main). GitHub computes the diff shown in the Files changed tab between the merge base — the common ancestor commit of head and base — and the current tip of the head branch. Every time the PR author pushes new commits, that diff is recalculated.

A review is a separate kind of object from an ordinary PR comment. Comments left on the Conversation tab are just discussion, not anchored to code. Comments left on a line inside Files changed are review comments: each one is anchored to a specific file, line, and diff position within a specific commit SHA. When you click a line and type a comment, it starts in a pending state — visible only to you — until you finish the review. Clicking Review changes lets you attach one overall verdict to the whole batch: COMMENTED, APPROVED, or CHANGES_REQUESTED. Submitting converts every pending comment into a real, visible comment and records the review event.

Repository maintainers can require reviews before a PR is mergeable. Under Settings > Branches > Branch protection rules, a rule can demand a minimum number of approving reviews on the latest commit, and can optionally dismiss stale approvals whenever new commits are pushed. A CODEOWNERS file takes this further: it maps file paths to specific users or teams who are automatically requested as reviewers, and a protection rule can require their approval specifically.

# .github/CODEOWNERS
# Require the payments team to review anything under /src/billing
/src/billing/  @acme-org/payments-team
*.yml          @acme-org/platform-team

Finally, a PR can be opened as a draft while work is still in progress — reviewers can look, but the "Ready for review" button must be clicked (or the PR converted via gh pr ready) before it counts toward required reviews or shows up as awaiting review by default.

Syntax

Reviews can be submitted from the GitHub web UI, or from the terminal with the official gh CLI, which is the most reproducible way to review code you have pulled and tested locally.

gh pr review <number> [--approve | --request-changes | --comment] [-b "<message>"]
Flag Meaning
--approve Submit a review with verdict APPROVED
--request-changes Submit a review with verdict CHANGES_REQUESTED; blocks merge if changes are required
--comment Submit a review with no verdict, just feedback (COMMENTED)
-b, --body The top-level review summary text
-F, --body-file Read the review body from a file instead of the command line
-R, --repo Target a repository other than the one in the current directory

Related commands you will use alongside a review: gh pr diff <number> prints the PR diff to your terminal, gh pr checkout <number> fetches and switches to the PR’s branch, and gh pr view <number> --comments shows the existing conversation.

Examples

Example 1: Pulling a PR locally, testing it, then approving

git fetch origin pull/42/head:feature/login-page
git switch feature/login-page
npm test

Output:

From github.com:acme-org/webapp
 * [new ref]         refs/pull/42/head -> feature/login-page
Switched to branch 'feature/login-page'

> webapp@1.4.0 test
> jest

Test Suites: 12 passed, 12 total
Tests:       48 passed, 48 total

The first line fetches the PR’s head commit directly from GitHub’s special pull/<number>/head ref and stores it as a local branch, without needing the contributor’s fork as a separate remote. After the tests pass locally, the reviewer submits an approval:

gh pr review 42 --approve -b "Verified locally, tests pass. Nice cleanup of the auth middleware."

Output:

Reviewed #42: pull/42

This creates an APPROVED review anchored to the commit that is currently checked out (the PR’s tip at fetch time). If the branch protection rule requires one approval, the PR’s merge button now unlocks — unless the author pushes another commit and the repository has "dismiss stale approvals" enabled, in which case a fresh approval is needed.

Example 2: Requesting changes with a specific reason

gh pr review 57 --request-changes -b "fetchUser() swallows the exception from the API call - please log and rethrow instead of returning null silently."

Output:

Reviewed #57: pull/57

A CHANGES_REQUESTED review is the strongest signal in the workflow: if the repository requires reviews before merging, this blocks the merge button entirely until the same reviewer (or another with sufficient permission) submits a new approving review on a later commit.

Example 3: Leaving a one-click suggested change

Suggested changes are a web-UI-only feature: in Files changed, select a line, click the suggestion icon, and edit the code directly inside the comment box. GitHub wraps it in a special fenced block:

```suggestion
    if (!user) {
      throw new Error(`User ${id} not found`);
    }
```

When the PR author clicks Commit suggestion (or batches several suggestions into Add suggestion to batch and commits them together), GitHub creates a real new commit on the PR’s head branch on the author’s behalf. A reviewer or author working locally just needs to catch up:

git switch feature/login-page
git pull origin feature/login-page

Output:

Updating a1b2c3d..e4f5a6b
Fast-forward
 src/users.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

Because the suggestion commit was made directly on top of the existing branch tip, the pull is a plain fast-forward — no merge, no rebase.

How It Works Step by Step

  • Opening Files changed diffs the merge-base commit of base and head against the head branch’s current tip commit.
  • Clicking a line and typing creates a pending review comment, stored anchored to (file path, diff position, commit SHA) but not yet visible to anyone else.
  • Clicking Start a review / Review changes and choosing a verdict, then Submit review, bundles every pending comment plus the verdict into one review event and makes it all visible at once.
  • If branch protection requires reviews, GitHub checks whether an APPROVED review exists for the exact commit currently at the branch tip; a subsequent push can invalidate that check depending on the "dismiss stale reviews" setting.
  • When the author pushes more commits, existing inline comments stay attached to their original commit and line, but GitHub marks the thread "outdated" if the surrounding diff context changed; a reviewer or author can mark a thread "Resolved" to collapse it once addressed.
  • Merging finally applies one of three strategies (merge commit, squash and merge, or rebase and merge) to fold the reviewed commits into the base branch.

Common Mistakes

Mistake 1: Leaving line comments but never submitting the review

Pending review comments are private until you click Submit review. It is common to leave several inline comments, close the tab, and assume the author saw them — they did not, because the review was never submitted, so the comments stayed pending. Always finish with an explicit Submit review click, or with gh pr review <number> --comment -b "..." from the CLI, which submits immediately.

Mistake 2: Force-pushing a PR branch mid-review

# Wrong - rewrites the commits reviewers already commented on
git rebase -i HEAD~3
git push --force origin feature/login-page

Rebasing and force-pushing after a review has started replaces the SHAs that existing review comments are anchored to. GitHub does its best to reattach comments to the new diff, but context can be lost, and a bare --force can silently discard commits a reviewer already approved if the remote had moved. Prefer pushing small fixup commits instead of rewriting history while a review is open, and let GitHub’s Squash and merge button clean up the history at merge time:

git add src/users.js
git commit -m "fix: rethrow instead of swallowing fetchUser error"
git push origin feature/login-page

If you genuinely must rewrite history on a PR branch, use git push --force-with-lease instead of bare --force — it fails safely if the remote has commits you have not fetched, rather than overwriting them.

Mistake 3: Reviewing a stale local checkout

# Wrong - reviewing whatever was checked out hours ago, missing the author's latest fixes
git switch feature/login-page
npm test

If you checked the branch out earlier and the author pushed since, you are testing and commenting on outdated code, which produces confusing "I already fixed that" replies. Always fetch immediately before reviewing:

git fetch origin
git switch feature/login-page
git merge --ff-only origin/feature/login-page

Best Practices

  • Keep pull requests small and focused; large diffs get shallow, rubber-stamp reviews because no human can hold 800 changed lines in their head.
  • Use --request-changes only for things that genuinely must be fixed before merge; use --comment for suggestions, questions, and nits so you do not block the author unnecessarily.
  • Prefer suggested changes for small, unambiguous fixes (typos, missing null checks) — the author can accept them with one click instead of retyping your suggestion.
  • Set up a CODEOWNERS file so the right specialists are automatically requested on sensitive paths, instead of relying on someone remembering to add reviewers manually.
  • Avoid force-pushing a PR branch once review has started; push additional commits instead, and let Squash and merge tidy history on the base branch.
  • Resolve a review conversation only after re-checking that the concern was actually addressed, not just because the author replied.
  • Open work-in-progress as a draft PR so it is clear it is not yet ready for a full review pass.
  • Write review comments as questions or observations aimed at the code, not the author ("this loop re-fetches on every iteration" rather than "you wrote a slow loop").

Practice Exercises

  • Exercise 1: Find an open pull request in a repository you have access to (or open one against your own fork). Fetch its head branch locally using the pull/<number>/head ref, run any tests, then submit a review with gh pr review using either --approve or --comment. Confirm on the PR’s Conversation tab that your review appears with the correct verdict.
  • Exercise 2: Add a .github/CODEOWNERS file to a test repository that maps one directory to yourself, open a PR that touches a file in that directory, and observe that you are automatically added as a requested reviewer.
  • Exercise 3: Submit a --request-changes review on a test PR, then as the "author" push a small fixup commit addressing it (without force-pushing), and mark the resulting conversation thread as resolved. End state: the PR shows the earlier review as CHANGES_REQUESTED in history, but the thread is collapsed as resolved and a new commit is visible in Files changed.

Summary

  • A GitHub review bundles inline comments plus one verdict — APPROVED, CHANGES_REQUESTED, or COMMENTED — into a single event anchored to a specific commit.
  • Inline comments start as private "pending" comments and only become visible once the review is submitted.
  • The gh pr review command lets you approve, comment on, or request changes to a PR entirely from the terminal, after fetching and testing the branch locally.
  • Suggested changes let a reviewer propose an exact code edit that the author commits with one click, creating a new commit on the PR branch.
  • Branch protection rules and CODEOWNERS can require specific approvals before a PR is mergeable, and can dismiss stale approvals when new commits arrive.
  • Avoid force-pushing a PR branch once review has begun; it can orphan review comments and, with a bare --force, silently discard commits — use --force-with-lease if a rewrite is unavoidable.