stefan's blag and stuff

Blog – 2016-06-16 – Git cherrypick and git revert

Last Friday I dived into the internals of git cherry-pick and git revert. I am currently working on some git subtree merge improvements and was curious whether I can add an extra argument in cherry-pick. Searching for the internals of cherry-pick reveals the first surprising fact to me:

The usually method to find the command is to use find in the source directory:

$ cd ~/git/git/
$ git checkout v2.9.0-rc0
$ find -name '*cherry-pick*'
./t/ ...  # Testcases

Hmm. There are no source files like cherry-pick.c or something. Where is the code? Try grepping for it:

$ git grep cherry-pick
builtin/revert.c: * This implements the builtins revert and cherry-pick.
builtin/revert.c:   N_("git cherry-pick [<options>] <commit-ish>..."),
builtin/revert.c:   N_("git cherry-pick <subcommand>"),

Ah. The commands cherry-pick and revert are implemented in conjunction. In fact this makes senses. Both commands take an existing diff of a commit, apply it to the current source tree and create a new commit. The only differences is that revert inverses the diff before it is applied.

Jumping through the implementation I noticed that these commands are using the git internal merge algorithm to apply the diff to the source tree. This should be the reason why cherry-pick is so much better than git am or git apply. The last two commands fail at once when the source files are changed slightly, whereas cherry-pick can apply it successfully.

Handmade cherry-pick and revert

So here is a tutorial for a self made cherry-pick and revert command. The general idea is to use the recursive merge startegy to apply a diff onto the current tree. There is a plumbing command git merge-recursive, so we don't have to use git merge directly. For the cherry-pick usage the command looks like:

git merge-recursive <commit>^ -- HEAD <commit>

The above template means: Take the changes between <commit>^ and <commit> and place them onto HEAD. A full cherry-pick example follows:

First checkout the linux kernel source via tag v4.6-rc7. Then cherry pick the commit v4.6 (Linux Torvalds increments the version number) and create a commit with the same commit message.

$ git checkout v4.6-rc7
$ git merge-recursive v4.6^ -- HEAD v4.6
$ (echo -n Cherry picking: && git log -1 --pretty=format:%B v4.6) \
    | git commit -F -
$ git show  # look at your work

These commands are nearly equivalent to

$ git checkout v4.6-rc7
$ git cherry-pick v4.6
$ git show

The same can be done for git revert. Here the merge template is

git merge-recursive <commit> -- HEAD <commit>^

It means: Take the changes between <commit> and <commit>^ and place them onto top of HEAD. Pleas note that here caret character is reversed. Again an example in the linux kernel source tree follows:

$ git checkout v4.6-rc7
$ git merge-recursive v4.6-rc7 -- HEAD v4.6-rc7^
$ git commit -m "revert commit v4.6-rc7"
$ git show  # look at your work

These commands are equivalent to

$ git checkout v4.6-rc7
$ git revert v4.6-rc7
$ git show  # look at your work

Happy cherry picking.