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.
$ 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.