Topic · Collaboration & Remotes

Code Review with Git

Code review is mostly a social process, but the Git habits underneath it decide whether a reviewer can engage with your change in twenty minutes or has to fight for two hours. The goal of this page is the Git half: how to shape commits, diffs, and branches so the human reading them has a chance.

The reviewability principle

In any project that does code review, the reviewer's time is the bottleneck. Your branch could be brilliant, but if it lands on the reviewer as a fifty-file pull request with one giant commit titled updates, you have asked them to reconstruct your thinking from scratch. They will procrastinate. When they finally read it, they will miss things. The review will take days.

A series of small, coherent commits, each with a message that explains what the commit does and why, points the reviewer through the change in the same order you discovered it. They can agree commit by commit, raise targeted questions, and finish in one sitting. That is what reviewability means: the same code, shaped so it costs less to read.

Hard to review: one giant commit, mixed concerns commit f2a1c3e updates + logic + rename + reformat + new dep + unrelated typo fix (47 files) Easy to review: linear series, each commit one coherent step rename Foo -> Bar reformat whitespace only extract helper no behavior change new logic small, focused tests covers the logic Same code in both rows. The bottom row costs the reviewer a fraction of the time.
Whose time

If you spend an extra fifteen minutes shaping commits, and three reviewers each save thirty minutes, you have already won. Optimize for the side of the conversation that has fewer hours to spare.

Commit hygiene before review

Most of the work of making a change reviewable happens before anyone else sees it. Three habits cover almost everything.

Each commit is a coherent change

One logical step per commit. A rename is a commit. Extracting a helper without changing behavior is a commit. The new behavior is a commit. The test that exercises it is a commit. The message explains what the commit does and, more importantly, why. The how is in the diff. See making good commits for the message conventions used throughout this knowledge base.

Interactive rebase to clean up

Real development is messy. You make a commit, find a typo, fix it. You try a path, give up, come back. By the time you are ready for review, your branch contains the journey, not the destination. git rebase -i lets you rewrite that branch into the story you actually want to tell: squash fixups into the commits they fix, reorder so each step builds on the previous, drop dead-end experiments. See rebasing for the mechanics.

Use --fixup during development, then autosquash before review

The cleanest workflow is to commit fixups against a specific earlier commit while you are working, then let Git squash them automatically right before you push. Mark a fixup with --fixup=<sha>:

fixup workflowshell
# Half an hour into review prep you spot a bug in an earlier commit.
git log --oneline
# 8a1b2c3 Add parser tests
# 4d5e6f7 Implement parser   <- the bug lives here

git add parser.py
git commit --fixup=4d5e6f7

# Later, before opening the PR: collapse fixups into their targets.
git rebase -i --autosquash origin/main

The --autosquash flag pre-arranges the rebase todo list so each fixup! commit lands immediately after its target with the right action. You confirm the list and Git does the rest. Set git config --global rebase.autoSquash true to make it the default.

Reading what you are submitting

Before clicking "open pull request", read your own change as if someone else wrote it. Two commands do the heavy lifting.

pre-PR self-reviewshell
# The commits your branch adds, oldest first.
git log --oneline --reverse origin/main..HEAD

# The cumulative diff your branch introduces, computed from the merge base.
git diff origin/main...HEAD

Notice the two-dot range in git log and the three-dot range in git diff. They mean different things, and the difference matters enough to deserve its own section.

Walk the log, then walk the diff. Ask the questions a reviewer will ask: Is each commit message true and complete? Do the changes match the messages? Are there debug prints, commented-out code, or stray files I forgot? Is there a commit that could be split? Is there a commit that should be squashed? You will catch real problems every time.

Two-dot vs three-dot ranges

Git accepts two range syntaxes that look almost identical and mean different things. Which one is right depends on the command.

Form With git log With git diff
A..B (two dots) Commits reachable from B but not from A. The standard "what does this branch add?" query. The straight diff between commits A and B as snapshots. Synonymous with git diff A B.
A...B (three dots) Commits reachable from either side but not both (symmetric difference). Useful when comparing two divergent branches. The diff from the merge base of A and B to B. This is "what did this branch add since it diverged from A?" — the diff a forge usually shows.

For day-to-day review prep:

  • git log origin/main..HEAD — "the commits I am proposing".
  • git diff origin/main...HEAD — "the net change my branch introduces, computed from where I branched off". This matches what GitHub or GitLab will show in the PR view, even if main has moved forward since you started.
Why three-dot for diff

If main advanced after you branched, a two-dot diff main..HEAD would include the inverse of every change main added — lines marked deleted that you never touched. The three-dot form anchors the diff at the merge base, so you only see your own work.

Habits that make diffs reviewable

A few small disciplines reduce review churn out of all proportion to how much they cost.

Do not mix reformatting with logic changes

If a hunk of the diff is "reindented the whole function" plus "changed one branch in the middle", the reviewer has to scan every changed line to find the real change. Make the reformat its own commit — even better, its own PR — so the logic commit shows only the real change.

