Migrating from SVN to Git, preserving branches and tags

Migrating from SVN to Git, preserving branches and tags

SVN was a great advance in its day, but it’s now clear that distributed version control systems are the way forward and that Git is the de facto standard. Having helped many clients migrate from SVN to Git, here are my notes for a pain-free transition that will preserve the tags and branches in your SVN repository.

While round-tripping between SVN and Git is entirely possible using git svn, I shall not discuss it here because my best advice is simply to embrace Git and leave SVN behind.

Contents

First import to a local staging repo

Examine the staging repo

Convert the SVN tags and branches to local Git tags and branches

Test pushing and cloning locally

Push your staging repo to the real host

Cleanup

First import to a local staging repo

Create a local staging directory

cd ~
mkdir staging
cd staging

Obviously you don’t need to call it ‘staging’. This directory can be wherever you like, you can always move it later. The rest of this document assumes that this is your current directory.

Initialise git svn

For a standard SVN layout

git svn init SVN_URL --stdlayout --prefix=svn/

Here SVN_URL is the fully qualified URL for the directory under which the standard, case-sensitive SVN directories trunk, branches and tags reside.

I highly recommend the option --prefix=svn/. It means that all the existing svn branches and tags will be prefixed with svn/, which helps prevent Git users from mistaking them for “native” Git branches and tags.

For a non-standard SVN layout

Suppose you had decided to depart from the standard and capitalise your “Trunk”, “Branches” and “Tags” dirs in SVN. No matter, we can use this:

git svn init SVN_URL -T Trunk -b Branches -t Tags --prefix=svn/

This syntax lets you describe any non-standard SVN layout. The -b and -t parts can be repeated as many times as necessary. See man git-svn for details.

Optional: review the config

git config --local --list

Among other output you will see a section like this:

svn-remote.svn.url=svn://svn.example.com
svn-remote.svn.fetch=some/path/trunk:refs/remotes/svn/trunk
svn-remote.svn.tags=some/path/tags/*:refs/remotes/svn/tags/*

Advanced users can tweak the config at this stage before performing the fetch.

Fetch from SVN to the staging repo

git svn fetch

Depending on the size of the repo and your bandwith, this can take anything from a minute to some hours. This initial fetch is particularly slow because git svn has to do a lot of O(N^2) work to figure out and translate the history. Rest assured that subsequent fetches will be dramatically faster, mere seconds usually.

When this command at last completes, you will have a fully-fledged Git repo that you can examine with the command line or your preferred Git GUI. But resist the temptation to make any changes to it just yet.

Back to Contents

Examine the staging repo

git status

You should see:

# On branch master
nothing to commit (working directory clean)

Now try:

git branch -a

You should see something like:

* master
  remotes/svn/tags/0.1.0
  remotes/svn/tags/0.2.0
  remotes/svn/tags/0.3.0
  remotes/svn/tags/0.4.0
  remotes/svn/trunk

Note that the SVN tags and branches (in this case there weren’t any branches) exist only as remote references. In the next section we’ll fix that up.

Back to Contents

Convert the SVN tags and branches to local Git tags and branches

SVN branches

To convert the remote SVN branches to local Git branches, use:

for branch in `git branch -r | grep "branches/" | sed 's/ branches\///'`; do
  git branch $branch refs/remotes/$branch
done

Yeah, I’m not a fan of bash script either, but it gets the job done.

SVN tags

Here it’s really a matter of taste and/or house policy whether you convert them to Git tags or Git branches, because in Git they’re much the same thing: bookmarks into Git’s directed acyclic graph of commit objects.

To convert the remote SVN tags to local Git tags, use:

for tag in `git branch -r | grep "tags/" | sed 's/ tags\///'`; do
  git tag -a -m"Converting SVN tags" $tag refs/remotes/$tag
done

To convert the remote SVN tags to local Git branches, use:

for tag in `git branch -r | grep "tags/" | sed 's/ tags\///'`; do
  git branch $tag refs/remotes/$tag
done

Back to Contents

Test pushing and cloning locally

Before pushing to the real host, it would be wise to test things by pushing to a local Git repo and then cloning the result.

Make a local bare Git repo

In Git parlance, a ‘bare’ repo is one that has no working copy.

cd ~
mkdir test
cd test
git init --bare

You now have a bare repo at ~/test.

Push to the test Git repo

cd ~/staging
git remote add test `~/test`
git push --all test
git push --tags test

We put ~/test in backquotes to expand it to an absolute path. If you’re giving an absolute path or a URL, omit the backquotes.

Despite its name, the --all option doesn’t push tags, so we have to push them separately.

Clone from the test Git repo

cd ~
mkdir aclone
cd aclone
git clone ~/test

There should now be a clone with a working copy in ~/aclone/test. Examine it and satisfy yourself that it’s all good. Assuming so, we can now go ahead and push to the real host.

Back to Contents

Push your staging repo to the real host

This assumes an admin on the real host (GitHub, Unfuddle, etc) has set up an empty Git repo for you.

I shall take Unfuddle as an example, in which case the URL will be something like git@example.unfuddle.com:example/blah.git

cd ~/staging
git remote add unfuddle REAL_HOST_URL
git push --all unfuddle
git push --tags unfuddle

In the above example I decided to name the remote ‘unfuddle’ rather than the default of ‘origin’. You can use whatever name you like, of course. Simply change it in all three lines.

Back to Contents

Cleanup

Remove the test remote

cd ~/staging
git remote rm test

Now the staging repo has forgotten all about our ‘test’ remote.

Discard the test and clone repos

cd ~
rm -rf aclone
rm -rf test

Either keep or delete the staging repo

If you envisage any need to round-trip between Git and SVN, I strongly recommend keeping the staging repo as it will save you the very time-consuming initial git svn fetch.

If on the other hand you are certain that the SVN repo is end-of-life, you can delete the staging repo:

cd ~
rm -rf staging

Back to Contents

That’s all folks! I hope that this article has been useful. Comments, questions and errata can be sent to @RichardBuckle on Twitter or App.net.

Shameless plug: I am available for contract iOS and Mac development. If you are interested in hiring me, please see http://www.sailmaker.co.uk/.

Updates:

  • 2013-05-06: Combined the git push commands into one line, from feedback by @alblue.
  • 2014-01-14: Reverted the above because git push does not allow combining the --all and --tags flags.