Cherry-picking and Moving Commits
Copy a commit onto another branch and live with the duplicate: ranges, -x for backports, conflict handling, patch-id detection, and moving work with format-patch when there is no shared remote.
Copy a commit onto another branch and live with the duplicate: ranges, -x for backports, conflict handling, patch-id detection, and moving work with format-patch when there is no shared remote.
A production bug gets fixed on main at four in the afternoon. The
release branch, cut two weeks ago, needs that fix tonight and
nothing else — not the sixty commits that landed since the cut.
Merging main in brings every one.
Stashing parked work in time; this puts a change somewhere else entirely. By the end you will be able to copy a commit or a range onto another branch, escape a pick that conflicts, spot the duplicate you created before a merge does, and move commits between machines that share no remote.
A commit stores a snapshot, but Git can also read it as a
change: the difference between its tree and its parent's.
git cherry-pick applies that difference where HEAD stands and
commits the result.
git switch release-2.1
git cherry-pick 9f2c1abHere release-2.1 was cut at B and the fix is E:
before
main A---B---C---D---E E is the fix
\
release-2.1 R1---R2
after
main A---B---C---D---E untouched, E is still here
\
release-2.1 R1---R2---E' E's change, new commitThe prime mark on E' is the whole lesson in one character. A
commit's identity is a hash over its tree, parent, author and
committer fields, and message, so E' — new parent, new tree — is a
different commit making the same edit: a re-enactment rather
than a relocation.
Cherry-pick takes any commit-ish, not just a raw hash:
git cherry-pick 9f2c1ab # a specific commit
git cherry-pick feature-login # the tip of that branch
git cherry-pick v2.0.3 # the commit a tag points atRanges follow Git's half-open rule, where people lose commits
without noticing. In A..B, A is an exclusion point: you get
everything reachable from B but not A. Name its parent to
include it:
git cherry-pick 9f2c1ab..4d7e0aa # excludes 9f2c1ab
git cherry-pick 9f2c1ab^..4d7e0aa # includes 9f2c1ab^ fails only on a root commit, which has no parent to name. A
range applies oldest first, each commit becoming a new commit, so if
the fourth of six conflicts the first three are already committed
and Git stops with three left.
-n (long form --no-commit) applies the change and leaves it
staged. Over a range every pick accumulates in the index, so five
related commits can land as one:
Six months from now, someone reading release-2.1 will find a fix
and need to know whether it also exists on main. E' carries no
pointer back to E, so -x writes that link into the message:
That line is plain text rather than metadata, but it is greppable and survives forever.
Use -x when the source commit is public and permanent: backporting
onto a release or maintenance branch, or picking into a hotfix
branch that will be tagged. Skip it for a local branch you will
rebase, squash or delete — the hash would point at a commit nobody
else can look up, which is why -x is off by default.
A cherry-pick is a three-way merge in disguise: the merge base is
the picked commit's parent, "ours" is your HEAD, "theirs" is the
picked commit. It conflicts for the ordinary reason — the code here
differs from what the change was written against.
Git stops with conflict markers, records the commit being applied in
CHERRY_PICK_HEAD, and — for a range — keeps the to-do list in
.git/sequencer. Four ways out:
--abort and --quit both mean "stop" and disagree about the picks
that already succeeded. --abort rewinds the branch to where it
pointed before the first pick; --quit clears the in-progress state
and the to-do list but leaves history and working tree alone, so
commits already made stay made. Keep what worked: --quit. Undo the
lot: --abort.
If your resolution turns out identical to what is already there,
--continue refuses: nothing to commit. The change was already on
this branch, and git cherry-pick --skip is the right answer.
One change now sits on two branches under two hashes, with no record in Git relating them — harmless until those branches meet in a merge.
The merge is quiet.
Both sides made the same edit, the three-way merge finds they agree, and the result does almost nothing.
A phantom conflict.
Someone reformatted the file, or amended the fix after you picked it. Git sees two different edits to the same lines from a base that has neither, and stops.
The two sides are recognisably one fix in two spellings — and merging has no notion of two commits being the same patch.
Git can sometimes spot the duplicate. git patch-id hashes a diff's
content, ignoring line numbers, whitespace and commit metadata, so
two commits making the same change share a patch id despite
different hashes.
git cherry asks the same question: which of my commits already
exist over there? It marks each - when an equivalent patch is
upstream, + when not.
Inside a normal log, main...release-2.1 means "commits on either
side but not both", --left-right labels the side, and
--cherry-mark replaces that marker with = when the other side
has an equivalent:
--cherry is shorthand for the common case:
--right-only --cherry-mark --no-merges, answering "what is on my
branch that upstream lacks in any form".
Patch-id matching is exact, though: if you resolved a conflict
during the pick, or tweaked the change to fit the older branch, the
diff differs and Git will not pair them. Treat it as a heuristic —
one more reason to use -x, whose message line survives edits a
patch id does not.
Cherry-pick is right when one discrete change must reach a line of
history that cannot receive everything else: a hotfix backported
onto release-2.1, a security fix pushed into three maintenance
branches, one commit rescued from an abandoned experiment.
It is also the clean fix for a commit made on the wrong branch — copy it where it belongs, then drop it from where it does not:
That reset --hard discards the commit and any uncommitted changes
in your working tree, and rewrites main, so only do it while the
commit is local and unpushed.
The smell is cherry-pick as an integration strategy. When commits
reach main by being picked off feature branches rather than
merged, the histories fork: main accumulates copies, the branch
keeps the originals, the merge base never advances, and every merge
re-litigates changes already there. The signs are picking in both
directions and picking the same commit twice. Cherry-pick moves an
exception; merge moves the rule.
You can also avoid the duplicate entirely: fix on the oldest branch that needs the change and merge it forward into the newer ones. Then there is only ever one commit, the merge base advances, and no later merge has anything to be confused about.
Sometimes the destination is another machine with no shared remote in between: an air-gapped build box, a reviewer who wants the change as an email attachment. Git answered this before hosting existed — the kernel still works this way.
There are two routes, and choosing between them is the whole decision: one moves commits, the other content.
git format-patch is the commits route, writing one file per commit
in mbox format, numbered so the order survives the trip:
Each file is a plain-text email with the change below it:
That first date is not a bug and not the commit's date — it is a
fixed sentinel so mail tools recognise an mbox. The real date is the
Date: header.
git am ("apply mailbox") turns those files back into real commits
on the far machine, preserving author name, email, date and
message. You become the committer; the author stays.
A patch that does not apply cleanly stops the series, like a
cherry-pick, with --continue, --skip and --abort waiting.
First, try three-way mode:
A plain am matches surrounding context, so it fails outright if
the file has drifted. --3way (short form -3) uses the blob
identifiers in the patch's index line to reconstruct the sender's
version of the file and merge properly. It needs those blobs in your
repository and can leave conflicts — better than a refusal.
The content route skips commits altogether:
What arrives is edits, unstaged in the working tree as though you had typed them: no author, no message, no history.
So: format-patch and am when the work is committed and
authorship matters — contributing a change, handing a series to a
maintainer. diff and apply when you are shuttling work in
progress that the receiving side will commit.
If you want two branches checked out at once — building one while
editing the other — that is git worktree add: one repository,
several working directories, one history, covered in the advanced
course.
Next comes branching models for teams, the sequel to this lesson's last argument: whether a fix gets picked onto a release branch or merged forward from an older one is a decision a team makes once, not a command chosen in the moment.
Practise on a real repository first. Make a small commit on one
branch, cherry-pick it onto another with -x, then run
git cherry -v and git log --cherry-mark --left-right between the
two until one change wearing two hashes stops feeling strange. Then
run git format-patch -1 on that commit, delete the branch, and
bring it back with git am — watching the author survive the round
trip is the fastest way to believe what these tools preserve.
Fix off-by-one in retry backoff
(cherry picked from commit 9f2c1ab3e4d5f60718293a4b5c6d7e8f90123456)= 3b8f21c Fix off-by-one in retry backoff
< d40a7e9 Add connection pool metrics
> 22b4f80 Bump patch version to 2.1.3From 9f2c1ab3e4d5f60718293a4b5c6d7e8f90123456 Mon Sep 17 00:00:00 2001
From: Dana Okafor <dana@example.com>
Date: Tue, 4 Mar 2025 11:42:08 +0000
Subject: [PATCH 1/2] Fix off-by-one in retry backoff
The backoff loop ran one extra attempt.
---
src/retry.ts | 4 ++--
diff --git a/src/retry.ts b/src/retry.tsgit cherry-pick -n 9f2c1ab^..4d7e0aa
git status # five commits' worth, staged
git commit -m "Backport retry fixes from main"git cherry-pick -x 9f2c1ab# edit the files, then stage the resolution
git add src/retry.ts
git cherry-pick --continue # commit this one, carry on with the rest
git cherry-pick --skip # abandon this one, carry on with the rest
git cherry-pick --abort # undo everything, back to the start
git cherry-pick --quit # stop here, keep what already landedgit show 9f2c1ab | git patch-id --stablegit cherry -v main release-2.1git log --oneline --left-right --cherry-mark main...release-2.1git switch feature-checkout
git cherry-pick main # copy main's tip commit onto here
git switch main
git reset --hard HEAD~1 # drop it from maingit format-patch -1 9f2c1ab # one commit
git format-patch -3 # the last three commits
git format-patch main..feature # a branch, one file each
git format-patch main..feature -o /tmp/outgoing
git format-patch --stdout main..feature > series.mboxgit am 0001-fix-off-by-one-in-retry-backoff.patch
git am *.patch # a whole series, in ordergit am --3way 0001-fix-off-by-one-in-retry-backoff.patchgit diff > work.patch # unstaged changes only
git diff --staged > work.patch # staged changes only
git diff HEAD > work.patch # everything since the last commit
git apply --check work.patch # would it apply? changes nothing
git apply --stat work.patch # which files, how much
git apply work.patch # apply as uncommitted changes
git apply -3 work.patch # three-way fallback, as with amgit cherry-pick <commit> # copy one commit onto this branch
git cherry-pick <branch> # copy that branch's tip commit
git cherry-pick A..B # copy a range, excluding A
git cherry-pick A^..B # copy a range, including A
git cherry-pick -n <commit> # apply and stage, do not commit
git cherry-pick -x <commit> # record "(cherry picked from ...)"
git cherry-pick --continue # after staging a resolution
git cherry-pick --skip # drop this one, continue the range
git cherry-pick --abort # undo it all, back to the start
git cherry-pick --quit # stop, keep what already landed
git show <commit> | git patch-id --stable # content hash of a diff
git cherry -v <upstream> <head> # "-" = already there, "+" = not
git log --oneline --left-right --cherry-mark <a>...<b>
git log --oneline --cherry <upstream>...<head> # right side only
git format-patch -1 <commit> # one commit as a .patch file
git format-patch -3 # the last three commits
git format-patch main..feature # a branch as a numbered series
git format-patch main..feature -o <dir> # write them elsewhere
git am <file>.patch # apply, keeping author and date
git am *.patch # apply a whole series in order
git am --3way <file>.patch # merge properly if context drifted
git am --continue # after staging a resolution
git am --skip # drop this patch, continue
git am --abort # undo the whole series
git diff HEAD > work.patch # raw diff, no commit metadata
git diff --binary HEAD > work.patch # include binary changes
git apply --check work.patch # test it, change nothing
git apply --stat work.patch # what it would touch
git apply work.patch # apply as uncommitted changes
git worktree add ../dir <branch> # a second checkout, same repo