04 Jan 2009

Commit a linear git history to subversion

Update: Deskin points out that there is a commit to git that fixes this problem by adding a --root option to rebase --onto. So when you get a new build of git, you can probably ignore everything here.

The other day I needed to commit a plain old (git-svn free) git repository to subversion. Why, you ask? I had been working on a small project at work in my own little git repository and needed to get it into our official version control. I could, of course, just commit the latest version that I had, but that would not record any of my git commit history in subversion. I needed a better option. git-svn can save each individual commit of a linear commit history to subversion, so I figured I could just apply git-svn metadata to my repository and go from there.

This post is a bit long, as I explain the whole process for the solution. If you’re interested in just the solution, scroll to the bottom.

I’m going to create a small git repository for illustrative purposes: $ git init Initialized empty Git repository in /Users/bdimchef/testgit/.git/

For this howto, I’m using a blank subversion repository, but you can use this method with any subversion repository that has an empty directory to put your new project.

$ svnadmin create /Users/bdimchef/testsvn
$ svn mkdir file:///Users/bdimchef/testsvn/trunk
$ svn mkdir file:///Users/bdimchef/testsvn/branches
$ svn mkdir file:///Users/bdimchef/testsvn/tags

Then, I added my svn metadata to my git repository and fetched the contents of svn:

$ git svn init -s file:///Users/bdimchef/testsvn
$ git svn fetch

Now I was ready to commit:

$ git svn dcommit
Can't call method "full_url" on an undefined value at /opt/local/libexec/git-core/git-svn line 425.

What this error means (in our case) is that it can’t figure out where to dcommit. If we try to rebase, it’s clearer what the problem is:

$ git svn rebase
Unable to determine upstream SVN information from working tree history

Our problem is that we have two disjoint histories in our git repository: The history that we made in git, and the history from our svn repository. Since they share no common ancestors, git svn can’t figure out where to commit its changes. Take a look at gitk to see what’s going on here.

It turns out that this problem is pretty easy to fix with svn rebase. But there is one little trick that will get you if you’re not careful. And before we get carried away doing too many rebases, lets just make a backup branch of master just in case.

$ git branch master.bak master

Lets try one approach. git rebase --onto A B C takes the commit range B..C and puts it onto A. In this case, we want to take all our commits and put them on to trunk:

$ git rebase --onto trunk master~2 master
Applying: added bar
Applying: added baz

Oops! That only applied bar and baz. It forgot about our first commit, foo. Lets go back one more.

$ git rebase --onto trunk master~3 master
fatal: Needed a single revision
invalid upstream master~3

Well, that didn’t work either. The problem is that git rebase --onto rebases the range beginning after commit B. In this case, the commit after foo, which is bar. And there is no commit before foo, so there’s no way to rebase a range starting with foo included. The simple solution to this problem is to git cherry-pick foo first, then do the rebase. First we’ll create a named branch for our rebase.

The Solution

$ git co -b svnrebase trunk                    # create a temporary branch
$ git cherry-pick master~2                     # cherry pick the first commit
$ git rebase --onto svnrebase master~2 master  # rebase the 2nd through current commit
$ git svn dcommit                              # finally commit the results to svn

And that worked! So, generally, the secret to joining two separate histories together is to cherry-pick the first commit, and then rebase the rest on top of it.

Thanks to charon on #git for the cherry-pick idea.

blog comments powered by Disqus