Great work!

XP to next level

BugEater
EN

The Duplicate-Commit Trap

Learning Objectives

By the end of this lesson you will be able to:

  • Predict what happens when a cherry-picked branch is later merged
  • Use git cherry to list commits that are already present as copies
  • Read git log --left-right --cherry-mark to compare two branches
  • Choose a picking strategy that does not create duplicates

The Setup

This is the failure mode Module 3 has been pointing at since Lesson 1.

Monday: feature/refund-limit contains commit C, a fix you need urgently. You cherry-pick it onto release/4.2, where it becomes C'.

        C            feature/refund-limit
       /
A───B───────         develop
 \
  X───Y───C'         release/4.2

Three weeks later, feature/refund-limit is finished and merged into develop, and develop is merged into release/4.3, which was cut from release/4.2. Now C and C' — the same change, twice — are both in the ancestry of one branch.

What Actually Happens

Usually, surprisingly little. Git's merge is content-based: if C' already applied exactly the change C wants to make, the merge sees the lines are already in their target state and produces no conflict. The history contains both commits, but the files are correct.

That is the good case, and it is why this trap goes unnoticed for so long.

The bad cases are real, though:

Non-idempotent changes. A commit that appends a line to a list, increments a version, or adds an entry to a config array applies twice and produces two lines. Nothing conflicts. Nothing is flagged. The file is simply wrong.

Adapted picks. If you resolved a conflict while picking, C' is not identical to C. When C arrives on the merge, Git has two genuinely different versions of the same intent and conflicts — usually at the least convenient moment, in code nobody currently has in their head.

Confusing history. git log shows the same fix twice, with different IDs and dates. Anyone reconstructing a timeline — which is exactly what a tester does during a regression investigation — has to work out which one actually shipped.

Detecting It: git cherry

There is a purpose-built command for this, and almost nobody knows it:

git cherry -v release/4.2 feature/refund-limit
- 8f3c2a1 fix: clamp refund amount to configured limit
+ 4d9e7b2 test: cover the clamped-refund path

Read the symbols:

  • - — this commit's change is already present in release/4.2, under a different ID. Picking or merging it adds nothing.
  • + — this commit is genuinely missing from release/4.2.

Git determines this by comparing patch IDs: a hash of the change itself, independent of parent and metadata. Two commits with the same diff have the same patch ID even when their commit IDs differ.

This is the check to run before backporting a batch. Everything marked - is already there.

Comparing Two Branches

For a symmetric view:

git log --oneline --left-right --cherry-mark main...release/4.2

Note the three dots — this is the symmetric difference, "commits on either branch but not both".

= 8f3c2a1 fix: clamp refund amount        ← present on both, as copies
< 1a77c60 feat: add refund audit log      ← only on main
> 4d9e7b2 fix: correct rounding in totals ← only on release/4.2

= means a copy exists on the other side. < and > say which branch a commit is unique to.

This is one of the most useful commands in the whole trail for a tester. "What is in this release that is not in main?" is a question you will be asked repeatedly, and this answers it in one line.

Avoiding the Trap

Prefer picking in the direction that will never be merged back. Picking a fix from develop onto a release branch is safe if that release branch is never merged into develop. Establish which way integration flows in your project, and pick against the flow.

When both directions merge, merge instead of picking. If release/4.2 will eventually flow back, do not pick from it — let the merge carry the change.

Pick a commit, not a resolution. The more you adapt a pick, the more C' diverges from C, and the more likely the eventual merge conflicts. If a fix needs heavy adaptation, consider writing it fresh on the target branch and referencing the original in the message.

Record the pick. -x puts the original ID in the message. When the duplicate is later noticed, that line is what turns a mystery into a two-second explanation.

Pro Tip: Before any backport batch, run git cherry -v <target> <source>. Ten seconds, and it removes the entire class of "I picked something that was already there".

Key Takeaways

  • A picked commit and its original are different commits containing the same change
  • Merging them together usually resolves silently, because Git merges by content
  • It fails for non-idempotent changes, and conflicts when the pick was adapted
  • git cherry -v <upstream> <head> marks with - what is already present as a copy
  • git log --left-right --cherry-mark A...B compares two branches symmetrically
  • Pick against the direction of integration; merge when the branches flow both ways
  • -x on every cross-branch pick makes duplicates explainable later

Quiz

A branch containing commit C is merged into a branch that already has C', a cherry-picked copy. What usually happens?

Which kind of change breaks in that "quiet success" case?

In git cherry -v release/4.2 feature/x, what does a - in front of a commit mean?

Which strategy avoids creating duplicates in the first place?