Finding unported commits with git cherry Jump to heading
Branch comparisons by hash are useless once cherry-picks are involved: every backport has a new hash, so git log release..main lists fixes that were backported as if they were missing. git cherry compares by content instead. It computes a patch ID for each commit β a hash of the diff, ignoring whitespace and line numbers β and marks each commit on one branch as having an equivalent on the other or not. That turns βwhich fixes from main are still missing on the release branch?β into a single command, and it catches backports made without -x as long as the change was applied unaltered. This page shows how to read its output and build a regular report from it, within cherry-pick and backporting.
When to use this approach Jump to heading
- You need to know which fixes on main have not reached a release branch.
- Some backports were made without
-x, so message-based searches miss them. - You maintain long-lived branches that exchange changes by cherry-pick, and want to check nothing went missing in either direction.
- You want a weekly report that keeps backport debt visible, alongside automating backport pull requests with labels.
Step 1 β Run git cherry and read the signs Jump to heading
git cherry <upstream> <head> lists commits in head that are not in upstream, each prefixed with + or -. A minus means an equivalent change already exists in upstream; a plus means it does not.
# Which commits on main since the release cut have no equivalent on release/2.4?
git cherry -v origin/release/2.4 origin/main "$(git merge-base origin/release/2.4 origin/main)"
# - 4e7a91c Fix double discount on credit notes (already backported)
# + 91b03ee Add CSV export endpoint (feature: not expected on release)
# + c5d2e8a Fix timezone in invoice due dates (fix: missing!) The third argument limits the comparison to commits after the release cut. Without it, git cherry walks the whole history and the output is long and mostly irrelevant.
Step 2 β Understand the limit: adapted backports look missing Jump to heading
Patch IDs match only identical changes. A backport that was adapted to fit older code has a different diff and therefore a different patch ID, so git cherry marks the original as + even though it was handled. Combine both signals: a + commit that also has a -x record on the target was backported with changes.
target=origin/release/2.4
git cherry "$target" origin/main "$(git merge-base "$target" origin/main)" | awk '$1=="+"{print $2}' |
while read -r sha; do
if git log --format=%h --grep="cherry picked from commit $sha" "$target" | grep -q .; then
echo "adapted $(git log -1 --format='%h %s' "$sha")"
else
echo "MISSING $(git log -1 --format='%h %s' "$sha")"
fi
done # Verification: compute patch IDs directly to see why two commits do or do not match
git show 4e7a91c | git patch-id --stable
git show a8c21f0 | git patch-id --stable Step 3 β Filter to commits that should have been ported Jump to heading
Not every commit on main belongs on a release branch. Filter the MISSING list to fixes β by conventional commit type, by label, or by a trailer β so the report shows backport debt, not every feature.
# Keep only conventional-commit fixes and security changes
... | grep -E '^MISSING [0-9a-f]+ (fix|security|perf)(\(|:)' Step 4 β Check the other direction too Jump to heading
Fixes made directly on a release branch β hotfixes under pressure β must reach main, or the bug returns in the next release. Run the comparison the other way round.
# Commits on release/2.4 with no equivalent on main (should be forward-ported)
git cherry -v origin/main origin/release/2.4 "$(git merge-base origin/main origin/release/2.4)" | grep '^+' Anything listed is either a release-only change (a version bump, a branch-specific configuration) or a fix that main still lacks. The hotfix branches that do not drift from main pattern is designed to keep this list short.
Step 5 β Run it weekly and publish the result Jump to heading
A report nobody sees does nothing. Run the comparison for every supported release branch on a schedule and post the missing fixes where release managers look.
on: { schedule: [{ cron: "0 7 * * 1" }] }
jobs:
unported:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: |
for b in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin/release/); do
echo "## $b"; ./ci/unported.sh "$b" origin/main
done > report.md
- run: gh issue comment 77 --body-file report.md
env: { GH_TOKEN: "${{ github.token }}" } Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
How is git cherry different from git log --cherry-pick? Jump to heading
They use the same patch-ID matching. git log --cherry-pick --right-only release...main shows commits on main with no equivalent on release, with full log formatting options. git cherry is a simpler, script-friendly output of the same idea.
Does whitespace or line movement break the matching? Jump to heading
Patch IDs ignore whitespace differences and line numbers, so a change applied at a different position in the file still matches. Any change to the actual added or removed lines produces a different ID.
Can merge commits be compared this way? Jump to heading
git cherry skips merge commits. If you backport whole pull requests with -m 1, compare against the pull requestsβ individual commits, or rely on the -x records for those.
Related Jump to heading
- Cherry-Pick & Backporting β the parent topic.
- Recording Backports with cherry-pick -x β the records that identify adapted backports.
- When to Cherry-Pick vs Backport a Full Branch β choosing the approach in the first place.
- Maintaining Release Branches for Patch Versions β the branches this report watches.