Skip to main content
CodeOath
← All posts

Git95 min total · 17 parts

Git Internals and Workflows: Branching, Merging, and Rebasing

Part 10 of 17 · ~3 min

Cherry-Picking Commits

While your branch is in review, a clinic calls. They are still on version 2.2 — they have not upgraded, which is the entire reason release/2.2 is still a branch — and their payroll export is dropping the last row of every pay period. It is an off-by-one in the export loop, it is twelve characters to fix, and it is costing somebody money every fortnight.

You fix it on main, because that is where fixes belong:

git switch main
# fix src/payroll/export.js
git commit -m "Stop the payroll export from dropping the final row"
# → b51e6c2

Now it has to reach release/2.2, and merging main into release/2.2 is emphatically not an option: main also contains the timezone rewrite, the virtualised roster grid, and a half-reviewed shift-swap feature. Those clinics are getting a twelve-character bug fix, not four months of changes.

Point git cherry-pick at a single commit anywhere in the project's history, and it plucks out just that commit's changes, then rebuilds them as a fresh commit sitting on whichever branch you're currently on.

git switch release/2.2
git cherry-pick -x b51e6c2
[release/2.2 7ab3f09] Stop the payroll export from dropping the final row
 1 file changed, 1 insertion(+), 1 deletion(-)

-x is worth making a habit. It appends a line to the new commit's message recording where the change came from:

Stop the payroll export from dropping the final row

(cherry picked from commit b51e6c2d7f4a83c05e19b6220ac8df31e9704b6a)

Six months on, when somebody asks why this fix is on a release branch and whether it ever made it to main, that line is the answer. Without it, the two commits have no visible relationship at all — and that is because they genuinely do not have one.

Which is the important caveat. A cherry-picked commit is a new commit with a different hash, for exactly the reason a rebased commit is: different parent, different hash. Git has no built-in way to recognize that one commit is just a copy of another's content — as far as its object model is concerned, they are two unrelated commits that happen to carry identical text.

Most of the time this costs you nothing. It starts costing you when the two branches are later merged, because Git compares snapshots against a shared ancestor and finds the same change present on both sides, arrived at independently. Often it resolves that silently and correctly. Sometimes — particularly when the surrounding lines have since drifted apart — you get a conflict that looks absurd, where both sides appear to contain the same fix and Git is asking you to choose. Recognising it is most of the battle: a conflict whose two sides are near-identical is usually a cherry-pick coming home.

Cherry-pick handles conflicts with the same vocabulary as everything else, and — like rebase, and for the same reason — HEAD during the conflict is the branch you are picking onto:

git cherry-pick --continue
git cherry-pick --abort
git cherry-pick --skip

It also takes ranges, which is how you move a run of commits without moving the branch they live on:

git cherry-pick 6e2ac18..b51e6c2   # exclusive of the first, inclusive of the last
git cherry-pick 6e2ac18^..b51e6c2  # include 6e2ac18 itself

At which point it is worth asking whether you want git rebase --onto instead, which does the same job in one operation and is covered in the workflows chapter.

One practical note about how you got here: you were mid-edit on swap.js when the clinic called, and git switch main with a dirty working tree either refuses or drags your changes along. What you actually did was stash them, which is two chapters away.