Everything in this article will be based on the following (carefully constructed) example, which covers most of the Git log patterns that tend to occur on projects.

The example scenario that runs through this article is this example’s chronological Git commit history.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ git --no-pager log --oneline --graph --date-order
* f2c1619 (HEAD -> red) R6
*   e6899ea R5 merge branch 'blue' into 'red'
|\
* \   0979d45 R4 merge branch 'green' into 'red'
|\ \
| | * 186da41 (blue) B3
| * | c950910 (green) G3
* | | 17e2629 R3
| | * 69edfc9 B2
| * | 059425a G2
| * | 05719c8 G1
| | * ebb218d B1
| |/
* / 8c6595b R2
|/
* 6581ff8 R1
* 2787f8f (master) init commit

Quickly create the example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mkdir cherry-pick; cd cherry-pick/
git init
echo "init" >> init; git add -A; git commit -m "init commit"; sleep 1
git checkout -b red
echo "red" >> red; git add -A; git commit -m "R1"; sleep 1
git branch green
git branch blue
echo "red" >> red; git add -A; git commit -m "R2"; sleep 1
git checkout blue
echo "blue" >> blue; git add -A; git commit -m "B1"; sleep 1
git checkout green
echo "green" >> green; git add -A; git commit -m "G1"; sleep 1
echo "green" >> green; git add -A; git commit -m "G2"; sleep 1
git checkout blue
echo "blue" >> blue; git add -A; git commit -m "B2"; sleep 1
git checkout red
echo "red" >> red; git add -A; git commit -m "R3"; sleep 1
git checkout green
echo "green" >> green; git add -A; git commit -m "G3"; sleep 1
git checkout blue
echo "blue" >> blue; git add -A; git commit -m "B3"; sleep 1
git checkout red
git merge green -m "R4 merge branch 'green' into 'red'"; sleep 1
git merge blue -m "R5 merge branch 'blue' into 'red'"; sleep 1
echo "red" >> red; git add -A; git commit -m "R6"; sleep 1

git --no-pager log --oneline --graph --date-order

A diagram of the current Git commit history is shown below.

git commit history diagram

The basic principle of the Git cherry-pick command is to migrate commits to a target version based on the diff information in the commit that the user has selected. A typical application of this feature is to apply hotfixes to other LTS releases.

The general usage of git cherry-pick is as follows.

1
git cherry-pick [options] <commit>...

Here the <commit>... is the commit (set) that the user wants to port, which is the main point of this article.

<commit> can be either a single commit or a revision range. If it is a revision range, the command resolves all commits in that revision range into a single commit. cherry-pick can accept multiple <commit>s at the same time, which is similar to the -no-walk behavior in git rev-list.

So let’s explore the case where <commit>... is a single commit and revision range.

Single commit

Normal commit

Going back to the example above, if we only need to pick out G2, we can do this.

1
2
3
4
5
# Go back to master and create a new branch for testing
$ git checkout master
$ git checkout -b cp-single-normal-commit
# The committed SHA value for G2 is 059425a
$ git cherry-pick 059425a

In this case, a merge conflict will occur and the output is shown below.

1
2
3
4
5
CONFLICT (modify/delete): green deleted in HEAD and modified in 059425a (G2). Version 059425a (G2) of green left in tree.
error: could not apply 059425a... G2
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

This tells us this information: the green file does not exist in the current (staged) version of HEAD, but it exists in the selected G2 commit. If you need the file, use git add to commit it to the staging area, or use git rm to discard the green file if you want to keep the current staging version in its current state, i.e. delete it.

We want to keep the green file after we select G2, so we do the following.

1
2
3
4
# commit green to staging area
$ git add green
# All merge conflicts have been fixed, continue with cherry-pick
$ git cherry-pick --continue

At this point the cherry-pick operation is complete. If you continue with git cherry-pick --continue, you will see error: no cherry-pick or revert in progress, which means that no cherry-pick task is currently in progress.

If you look at the current commit log, you will see that G2 is already on our current branch, cp-single-normal-commit.

1
2
3
$ git --no-pager log --oneline --graph --date-order
* 0457362 (HEAD -> cp-single-normal-commit) G2
* 2787f8f (master) init commit

Merge commit

What if we want to select a merge commit, for example, R4.

1
2
3
4
5
# Go back to master and create a new branch for testing
$ git checkout master
$ git checkout -b cp-single-merge-commit
# The commit SHA value for R4 is 0979d45
$ git cherry-pick 0979d45

After executing this cherry-pick command, you will get the following output.

1
2
error: commit 0979d45f1b46f72730188c5c01b3f2c7f41b18e6 is a merge but no -m option was given.
fatal: cherry-pick failed

By default, cherry-pick does not handle merge commits and reports an error. This is because in a merge commit, there are multiple parents, and Git doesn’t know which parent to use as the mainline.

The error message also tells us that if you want to select a merge commit, you need to use the -m (or -mainline) option to specify which parent is the mainline.

With the git show command, you can get multiple parents of a merge commit, numbered from 1. Since the mainline parent we need to pick in this example is R3(17e2629), we choose -m 1 in cherry-pick.

1
2
3
4
5
6
7
$ git --no-pager show 0979d45
commit 0979d45f1b46f72730188c5c01b3f2c7f41b18e6
Merge: 17e2629 c950910
Author: Triple-Z <me@triplez.cn>
Date:   Thu Mar 31 01:29:31 2022 +0800

    R4 merge branch 'green' into 'red'

