Mert Özen Further With Every Line
DevOps Git & GitHub — #19

Git cherry-pick: Moving a Single Commit to Another Branch

Mert Özen Nov 6, 2026 9 min 2 views
Git cherry-pick: Moving a Single Commit to Another Branch

We carry a hotfix from main to a release branch with git cherry-pick: why the new commit gets a new hash, author versus committer, leaving a trail with -x, picking a range of commits, --continue/--abort/--skip during a conflict, finding equivalent commits with git cherry and when cherry-pick becomes a smell.

Version note: The commands in this post were tested with Git 2.54.

Guess First

The bookstore repo has two branches. main keeps moving forward; release/1.0 represents the version running at the customer, and no new features go into it. Last week you fixed a bug in the cart total on main. Now the same fix has to land on release/1.0, but the new features that came after it must not.

$ git log --oneline --graph --all
* 706e4da (HEAD -> main) Reject negative prices
* 9986143 Add qty validation
* 2abb9e8 Round total to two decimals
* 287f848 Rename price to unitPrice
* 671b87a Fix total for items without qty
* e40e9bd Add search by title
* a7bb319 (release/1.0) Add .gitignore
* d6db968 Add cart total
* ff579a7 Add book list
* 7ff0565 Add README

The fix is 671b87a. When you carry it over to release/1.0, will the new commit there still have the hash 671b87a? Think about it before you read on; the rest of the post revolves around this question.

What Does Cherry-pick Do?

git merge brings in a branch's whole history. What you want is a single change out of that history. git cherry-pick does exactly that: it takes the change the given commit introduced, meaning the diff between that commit and its parent, and applies it as a new commit on the tip of the branch you are on.

The key word is "new". A commit's hash is computed from its content, its parent, its author and its dates. The commit created by cherry-pick has a different parent and a different commit date, so it has a different hash too. The answer to the guessing question: no, you will not see the same hash.

Think of copying a recipe from one notebook to another. The recipe is the same, but in the new notebook it sits on a different page, between different pages. To Git these are two separate pages.

Carrying a Hotfix to the Release Branch

First take a look at the commit you are about to carry. Moving a commit without knowing what it changes is like putting a box on another shelf without knowing what is inside.

$ git show --stat 671b87a
commit 671b87a8bbd577707a2e31d6e5df934d3909c774
Author: Ada Lovelace <ada@example.com>
Date:   Sat Nov 7 10:06:00 2026 +0300

    Fix total for items without qty

 src/cart.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

One file, one line. Switch to the target branch and pick the commit:

$ git switch release/1.0
Switched to branch 'release/1.0'
$ git cherry-pick 671b87a
[release/1.0 5e32f5f] Fix total for items without qty
 Date: Sat Nov 7 10:06:00 2026 +0300
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline -3
5e32f5f (HEAD -> release/1.0) Fix total for items without qty
a7bb319 Add .gitignore
d6db968 Add cart total

The new commit is 5e32f5f. The Date: line in the output can look odd at first: Git is reminding you that this commit's authorship date is not today. A commit carries two dates and two people:

$ git show -s --format=fuller HEAD
commit 5e32f5fd546ea330660777826ea169456e9600e5 (HEAD -> release/1.0)
Author:     Ada Lovelace <ada@example.com>
AuthorDate: Sat Nov 7 10:06:00 2026 +0300
Commit:     Ada Lovelace <ada@example.com>
CommitDate: Sat Nov 7 10:11:00 2026 +0300

    Fix total for items without qty

The author is the person who first wrote the change, and cherry-pick keeps it. The committer is the person who applied the commit to this branch. If you cherry-pick a fix Alan wrote, Alan stays the author and you become the committer. Nobody's work gets lost.

Picking the Same Commit Twice

An hour later you can't remember whether you carried the fix over, so you run the command again:

$ git cherry-pick 671b87a
The previous cherry-pick is now empty, possibly due to conflict resolution.
If you wish to commit it anyway, use:

    git commit --allow-empty

