Resolving cherry-pick conflicts on older branches Jump to heading
A fix that applies cleanly to main often conflicts on a release branch cut six months ago. The conflict is rarely about the fix itself. It is about everything that happened on main between the release cut and the fix: a function was renamed, a module was split, a dependency was upgraded. The fix was written against that newer code. Resolving the conflict means understanding what the fix depends on and either bringing those dependencies along or rewriting the fix for the old code. Doing it carelessly produces a backport that compiles and quietly does not fix the bug. This page walks through the analysis, within cherry-pick and backporting.
When to use this approach Jump to heading
git cherry-pickstops with conflicts when applying a fix from main to a release branch.- The release branch is several months behind main, with significant refactoring in between.
- You maintain more than one supported version, as in supporting multiple maintained versions.
- The decision to cherry-pick rather than merge a branch has already been made; if not, see when to cherry-pick vs backport a full branch.
Step 1 — Cherry-pick with the context you will need Jump to heading
Use -x so the backport records its origin, and make sure conflict markers include the base. In a cherry-pick, the base is the fix commit’s parent on main, which is exactly the “before” the fix was written against.
git switch release/2.3
git config merge.conflictStyle zdiff3
git cherry-pick -x 4e7a91c
# CONFLICT (content): Merge conflict in src/billing/invoice.py Step 2 — Find what the fix depends on Jump to heading
Look at the commits that changed the conflicted lines on main between the release cut and the fix. Those are the candidates for missing prerequisites.
cut=$(git merge-base release/2.3 main)
fix=4e7a91c
# Commits on main, after the cut and before the fix, touching the conflicted file
git log --oneline "$cut..$fix^" -- src/billing/invoice.py
# Narrow to the specific lines the fix changes
git log --oneline -L '/def apply_discount/,+30:src/billing/invoice.py' "$cut..$fix^" The -L form traces the history of a function or line range, which is usually far more precise than file-level history. Each commit it shows changed the lines the fix touches.
# Verification: compare the function as main had it before the fix with the release branch's version
git show "$fix^:src/billing/invoice.py" > /tmp/main-before-fix.py
git show release/2.3:src/billing/invoice.py > /tmp/release.py
diff -u /tmp/release.py /tmp/main-before-fix.py | head -40 Step 3 — Decide: bring prerequisites, or adapt the fix Jump to heading
Two ways forward, and the right one depends on the prerequisite.
# Option A: bring a small prerequisite first, in order
git cherry-pick --abort
git cherry-pick -x 91b03ee 4e7a91c # helper, then fix
# Option B: adapt — resolve by applying the fix's intent to the old code
git diff "$fix^" "$fix" -- src/billing/invoice.py # read what the fix actually does
# then edit the conflicted region so the old code gains the same behaviour Step 4 — Resolve by re-applying intent, not by merging text Jump to heading
When adapting, do not treat the conflict as “choose between two versions”. The release branch’s version is correct for the release branch; the fix’s version is correct for main. What you want is the release branch’s code with the fix’s behavioural change applied. Read the fix’s diff, then make the equivalent change by hand.
<<<<<<< HEAD
total = sum(line.amount for line in lines)
if customer.discount:
total -= total * customer.discount
||||||| parent of 4e7a91c (Fix double discount on credit notes)
total = calculate_total(lines, customer)
=======
total = calculate_total(lines, customer, skip_discount=invoice.is_credit_note)
>>>>>>> 4e7a91c (Fix double discount on credit notes) The fix adds “skip the discount for credit notes”. The release branch inlines the discount logic, so the adapted version guards the inline code:
total = sum(line.amount for line in lines)
if customer.discount and not invoice.is_credit_note:
total -= total * customer.discount git add src/billing/invoice.py
git cherry-pick --continue # -x appends "(cherry picked from commit 4e7a91c…)" Add a sentence to the commit message saying the fix was adapted, so the next person comparing branches knows the code differs on purpose.
Step 5 — Test the backport on the old code, including the original bug Jump to heading
A backport is new code on that branch. Run the release branch’s test suite, and port the fix’s regression test too — adapting it if needed — so you have evidence the bug is fixed on this version, not just that nothing else broke.
git show "$fix" --stat | grep -i test # the fix's own test, if any
git checkout "$fix" -- tests/billing/test_credit_notes.py # bring the test, then adapt
pytest tests/billing -q Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Should I just backport the refactor too, to keep branches similar? Jump to heading
Rarely. A release branch exists to change as little as possible. A refactor brings risk unrelated to the bug, and if it introduces a problem, it does so in the version customers are running. Adapt the fix instead.
How do I backport to several older branches at once? Jump to heading
Start with the newest release branch and work backwards, using each adapted backport as the source for the next older one. Each step crosses less history, so conflicts stay smaller.
What if the bug does not exist on the old branch? Jump to heading
Check before backporting: reproduce it with the regression test on the release branch first. Code that never had the bug does not need the fix, and applying it anyway adds risk for nothing.
Related Jump to heading
- Cherry-Pick & Backporting — the parent topic.
- Cherry-Picking Hotfixes Across Release Branches — the routine case without heavy drift.
- Tracing a Function History with git log -L — the tracing technique used in step 2.
- Recording Backports with cherry-pick -x — keeping the link between fix and backport.