Automatically backport commits using git
For those who just want the result please scroll to the bottom, otherwise you can read the story.
I started looking into how to backport commits since I know how easily git forward ports commits and figured there had to be a better way then the tricks and manual methods recommended in documentation and various results from google. My original plan was to make a plugin that would re-roll patches that need forward porting automatically on drupal.org and if possible backport as well. Over the course of an hour and half I found a variety of snippets that got me part of the way there, developed a basic idea of how to accomplish backporting in git, and slowly refined it to built-in commands. Huge thanks to cbreak and FauxFaux in #git on freenode!
The basic idea I started from was to simply remove all the commits between the commit you want to patch on top of and the last commit in the tree. To test my approach I started by creating a simple situation representing 7.x -> 8.x using the following.
$ git checkout -b test 8.x
$ git mv core/CHANGELOG.txt core/CHANGELOG2.txt
$ git commit -m "move"
$ echo "hello world" > core/CHANGELOG2.txt
$ git commit -am "edit"
I then proceeded to remove the 2nd to last commit ("move") to see if the last commit ("edit") would be updated to edit core/CHANGELOG.txt instead of core/CHANGELOG2.txt.
$ git rebase -i HEAD ~3 # commented out the "move" commit and saved
$ git show
To my delight the "edit" commit was indeed updated.
The next step was to figure out how to remove all the commits between the one to be backported and the last point at which 7.x and 8.x "last overlap" or at the point 8.x was branched (those are not necessarily the same and in Drupal's case they are not). I first envisioned dumping git rebase -i to a file, editing with script to remove commits and then rebasing. To that end it was suggested to change the GIT_EDITOR environment variable to a grep command and then use git rebase -i which would work, but the better approach is to use --onto which does exactly that.
$ git rebase --onto=A B
In simplest terms: remove all commits between A and B non-inclusively.
Now I simply needed a way to find the best point to merge onto. There are several snippets in stackexchange and elsewhere that find the "oldest ancestor" which would work, but a) are not optimal, and b) require long snippets that are not easy to understand. When I dug around in #git I was pointed to git merge-base which finds the best point for such an operation.
$ git merge-base 7.x 8.x
Finds the last common commit between the 7.x and 8.x branches. Using this we get the following.
$ git rebase --onto=`git merge-base 7.x 8.x` HEAD~1
Backport the latest commit onto the merge-base for 7.x and 8.x. Finally, we just need to forward port the patch to the end of the 7.x branch which is easy.
$ git rebase 7.x
Result
So putting it all together we get the following powerful one-liner that will backport the last commit in the 8.x branch to the 7.x branch.
$ git rebase --onto=`git merge-base 7.x 8.x` HEAD~1 && git rebase 7.x
If you want to backport something other than the last commit simply checkout a branch with that commit as the HEAD.
$ git checkout -b backport-branch commit-to-backport
This can also be extended to backport a patch straight on drupal.org by downloading and committing the patch first.
$ your facorite download method (wget, curl, etc)
$ git apply some.patch
$ git commit -am "patch to backport"
$ git rebase --onto=`git merge-base 7.x 8.x` HEAD~1 && git rebase 7.x
$ git show > some.patch
You can of course use a fancier git format-patch and what not, but this gets the point across. Enjoy!
Tags: