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.
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.
For a standard SVN layout
git svn init SVN_URL --stdlayout --prefix=svn/
SVN_URL is the fully qualified URL for the directory under which the standard, case-sensitive SVN directories
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.
You should see:
# On branch master nothing to commit (working directory clean)
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.
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.
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
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
Push to the test Git repo
cd ~/staging git remote add test `~/test` git push --all test git push --tags test
~/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.
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
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.
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
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/.
- 2013-05-06: Combined the
git pushcommands into one line, from feedback by @alblue.
- 2014-01-14: Reverted the above because
git pushdoes not allow combining the