Migrating from Subversion to Git Jump to heading

A Subversion conversion is the one migration where the source and the result have no shared vocabulary. Subversion branches are directory copies, its tags are also directory copies, its revisions are repository-wide rather than per-file, and its authors are usernames with no email address. Converting is well-trodden, but the defaults produce a repository where every commit is attributed to jsmith <jsmith@localhost> and the branches are remote-tracking refs nobody can push. This recipe produces a clean result, within repository migration and consolidation.

When to use this approach Jump to heading

  • A Subversion repository still holds history you need to keep.
  • Authorship must be attributable to real people afterwards.
  • Branches and tags created in Subversion should exist as Git refs.
  • You can accept that commit ids are new β€” nothing carries over.
  • If you only need the current files, export the working copy and commit it once; the conversion below is about history.

Step 1 β€” Build the author map before anything else Jump to heading

Identities are baked in at conversion time. Fixing them afterwards means a second rewrite.

# Every author in the Subversion history
svn log --quiet https://svn.example.com/repo \
  | awk '/^r[0-9]+/ {print $3}' | sort -u > /tmp/svn-authors.txt
wc -l /tmp/svn-authors.txt
# The map format: svn-username = Real Name <email>
{
  while read -r u; do printf '%s = %s <%[email protected]>\n' "$u" "$u" "$u"; done
} < /tmp/svn-authors.txt > authors.txt

# Then correct it by hand β€” this is the part that needs a person
head -5 authors.txt
# Verification: no entry still has a placeholder address
grep -c '@example.com' authors.txt
What Subversion means against what Git meansSubversion branches and tags are directory copies, so the converter has to be told which directories are which. Revisions are repository-wide numbers rather than content hashes, and authors are bare usernames with no address.SubversionGitbrancha directory copya ref to a committagalso a directory copyan annotated objectrevisiona global numbera content hashauthora usernamename and emailthe converter guesses the first two from layout and cannot guess the fourth at all

Step 2 β€” Mirror the Subversion repository locally Jump to heading

Converting over the network is slow enough that a failure halfway through is expensive. A local mirror makes reruns cheap.

svnadmin create /tmp/svn-mirror
printf '#!/bin/sh\nexit 0\n' > /tmp/svn-mirror/hooks/pre-revprop-change
chmod +x /tmp/svn-mirror/hooks/pre-revprop-change

svnsync init file:///tmp/svn-mirror https://svn.example.com/repo
svnsync sync file:///tmp/svn-mirror
# Verification: the mirror holds every revision
svnlook youngest /tmp/svn-mirror
svn info https://svn.example.com/repo | grep '^Revision'

Step 3 β€” Convert, telling the tool about the layout Jump to heading

# Standard trunk/branches/tags layout
git svn clone file:///tmp/svn-mirror \
  --stdlayout \
  --authors-file=authors.txt \
  --no-metadata \
  converted
# Non-standard layouts need explicit paths
git svn clone file:///tmp/svn-mirror \
  --trunk=main-line --branches=feature-branches --tags=releases \
  --authors-file=authors.txt --no-metadata converted
# Verification: commit count should match the revision count reasonably closely
git -C converted rev-list --count --all
svnlook youngest /tmp/svn-mirror

--no-metadata drops the git-svn-id trailer from every message. Keep it only if you intend to keep synchronising with Subversion; for a one-way migration the trailer is noise that outlives its usefulness.

Step 4 β€” Turn Subversion’s refs into real Git refs Jump to heading

The conversion leaves branches and tags as remote-tracking refs, which nobody can use directly.

cd converted

# Tags: Subversion tag directories become refs/remotes/origin/tags/*
git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes/origin/tags |
while read -r ref sha; do
  name=${ref#origin/tags/}
  git tag -a "$name" -m "Imported from Subversion tag $name" "$sha"
  git update-ref -d "refs/remotes/$ref"
done
# Branches: everything else under refs/remotes/origin
git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes/origin |
while read -r ref sha; do
  name=${ref#origin/}
  [ "$name" = trunk ] && continue
  git branch "$name" "$sha" 2>/dev/null
  git update-ref -d "refs/remotes/$ref"
done
git branch -m trunk main 2>/dev/null || git branch -M main
# Verification: real refs, no leftovers
git tag | wc -l
git branch | wc -l
git for-each-ref refs/remotes | wc -l      # expect 0
From a Subversion mirror to a usable Git repositoryThe mirror makes reruns cheap. The conversion applies the author map and the layout. The ref reconstruction turns Subversion's directory copies into real branches and annotated tags, and only then is the result worth verifying.svnsync mirrorlocal, rerunnablegit svn cloneauthors maplayout flagsRebuild refstags and branchesVerifytrees and filesskipping the third box leaves a repository whose branches nobody can push Which layout flags your repository needsA repository using the conventional trunk, branches and tags directories converts with one flag. Anything else must have its paths named explicitly, and a repository with no branch directories at all should be converted as a single line of history.How is the Subversion repository laid out?trunk, branches, tags--stdlayoutone flagcustom directory namesName each path--trunk --branches --tagsno branches at allConvert trunk onlysingle line of historyguessing wrong here produces branches that look like directories in the result

Step 5 β€” Verify against the source Jump to heading

# The working tree at the tip must match a Subversion export exactly
svn export file:///tmp/svn-mirror/trunk /tmp/svn-export --quiet
diff -r --exclude=.git --exclude=.svn /tmp/svn-export converted && echo "trees identical"
# Spot-check an old revision too, not only the tip
rev=500
svn export -r "$rev" file:///tmp/svn-mirror/trunk /tmp/svn-r500 --quiet
sha=$(git -C converted log --format='%H' --all | tail -n +1 | sed -n "${rev}p")
git -C converted archive "$sha" | tar -t | head -3
# And that authorship is real
git -C converted shortlog -sne --all | head

SAFETY WARNING β€” do not switch off the Subversion server the day the conversion completes. Keep it read-only for at least a release cycle: conversions routinely surface a branch nobody mentioned or a tag referenced by an old build, and with the source still readable those are lookups rather than losses. Announce the freeze, redirect writes, and archive rather than delete.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why is the commit count different from the revision count? Jump to heading

Subversion revisions are repository-wide, so a revision that touched only a branch directory or created a tag produces no commit on the converted trunk. Expect the Git count to be lower, and verify by comparing trees rather than counts.

Can we keep committing to Subversion during the conversion? Jump to heading

You can, using git svn fetch to pick up new revisions, but running two systems in parallel is where conversions go wrong. A short freeze is almost always cheaper than a period where both are writable and someone has to reconcile them.

What about Subversion externals? Jump to heading

They have no Git equivalent and are not converted. Decide per external whether it becomes a submodule, a subtree, or a vendored copy β€” the trade-offs are in pinning and updating Git submodules safely and converting a submodule to a subtree.