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.

Comparing branches by hash and by patch IDA hash comparison lists every cherry-picked fix as missing, because backports have new hashes. A patch-ID comparison recognises that the backport contains the same change and lists only commits that truly have no equivalent.git log release..maingit cherry release mainbackported fixlisted as missingmarked - (present)un-backported fixlistedmarked + (missing)adapted backportlistedmarked + (looks missing)patch IDs understand cherry-picks β€” except ones that were changed on the way

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)(\(|:)'
From git cherry to an actionable reportgit cherry lists commits without an equivalent on the release branch. Commits with a -x record there are marked adapted rather than missing. The remainder is filtered to fixes, and the final list is what the release manager reviews.git cherry+ / - by patch ID-x lookupadapted, not missingFilterfix / security onlyReporttrue backport debteach stage removes a kind of false alarm

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 }}" }
How backport debt surfacesA fix merges to main on Monday without a backport label. The weekly report lists it as missing on the release branch the following Monday. The release manager backports it that day, and the next report no longer shows it.Fix mergedno backport labelMon wk1Reportlisted as MISSINGMon wk2Backported-x recordedMon wk2Reportno longer listedMon wk3at most a week between a forgotten backport and someone knowing about it

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.