Reviewing a pull request in a second worktree Jump to heading

Reading a diff in a browser tells you what changed. Running the branch tells you whether it works — and for anything involving state, timing or a user interface, the second question is the one that matters. The obstacle is that checking out someone else’s branch means abandoning your own working state, so most reviews stay at the reading stage. A detached worktree removes the obstacle entirely, as part of Git worktrees and parallel development.

When to use this approach Jump to heading

  • The change affects behaviour you cannot evaluate by reading.
  • You have uncommitted work you do not want to stash.
  • You want to run the branch’s tests against your own environment.
  • The change is large enough that navigating it in an editor beats a web diff.
  • If the change is three lines of configuration, read it in the browser and save the ceremony.

Step 1 — Fetch the pull request ref Jump to heading

Forges expose pull requests as refs, including those from forks, so you do not need the contributor’s remote.

# Fetch a specific pull request into a local ref
git fetch origin 'pull/812/head:refs/remotes/origin/pr/812'

# Or configure the refspec once and fetch them all
git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch origin
# Verification: the ref exists and points where you expect
git rev-parse --short origin/pr/812
git log --oneline -1 origin/pr/812

The second form is worth doing once per clone. After it, every pull request is available as origin/pr/<number> without another fetch command to remember.

Step 2 — Create a detached worktree for it Jump to heading

Detached is deliberate: you are not going to commit here, and not claiming a branch keeps the review worktree from blocking anyone else’s checkout.

git worktree add --detach ../review-812 origin/pr/812
cd ../review-812
# Verification: you are on the pull request's commit, with your own work untouched
git log --oneline -1
git -C ../app status --short | head -3     # your original worktree, unchanged
Stashing against a review worktreeStashing interrupts your work, risks a conflict on restore, and leaves you switching context in the same directory. A detached worktree leaves your editor, your build and your uncommitted changes exactly as they were.Stash and switchDetached worktreeyour work in progressstashed, restore lateruntouchedconflict risk on returnrealnoneeditor statelostpreserveddisk costnoneone working copythe only cost of the worktree is disk, which is the cheapest thing in the comparison

Step 3 — Read the diff against the merge base, not the tip Jump to heading

Comparing against your local default branch shows changes that came from elsewhere. The merge base is what the author actually changed.

base=$(git merge-base origin/main HEAD)

# The change itself
git diff --stat "$base"...HEAD
git diff "$base"...HEAD -- ':!*.lock' ':!**/generated/**'
# Commit by commit, which is how the author intended it to be read
git log --oneline --reverse "$base"..HEAD
git show --stat "$(git rev-list --reverse "$base"..HEAD | head -1)"

The three-dot form is the important detail: base...HEAD shows what the branch introduced, while base..HEAD in a diff context shows something subtly different and usually not what a reviewer wants.

Step 4 — Run it Jump to heading

This is the whole point of the exercise, and it is where the ignored-files problem from the parent topic appears.

# Dependencies are not shared between worktrees
npm ci

# Ignored local configuration has to be copied
cp ../app/.env.local .env.local 2>/dev/null || true

# Then run whatever answers your question
npm test -- --findRelatedTests $(git diff --name-only "$base"...HEAD)
npm run dev
# Verification: the branch's own test suite passes in your environment
npm test 2>&1 | tail -5
The review loop, without touching your own workFetch the pull request ref, create a detached worktree, install what version control does not carry, then read and run. When the review is finished the worktree is removed and nothing about your own branch has changed.Fetch pr reforigin/pr/812Detached worktree../review-812Install and rundeps, env, testsRemoveworktree removeyour own worktree is untouched throughout — no stash, no switch, no restore What two-dot and three-dot show a reviewerTwo-dot diffs compare the two tips directly, so anything merged into the target branch since the author started appears as though they removed it. Three-dot compares against the merge base, which is the set of changes the author actually made.three-dot compares against M, not against the tip of mainmainAMDEpull requestMC1C2C3D and E are not the author's work — two-dot would show them as deletions

Step 5 — Leave feedback and clean up Jump to heading

# Comment from the terminal while the context is fresh
gh pr review 812 --comment --body "Ran it locally; the retry path loops when the upstream returns 503. Repro: …"
cd ../app
git worktree remove ../review-812
git worktree list
# Verification: nothing stale remains
git worktree prune --verbose && git worktree list --porcelain | grep -c '^worktree'

SAFETY WARNING — a review worktree runs code from a branch you have not read yet, with your credentials and your environment. Dependency installation alone executes lifecycle scripts from the contributor’s lockfile. For contributions from outside your team, install with scripts disabled, review the dependency diff before running anything, and consider a container rather than your own machine.

# The cautious install for an untrusted branch
npm ci --ignore-scripts
git diff "$base"...HEAD -- package.json package-lock.json

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why detached rather than checking out the branch? Jump to heading

Because a review is read-only and a detached worktree does not claim the branch. If you check it out and the author force-pushes, you are left reconciling a branch you never intended to own. Detached, a re-fetch and a git checkout --detach gets you the new tip with nothing to reconcile.

How do I review the second version after a force-push? Jump to heading

Fetch again and compare the two tips with git range-diff, which shows how the commits themselves changed rather than re-showing the whole branch. The technique is covered in reviewing a force-push with git range-diff.

Can I keep one review worktree and reuse it? Jump to heading

Yes, and it is a good habit: a single ../review directory that you re-point with git checkout --detach origin/pr/<n> avoids accumulating directories and keeps its dependency install warm. Only the dependency tree needs refreshing when the lockfile differs.