Separate rename commits

Git detects renames heuristically by comparing file content; the detection is good but not perfect, and it gets worse when you rename and edit a file in the same commit. A standalone "rename Foo to Bar" commit gives Git the cleanest possible signal and gives the reviewer a diff they can scroll past with confidence.

Resist the urge to reformat the whole file

If your editor reformatted a file on save before you touched it, undo the formatting and reapply it as a dedicated commit, or revert the file and configure your editor to leave it alone for this change. The drive-by reformat is the single most common review-killer.

Keep generated files out of regular commits

Compiled output, bundle artifacts, lockfile mass-updates, and snapshot test fixtures should either live outside the repository entirely (via .gitignore) or be regenerated by a script as their own commit. A reviewer cannot meaningfully review a 12,000-line bundle diff.

Stacked changes

Some features are genuinely too big for a single reviewable PR. The escape hatch is a stack: split the work into a chain of small branches, each branching off the previous one, each submitted as its own PR. They merge in order, top of stack down to bottom.

a three-deep stackshell
main
  feature/part-1   <- PR #101  (foundation: types and storage)
    feature/part-2 <- PR #102  (built on part-1: read path)
      feature/part-3 <- PR #103  (built on part-2: write path + UI)

Each PR is small enough to actually review. The stack lets you keep building while earlier parts are still in review, instead of accumulating a single 3,000-line behemoth that no one wants to start. The cost is rebasing: when part-1 changes during review, part-2 and part-3 have to be rebased onto the new tip. Tools like git-spice, Graphite, gh stack, and Sapling automate the bookkeeping, but the underlying technique is plain Git: branches off branches, rebases when the base moves.

Responding to review

You have feedback. You need to address it without losing the reviewer's place. There are two schools, and most teams pick a hybrid.

Add new commits on top

Each round of feedback becomes a new commit ("Address review: rename helper", "Address review: handle empty input"). The original commits stay untouched. The reviewer can see exactly what changed since they last looked — the new commits are the response. Common on GitHub, encouraged by GitHub's "view changes since your last review" feature.

Amend and force-with-lease

You edit the original commits with git commit --amend or git rebase -i, so the final history reads cleanly: each commit is the right size and shape, with no "address review" noise. You publish with git push --force-with-lease, which refuses to overwrite the remote if someone else has pushed in the meantime. The downside: the reviewer loses the "what changed since last review" diff unless the forge supports range-diff (GitHub does, since 2022).

The hybrid most teams settle on

Add commits during review for clarity, then squash and clean up just before merge — either by running git rebase -i yourself or by clicking "Squash and merge" on the forge. You get a clean final history without losing the round-by-round trail.

Force-pushing rule

Never git push --force a shared branch. Always use git push --force-with-lease, which refuses to overwrite the remote if someone else has pushed since you last fetched. The plain --force silently discards their work.

Inspection options that help reviewers

When the cumulative diff is hard to read, Git has several flags that change how the diff is rendered without changing the underlying change. Use them in your self-review; suggest them to reviewers when a diff is misleading by default.

readable diffs for tricky changesshell
# Highlight blocks of code that were moved rather than added/deleted.
# Invaluable when reviewing extraction refactors.
git diff --color-moved=zebra origin/main...HEAD

# Word-level diff: show changes inside lines, not just whole-line replacements.
# Best for prose, docs, and config tweaks.
git diff --word-diff=color origin/main...HEAD

# Ignore whitespace entirely when comparing.
# Useful when an unrelated reformat snuck in and you want the real change.
git diff -w origin/main...HEAD

# Catch whitespace errors before you push.
git diff --check

The --color-moved option is the standout. Without it, moving a 30-line block from one file to another shows as 30 deletions and 30 additions and looks identical to a rewrite. With --color-moved=zebra, Git paints the moved blocks in alternating tints so you can see at a glance that nothing inside them changed.

git range-diff for review rounds

The hardest case for a reviewer is "you rebased my branch and now I cannot tell what changed". That is what git range-diff is for. It compares two versions of a patch series and shows, commit by commit, how the series evolved.

compare two versions of a patch seriesshell
# Before rebasing, tag the current tip so you can refer back to it.
git tag review-round-1

# ...do your rebase, fixups, amends...

# Now compare the old series to the new one, with origin/main as the base.
git range-diff origin/main review-round-1 HEAD

Output shows each commit from the old series paired with its closest match in the new series, plus the inter-commit diff. Commits that exist only on one side are flagged. It is the same idea as GitHub's "changes since your last review", but it works locally and across arbitrary rebases.

Tip

Tag the tip of your branch at the start of every review round (review-round-1, review-round-2). When the reviewer asks "what did you change since last time?", you can produce a precise commit-by-commit answer with git range-diff review-round-N HEAD instead of waving at a force-push.

Common pitfalls

