Skip to content

AI-Assisted Git Code Reviews

Lesson 4 of 8Intermediate12 min readGitHub Copilot & AI Engineering · AI + GitVerified: git 2.43.0 on Ubuntu 24.04, September 2026

The cheapest place to catch a problem is before anybody else has spent attention on it.

Self-review is the layer everybody agrees is valuable and almost nobody does properly, because reading your own diff at the end of a long day is exactly when you are least able to see it. That is a reasonable job to hand to something that does not get tired.

This lesson is local review — before the push, before the pull request. Copilot code review is the pull request layer, and the two are complementary rather than duplicative.

Three diffs, three questions.

Terminal window
{/* What I am about to commit */}
git diff --staged
Terminal window
{/* What I have changed but not staged often the accidental part */}
git diff
Terminal window
{/* Everything on this branch that is not on main what a reviewer will see */}
git diff main...HEAD

The third is the one that matters most before opening a pull request, and the one people look at least. It is what the reviewer sees.

CommandScopeUse before
git diffUnstaged changesStaging — catches what you did not mean to touch
git diff --stagedStaged changesCommitting
git diff HEADBothCommitting everything
git diff main...HEADThe whole branch vs the merge baseOpening a pull request
git diff main..HEADThe branch vs main’s current tipRarely what you want

The two-dot and three-dot forms differ in a way that matters.

main...HEAD (three dots) shows what your branch changed relative to where it diverged. This is the pull request diff.

main..HEAD (two dots) shows the difference between the two current tips, which includes the effect of anything that landed on main since you branched, inverted. On a long-lived branch this is confusing and much larger.

Use three dots. It is what GitHub shows, so it is what the reviewer will be reading.

On a branch that added b.txt while main separately added c.txt:

$ git diff --stat main...HEAD
b.txt | 1 +
1 file changed, 1 insertion(+)
$ git diff --stat main..HEAD
b.txt | 1 +
c.txt | 1 -
2 files changed, 1 insertion(+), 1 deletion(-)

The two-dot form reports that your branch deletes c.txt — a file you have never touched. It does not; main added it after you branched, and comparing tips makes its absence from your branch look like a deletion.

Hand that to a reviewer, or to a model, and you get questions about why you removed a file. On a branch that is a week behind a busy main, this noise can be most of the diff.

“Review this” produces a list of observations of uneven value. Specific questions produce findings.

The set worth asking, in rough order of what actually catches things:

“What did I leave in?” Debug statements, commented-out code, a TODO written twenty minutes ago, a hardcoded local path, a test with .only on it. This is the highest-yield question and the most embarrassing category to have a reviewer find.

“What does this change that the diff does not make obvious?” A changed default, a removed guard, an altered return type, a narrowed condition. Second-order effects are what human reviewers miss when skimming.

“What is missing?” Error handling on a new code path, a test for the new branch, a null check, an updated call site. Absence is hard for humans to see and is exactly what a systematic reader is good at.

“What would a reviewer ask?” Genuinely useful for pre-empting a round trip. If the model asks why you did something a particular way, so will a person — and the answer belongs in the pull request description.

“Is there anything security-relevant here?” Broad, and worth asking on any diff touching input handling, authentication, or anything that constructs a query or a command. Treat the output as prompts to check rather than as findings.

The unstaged diff deserves its own pass, because it answers a different question: what did I touch that I did not mean to?

Typical finds: a config file modified while experimenting, a dependency added and then abandoned, a formatter that rewrote a file you only opened, a .env you edited.

Terminal window
{/* Everything modified, including untracked files */}
git status --short
Terminal window
{/* What changed in files Git already tracks */}
git diff

That second one is worth a model’s attention specifically when the diff is larger than you expected. “Which of these changes are unrelated to the feature I was working on?” is a question that sorts a messy working tree quickly.

This connects to staging discipline: a commit containing three unrelated changes is a commit with no good message and a confusing review. The fix is git add -p, and the moment to notice is here.

An AI reviewer will report things that are not problems. Knowing the pattern saves you from either chasing everything or dismissing everything.

Common false positives:

  • Missing error handling that is handled upstream. It cannot see the caller.
  • “Unused” variables used in a file it was not given.
  • Style objections that contradict your linter, which is authoritative.
  • Security concerns about test fixtures, where the “hardcoded credential” is deliberately fake — a finding worth a second of thought each time, because occasionally it is right and the fixture is real.
  • Suggestions to add null checks in a codebase with strict typing that makes them unreachable.

Every one of these has the same root cause: the model saw the diff, not the codebase. The fix is usually to supply the missing file rather than to argue.

Findings worth taking seriously without much scrutiny:

  • Leftover debug output — easy to verify, trivially true or false
  • A code path with no test — checkable in seconds
  • An inconsistency between two parts of your own diff
  • A changed default or removed guard it noticed and you did not

The calibration that works: treat findings as questions rather than defects. A question costs seconds to answer and does not require you to be right about whether the model was.

Most weak local reviews are context problems rather than prompt problems, and there are three cheap ways to fix them.

Include the branch’s purpose. One sentence. “This branch adds retry handling to the payment client” changes what counts as a finding — without it, the model does not know that a change to the retry count is the point rather than an accident.

Include the files the diff calls into. The single most effective addition. A diff that modifies a function is much better reviewed alongside the function’s callers, and “missing error handling” false positives largely disappear once the caller is visible.

Include the test file. It answers “is this covered” directly rather than by inference, and it lets the model tell you which existing test now needs updating.

A practical way to assemble that:

Terminal window
{/* The branch diff, plus the files it touches in full */}
git diff main...HEAD
git diff --name-only main...HEAD | xargs -r cat

For a small branch this is enough. For a large one it is too much, and the better move is to review file by file — which is also how a human reviewer will read it.

A useful variant when the branch will be merged with its history intact rather than squashed.

Terminal window
{/* Review one commit as its own change */}
git show a3f9c21
Terminal window
{/* Each commit in the branch, in order */}
git log --oneline main...HEAD

The question this answers that the branch diff cannot: does each commit stand on its own? A branch whose net diff is fine but whose middle commit leaves the tests broken is a branch that cannot be bisected through later, which matters exactly when you are hunting a regression.

Worth asking:

Here are the commits on this branch, each as a separate diff. Does each one leave the codebase in a working state? Is any of them doing two unrelated things?

That second question is the one that produces better history, and it is easier to act on before the branch is pushed than after.

Worth being clear, because this layer is easy to over-trust once it starts catching things.

It has not run anything. Every finding is from reading. Tests are a different kind of evidence.

It does not know your architecture. Whether this belongs in this module is a design question.

It does not know your product. Whether the behaviour is correct is a requirements question.

It sees the diff, not the system. The effect of your change on a caller three modules away is invisible unless you supply the caller.

It is not a security review. It may notice a pattern. Code scanning and CodeQL do a fundamentally different and more rigorous thing — building a model of the program and tracing data flow through it — and they run on the pull request where their findings are recorded rather than read once and forgotten.

  1. Before staging: git status --short and git diff. What did I touch that I did not mean to?

  2. Stage deliberately. git add -p if there is more than one logical change.

  3. Before committing: git diff --staged. What did I leave in?

  4. Before pushing: git diff main...HEAD. What will a reviewer see, and what will they ask?

  5. Write the pull request description from what you learned in step 4 — the questions it raised are the things the description should pre-empt.

Step 4 is the highest-value one and takes about a minute on a normal branch. It is also where the pull request summary comes from, so it is not extra work.

Copilot CLI has this built in — /review and /diff operate on your changes without you assembling the input. That is a real ergonomic difference: the friction in local review is gathering the diff, and removing it is most of the reason people skip the step.

The technique is identical either way. Where you are piping diffs to an assistant by hand, the commands above are the whole toolkit.

Reviewing your own work with a tool that agrees readily has a specific failure mode: it can confirm rather than challenge.

A model asked “is this change good?” will usually find something positive to say. A model asked “what is wrong with this?” will find something to criticise. Neither is an assessment; both are responses to the framing.

The framing that produces useful output is neutral and specific — “what did I leave in”, “which new paths lack tests”, “what changed that the diff does not make obvious”. Those have answers that are true or false independently of how the question was asked, which is what makes them worth asking.

Avoid asking whether the change is ready. That is the judgement you are supposed to be making.

Local review is the first of several, and its value comes from being the cheapest — not from being the most thorough.

LayerRunsCatchesCosts
Local AI reviewBefore pushLeftovers, omissions, obvious riskSeconds, no CI time
Linters and formattersPre-commit or CIStyle, some correctnessDeterministic, fast
TestsCIBehaviourThe only layer that proves anything
Code scanningCIVulnerability patterns, with data flowMinutes
Copilot code reviewPull requestThe same class as local, on the final diffReviewer attention
Human reviewPull requestDesign, intent, contextThe scarcest resource

Two observations.

Local review and Copilot code review overlap deliberately. They find similar things, and catching something locally means it never consumes reviewer attention. The duplication is the point — the cheapest layer that catches a problem is the right one.

Only one row proves anything. Everything else reads. That is not an argument against the reading layers; it is the reason none of them is a substitute for running the code.

The practical goal of local review is narrow and worth stating: arrive at the pull request with nothing in it that a reviewer would find embarrassing to point out. Not a perfect change — one where human attention goes to the design rather than to a stray console.log.

Reviewing with two dots instead of three. On a long-lived branch, main..HEAD shows a confusing superset and is not what the reviewer sees.

Asking “review this” and getting a list. Specific questions produce findings.

Skipping the unstaged pass. The accidental changes are in that diff, not the staged one.

Treating every finding as a defect. Most are questions; some are the model lacking context.

Arguing with a false positive instead of supplying the file. It could not see the caller.

Using local review as a substitute for tests. It has not executed anything.

Not writing down what it asked. The questions it raised are the pull request description.

Self-review fails not because people disagree with it but because it happens at the moment they most want to be finished. Three things make it stick.

Attach it to a command you already run. The review happens between git add and git commit, or between git commit and git push — not as a separate activity requiring a decision.

Keep it under two minutes. A five-question review that takes ninety seconds gets done. A thorough one that takes fifteen minutes gets skipped on exactly the branches that needed it, because those are the rushed ones.

Automate the assembly, not the judgement. The friction is gathering the diff and framing the question. A shell alias or a prompt file removes that; the reading stays yours.

Terminal window
{/* A single command that produces the review input */}
git config --global alias.prd 'diff main...HEAD'

One caution on automating further. A pre-push hook that runs a review and prints findings is technically easy and works against the goal — output that appears without being asked for gets scrolled past, and the habit you wanted was reading. Prompt the review; do not print it.

The signal that it is working is not the number of findings. It is a reduction in review comments of the “you left a debug statement in” variety, which is the category that costs a round trip and teaches a reviewer that this branch needs a careful read.

Local AI review is a fresh pair of eyes on your own diff, available at the moment yours are worst. It reads carefully, notices absence, knows nothing about your system, and has run nothing — so treat findings as questions and let the tests answer.

  • Three diffs answer three different questions; main...HEAD is what a reviewer sees
  • Three dots compares against the merge base; two dots compares tips and is usually wrong
  • The highest-yield question is “what did I leave in”
  • Asking about absence — missing tests, missing error handling — catches what humans skim past
  • False positives almost always mean the model lacked a file, not that it was wrong to ask
  • Findings are cheaper treated as questions than adjudicated as defects
  • Local review executes nothing and is not a security review
  • The questions raised during self-review are the content of your pull request description

Use a branch with real changes on it.

  1. Run git status --short and git diff. Predict: is there anything in the working tree you had forgotten about?

  2. Run git diff main..HEAD and git diff main...HEAD on a branch that is a few commits behind. Predict: how different are they, and which matches what GitHub shows?

  3. Ask for a review of the branch diff with “review this”. Note how many findings are actionable.

  4. Ask again with three specific questions. Compare the actionable proportion.

  5. Ask “for each new code path, is there a test?” Predict: does it find a gap you knew about, or one you did not?

  6. Take one false positive and supply the file it was missing. Predict: does the finding survive?

  7. Write the pull request description from the questions it raised.

AI-assisted engineering learning pathEleven lessons on getting value from Copilot and agents without giving up review discipline.