Tuesday, February 17, 2009

Moving from Mercurial to Subversion

Somehow all the world seems to move Subversion repositories into Mercurial but not the other way around.

In many ways I understand that, but I had to move my own little project stored in a local Mercurial repository onto the official infrastructure, which happens to use Subversion. That cost me first some time searching for a migration solution and then hacking one myself.

Interestingly the main issues I had in writing my migration script where mostly Subversion related. While hg sometimes required parsing some output to get to particular information (scary if I should ever need to reuse this script again), svn threw a few more problems at me:
  • svn returns zero unless something really went wrong. Even doing "svn status" on a non-SVN location is perfectly ok according to the return code
  • it is really easy to create a working copy that confuses svn, up to the point where any standard command fails due to supposed locking issues, while a "svn cleanup" as proposed by the other commands just whinges about something not being a working copy without actually fixing anything
  • generally error reporting seems arbitrary at times
  • some commands have weird behavior, most noticeably "svn add" recurses into new folders, but not existing ones. Somehow I expected recursion to be unconditional or at least have an option to do so

The one weird thing Mercurial does is to somehow move existing .svn folders around into new directories (I believe that happens only if the directory is a result of a move). Once that happened the SVN working copy is entirely broken until those folders are removed.

After running it against the central repository I also figured that I probably should have passed the original commit timestamp along in the commit message. I'd certainly add that if I would ever do it again since it is easily extracted from the "hg log" command. Similarly the original committer could be passed along, which in my case just didn't matter since it matched anyway.

Here is the resulting script, which worked for me. Since it is hacked together it is probably not reusable straight away, but could be useful as inspiration or basis of the next hack. Read the comments.


# DANGER: written as a once-off script, not suitable for naive consumption. Use at
# your own peril. It has been tested on one case only.
# Potential issues for reuse:
# * no sanity checks anywhere (don't call it with the wrong parameters)
# * no handling of branches
# * certain layout of the results of hg commands is assumed
# * all commits come from the user running the script
# * move operations probably won't appear in the history as such
# * we assume the SVN side doesn't change
# Also note: .hgignore will be checked in and probably contains some entries
# that should be added to svn:ignore after the operation

HG_SOURCE=$1 # the source is a normal hg repository
SVN_TARGET=$2 # the target is a folder within a SVN working copy (e.g. /trunk)

export QUIET_FLAG=-q # -q for quiet, empty for verbose

echo Converting Mercurial repository at $HG_SOURCE into Subversion working copy at $SVN_TARGET


hg init .

pushd $HG_SOURCE
TIP_REV=`hg tip | head -1 | sed -e "s/[^ ]* *\([^:]*\)/\1/g"`
popd # out of $HG_SOURCE

for i in `seq $TIP_REV`
echo "Fetching Mercurial revision $i/$TIP_REV"
hg $QUIET_FLAG pull -u -r $i $HG_SOURCE
# in the next line use sed since 'tail --lines=-5' leaves too much for one-line messages
HG_LOG_MESSAGE=`hg -R $HG_SOURCE -v log -r $i | sed -n "6,$ p" | head --lines=-2`
echo "- removing deleted files"
for fileToRemove in `svn status | grep '^!' | sed -e 's/^! *\(.*\)/\1/g'`
svn remove $QUIET_FLAG $fileToRemove
echo "- removing empty directories" # needed since Mercurial doesn't manage directories
for dirToRemove in `svn status | grep '^\~' | sed -e 's/^\~ *\(.*\)/\1/g'`
if [ "X`ls -am $dirToRemove`" = "X., .., .svn" ]
rm -rf $dirToRemove # remove first, otherwise working copy is broken for some reason only SVN knows
svn remove $QUIET_FLAG $dirToRemove
echo "- adding files to SVN control"
# 'svn add' recurses only into new folders, so we need to recurse ourselves
for fileToAdd in `svn status | grep '^\?' | grep -v "^\? *\.hg$" | sed -e 's/^\? *\(.*\)/\1/g'`
if [ -d $fileToAdd ]
# Mercurial seems to copy existing directories on moves or something like that -- we
# definitely get some .svn subdirectories in newly created directories if the original
# action was a move. New directories should never contain a .svn folder since that breaks
for accidentalSvnFolder in `find $fileToAdd -type d -name ".svn"`
rm -rf $accidentalSvnFolder
svn add $QUIET_FLAG $fileToAdd
echo "- committing"
echo "- done"

popd # out of $SVN_TARGET

Somehow it seems I'll have to avoid Mercurial a bit longer until I'm in an environment where I know it is supported. It's not such a surprising result, but a real pity since I quite like it so far.

1 comment:

Misttar said...

Nice work, I used this for a base to do my own conversion. I posted my updated script here: