Backporting a topic branch with git
As a maintainer of Plone and Dexterity, I frequently find myself with the need to merge a pull request not only to the master branch, but also to a maintenance branch used for bugfix releases to older versions of the software.
When the pull request involves a single commit, it's pretty straightforward: I merge the pull request to master through github's UI, or via the command line with git merge. Then I check out the maintenance branch and use "git cherry-pick" to apply the changeset from the commit relative to the older branch.
But this quickly gets annoying if the branch I'm trying to merge involved multiple commits, and I have to cherry-pick each one in turn. So here's a different approach I used yesterday to backport a changeset from zedr (Rigel di Scala) to add an Italian translation to plone.dexterity.
His pull request was against the master branch, so I first merged that using the github UI.
Then I needed to apply the same change to the 1.x branch of plone.dexterity.
In my local copy of the plone.dexterity repository, I added zedr's fork as a remote, and fetched it.
$ git remote add zedr https://github.com/zedr/plone.dexterity.git
$ git fetch zedr
* [new branch] 1.x -> zedr/1.x
* [new branch] davisagli-extend-fsschema -> zedr/davisagli-extend-fsschema
* [new branch] jbaumann-locking -> zedr/jbaumann-locking
* [new branch] jmehring-drafts -> zedr/jmehring-drafts
* [new branch] master -> zedr/master
* [new branch] toutpt-unicode -> zedr/toutpt-unicode
Next I created a new branch called "zedr-merge" specifically for carrying out the merge, based on zedr's forked master branch which contained the change. I needed a temporary branch for this in order to carry out the rebase in the next step.
$ git co -b zedr-merge zedr/master
Branch zedr-merge set up to track remote branch master from zedr.
Switched to a new branch 'zedr-merge'
Now for the fun part. I used rebase to modify the zedr-merge branch's history so that it contains the commits from the 1.x branch, followed by only the existing commits from the zedr-merge branch that I wanted.
$ git rebase -i --onto 1.x a36f40743d67da8e6d5c7b0aee81e786a2de9f5e
Let's break this down. The rebase command will first store all commits on zedr-merge from a36f40743d67da8e6d5c7b0aee81e786a2de9f5e to HEAD in a temporary location (I found this hash using git log to identify the last commit prior to the changes I was trying to backport). Next it resets the history and state of the zedr-merge branch to be equivalent to that of the 1.x branch (because of the "--onto 1.x"). Finally it reapplies the changes that were stored in the temporary location. The end result is that we have exactly the history we want—that is, the 1.x history plus the relevant portion of the zedr/master history—but it is on the zedr-merge branch rather than on 1.x where we want it to end up. We'll deal with that in a moment.
Notice one more thing about the rebase command. I used the -i flag, which means interactive rebase. This means that I'll be prompted in an editor with a list of commits, and can choose to "pick" (include), remove, or "squash" (merge into the prior commit) each commit. In my case, since zedr had a number of commits making small changes to his translation file that were really all part of the same change at the macro level (adding the Italian translation file), I squashed them all together so that I ended up with a single commit at the HEAD of the zedr-merge branch which accomplished the same changes as all of the commits zedr had made on his master branch.
pick 8562549 Added Italian translation
squash d035596 Correct timestamps
squash 9fa9904 Removed template header
squash 4a97700 cancel does not really mean that; fixed (thanks gborelli)
squash aef40f1 messags now coherent with the standard Italian translations found elsewhere
squash 621e185 Updated changelog
# Rebase a36f407..621e185 onto 9408d8d
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
At this point, since I had a single commit that I cared about on zedr-merge, it was a simple matter to use git cherry-pick to apply it to the 1.x branch instead.
$ git co 1.x
$ git cherry-pick 6dec9429d7994f02e332e22f8009a687ee3944c0
And now git log shows the change all together nicely in one commit:
$ git log 1.x
Author: zedr <email@example.com>
Date: Mon Nov 21 12:29:32 2011 +0100
Added Italian translation
Removed template header
cancel does not really mean that; fixed (thanks gborelli)
messags now coherent with the standard Italian translations found elsewhere
This is really the story of my first realization of the power of git rebase. But one word of warning: you should not rebase commits if they have been shared (i.e. by pushing to github) and others may have based further work on them. It can lead to duplicate commits in the history if the derivative branch is later merged as well. In my case this is not a concern, though, since the maintenance branch will never be merged back to master.
I really don't know if this is the best method for my use case, but it at least had the end effect I was going for in this case. I'm still not quite sure what the simplest approach would be if I wanted to end up with all the commits from zedr/master separately in the history of the 1.x branch, instead of squashing them. I'd be interested to hear if other people are using different approaches.