Pitfall 1

Force-pushing during review without --force-with-lease. A plain git push --force overwrites whatever is on the remote, including commits your reviewer pushed to suggest a fix or another collaborator added. Always use --force-with-lease (or its safer cousin --force-if-includes), which aborts if the remote has moved since you last fetched.

Pitfall 2

Mixing logic and formatting in one commit. The reviewer cannot tell which character changes matter. Either rebase to split the commit, or revert the format change and apply it later as its own commit. Configuring your formatter to run only on the lines you touched (most modern formatters support this) prevents the problem at source.

Pitfall 3

The mega-commit "everything I did this week". Forty files, one message, no narrative. The cure is to commit more often as you work and to rebase interactively before submitting. If you are partway through and discover the mess, git reset --soft origin/main un-commits everything while keeping the changes staged; you can then commit them in coherent batches.

Pitfall 4

Submitting against a stale base. If your branch was forked from main two weeks ago and main has moved since, the PR diff can include surprises — merge conflicts, broken tests against the current tip, or code already changed upstream. git fetch origin && git rebase origin/main before opening the PR and again before requesting re-review. Use the three-dot diff (origin/main...HEAD) to verify you are seeing only your changes.

Worked examples

Example 1: Reshape a messy branch before opening a PR

You finished a feature. The branch has eleven commits, half of them wip, fix typo, or oops. You want to submit three clean commits: schema, query layer, UI.

shape the branchshell
git fetch origin
git rebase origin/main           # land on the current main

git log --oneline origin/main..HEAD
# 11 commits, much noise

git rebase -i origin/main        # opens the todo list
# In the editor, change "pick" to "squash" (s) or "fixup" (f)
# for the noise commits; reorder lines to group related work.

git log --oneline origin/main..HEAD
# 3 clean commits, each a coherent step

Now run git diff origin/main...HEAD and walk the cumulative change. If anything surprises you, the reviewer would be surprised too.

Example 2: Address review feedback with --fixup

The reviewer asks for two changes: rename a variable in commit 4d5e6f7, and add an edge-case test in commit 8a1b2c3. You want the final history to keep its three-commit shape, with the fixes folded into the right commits.

fold review fixes into their target commitsshell
git add parser.py
git commit --fixup=4d5e6f7        # creates "fixup! Implement parser"

git add tests/parser_test.py
git commit --fixup=8a1b2c3        # creates "fixup! Add parser tests"

git rebase -i --autosquash origin/main
# Todo list is pre-arranged. Save and exit.

git push --force-with-lease

The remote branch now shows three clean commits again. Run git range-diff origin/main review-round-1 HEAD to confirm only the intended commits changed, then post that summary as your reply to the review.

Example 3: Make a refactor diff readable with --color-moved

You extracted a helper function from handler.py into a new helpers/parsing.py. The naive diff shows 80 lines deleted and 80 added, and the reviewer cannot tell whether anything inside the moved block changed.

show the move as a moveshell
git diff --color-moved=zebra origin/main...HEAD -- handler.py helpers/parsing.py

Moved blocks are now painted in alternating tints, so the reviewer can confirm at a glance that the extraction is pure. Mention the flag in the PR description: "Best read with --color-moved=zebra; the move is pure relocation."

Example 4: Verify what your branch actually adds, with the base moved

You branched off main ten days ago. main has advanced by sixty commits. You want to see only your contribution, not a confusing mix that includes the inverse of upstream work.

diff from the merge baseshell
git fetch origin

# Wrong: includes the inverse of every change upstream made since you branched.
git diff origin/main..HEAD

# Right: diff from the merge base of origin/main and HEAD to HEAD.
git diff origin/main...HEAD

The three-dot form is what the forge shows in the PR view. Using it locally means your self-review sees exactly what your reviewer will see.

Sources & further reading

  • Pro Git, 5.2: Contributing to a Project Textbook Scott Chacon and Ben Straub

    The canonical chapter on preparing patches for review: commit guidelines, splitting work into logical changesets, message conventions, and git diff --check for whitespace before submission.

  • git range-diff Reference Git project

    Official reference for the command that compares two versions of a patch series — the right tool for "what changed since the last review round?"

  • git diff Reference Git project

    Reference for git diff. Authoritative source on two-dot vs three-dot semantics, --color-moved, --word-diff, and --ignore-all-space.

  • How to Write a Git Commit Message Article Chris Beams

    The seven rules — 50-character subject, imperative mood, body explains what and why — that most modern projects implicitly assume.

  • About pull request reviews Tutorial GitHub Docs

    Forge-side counterpart to this page. Covers the review UI: requesting reviewers, leaving comments, approving, and the "view changes since your last review" feature that pairs with range-diff.

  • Google's published code review guide. The two sub-guides ("How To Do A Code Review" and "The CL Author's Guide") are the cultural counterpart to the Git mechanics here — small CLs, fast turnaround, optimizing for reviewer time.