--- a/hgext/convert/__init__.py Wed Nov 07 21:10:30 2007 -0600
+++ b/hgext/convert/__init__.py Wed Nov 07 21:13:56 2007 -0600
@@ -5,12 +5,12 @@
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
-from common import NoRepo, SKIPREV, converter_source, converter_sink
+from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile
from cvs import convert_cvs
from darcs import darcs_source
from git import convert_git
from hg import mercurial_source, mercurial_sink
-from subversion import svn_source, debugsvnlog
+from subversion import debugsvnlog, svn_source, svn_sink
import filemap
import os, shutil
@@ -29,6 +29,7 @@
sink_converters = [
('hg', mercurial_sink),
+ ('svn', svn_sink),
]
def convertsource(ui, path, type, rev):
@@ -57,23 +58,10 @@
self.ui = ui
self.opts = opts
self.commitcache = {}
- self.revmapfile = revmapfile
- self.revmapfilefd = None
self.authors = {}
self.authorfile = None
- self.maporder = []
- self.map = {}
- try:
- origrevmapfile = open(self.revmapfile, 'r')
- for l in origrevmapfile:
- sv, dv = l[:-1].split()
- if sv not in self.map:
- self.maporder.append(sv)
- self.map[sv] = dv
- origrevmapfile.close()
- except IOError:
- pass
+ self.map = mapfile(ui, revmapfile)
# Read first the dst author map if any
authorfile = self.dest.authorfile()
@@ -161,16 +149,6 @@
return s
- def mapentry(self, src, dst):
- if self.revmapfilefd is None:
- try:
- self.revmapfilefd = open(self.revmapfile, "a")
- except IOError, (errno, strerror):
- raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
- self.map[src] = dst
- self.revmapfilefd.write("%s %s\n" % (src, dst))
- self.revmapfilefd.flush()
-
def writeauthormap(self):
authorfile = self.authorfile
if authorfile:
@@ -217,7 +195,7 @@
dest = SKIPREV
else:
dest = self.map[changes]
- self.mapentry(rev, dest)
+ self.map[rev] = dest
return
files, copies = changes
parents = [self.map[r] for r in commit.parents]
@@ -245,13 +223,13 @@
self.dest.copyfile(copyf, f)
newnode = self.dest.putcommit(filenames, parents, commit)
- self.mapentry(rev, newnode)
+ self.map[rev] = newnode
def convert(self):
try:
self.source.before()
self.dest.before()
- self.source.setrevmap(self.map, self.maporder)
+ self.source.setrevmap(self.map)
self.ui.status("scanning source...\n")
heads = self.source.getheads()
parents = self.walktree(heads)
@@ -281,7 +259,7 @@
# write another hash correspondence to override the previous
# one so we don't end up with extra tag heads
if nrev:
- self.mapentry(c, nrev)
+ self.map[c] = nrev
self.writeauthormap()
finally:
@@ -292,8 +270,7 @@
self.dest.after()
finally:
self.source.after()
- if self.revmapfilefd:
- self.revmapfilefd.close()
+ self.map.close()
def convert(ui, src, dest=None, revmapfile=None, **opts):
"""Convert a foreign SCM repository to a Mercurial one.
@@ -307,6 +284,7 @@
Accepted destination formats:
- Mercurial
+ - Subversion (history on branches is not preserved)
If no revision is given, all revisions will be converted. Otherwise,
convert will only import up to the named revision (given in a format
--- a/hgext/convert/common.py Wed Nov 07 21:10:30 2007 -0600
+++ b/hgext/convert/common.py Wed Nov 07 21:13:56 2007 -0600
@@ -1,7 +1,8 @@
# common code for the convert extension
-import base64
+import base64, errno
import cPickle as pickle
from mercurial import util
+from mercurial.i18n import _
def encodeargs(args):
def encodearg(s):
@@ -54,11 +55,8 @@
def after(self):
pass
- def setrevmap(self, revmap, order):
- """set the map of already-converted revisions
-
- order is a list with the keys from revmap in the order they
- appear in the revision map file."""
+ def setrevmap(self, revmap):
+ """set the map of already-converted revisions"""
pass
def getheads(self):
@@ -190,3 +188,105 @@
filter empty revisions.
"""
pass
+
+ def before(self):
+ pass
+
+ def after(self):
+ pass
+
+
+class commandline(object):
+ def __init__(self, ui, command):
+ self.ui = ui
+ self.command = command
+
+ def prerun(self):
+ pass
+
+ def postrun(self):
+ pass
+
+ def _run(self, cmd, *args, **kwargs):
+ cmdline = [self.command, cmd] + list(args)
+ for k, v in kwargs.iteritems():
+ if len(k) == 1:
+ cmdline.append('-' + k)
+ else:
+ cmdline.append('--' + k.replace('_', '-'))
+ try:
+ if len(k) == 1:
+ cmdline.append('' + v)
+ else:
+ cmdline[-1] += '=' + v
+ except TypeError:
+ pass
+ cmdline = [util.shellquote(arg) for arg in cmdline]
+ cmdline += ['<', util.nulldev]
+ cmdline = util.quotecommand(' '.join(cmdline))
+ self.ui.debug(cmdline, '\n')
+
+ self.prerun()
+ try:
+ return util.popen(cmdline)
+ finally:
+ self.postrun()
+
+ def run(self, cmd, *args, **kwargs):
+ fp = self._run(cmd, *args, **kwargs)
+ output = fp.read()
+ self.ui.debug(output)
+ return output, fp.close()
+
+ def checkexit(self, status, output=''):
+ if status:
+ if output:
+ self.ui.warn(_('%s error:\n') % self.command)
+ self.ui.warn(output)
+ msg = util.explain_exit(status)[0]
+ raise util.Abort(_('%s %s') % (self.command, msg))
+
+ def run0(self, cmd, *args, **kwargs):
+ output, status = self.run(cmd, *args, **kwargs)
+ self.checkexit(status, output)
+ return output
+
+
+class mapfile(dict):
+ def __init__(self, ui, path):
+ super(mapfile, self).__init__()
+ self.ui = ui
+ self.path = path
+ self.fp = None
+ self.order = []
+ self._read()
+
+ def _read(self):
+ try:
+ fp = open(self.path, 'r')
+ except IOError, err:
+ if err.errno != errno.ENOENT:
+ raise
+ return
+ for line in fp:
+ key, value = line[:-1].split(' ', 1)
+ if key not in self:
+ self.order.append(key)
+ super(mapfile, self).__setitem__(key, value)
+ fp.close()
+
+ def __setitem__(self, key, value):
+ if self.fp is None:
+ try:
+ self.fp = open(self.path, 'a')
+ except IOError, err:
+ raise util.Abort(_('could not open map file %r: %s') %
+ (self.path, err.strerror))
+ self.fp.write('%s %s\n' % (key, value))
+ self.fp.flush()
+ super(mapfile, self).__setitem__(key, value)
+
+ def close(self):
+ if self.fp:
+ self.fp.close()
+ self.fp = None
--- a/hgext/convert/darcs.py Wed Nov 07 21:10:30 2007 -0600
+++ b/hgext/convert/darcs.py Wed Nov 07 21:13:56 2007 -0600
@@ -1,6 +1,6 @@
# darcs support for the convert extension
-from common import NoRepo, commit, converter_source, checktool
+from common import NoRepo, checktool, commandline, commit, converter_source
from mercurial.i18n import _
from mercurial import util
import os, shutil, tempfile
@@ -17,9 +17,10 @@
except ImportError: ElementTree = None
-class darcs_source(converter_source):
+class darcs_source(converter_source, commandline):
def __init__(self, ui, path, rev=None):
- super(darcs_source, self).__init__(ui, path, rev=rev)
+ converter_source.__init__(self, ui, path, rev=rev)
+ commandline.__init__(self, ui, 'darcs')
if not os.path.exists(os.path.join(path, '_darcs', 'inventory')):
raise NoRepo("couldn't open darcs repo %s" % path)
@@ -42,7 +43,8 @@
output, status = self.run('init', repodir=self.tmppath)
self.checkexit(status)
- tree = self.xml('changes', '--xml-output', '--summary')
+ tree = self.xml('changes', xml_output=True, summary=True,
+ repodir=self.path)
tagname = None
child = None
for elt in tree.findall('patch'):
@@ -62,31 +64,9 @@
self.ui.debug('cleaning up %s\n' % self.tmppath)
shutil.rmtree(self.tmppath, ignore_errors=True)
- def _run(self, cmd, *args, **kwargs):
- cmdline = ['darcs', cmd, '--repodir', kwargs.get('repodir', self.path)]
- cmdline += args
- cmdline = [util.shellquote(arg) for arg in cmdline]
- cmdline += ['<', util.nulldev]
- cmdline = ' '.join(cmdline)
- self.ui.debug(cmdline, '\n')
- return util.popen(cmdline)
-
- def run(self, cmd, *args, **kwargs):
- fp = self._run(cmd, *args, **kwargs)
- output = fp.read()
- return output, fp.close()
-
- def checkexit(self, status, output=''):
- if status:
- if output:
- self.ui.warn(_('darcs error:\n'))
- self.ui.warn(output)
- msg = util.explain_exit(status)[0]
- raise util.Abort(_('darcs %s') % msg)
-
- def xml(self, cmd, *opts):
+ def xml(self, cmd, **kwargs):
etree = ElementTree()
- fp = self._run(cmd, *opts)
+ fp = self._run(cmd, **kwargs)
etree.parse(fp)
self.checkexit(fp.close())
return etree.getroot()
@@ -102,15 +82,15 @@
desc=desc.strip(), parents=self.parents[rev])
def pull(self, rev):
- output, status = self.run('pull', self.path, '--all',
- '--match', 'hash %s' % rev,
- '--no-test', '--no-posthook',
- '--external-merge', '/bin/false',
+ output, status = self.run('pull', self.path, all=True,
+ match='hash %s' % rev,
+ no_test=True, no_posthook=True,
+ external_merge='/bin/false',
repodir=self.tmppath)
if status:
if output.find('We have conflicts in') == -1:
self.checkexit(status, output)
- output, status = self.run('revert', '--all', repodir=self.tmppath)
+ output, status = self.run('revert', all=True, repodir=self.tmppath)
self.checkexit(status, output)
def getchanges(self, rev):
--- a/hgext/convert/filemap.py Wed Nov 07 21:10:30 2007 -0600
+++ b/hgext/convert/filemap.py Wed Nov 07 21:13:56 2007 -0600
@@ -128,7 +128,7 @@
self.children = {}
self.seenchildren = {}
- def setrevmap(self, revmap, order):
+ def setrevmap(self, revmap):
# rebuild our state to make things restartable
#
# To avoid calling getcommit for every revision that has already
@@ -143,7 +143,7 @@
seen = {SKIPREV: SKIPREV}
dummyset = util.set()
converted = []
- for rev in order:
+ for rev in revmap.order:
mapped = revmap[rev]
wanted = mapped not in seen
if wanted:
@@ -157,7 +157,7 @@
arg = None
converted.append((rev, wanted, arg))
self.convertedorder = converted
- return self.base.setrevmap(revmap, order)
+ return self.base.setrevmap(revmap)
def rebuild(self):
if self._rebuilt:
--- a/hgext/convert/subversion.py Wed Nov 07 21:10:30 2007 -0600
+++ b/hgext/convert/subversion.py Wed Nov 07 21:13:56 2007 -0600
@@ -17,9 +17,13 @@
import locale
import os
+import re
import sys
import cPickle as pickle
-from mercurial import util
+import tempfile
+
+from mercurial import strutil, util
+from mercurial.i18n import _
# Subversion stuff. Works best with very recent Python SVN bindings
# e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
@@ -28,6 +32,7 @@
from cStringIO import StringIO
from common import NoRepo, commit, converter_source, encodeargs, decodeargs
+from common import commandline, converter_sink, mapfile
try:
from svn.core import SubversionException, Pool
@@ -149,9 +154,9 @@
self.head = self.revid(self.last_changed)
self._changescache = None
- def setrevmap(self, revmap, order):
+ def setrevmap(self, revmap):
lastrevs = {}
- for revid in revmap.keys():
+ for revid in revmap.iterkeys():
uuid, module, revnum = self.revsplit(revid)
lastrevnum = lastrevs.setdefault(module, revnum)
if revnum > lastrevnum:
@@ -664,3 +669,198 @@
pool = Pool()
rpath = '/'.join([self.base, path]).strip('/')
return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
+
+pre_revprop_change = '''#!/bin/sh
+
+REPOS="$1"
+REV="$2"
+USER="$3"
+PROPNAME="$4"
+ACTION="$5"
+
+if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
+if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
+if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
+
+echo "Changing prohibited revision property" >&2
+exit 1
+'''
+
+class svn_sink(converter_sink, commandline):
+ commit_re = re.compile(r'Committed revision (\d+).', re.M)
+
+ def prerun(self):
+ if self.wc:
+ os.chdir(self.wc)
+
+ def postrun(self):
+ if self.wc:
+ os.chdir(self.cwd)
+
+ def join(self, name):
+ return os.path.join(self.wc, '.svn', name)
+
+ def revmapfile(self):
+ return self.join('hg-shamap')
+
+ def authorfile(self):
+ return self.join('hg-authormap')
+
+ def __init__(self, ui, path):
+ converter_sink.__init__(self, ui, path)
+ commandline.__init__(self, ui, 'svn')
+ self.delete = []
+ self.wc = None
+ self.cwd = os.getcwd()
+
+ path = os.path.realpath(path)
+
+ created = False
+ if os.path.isfile(os.path.join(path, '.svn', 'entries')):
+ self.wc = path
+ self.run0('update')
+ else:
+ if os.path.isdir(os.path.dirname(path)):
+ if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
+ ui.status(_('initializing svn repo %r\n') %
+ os.path.basename(path))
+ commandline(ui, 'svnadmin').run0('create', path)
+ created = path
+ path = 'file://' + path
+ wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
+ ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
+ self.run0('checkout', path, wcpath)
+
+ self.wc = wcpath
+ self.opener = util.opener(self.wc)
+ self.wopener = util.opener(self.wc)
+ self.childmap = mapfile(ui, self.join('hg-childmap'))
+
+ if created:
+ hook = os.path.join(created, 'hooks', 'pre-revprop-change')
+ fp = open(hook, 'w')
+ fp.write(pre_revprop_change)
+ fp.close()
+ util.set_exec(hook, True)
+
+ def wjoin(self, *names):
+ return os.path.join(self.wc, *names)
+
+ def putfile(self, filename, flags, data):
+ if 'l' in flags:
+ self.wopener.symlink(data, filename)
+ else:
+ try:
+ if os.path.islink(self.wjoin(filename)):
+ os.unlink(filename)
+ except OSError:
+ pass
+ self.wopener(filename, 'w').write(data)
+ was_exec = util.is_exec(self.wjoin(filename))
+ util.set_exec(self.wjoin(filename), 'x' in flags)
+ if was_exec:
+ if 'x' not in flags:
+ self.run0('propdel', 'svn:executable', filename)
+ else:
+ if 'x' in flags:
+ self.run0('propset', 'svn:executable', '*', filename)
+
+ def delfile(self, name):
+ self.delete.append(name)
+
+ def copyfile(self, source, dest):
+ # SVN's copy command pukes if the destination file exists, but
+ # our copyfile method expects to record a copy that has
+ # already occurred. Cross the semantic gap.
+ wdest = self.wjoin(dest)
+ exists = os.path.exists(wdest)
+ if exists:
+ fd, tempname = tempfile.mkstemp(
+ prefix='hg-copy-', dir=os.path.dirname(wdest))
+ os.close(fd)
+ os.unlink(tempname)
+ os.rename(wdest, tempname)
+ try:
+ self.run0('copy', source, dest)
+ finally:
+ if exists:
+ try:
+ os.unlink(wdest)
+ except OSError:
+ pass
+ os.rename(tempname, wdest)
+
+ def dirs_of(self, files):
+ dirs = set()
+ for f in files:
+ if os.path.isdir(self.wjoin(f)):
+ dirs.add(f)
+ for i in strutil.rfindall(f, '/'):
+ dirs.add(f[:i])
+ return dirs
+
+ def add_files(self, files):
+ add_dirs = [d for d in self.dirs_of(files)
+ if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
+ if add_dirs:
+ self.run('add', non_recursive=True, quiet=True, *add)
+ if files:
+ self.run('add', quiet=True, *files)
+ return files.union(add_dirs)
+
+ def tidy_dirs(self, names):
+ dirs = list(self.dirs_of(names))
+ dirs.sort(reverse=True)
+ deleted = []
+ for d in dirs:
+ wd = self.wjoin(d)
+ if os.listdir(wd) == '.svn':
+ self.run0('delete', d)
+ deleted.append(d)
+ return deleted
+
+ def addchild(self, parent, child):
+ self.childmap[parent] = child
+
+ def putcommit(self, files, parents, commit):
+ for parent in parents:
+ try:
+ return self.childmap[parent]
+ except KeyError:
+ pass
+ entries = set(self.delete)
+ if self.delete:
+ self.run0('delete', *self.delete)
+ self.delete = []
+ files = util.frozenset(files)
+ entries.update(self.add_files(files.difference(entries)))
+ entries.update(self.tidy_dirs(entries))
+ fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
+ fp = os.fdopen(fd, 'w')
+ fp.write(commit.desc)
+ fp.close()
+ try:
+ output = self.run0('commit',
+ username=util.shortuser(commit.author),
+ file=messagefile,
+ *list(entries))
+ try:
+ rev = self.commit_re.search(output).group(1)
+ except AttributeError:
+ self.ui.warn(_('unexpected svn output:\n'))
+ self.ui.warn(output)
+ raise util.Abort(_('unable to cope with svn output'))
+ if commit.rev:
+ self.run('propset', 'hg:convert-rev', commit.rev,
+ revprop=True, revision=rev)
+ if commit.branch and commit.branch != 'default':
+ self.run('propset', 'hg:convert-branch', commit.branch,
+ revprop=True, revision=rev)
+ for parent in parents:
+ self.addchild(parent, rev)
+ return rev
+ finally:
+ os.unlink(messagefile)
+
+ def puttags(self, tags):
+ self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
--- a/tests/test-convert-svn Wed Nov 07 21:10:30 2007 -0600
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-#!/bin/sh
-
-"$TESTDIR/hghave" svn svn-bindings || exit 80
-
-fix_path()
-{
- tr '\\' /
-}
-
-echo "[extensions]" >> $HGRCPATH
-echo "convert = " >> $HGRCPATH
-
-svnadmin create svn-repo
-
-echo % initial svn import
-mkdir t
-cd t
-echo a > a
-cd ..
-
-svnpath=`pwd | fix_path`
-# SVN wants all paths to start with a slash. Unfortunately,
-# Windows ones don't. Handle that.
-expr $svnpath : "\/" > /dev/null
-if [ $? -ne 0 ]; then
- svnpath='/'$svnpath
-fi
-
-svnurl=file://$svnpath/svn-repo/trunk
-svn import -m init t $svnurl | fix_path
-
-echo % update svn repository
-svn co $svnurl t2 | fix_path
-cd t2
-echo b >> a
-echo b > b
-svn add b
-svn ci -m changea
-cd ..
-
-echo % convert to hg once
-hg convert $svnurl
-
-echo % update svn repository again
-cd t2
-echo c >> a
-echo c >> b
-svn ci -m changeb
-cd ..
-
-echo % test incremental conversion
-hg convert $svnurl
-
-echo % test filemap
-echo 'include b' > filemap
-hg convert --filemap filemap $svnurl fmap
-echo '[extensions]' >> $HGRCPATH
-echo 'hgext.graphlog =' >> $HGRCPATH
-hg glog -R fmap --template '#rev# #desc|firstline# files: #files#\n'
-
-########################################
-
-echo "# now tests that it works with trunk/branches/tags layout"
-echo
-echo % initial svn import
-mkdir projA
-cd projA
-mkdir trunk
-mkdir branches
-mkdir tags
-cd ..
-
-svnurl=file://$svnpath/svn-repo/projA
-svn import -m "init projA" projA $svnurl | fix_path
-
-
-echo % update svn repository
-svn co $svnurl/trunk A | fix_path
-cd A
-echo hello > letter.txt
-svn add letter.txt
-svn ci -m hello
-
-echo world >> letter.txt
-svn ci -m world
-
-svn copy -m "tag v0.1" $svnurl/trunk $svnurl/tags/v0.1
-
-echo 'nice day today!' >> letter.txt
-svn ci -m "nice day"
-cd ..
-
-echo % convert to hg once
-hg convert $svnurl A-hg
-
-echo % update svn repository again
-cd A
-echo "see second letter" >> letter.txt
-echo "nice to meet you" > letter2.txt
-svn add letter2.txt
-svn ci -m "second letter"
-
-svn copy -m "tag v0.2" $svnurl/trunk $svnurl/tags/v0.2
-
-echo "blah-blah-blah" >> letter2.txt
-svn ci -m "work in progress"
-cd ..
-
-echo % test incremental conversion
-hg convert $svnurl A-hg
-
-cd A-hg
-hg glog --template '#rev# #desc|firstline# files: #files#\n'
-hg tags -q
-cd ..
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-sink Wed Nov 07 21:13:56 2007 -0600
@@ -0,0 +1,91 @@
+#!/bin/sh
+
+"$TESTDIR/hghave" svn svn-bindings || exit 80
+
+echo "[extensions]" >> $HGRCPATH
+echo "convert = " >> $HGRCPATH
+
+hg init a
+
+echo a > a/a
+echo % add
+hg --cwd a ci -d '0 0' -A -m 'add a file'
+
+echo a >> a/a
+echo % modify
+hg --cwd a ci -d '1 0' -m 'modify a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=2 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+cmp a/a a-hg-wc/a && echo same || echo different
+
+hg --cwd a mv a b
+echo % rename
+hg --cwd a ci -d '2 0' -m 'rename a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+
+hg --cwd a cp b c
+echo % copy
+hg --cwd a ci -d '3 0' -m 'copy a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+
+hg --cwd a rm b
+echo % remove
+hg --cwd a ci -d '4 0' -m 'remove a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+
+chmod +x a/c
+echo % executable
+hg --cwd a ci -d '5 0' -m 'make a file executable'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+test -x a-hg-wc/c && echo executable || echo not executable
+
+echo % branchy history
+
+hg init b
+echo base > b/b
+hg --cwd b ci -d '0 0' -Ambase
+
+echo left-1 >> b/b
+echo left-1 > b/left-1
+hg --cwd b ci -d '1 0' -Amleft-1
+
+echo left-2 >> b/b
+echo left-2 > b/left-2
+hg --cwd b ci -d '2 0' -Amleft-2
+
+hg --cwd b up 0
+
+echo right-1 >> b/b
+echo right-1 > b/right-1
+hg --cwd b ci -d '3 0' -Amright-1
+
+echo right-2 >> b/b
+echo right-2 > b/right-2
+hg --cwd b ci -d '4 0' -Amright-2
+
+hg --cwd b up -C 2
+hg --cwd b merge
+hg --cwd b revert -r 2 b
+hg --cwd b ci -d '5 0' -m 'merge'
+
+hg convert -d svn b
+echo % expect 4 changes
+(cd b-hg-wc; svn up; svn st -v; svn log --xml -v | sed 's,<date>.*,<date/>,')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-sink.out Wed Nov 07 21:13:56 2007 -0600
@@ -0,0 +1,251 @@
+% add
+adding a
+% modify
+1:10307c220ed9
+assuming destination a-hg
+initializing svn repo 'a-hg'
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+1 add a file
+0 modify a file
+At revision 2.
+ 2 2 test .
+ 2 2 test a
+<?xml version="1.0"?>
+<log>
+<logentry
+ revision="2">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="M">/a</path>
+</paths>
+<msg>modify a file</msg>
+</logentry>
+<logentry
+ revision="1">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="A">/a</path>
+</paths>
+<msg>add a file</msg>
+</logentry>
+</log>
+a:
+a
+
+a-hg-wc:
+a
+same
+% rename
+2:6e45a219686e
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 rename a file
+At revision 3.
+ 3 3 test .
+ 3 3 test b
+<?xml version="1.0"?>
+<log>
+<logentry
+ revision="3">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="D">/a</path>
+<path
+ copyfrom-path="/a"
+ copyfrom-rev="2"
+ action="A">/b</path>
+</paths>
+<msg>rename a file</msg>
+</logentry>
+</log>
+a:
+b
+
+a-hg-wc:
+b
+% copy
+3:d811dc81efbb
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 copy a file
+At revision 4.
+ 4 4 test .
+ 4 3 test b
+ 4 4 test c
+<?xml version="1.0"?>
+<log>
+<logentry
+ revision="4">
+<author>test</author>
+<date/>
+<paths>
+<path
+ copyfrom-path="/b"
+ copyfrom-rev="3"
+ action="A">/c</path>
+</paths>
+<msg>copy a file</msg>
+</logentry>
+</log>
+a:
+b
+c
+
+a-hg-wc:
+b
+c
+% remove
+4:045e93063aca
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 remove a file
+At revision 5.
+ 5 5 test .
+ 5 4 test c
+<?xml version="1.0"?>
+<log>
+<logentry
+ revision="5">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="D">/b</path>
+</paths>
+<msg>remove a file</msg>
+</logentry>
+</log>
+a:
+c
+
+a-hg-wc:
+c
+% executable
+5:7eda3f4b5331
+svn: Path 'b' does not exist
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 make a file executable
+abort: svn exited with status 1
+At revision 5.
+ 5 5 test .
+ M 5 4 test c
+<?xml version="1.0"?>
+<log>
+<logentry
+ revision="5">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="D">/b</path>
+</paths>
+<msg>remove a file</msg>
+</logentry>
+</log>
+executable
+% branchy history
+adding b
+adding left-1
+adding left-2
+1 files updated, 0 files merged, 2 files removed, 0 files unresolved
+adding right-1
+adding right-2
+3 files updated, 0 files merged, 2 files removed, 0 files unresolved
+warning: conflicts during merge.
+merging b
+merging b failed!
+2 files updated, 0 files merged, 0 files removed, 1 files unresolved
+There are unresolved merges, you can redo the full merge using:
+ hg update -C 2
+ hg merge 4
+assuming destination b-hg
+initializing svn repo 'b-hg'
+initializing svn wc 'b-hg-wc'
+scanning source...
+sorting...
+converting...
+5 base
+4 left-1
+3 left-2
+2 right-1
+1 right-2
+0 merge
+% expect 4 changes
+At revision 4.
+ 4 4 test .
+ 4 3 test b
+ 4 2 test left-1
+ 4 3 test left-2
+ 4 4 test right-1
+ 4 4 test right-2
+<?xml version="1.0"?>
+<log>
+<logentry
+ revision="4">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="A">/right-1</path>
+<path
+ action="A">/right-2</path>
+</paths>
+<msg>merge</msg>
+</logentry>
+<logentry
+ revision="3">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="M">/b</path>
+<path
+ action="A">/left-2</path>
+</paths>
+<msg>left-2</msg>
+</logentry>
+<logentry
+ revision="2">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="M">/b</path>
+<path
+ action="A">/left-1</path>
+</paths>
+<msg>left-1</msg>
+</logentry>
+<logentry
+ revision="1">
+<author>test</author>
+<date/>
+<paths>
+<path
+ action="A">/b</path>
+</paths>
+<msg>base</msg>
+</logentry>
+</log>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-source Wed Nov 07 21:13:56 2007 -0600
@@ -0,0 +1,115 @@
+#!/bin/sh
+
+"$TESTDIR/hghave" svn svn-bindings || exit 80
+
+fix_path()
+{
+ tr '\\' /
+}
+
+echo "[extensions]" >> $HGRCPATH
+echo "convert = " >> $HGRCPATH
+
+svnadmin create svn-repo
+
+echo % initial svn import
+mkdir t
+cd t
+echo a > a
+cd ..
+
+svnpath=`pwd | fix_path`
+# SVN wants all paths to start with a slash. Unfortunately,
+# Windows ones don't. Handle that.
+expr $svnpath : "\/" > /dev/null
+if [ $? -ne 0 ]; then
+ svnpath='/'$svnpath
+fi
+
+svnurl=file://$svnpath/svn-repo/trunk
+svn import -m init t $svnurl | fix_path
+
+echo % update svn repository
+svn co $svnurl t2 | fix_path
+cd t2
+echo b >> a
+echo b > b
+svn add b
+svn ci -m changea
+cd ..
+
+echo % convert to hg once
+hg convert $svnurl
+
+echo % update svn repository again
+cd t2
+echo c >> a
+echo c >> b
+svn ci -m changeb
+cd ..
+
+echo % test incremental conversion
+hg convert $svnurl
+
+echo % test filemap
+echo 'include b' > filemap
+hg convert --filemap filemap $svnurl fmap
+echo '[extensions]' >> $HGRCPATH
+echo 'hgext.graphlog =' >> $HGRCPATH
+hg glog -R fmap --template '#rev# #desc|firstline# files: #files#\n'
+
+########################################
+
+echo "# now tests that it works with trunk/branches/tags layout"
+echo
+echo % initial svn import
+mkdir projA
+cd projA
+mkdir trunk
+mkdir branches
+mkdir tags
+cd ..
+
+svnurl=file://$svnpath/svn-repo/projA
+svn import -m "init projA" projA $svnurl | fix_path
+
+
+echo % update svn repository
+svn co $svnurl/trunk A | fix_path
+cd A
+echo hello > letter.txt
+svn add letter.txt
+svn ci -m hello
+
+echo world >> letter.txt
+svn ci -m world
+
+svn copy -m "tag v0.1" $svnurl/trunk $svnurl/tags/v0.1
+
+echo 'nice day today!' >> letter.txt
+svn ci -m "nice day"
+cd ..
+
+echo % convert to hg once
+hg convert $svnurl A-hg
+
+echo % update svn repository again
+cd A
+echo "see second letter" >> letter.txt
+echo "nice to meet you" > letter2.txt
+svn add letter2.txt
+svn ci -m "second letter"
+
+svn copy -m "tag v0.2" $svnurl/trunk $svnurl/tags/v0.2
+
+echo "blah-blah-blah" >> letter2.txt
+svn ci -m "work in progress"
+cd ..
+
+echo % test incremental conversion
+hg convert $svnurl A-hg
+
+cd A-hg
+hg glog --template '#rev# #desc|firstline# files: #files#\n'
+hg tags -q
+cd ..
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-source.out Wed Nov 07 21:13:56 2007 -0600
@@ -0,0 +1,114 @@
+% initial svn import
+Adding t/a
+
+Committed revision 1.
+% update svn repository
+A t2/a
+Checked out revision 1.
+A b
+Sending a
+Adding b
+Transmitting file data ..
+Committed revision 2.
+% convert to hg once
+assuming destination trunk-hg
+initializing destination trunk-hg repository
+scanning source...
+sorting...
+converting...
+1 init
+0 changea
+% update svn repository again
+Sending a
+Sending b
+Transmitting file data ..
+Committed revision 3.
+% test incremental conversion
+assuming destination trunk-hg
+destination trunk-hg is a Mercurial repository
+scanning source...
+sorting...
+converting...
+0 changeb
+% test filemap
+initializing destination fmap repository
+scanning source...
+sorting...
+converting...
+2 init
+1 changea
+0 changeb
+o 1 changeb files: b
+|
+o 0 changea files: b
+
+# now tests that it works with trunk/branches/tags layout
+
+% initial svn import
+Adding projA/trunk
+Adding projA/branches
+Adding projA/tags
+
+Committed revision 4.
+% update svn repository
+Checked out revision 4.
+A letter.txt
+Adding letter.txt
+Transmitting file data .
+Committed revision 5.
+Sending letter.txt
+Transmitting file data .
+Committed revision 6.
+
+Committed revision 7.
+Sending letter.txt
+Transmitting file data .
+Committed revision 8.
+% convert to hg once
+initializing destination A-hg repository
+scanning source...
+sorting...
+converting...
+3 init projA
+2 hello
+1 world
+0 nice day
+updating tags
+% update svn repository again
+A letter2.txt
+Sending letter.txt
+Adding letter2.txt
+Transmitting file data ..
+Committed revision 9.
+
+Committed revision 10.
+Sending letter2.txt
+Transmitting file data .
+Committed revision 11.
+% test incremental conversion
+destination A-hg is a Mercurial repository
+scanning source...
+sorting...
+converting...
+1 second letter
+0 work in progress
+updating tags
+o 7 update tags files: .hgtags
+|
+o 6 work in progress files: letter2.txt
+|
+o 5 second letter files: letter.txt letter2.txt
+|
+o 4 update tags files: .hgtags
+|
+o 3 nice day files: letter.txt
+|
+o 2 world files: letter.txt
+|
+o 1 hello files: letter.txt
+|
+o 0 init projA files:
+
+tip
+v0.2
+v0.1
--- a/tests/test-convert-svn.out Wed Nov 07 21:10:30 2007 -0600
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-% initial svn import
-Adding t/a
-
-Committed revision 1.
-% update svn repository
-A t2/a
-Checked out revision 1.
-A b
-Sending a
-Adding b
-Transmitting file data ..
-Committed revision 2.
-% convert to hg once
-assuming destination trunk-hg
-initializing destination trunk-hg repository
-scanning source...
-sorting...
-converting...
-1 init
-0 changea
-% update svn repository again
-Sending a
-Sending b
-Transmitting file data ..
-Committed revision 3.
-% test incremental conversion
-assuming destination trunk-hg
-destination trunk-hg is a Mercurial repository
-scanning source...
-sorting...
-converting...
-0 changeb
-% test filemap
-initializing destination fmap repository
-scanning source...
-sorting...
-converting...
-2 init
-1 changea
-0 changeb
-o 1 changeb files: b
-|
-o 0 changea files: b
-
-# now tests that it works with trunk/branches/tags layout
-
-% initial svn import
-Adding projA/trunk
-Adding projA/branches
-Adding projA/tags
-
-Committed revision 4.
-% update svn repository
-Checked out revision 4.
-A letter.txt
-Adding letter.txt
-Transmitting file data .
-Committed revision 5.
-Sending letter.txt
-Transmitting file data .
-Committed revision 6.
-
-Committed revision 7.
-Sending letter.txt
-Transmitting file data .
-Committed revision 8.
-% convert to hg once
-initializing destination A-hg repository
-scanning source...
-sorting...
-converting...
-3 init projA
-2 hello
-1 world
-0 nice day
-updating tags
-% update svn repository again
-A letter2.txt
-Sending letter.txt
-Adding letter2.txt
-Transmitting file data ..
-Committed revision 9.
-
-Committed revision 10.
-Sending letter2.txt
-Transmitting file data .
-Committed revision 11.
-% test incremental conversion
-destination A-hg is a Mercurial repository
-scanning source...
-sorting...
-converting...
-1 second letter
-0 work in progress
-updating tags
-o 7 update tags files: .hgtags
-|
-o 6 work in progress files: letter2.txt
-|
-o 5 second letter files: letter.txt letter2.txt
-|
-o 4 update tags files: .hgtags
-|
-o 3 nice day files: letter.txt
-|
-o 2 world files: letter.txt
-|
-o 1 hello files: letter.txt
-|
-o 0 init projA files:
-
-tip
-v0.2
-v0.1
--- a/tests/test-convert.out Wed Nov 07 21:10:30 2007 -0600
+++ b/tests/test-convert.out Wed Nov 07 21:13:56 2007 -0600
@@ -11,6 +11,7 @@
Accepted destination formats:
- Mercurial
+ - Subversion (history on branches is not preserved)
If no revision is given, all revisions will be converted. Otherwise,
convert will only import up to the named revision (given in a format