Thursday, March 31, 2011

Using git cherry-pick

One Git command that I've found to be really useful lately is "git cherry-pick". It lets you take a single commit from one branch and apply it to another.

For example, say you're working on your "add_blinking_lights" feature branch, and you find a bug in the "rotate_missile_chamber" method. You fix it and commit to the branch between a couple of other feature commits.

It's going to be a while before you merge this feature back into your master branch, but you do want that bugfix to go live on the next deploy. So, on the feature branch, you do this:

cyborg[add_blinking_lights]% git log -3
e9dd159 Added tests for blink rate
a8e358c Fixed bug in rotate_missile_chamber
a7d3302 Added tests for lights turning on

OK, so you know the hash for the bugfix. Now you do "git checkout master."

On the master branch you run "git cherry-pick a8e358c". Here's what that command does:

  • Look at that commit on the feature branch
  • See what changes it introduced
  • Create a new commit on the master branch that introduces the same changes

Notice that it creates a new commit on the master branch. If, on master, you run "git log", you'll see a different hash for the same commit message. Why?

This is because of how Git models what a commit is. A commit is a complete snapshot of the whole repository, and the hash for a given commit reflects the state of every file in the whole directory - it is a hash of all their hashes.

So clearly, since master branch doesn't have all of the commits from the feature branch, a complete snapshot of it at the time the bugfix is applied will generate a different hash than a complete snapshot of the feature branch at the time the bugfix is applied there. Thus, different hashes.

But when you do merge the feature branch into master, that won't matter; the hashes for the individual file where you made the bugfix will be the same, because their contents will be the same, so there will be nothing to update on master for that file.

Of course, another way of approaching this problem would have been to switch to master before committing the bugfix in the first place, then merge master into the feature branch, or cherry-pick from master to the feature, or rebase the feature on master. But the method above is simple, and if you did already commit the bugfix on your branch, is the best way to get that commit into production.

3 comments:

  1. What do you do when your bugfix consists of more than one commit? Say that, in the example above, 'fixed bug in rotate_missile_chambers' was not a single commit, but rather a branch off of 'add_blinking_lights' consisting of several commits, and I want to merge the bugfix branch (but not everything else from 'add_blinking_lights') back into 'master'.

    ReplyDelete
  2. @gcbenison: good question. There are a couple of things you could do. One would be to just do multiple cherry-pick commands to grab each of the commits you need.

    If that seems too messy and you'd rather see those changes as a single commit on master, you could introduce an intermediary step: start by checking out master and create a branch from there called 'temporary_branch' or something like that. Then cherry-pick whichever commits are needed from 'add_blinking_lights' into the temporary branch. Finally, check out master and do 'git merge --squash temporary_branch'.

    The '--squash' part will tell git that, instead of bringing in the actual commits that the other branch has, it should generate a diff of the changes that the other branch has, then make those changes on master and stage them. You can then commit them on master as a single commit with a single, clear commit message.

    When you delete the temp branch, you will see that git tells you it has unmerged commits. That's true, because you didn't pull in its actual commits, but you did get the changes they introduce.

    I actually like to use the 'merge --squash' branch when merging in feature branches in general, because it lets me make a nice clean commit for the rest of the team to see instead of several commits with missteps, cleanup, etc. The only downside is that you'll want to delete that branch immediately afterwards, because otherwise it will be hard to tell if you've merged it in or not. If you've merged it in normally and try to merge again, git will just tell you 'already up to date'.

    ReplyDelete
  3. @gcbenison: Another, easier way to do it uses a rebase.

    1. git checkout add_blinking_lights -b bugfix_in_one_commit
    create a new branch from add_blinking_lights called bugfix_in_one_commit and check it out

    2. git rebase -i --onto head~10 head~10
    do an interactive rebase of this new branch, taking the last 10 commits and rebase them onto themselves. this actally lets you reorder, squash, delete, rename and edit commits. It will open an editor with all 10 commits and the word "pick" in front of them. You can reorder those lines as you please. So make sure, all of the bugfix commits are next to each other. then pick (or reword) the first and squash all others (write squash or s instead of pick in front of them).
    You will end up with ONE commit containing all your changes.

    3. Cherrypick this as Nathan described.

    4. Delete the new branch.

    The advantages are:
    Everything will happen in a new branch, your original branches will be safe and untouched.
    The interactive rebase is much easier then manually cherry picking multiple commits.

    ReplyDelete