Let’s try it again.

1
$ git cherry-pick -m 1 0979d45

cherry-pick is done! If you look at the current commit record, you will see that a new R4 commit has been generated on the cp-single-merge-commit branch.

1
2
3
$ git --no-pager log --oneline --graph --date-order
* 987aba7 (HEAD -> cp-single-merge-commit) R4 merge branch 'green' into 'red'
* 2787f8f (master) init commit

Now let’s go back to what just happened with -m 1. If you look at the files on the cp-single-merge-commit test branch now, you will see green and no red.

1
2
3
4
$ ls -lh
total 16
-rw-r--r--  1 triplez  staff    18B  4  7 19:06 green
-rw-r--r--  1 triplez  staff     5B  3 31 01:29 init

This is because when we pick the merge commit, we are using mainline 1, the red branch. So the fact that cherry-pick is based on red and is looking for differences between the mainline 2 green branch and red, the changes made on the green branch are the ones selected.

Revision range

There are several ways to represent a revision in Git, but here we’ll focus on the revision range.

For revision range, there are six ways to represent it.

  1. ^<rev> : denotes exclusion of <rev> and all its reachable parent commits.

  2. <r1>..<r2>: equivalent to ^r1 r2, i.e. includes <r2> and its reachable parent commit, and excludes <r1> and its reachable parent commit.

    If you need to include <r1>, you can use this writing style: <r1>^..<r2> .

  3. <r1>..<r2>: includes all <r1> or <r2> and their reachable parent commits, and excludes the common parent commits reachable by both <r1> and <r2>.

  4. <rev>^@ : includes all parents of <rev>, but excludes <rev> itself.

  5. <rev>^! : includes <rev> itself, but excludes all parents of <rev>. That is, it indicates a single <rev> commit.

    Note: <rev> (for <rev> and all its parents) is different in the context of revision range than <rev>^! . Both are considered identical only if the --no-walk argument is specified (both denote only <rev> itself).

  6. <rev>^-[<n>] : Includes <rev> and all its parents, but excludes the <n>th parent of <rev> and all its reachable parents. The default value of <n> is 1.

It seems complicated, so let’s use the scenario in the text to give two examples of range representations.

First, consider the case where <r1> and <r2> are both on the same branch, such as G1 (05719c8) and G3 (c950910).

1
2
3
4
5
# Go back to master and create a new branch for testing
$ git checkout master
$ git checkout -b cp-range-same-branch
# The commit SHA value of G1 is 05719c8, and the SHA value of G3 is c950910.
$ git cherry-pick 05719c8^..c950910

The meaning of 05719c8(G1)^. .c950910(G3) should mean.

  • includes G3 and all of its parents.
  • and excludes all parents of G1 (does not exclude G1).

The result should therefore be a selection of all commits from G1 to G3, schematically shown below, with the included nodes in yellow and the excluded nodes in gray.

git cheery-pick G1^..G3

Let’s take a look at the current commits.

Bingo! The three commits G1, G2 and G3 have been selected.

1
2
3
4
5
$ git --no-pager log --oneline --graph --date-order
* 32eac39 (HEAD -> cp-range-same-branch) G3
* d3b1130 G2
* c82c4c7 G1
* 2787f8f (master) init commit

What about <r1> and <r2> on different branches?

Let’s take G1 (05719c8) and B2 (69edfc9) as use cases.

1
2
3
4
5
# Go back to master and create a new branch for testing
$ git checkout master
$ git checkout -b cp-range-diff-branch
# The commit SHA value of G1 is 05719c8 and the SHA value of B2 is 69edfc9
$ git cherry-pick 05719c8^..69edfc9

The meaning of 05719c8(G1)^. .69edfc9(B2) should mean.

  • includes B2 and all its parents.
  • and excludes all parents of G1 (does not exclude G1).

Since B2 and all of its parents do not include G1. Therefore we can interpret G1^. B2 as the result of including B2 and all its parents, and excluding the common parent of B2 and G1. Naturally, only B1 and B2 are left as commits. The diagram is as follows, with the included nodes in yellow and the excluded nodes in gray.

git cherry-pick G1^..B2

Let’s look at the current commit record again, and indeed only two commits are selected, B1 and B2.

1
2
3
4
$ git --no-pager log --oneline --graph --date-order
* e63f214 (HEAD -> cp-range-diff-branch) B2
* aed6717 B1
* 2787f8f (master) init commit

Rerere

Rerere is “reuse recorded resolution”, which is a method to simplify conflict resolution.

If you do a lot of merge, rebase or cherry-pick, or are maintaining a branch that is different from the trunk for a long time, it is highly recommended to enable the rerere feature.

Enabling rerere is very simple and requires only one global configuration.

1
$ git config --global rerere.enabled true

You can also create a .git/rr-cache folder directly in your local repository to enable rerere for that repository.

What’s next

In the process of writing this article, I also came across the series of Stop cherry-picking, start merging articles written by Raymond Chen of Microsoft, in which he mentions the pitfall that cherry-picking may cause in many engineering practices. In the following time, I will read the series of articles one by one and analyze whether cherry-picking can bring us enough benefits in common software development workflows and whether we should stop cherry-picking, start merging according to the cases in the articles.