Reviewing conflict resolutions with remerge-diff Jump to heading

A merge commit can contain code that no reviewer ever saw. When a merge conflicts, whoever resolves it edits the conflicted files and commits the result. Those edits might be a careful combination of both sides, a wrong pick that drops a fix, or an unrelated change slipped in while the files were open. Standard views of a merge hide all of it: the default combined diff omits hunks that resolved to one side, and a diff against either parent shows the entire other branch. --remerge-diff (Git 2.36+) shows something better: Git re-runs the merge automatically, keeping the conflict markers, and diffs that against what was actually committed. The result is exactly the human contribution to the merge. This page shows how to read it and build it into review, within semantic conflicts and merge verification.

When to use this approach Jump to heading

  • Merges on your long-lived branches regularly involve conflict resolution.
  • You want to review what was changed during a resolution, not the whole incoming branch.
  • An audit or incident needs to know whether a merge introduced code beyond its two sides.
  • Resolutions are replayed by rerere, as in rerere conflict automation, and you want to check them in context.

Step 1 — Show a merge’s resolution Jump to heading

Run git show --remerge-diff on a merge commit. Files that merged cleanly show nothing. Files that conflicted show a diff from the auto-merged version — with conflict markers — to the committed version.

git show --remerge-diff 5a9e3c1
diff --git a/src/billing/totals.py b/src/billing/totals.py
remerge CONFLICT (content): Merge conflict in src/billing/totals.py
--- a/src/billing/totals.py
+++ b/src/billing/totals.py
@@ -40,9 +40,5 @@ def invoice_total(lines, customer, invoice):
-<<<<<<< 2d4b6f8 (Round credit notes half-even)
-    total = round_half_even(sum(l.amount for l in lines))
-=======
-    total = sum(l.amount for l in lines) - discount(customer)
->>>>>>> 9e01b55 (Apply customer discount in totals)
+    total = round_half_even(sum(l.amount for l in lines) - discount(customer))

Read it as: here is the conflict Git produced, and here is what the person replaced it with. In the example, both sides’ intents survived — rounding and the discount — which is a good resolution.

How remerge-diff is computedGit takes the merge commit's two parents, re-runs the merge with the default strategy, keeping conflict markers where it cannot resolve, and diffs that automatic result against the tree that was actually committed. Only human edits remain in the output.Merge commitparents ^1, ^2Re-mergemarkers keptDiffauto vs committedOutputhuman edits onlya clean merge with no extra edits produces an empty remerge-diff

Step 2 — Spot the patterns that need attention Jump to heading

Most resolutions look like the example: markers replaced by a combination. Three other patterns deserve a closer look.

Reading a remerge-diffIf markers are replaced by a combination of both sides, the resolution is probably sound. If one side's content is simply deleted, check that it was meant to be dropped. If changes appear in files that did not conflict, someone edited during the merge, and those edits need review like any other change.What does the remerge-diff show?markers → combinationLikely fineboth intents keptone side deletedCheck intentwas the fix dropped?edits in clean filesReview fullyunreviewed changethe third case is the one most review tools never surface
# Merges whose resolutions touched files that did not conflict ("evil merges")
for m in $(git rev-list --merges --since=1.month main); do
  extra=$(git show --remerge-diff --format= "$m" | grep -c '^diff --git' || true)
  conflicted=$(git show --remerge-diff --format= "$m" | grep -c '^remerge CONFLICT' || true)
  [ "$extra" -gt "$conflicted" ] && echo "$m edits $extra file(s), $conflicted conflicted"
done

Step 3 — Audit resolutions over a range Jump to heading

git log --remerge-diff applies the same view to every merge in a range, which makes it possible to audit a branch’s integration history — for a release, an incident, or a long-lived branch’s lifetime.

# Every resolution on main touching payments, in the last quarter
git log --merges --remerge-diff --since=3.months --format='%n=== %h %an %ad %s' --date=short \
  main -- src/payments/ | less

Combine it with the pickaxe to find which merge introduced a line during resolution, which ordinary -S searches miss because they skip merge diffs.

git log --merges --remerge-diff -S'round_half_even(sum' --oneline main

The pickaxe itself is covered in finding when code appeared with the pickaxe.

Step 4 — Put resolutions in front of reviewers Jump to heading

When a pull request’s branch merges main into itself and resolves conflicts, the forge’s diff shows the combined result, not the resolution. Post the remerge-diff of each merge on the branch as a review comment so reviewers see it.

jobs:
  resolutions:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0, ref: "${{ github.event.pull_request.head.sha }}" }
      - run: |
          out=$(for m in $(git rev-list --merges "origin/${{ github.base_ref }}..HEAD"); do
                  git show --remerge-diff --format='### %h %s' "$m"; done)
          [ -z "$out" ] && exit 0
          printf 'Conflict resolutions in this branch:\n\n```diff\n%s\n```\n' "$out" > body.md
          gh pr comment "${{ github.event.number }}" --body-file body.md
        env: { GH_TOKEN: "${{ github.token }}" }
What a reviewer sees, with and without remerge-diffWithout remerge-diff, a merge of main into a pull request branch appears as part of the combined diff, and the resolver's edits are indistinguishable from either side's changes. With a remerge-diff comment, the reviewer sees exactly what was typed during resolution.Forge diff only+ remerge-diff commentresolver's editsmixed into the diffshown separatelydropped fixeseasy to missvisible as deletionsnoisewhole branchconflicts onlya small comment turns the riskiest part of a merge into a reviewable diff

Step 5 — Encourage resolutions that review well Jump to heading

Some resolution habits make remerge-diffs easier to trust. Resolve conflicts only — keep unrelated fixes for a separate commit. Note non-obvious choices in the merge commit message. Prefer rebasing a private branch over merging main into it repeatedly, so the pull request has fewer merge commits to review.

git commit -e      # in the merge message, explain any resolution that is not an obvious combination

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Does remerge-diff work for octopus merges? Jump to heading

It is defined for two-parent merges. For octopus merges, Git falls back to another diff format. Octopus merges cannot contain conflict resolutions anyway, because the strategy refuses conflicts.

Why does a remerge-diff show changes in a file that did not conflict? Jump to heading

Either someone edited it while resolving (an “evil merge”), or rename detection or a merge driver behaved differently when the merge was originally made than when it is re-run now. The former needs review; the latter is rare and visible as an otherwise identical file.

Can rerere-applied resolutions be reviewed this way? Jump to heading

Yes, and they should be: a replayed resolution appears in the remerge-diff exactly like a hand-typed one. It is the most practical way to check that rerere applied an old resolution sensibly in a new context.