Otherwise, please use 'git cherry-pick --skip'
On branch release/1.0
You are currently cherry-picking commit 671b87a.
  (all conflicts fixed: run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

nothing to commit, working tree clean
$ git cherry-pick --skip
$ git status -s

Git tried to apply the change, ended up with an empty commit because the branch already had the same change, and stopped. --skip drops this commit and finishes the operation. You were lucky here, because the line was still unchanged. Had it changed in the meantime, the same command would have greeted you with a conflict. There is a better way to keep track of which commit went where.

Leaving a Trail with -x

The -x option appends the original commit's full hash to the end of the new commit's message. This time let's carry the two validation commits from main together. The range syntax is the same as in git log: A..B means the commits after A up to B; A itself is not included.

$ git cherry-pick -x 2abb9e8..706e4da
[release/1.0 46301d7] Add qty validation
 Date: Sat Nov 7 10:09:00 2026 +0300
 1 file changed, 5 insertions(+)
 create mode 100644 src/validate.js
[release/1.0 060fe43] Reject negative prices
 Date: Sat Nov 7 10:10:00 2026 +0300
 1 file changed, 5 insertions(+), 1 deletion(-)
$ git log -1
commit 060fe43547f42db0175fd6da048418ebbd98fcb8 (HEAD -> release/1.0)
Author: Ada Lovelace <ada@example.com>
Date:   Sat Nov 7 10:10:00 2026 +0300

    Reject negative prices
    
    (cherry picked from commit 706e4da750ab578951cdc4605e7abfe191bba3c2)

2abb9e8 was not part of the range; the first commit picked was 9986143. To include the first commit as well, write 2abb9e8^..706e4da; ^ means "the parent of this one". The commits are applied in order, oldest to newest.

The (cherry picked from commit ...) line saves your day six months later. When you look at a line on the release branch with git blame, you reach its original on main in one step. Make -x a habit for every cherry-pick onto release and hotfix branches. The only exception is picking from a local branch nobody else can see; there a reference to that hash is useless to everyone.

When a Conflict Shows Up: --continue, --abort, --skip

Now you want to carry the rounding commit. But that commit was written on top of the Rename price to unitPrice commit before it, and release/1.0 does not have that rename:

$ git cherry-pick -x 2abb9e8
Auto-merging src/cart.js
CONFLICT (content): Merge conflict in src/cart.js
error: could not apply 2abb9e8... Round total to two decimals
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
$ cat src/cart.js
function total(items) {
<<<<<<< HEAD
  return items.reduce((sum, item) => sum + item.price * (item.qty ?? 1), 0);
=======
  const sum = items.reduce((acc, item) => acc + item.unitPrice * (item.qty ?? 1), 0);
  return Math.round(sum * 100) / 100;
>>>>>>> 2abb9e8 (Round total to two decimals)
}

module.exports = { total };

You know this picture from the conflict post. The HEAD side is the line on release/1.0, the other side is the version from the commit you are trying to carry. Watch out: if you accept the commit as it is, unitPrice leaks into release/1.0, and since no book there has a unitPrice, the total becomes NaN. The right resolution combines the intent of both sides: take the rounding, keep the price name.

$ cat src/cart.js
function total(items) {
  const sum = items.reduce((acc, item) => acc + item.price * (item.qty ?? 1), 0);
  return Math.round(sum * 100) / 100;
}

module.exports = { total };
$ git add src/cart.js
$ git cherry-pick --continue
[release/1.0 3125d31] Round total to two decimals
 Date: Sat Nov 7 10:08:00 2026 +0300
 1 file changed, 2 insertions(+), 1 deletion(-)

--continue opens the commit message in your editor; the message and the -x line are already filled in, so just save and close. There are three ways out, and all of them are listed in the git status output. --continue records the resolution and moves on. --skip skips the current commit; if you are carrying a range, it continues with the rest. --abort puts everything back the way it was before the cherry-pick started:

$ git cherry-pick 287f848
Auto-merging src/cart.js
CONFLICT (content): Merge conflict in src/cart.js
error: could not apply 287f848... Rename price to unitPrice
...
$ git cherry-pick --abort
$ git status -s
$ git log --oneline -1
3125d31 (HEAD -> release/1.0) Round total to two decimals

If you get lost while resolving a conflict, --abort is always the safe exit. You start over with a calmer head.

The Cherry-pick Smell: The Same Change Twice

Now both branches have commits with the same messages but different hashes. Git can tell whether they are equivalent by looking at their patches:

$ git cherry -v main
- 5e32f5fd546ea330660777826ea169456e9600e5 Fix total for items without qty
- 46301d78e62685d1d0a57ddc08ec671af0110573 Add qty validation
- 060fe43547f42db0175fd6da048418ebbd98fcb8 Reject negative prices
+ 3125d31e4ff49fbd2c97f5835958a41c4f7d33a5 Round total to two decimals
$ git log --oneline --cherry-mark main...release/1.0
+ 3125d31 (HEAD -> release/1.0) Round total to two decimals
= 060fe43 Reject negative prices
= 46301d7 Add qty validation
= 5e32f5f Fix total for items without qty
= 706e4da (main) Reject negative prices
= 9986143 Add qty validation
+ 2abb9e8 Round total to two decimals
+ 287f848 Rename price to unitPrice
= 671b87a Fix total for items without qty
+ e40e9bd Add search by title

For each commit on release/1.0, git cherry checks whether main has a commit with the same patch: - means "yes", + means "no". --cherry-mark gives the same information while showing both sides together: = marks the ones with an equivalent, + the ones without. The rounding commit we resolved by hand came out as +, because its patch now differs from the original. Git cannot know the two carry the same intent; only the -x line tells you that.

This list also carries a warning. Having the same change twice in history is not a mistake on its own; it is normal for as long as a release branch lives. But if you keep moving commits between two long-lived feature branches, something is off. That change should probably have been merged into a shared branch. In a history like that the same lines meet again at every merge, the chance of conflicts goes up and git log becomes unreadable. Cherry-pick is a pair of tweezers, not a shovel.

That is why most teams follow an "upstream first" rule: the fix lands on main first, then gets carried to the release branches with -x. That way a fix always has one original record. On the CI side, the tests for that fix run on both branches separately; we will build that as a pipeline in the GitHub Actions series.

Common Pitfalls

Cherry-picking on the wrong branch. The command always writes to the tip of the branch you are on. Switch to the target with git switch first, then check the first line of git status.

Forgetting where the range starts. In A..B, A is not included. To see the commits one by one, run git log --oneline A..B first and look at the list.

Picking a dependent commit on its own. As in the rounding example, if the commit you carry leans on an earlier change you either get a conflict or, worse, broken code without any conflict. Read what the commit touches with git show before you carry it.

Cherry-picking a merge commit. A merge commit has two parents, so Git cannot know which one to diff against and asks for -m 1. We saw this in the revert post; the same rule applies to cherry-pick, but most of the time carrying the actual commits inside the merge is cleaner.

Skipping -x for shared commits. The line is missing later, and you end up comparing messages to find which commit was the original.

A Small Experiment

★☆☆ In your own bookstore repo, create a release/1.0 branch from main, make two more commits on main and carry the second one to the release branch with -x. Compare the author and committer dates with git show -s --format=fuller.

★★☆ Cherry-pick the same commit twice on purpose. Try it once with the line unchanged, and once with another commit touching the same line in between. Note which case gives you the empty-commit message and which one a conflict, and explain why.

★★★ Resolve a conflicting cherry-pick by hand, then look at the git cherry -v main and git log --cherry-mark output. Why does the resolved commit show up as +? Then try merging release/1.0 into main and guess which files will conflict.

Cherry-pick moves a single commit onto a new base. What if you want to rewrite all the commits on a branch, in order, on top of another commit? In the next post we do exactly that with git rebase: how it differs from merge, why the hashes change, the golden rule and --force-with-lease.