summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQijiang Fan <fqj1994@gmail.com>2012-11-15 17:11:37 +0800
committerQijiang Fan <fqj1994@gmail.com>2012-11-15 17:11:37 +0800
commit206ab31e9e0aae6866a27511724298dd00a7d90e (patch)
tree651928c31b80fee0b7cd4a6213e8c7642e615a7a
parent41ab46d551a1287a4dfb60d04e132af468923d95 (diff)
Upstream version 1.5
-rw-r--r--.hg_archival.txt4
-rw-r--r--.hgignore2
-rw-r--r--.hgtags1
-rw-r--r--hgsubversion/__init__.py2
-rw-r--r--hgsubversion/editor.py602
-rw-r--r--hgsubversion/help/subversion.rst37
-rw-r--r--hgsubversion/hooks/__init__.py0
-rw-r--r--hgsubversion/hooks/updatemeta.py31
-rw-r--r--hgsubversion/maps.py37
-rw-r--r--hgsubversion/pushmod.py9
-rw-r--r--hgsubversion/replay.py72
-rw-r--r--hgsubversion/stupid.py44
-rw-r--r--hgsubversion/svncommands.py167
-rw-r--r--hgsubversion/svnmeta.py26
-rw-r--r--hgsubversion/svnrepo.py116
-rw-r--r--hgsubversion/svnwrap/common.py91
-rw-r--r--hgsubversion/svnwrap/subvertpy_wrapper.py158
-rw-r--r--hgsubversion/svnwrap/svn_swig_wrapper.py129
-rw-r--r--hgsubversion/util.py98
-rw-r--r--hgsubversion/verify.py196
-rw-r--r--hgsubversion/wrappers.py351
-rwxr-xr-xsetup.py2
-rw-r--r--tests/comprehensive/test_stupid_pull.py4
-rw-r--r--tests/comprehensive/test_verify.py55
-rw-r--r--tests/comprehensive/test_verify_and_startrev.py116
-rw-r--r--tests/fixtures/addspecial.sh51
-rw-r--r--tests/fixtures/addspecial.svndump302
-rwxr-xr-xtests/fixtures/copies.sh29
-rw-r--r--tests/fixtures/copies.svndump158
-rw-r--r--tests/fixtures/correct.svndump103
-rw-r--r--tests/fixtures/corrupt.svndump97
-rwxr-xr-xtests/fixtures/delete_restore_trunk.sh31
-rw-r--r--tests/fixtures/delete_restore_trunk.svndump167
-rwxr-xr-xtests/fixtures/emptyrepo2.sh44
-rw-r--r--tests/fixtures/emptyrepo2.svndump129
-rwxr-xr-xtests/fixtures/invalid_utf8.sh34
-rw-r--r--tests/fixtures/invalid_utf8.tar.gzbin0 -> 8227 bytes
-rwxr-xr-xtests/fixtures/movetotrunk.sh34
-rw-r--r--tests/fixtures/movetotrunk.svndump154
-rwxr-xr-xtests/fixtures/revert.sh39
-rw-r--r--tests/fixtures/revert.svndump197
-rw-r--r--tests/fixtures/subdir_is_file_prefix.svndump72
-rw-r--r--tests/run.py7
-rw-r--r--tests/test_fetch_branches.py3
-rw-r--r--tests/test_fetch_command.py76
-rw-r--r--tests/test_fetch_mappings.py76
-rw-r--r--tests/test_fetch_renames.py7
-rw-r--r--tests/test_fetch_symlinks.py14
-rw-r--r--tests/test_helpers.py37
-rw-r--r--tests/test_hooks.py48
-rw-r--r--tests/test_pull.py28
-rw-r--r--tests/test_pull_fallback.py106
-rw-r--r--tests/test_push_autoprops.py107
-rw-r--r--tests/test_push_command.py35
-rw-r--r--tests/test_rebuildmeta.py53
-rw-r--r--tests/test_single_dir_clone.py9
-rw-r--r--tests/test_startrev.py71
-rw-r--r--tests/test_tags.py1
-rw-r--r--tests/test_updatemeta.py81
-rw-r--r--tests/test_util.py88
-rw-r--r--tests/test_utility_commands.py94
61 files changed, 4170 insertions, 762 deletions
diff --git a/.hg_archival.txt b/.hg_archival.txt
index bfa4098..ab50c72 100644
--- a/.hg_archival.txt
+++ b/.hg_archival.txt
@@ -1,4 +1,4 @@
repo: f2636cfed11500fdc47d1e3822d8e4a2bd636bf7
-node: 07234759a3f750029ccaa001837d42fa12dd33ee
+node: 77b22e5b4ea6c248e079afd0f1e544cb5690ce20
branch: default
-tag: 1.4
+tag: 1.5
diff --git a/.hgignore b/.hgignore
index 358b897..a7780ee 100644
--- a/.hgignore
+++ b/.hgignore
@@ -17,3 +17,5 @@ nbproject
.pydevproject
.settings
*.orig
+.noseids
+tests/fixtures/temp
diff --git a/.hgtags b/.hgtags
index ceff398..c7a54a9 100644
--- a/.hgtags
+++ b/.hgtags
@@ -6,3 +6,4 @@
708234ad6c97fb52417e0b46a86c8373e25123a5 1.2
4bbc6bf947f56a92e95a04a27b94a9f72d5482d7 1.2.1
0cbf9fd89672e73165e1bb4db1ec8f7f65b95c94 1.3
+07234759a3f750029ccaa001837d42fa12dd33ee 1.4
diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py
index f87b52f..41fed52 100644
--- a/hgsubversion/__init__.py
+++ b/hgsubversion/__init__.py
@@ -206,6 +206,8 @@ cmdtable = {
('', 'username', '', 'username for authentication'),
('', 'password', '', 'password for authentication'),
('r', 'rev', '', 'Mercurial revision'),
+ ('', 'unsafe-skip-uuid-check', False,
+ 'skip repository uuid check in rebuildmeta'),
],
'hg svn <subcommand> ...',
),
diff --git a/hgsubversion/editor.py b/hgsubversion/editor.py
index 286a6e3..8a2cf9e 100644
--- a/hgsubversion/editor.py
+++ b/hgsubversion/editor.py
@@ -1,6 +1,8 @@
import errno
-import cStringIO
import sys
+import tempfile
+import shutil
+import os
from mercurial import util as hgutil
from mercurial import revlog
@@ -10,24 +12,86 @@ import svnwrap
import util
import svnexternals
-class NeverClosingStringIO(object):
- def __init__(self):
- self._fp = cStringIO.StringIO()
-
- def __getattr__(self, name):
- return getattr(self._fp, name)
+class EditingError(Exception):
+ pass
+
+class FileStore(object):
+ def __init__(self, maxsize=None):
+ self._tempdir = None
+ self._files = {}
+ self._created = 0
+ self._maxsize = maxsize
+ if self._maxsize is None:
+ self._maxsize = 100*(2**20)
+ self._size = 0
+ self._data = {}
+ self._popped = set()
+
+ def setfile(self, fname, data):
+ if fname in self._popped:
+ raise EditingError('trying to set a popped file %s' % fname)
+
+ if self._maxsize < 0 or (len(data) + self._size) <= self._maxsize:
+ self._data[fname] = data
+ self._size += len(data)
+ else:
+ if self._tempdir is None:
+ self._tempdir = tempfile.mkdtemp(prefix='hg-subversion-')
+ # Avoid filename issues with these simple names
+ fn = str(self._created)
+ fp = hgutil.posixfile(os.path.join(self._tempdir, fn), 'wb')
+ try:
+ fp.write(data)
+ finally:
+ fp.close()
+ self._created += 1
+ self._files[fname] = fn
+
+ def delfile(self, fname):
+ if fname in self._popped:
+ raise EditingError('trying to delete a popped file %s' % fname)
+
+ if fname in self._data:
+ del self._data[fname]
+ elif fname in self._files:
+ path = os.path.join(self._tempdir, self._files.pop(fname))
+ os.unlink(path)
+
+ def getfile(self, fname):
+ if fname in self._popped:
+ raise EditingError('trying to get a popped file %s' % fname)
+
+ if fname in self._data:
+ return self._data[fname]
+ if self._tempdir is None or fname not in self._files:
+ raise IOError
+ path = os.path.join(self._tempdir, self._files[fname])
+ fp = hgutil.posixfile(path, 'rb')
+ try:
+ return fp.read()
+ finally:
+ fp.close()
+
+ def popfile(self, fname):
+ self.delfile(fname)
+ self._popped.add(fname)
+
+ def files(self):
+ return list(self._files) + list(self._data)
def close(self):
- # svn 1.7 apply_delta driver now calls close() on passed file
- # object which prevent us from calling getvalue() afterwards.
- pass
+ if self._tempdir is not None:
+ tempdir, self._tempdir = self._tempdir, None
+ shutil.rmtree(tempdir)
+ self._files = None
+ self._data = None
class RevisionData(object):
__slots__ = [
- 'file', 'files', 'deleted', 'rev', 'execfiles', 'symlinks', 'batons',
- 'copies', 'missing', 'emptybranches', 'base', 'externals', 'ui',
- 'exception',
+ 'file', 'added', 'deleted', 'rev', 'execfiles', 'symlinks',
+ 'copies', 'emptybranches', 'base', 'externals', 'ui',
+ 'exception', 'store',
]
def __init__(self, ui):
@@ -35,73 +99,74 @@ class RevisionData(object):
self.clear()
def clear(self):
- self.file = None
- self.files = {}
+ self.store = FileStore(util.getfilestoresize(self.ui))
+ self.added = set()
self.deleted = {}
self.rev = None
self.execfiles = {}
self.symlinks = {}
- self.batons = {}
# Map fully qualified destination file paths to module source path
self.copies = {}
- self.missing = set()
self.emptybranches = {}
- self.base = None
self.externals = {}
self.exception = None
- def set(self, path, data, isexec=False, islink=False):
- self.files[path] = data
+ def set(self, path, data, isexec=False, islink=False, copypath=None):
+ self.store.setfile(path, data)
self.execfiles[path] = isexec
self.symlinks[path] = islink
if path in self.deleted:
del self.deleted[path]
- if path in self.missing:
- self.missing.remove(path)
+ if copypath is not None:
+ self.copies[path] = copypath
+
+ def get(self, path):
+ if path in self.deleted:
+ raise IOError(errno.ENOENT, '%s is deleted' % path)
+ data = self.store.getfile(path)
+ isexec = self.execfiles.get(path)
+ islink = self.symlinks.get(path)
+ copied = self.copies.get(path)
+ return data, isexec, islink, copied
+
+ def pop(self, path):
+ ret = self.get(path)
+ self.store.popfile(path)
+ return ret
def delete(self, path):
self.deleted[path] = True
- if path in self.files:
- del self.files[path]
+ self.store.delfile(path)
self.execfiles[path] = False
self.symlinks[path] = False
self.ui.note('D %s\n' % path)
- def findmissing(self, svn):
-
- if not self.missing:
- return
-
- msg = 'fetching %s files that could not use replay.\n'
- self.ui.debug(msg % len(self.missing))
- root = svn.subdir and svn.subdir[1:] or ''
- r = self.rev.revnum
-
- files = set()
- for p in self.missing:
- self.ui.note('.')
- self.ui.flush()
- if p[-1] == '/':
- dir = p[len(root):]
- new = [p + f for f, k in svn.list_files(dir, r) if k == 'f']
- files.update(new)
- else:
- files.add(p)
-
- i = 1
- self.ui.note('\nfetching files...\n')
- for p in files:
- self.ui.note('.')
- self.ui.flush()
- if i % 50 == 0:
- svn.init_ra_and_client()
- i += 1
- data, mode = svn.get_file(p[len(root):], r)
- self.set(p, data, 'x' in mode, 'l' in mode)
-
- self.missing = set()
- self.ui.note('\n')
+ def files(self):
+ """Return a sorted list of changed files."""
+ files = set(self.store.files())
+ for g in (self.symlinks, self.execfiles, self.deleted):
+ files.update(g)
+ return sorted(files)
+ def close(self):
+ self.store.close()
+
+class CopiedFile(object):
+ def __init__(self, node, path, copypath):
+ self.node = node
+ self.path = path
+ self.copypath = copypath
+
+ def resolve(self, getctxfn, ctx=None):
+ if ctx is None:
+ ctx = getctxfn(self.node)
+ fctx = ctx[self.path]
+ data = fctx.data()
+ flags = fctx.flags()
+ islink = 'l' in flags
+ if islink:
+ data = 'link ' + data
+ return data, 'x' in flags, islink, self.copypath
class HgEditor(svnwrap.Editor):
@@ -110,54 +175,135 @@ class HgEditor(svnwrap.Editor):
self.ui = meta.ui
self.repo = meta.repo
self.current = RevisionData(meta.ui)
+ self._clear()
+
+ def setsvn(self, svn):
+ self._svn = svn
+
+ def _clear(self):
+ self._filecounter = 0
+ # A mapping of svn paths to CopiedFile entries
+ self._svncopies = {}
+ # A mapping of batons to (path, data, isexec, islink, copypath) tuples
+ # data is a SimpleStringIO if the file was edited, a string
+ # otherwise.
+ self._openfiles = {}
+ # A mapping of file paths to batons
+ self._openpaths = {}
+ self._deleted = set()
+ self._getctx = util.lrucachefunc(self.repo.changectx, 3)
+ # A stack of opened directory (baton, path) pairs.
+ self._opendirs = []
+ self._missing = set()
+
+ def _openfile(self, path, data, isexec, islink, copypath, create=False):
+ if path in self._openpaths:
+ raise EditingError('trying to open an already opened file %s'
+ % path)
+ if not create and path in self._deleted:
+ raise EditingError('trying to open a deleted file %s' % path)
+ if path in self._deleted:
+ self._deleted.remove(path)
+ self._filecounter += 1
+ baton = 'f%d-%s' % (self._filecounter, path)
+ self._openfiles[baton] = (path, data, isexec, islink, copypath)
+ self._openpaths[path] = baton
+ return baton
+
+ def _opendir(self, path):
+ self._filecounter += 1
+ baton = 'f%d-%s' % (self._filecounter, path)
+ self._opendirs.append((baton, path))
+ return baton
+
+ def _checkparentdir(self, baton):
+ if not self._opendirs:
+ raise EditingError('trying to operate on an already closed '
+ 'directory: %s' % baton)
+ if self._opendirs[-1][0] != baton:
+ raise EditingError('can only operate on the most recently '
+ 'opened directory: %s != %s' % (self._opendirs[-1][0], baton))
+
+ def _deletefile(self, path):
+ if self.meta.is_path_valid(path):
+ self._deleted.add(path)
+ if path in self._svncopies:
+ del self._svncopies[path]
+ self._missing.discard(path)
+
+ def addmissing(self, path, isdir=False):
+ svn = self._svn
+ root = svn.subdir and svn.subdir[1:] or ''
+ if not isdir:
+ self._missing.add(path[len(root):])
+ else:
+ # Resolve missing directories content immediately so the
+ # missing files maybe processed by delete actions.
+ rev = self.current.rev.revnum
+ path = path + '/'
+ parentdir = path[len(root):]
+ for f, k in svn.list_files(parentdir, rev):
+ if k != 'f':
+ continue
+ f = parentdir + f
+ if not self.meta.is_path_valid(f, False):
+ continue
+ self._missing.add(f)
@svnwrap.ieditor
def delete_entry(self, path, revision_bogus, parent_baton, pool=None):
+ self._checkparentdir(parent_baton)
br_path, branch = self.meta.split_branch_path(path)[:2]
if br_path == '':
if self.meta.get_path_tag(path):
# Tag deletion is not handled as branched deletion
return
self.meta.closebranches.add(branch)
+
+ # Delete copied entries, no need to check they exist in hg
+ # parent revision.
+ if path in self._svncopies:
+ del self._svncopies[path]
+ prefix = path + '/'
+ for f in list(self._svncopies):
+ if f.startswith(prefix):
+ self._deletefile(f)
+ if path in self._missing:
+ self._missing.remove(path)
+ else:
+ for f in list(self._missing):
+ if f.startswith(prefix):
+ self._missing.remove(f)
+
if br_path is not None:
ha = self.meta.get_parent_revision(self.current.rev.revnum, branch)
if ha == revlog.nullid:
return
- ctx = self.repo.changectx(ha)
+ ctx = self._getctx(ha)
if br_path not in ctx:
br_path2 = ''
if br_path != '':
br_path2 = br_path + '/'
# assuming it is a directory
self.current.externals[path] = None
- map(self.current.delete, [pat for pat in self.current.files.iterkeys()
- if pat.startswith(path + '/')])
for f in ctx.walk(util.PrefixMatch(br_path2)):
f_p = '%s/%s' % (path, f[len(br_path2):])
- if f_p not in self.current.files:
- self.current.delete(f_p)
- self.current.delete(path)
+ self._deletefile(f_p)
+ self._deletefile(path)
@svnwrap.ieditor
def open_file(self, path, parent_baton, base_revision, p=None):
- self.current.file = None
+ self._checkparentdir(parent_baton)
+ if not self.meta.is_path_valid(path):
+ return None
fpath, branch = self.meta.split_branch_path(path)[:2]
- if not fpath:
- self.ui.debug('WARNING: Opening non-existant file %s\n' % path)
- return
- self.current.file = path
self.ui.note('M %s\n' % path)
- if base_revision != -1:
- self.current.base = base_revision
- else:
- self.current.base = None
- if self.current.file in self.current.files:
- return
-
- if not self.meta.is_path_valid(path):
- return
+ if path in self._svncopies:
+ copy = self._svncopies.pop(path)
+ base, isexec, islink, copypath = copy.resolve(self._getctx)
+ return self._openfile(path, base, isexec, islink, copypath)
baserev = base_revision
if baserev is None or baserev == -1:
@@ -166,64 +312,92 @@ class HgEditor(svnwrap.Editor):
# replacing branch as parent, but svn delta editor provides delta
# agains replaced branch.
parent = self.meta.get_parent_revision(baserev + 1, branch, True)
- ctx = self.repo[parent]
+ ctx = self._getctx(parent)
if fpath not in ctx:
- self.current.missing.add(path)
- return
+ self.addmissing(path)
+ return None
fctx = ctx.filectx(fpath)
base = fctx.data()
- if 'l' in fctx.flags():
+ flags = fctx.flags()
+ if 'l' in flags:
base = 'link ' + base
- self.current.set(path, base, 'x' in fctx.flags(), 'l' in fctx.flags())
+ return self._openfile(path, base, 'x' in flags, 'l' in flags, None)
@svnwrap.ieditor
def add_file(self, path, parent_baton=None, copyfrom_path=None,
copyfrom_revision=None, file_pool=None):
- self.current.file = None
- self.current.base = None
- if path in self.current.deleted:
- del self.current.deleted[path]
+ self._checkparentdir(parent_baton)
+ # Use existing=False because we use the fact a file is being
+ # added here to populate the branchmap which is used with
+ # existing=True.
fpath, branch = self.meta.split_branch_path(path, existing=False)[:2]
- if not fpath:
- return
+ if not fpath or fpath not in self.meta.filemap:
+ return None
+ if path in self._svncopies:
+ raise EditingError('trying to replace copied file %s' % path)
+ if path in self._deleted:
+ self._deleted.remove(path)
if (branch not in self.meta.branches and
not self.meta.get_path_tag(self.meta.remotename(branch))):
- # we know this branch will exist now, because it has at least one file. Rock.
+ # we know this branch will exist now, because it has at
+ # least one file. Rock.
self.meta.branches[branch] = None, 0, self.current.rev.revnum
- self.current.file = path
if not copyfrom_path:
self.ui.note('A %s\n' % path)
- self.current.set(path, '', False, False)
- return
+ self.current.added.add(path)
+ return self._openfile(path, '', False, False, None, create=True)
self.ui.note('A+ %s\n' % path)
(from_file,
from_branch) = self.meta.split_branch_path(copyfrom_path)[:2]
if not from_file:
- self.current.missing.add(path)
- return
+ self.addmissing(path)
+ return None
# Use exact=True because during replacements ('R' action) we select
# replacing branch as parent, but svn delta editor provides delta
# agains replaced branch.
ha = self.meta.get_parent_revision(copyfrom_revision + 1,
from_branch, True)
- ctx = self.repo.changectx(ha)
- if from_file in ctx:
- fctx = ctx.filectx(from_file)
- flags = fctx.flags()
- self.current.set(path, fctx.data(), 'x' in flags, 'l' in flags)
- if from_branch == branch:
- parentid = self.meta.get_parent_revision(
- self.current.rev.revnum, branch)
- if parentid != revlog.nullid:
- parentctx = self.repo.changectx(parentid)
- if util.issamefile(parentctx, ctx, from_file):
- self.current.copies[path] = from_file
+ ctx = self._getctx(ha)
+ if from_file not in ctx:
+ self.addmissing(path)
+ return None
+
+ fctx = ctx.filectx(from_file)
+ flags = fctx.flags()
+ self.current.set(path, fctx.data(), 'x' in flags, 'l' in flags)
+ copypath = None
+ if from_branch == branch:
+ parentid = self.meta.get_parent_revision(
+ self.current.rev.revnum, branch)
+ if parentid != revlog.nullid:
+ parentctx = self._getctx(parentid)
+ if util.issamefile(parentctx, ctx, from_file):
+ copypath = from_file
+ return self._openfile(path, fctx.data(), 'x' in flags, 'l' in flags,
+ copypath, create=True)
+
+ @svnwrap.ieditor
+ def close_file(self, file_baton, checksum, pool=None):
+ if file_baton is None:
+ return
+ if file_baton not in self._openfiles:
+ raise EditingError('trying to close a non-open file %s'
+ % file_baton)
+ path, data, isexec, islink, copypath = self._openfiles.pop(file_baton)
+ del self._openpaths[path]
+ if not isinstance(data, basestring):
+ # Files can be opened, properties changed and apply_text
+ # never called, in which case data is still a string.
+ data = data.getvalue()
+ self.current.set(path, data, isexec, islink, copypath)
@svnwrap.ieditor
def add_directory(self, path, parent_baton, copyfrom_path,
copyfrom_revision, dir_pool=None):
- self.current.batons[path] = path
+ self._checkparentdir(parent_baton)
+ baton = self._opendir(path)
+
br_path, branch = self.meta.split_branch_path(path)[:2]
if br_path is not None:
if not copyfrom_path and not br_path:
@@ -231,16 +405,20 @@ class HgEditor(svnwrap.Editor):
else:
self.current.emptybranches[branch] = False
if br_path is None or not copyfrom_path:
- return path
+ return baton
if self.meta.get_path_tag(path):
del self.current.emptybranches[branch]
- return path
+ return baton
tag = self.meta.get_path_tag(copyfrom_path)
if tag not in self.meta.tags:
tag = None
- if not self.meta.is_path_valid(copyfrom_path):
- self.current.missing.add('%s/' % path)
- return path
+ if not self.meta.is_path_valid(copyfrom_path, existing=False):
+ # The source path only exists at copyfrom_revision, use
+ # existing=False to guess a possible branch location and
+ # test it against the filemap. The actual path and
+ # revision will be resolved below if necessary.
+ self.addmissing(path, isdir=True)
+ return baton
if tag:
changeid = self.meta.tags[tag]
source_rev, source_branch = self.meta.get_source_rev(changeid)[:2]
@@ -248,39 +426,61 @@ class HgEditor(svnwrap.Editor):
else:
source_rev = copyfrom_revision
frompath, source_branch = self.meta.split_branch_path(copyfrom_path)[:2]
- if frompath == '' and br_path == '':
- assert br_path is not None
- tmp = source_branch, source_rev, self.current.rev.revnum
- self.meta.branches[branch] = tmp
new_hash = self.meta.get_parent_revision(source_rev + 1, source_branch, True)
if new_hash == node.nullid:
- self.current.missing.add('%s/' % path)
- return path
- fromctx = self.repo.changectx(new_hash)
+ self.addmissing(path, isdir=True)
+ return baton
+ fromctx = self._getctx(new_hash)
if frompath != '/' and frompath != '':
frompath = '%s/' % frompath
else:
frompath = ''
+
+ copyfromparent = False
+ if frompath == '' and br_path == '':
+ pnode = self.meta.get_parent_revision(
+ self.current.rev.revnum, branch)
+ if pnode == new_hash:
+ # Data parent is topological parent and relative paths
+ # are the same, not need to do anything but restore
+ # files marked as deleted.
+ copyfromparent = True
+ # Get the parent which would have been used for this branch
+ # without the replace action.
+ oldpnode = self.meta.get_parent_revision(
+ self.current.rev.revnum, branch, exact=True)
+ if (oldpnode != revlog.nullid
+ and util.isancestor(self._getctx(oldpnode), fromctx)):
+ # Branch-wide replacement, unmark the branch as deleted
+ self.meta.closebranches.discard(branch)
+
+ svncopies = {}
copies = {}
for f in fromctx:
if not f.startswith(frompath):
continue
- fctx = fromctx.filectx(f)
dest = path + '/' + f[len(frompath):]
- self.current.set(dest, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags())
- if dest in self.current.deleted:
- del self.current.deleted[dest]
+ if not self.meta.is_path_valid(dest):
+ continue
+ if dest in self._deleted:
+ self._deleted.remove(dest)
+ if copyfromparent:
+ continue
+ svncopies[dest] = CopiedFile(new_hash, f, None)
if branch == source_branch:
copies[dest] = f
if copies:
# Preserve the directory copy records if no file was changed between
# the source and destination revisions, or discard it completely.
- parentid = self.meta.get_parent_revision(self.current.rev.revnum, branch)
+ parentid = self.meta.get_parent_revision(
+ self.current.rev.revnum, branch)
if parentid != revlog.nullid:
- parentctx = self.repo.changectx(parentid)
+ parentctx = self._getctx(parentid)
for k, v in copies.iteritems():
if util.issamefile(parentctx, fromctx, v):
- self.current.copies[k] = v
+ svncopies[k].copypath = v
+ self._svncopies.update(svncopies)
+
# Copy the externals definitions of copied directories
fromext = svnexternals.parse(self.ui, fromctx)
for p, v in fromext.iteritems():
@@ -288,62 +488,67 @@ class HgEditor(svnwrap.Editor):
if pp.startswith(frompath):
dest = (path + '/' + pp[len(frompath):]).rstrip('/')
self.current.externals[dest] = v
- return path
+ return baton
@svnwrap.ieditor
def change_file_prop(self, file_baton, name, value, pool=None):
+ if file_baton is None:
+ return
+ path, data, isexec, islink, copypath = self._openfiles[file_baton]
+ changed = False
if name == 'svn:executable':
- self.current.execfiles[self.current.file] = bool(value is not None)
+ changed = True
+ isexec = bool(value is not None)
elif name == 'svn:special':
- self.current.symlinks[self.current.file] = bool(value is not None)
+ changed = True
+ islink = bool(value is not None)
+ if changed:
+ self._openfiles[file_baton] = (path, data, isexec, islink, copypath)
@svnwrap.ieditor
def change_dir_prop(self, dir_baton, name, value, pool=None):
- if dir_baton is None:
+ self._checkparentdir(dir_baton)
+ if len(self._opendirs) == 1:
return
- path = self.current.batons[dir_baton]
+ path = self._opendirs[-1][1]
if name == 'svn:externals':
self.current.externals[path] = value
@svnwrap.ieditor
+ def open_root(self, edit_baton, base_revision, dir_pool=None):
+ # We should not have to reset these, unfortunately the editor is
+ # reused for different revisions.
+ self._clear()
+ return self._opendir('')
+
+ @svnwrap.ieditor
def open_directory(self, path, parent_baton, base_revision, dir_pool=None):
- self.current.batons[path] = path
+ self._checkparentdir(parent_baton)
+ baton = self._opendir(path)
p_, branch = self.meta.split_branch_path(path)[:2]
if p_ == '' or (self.meta.layout == 'single' and p_):
if not self.meta.get_path_tag(path):
self.current.emptybranches[branch] = False
- return path
+ return baton
@svnwrap.ieditor
def close_directory(self, dir_baton, dir_pool=None):
- if dir_baton is not None:
- del self.current.batons[dir_baton]
+ self._checkparentdir(dir_baton)
+ self._opendirs.pop()
@svnwrap.ieditor
def apply_textdelta(self, file_baton, base_checksum, pool=None):
- # We know coming in here the file must be one of the following options:
- # 1) Deleted (invalid, fail an assertion)
- # 2) Missing a base text (bail quick since we have to fetch a full plaintext)
- # 3) Has a base text in self.current.files, apply deltas
- base = ''
- if not self.meta.is_path_valid(self.current.file):
+ if file_baton is None:
return lambda x: None
-
- if self.current.file in self.current.deleted:
- msg = ('cannot apply textdelta to %s: file is deleted'
- % self.current.file)
- raise IOError(errno.ENOENT, msg)
-
- if (self.current.file not in self.current.files and
- self.current.file not in self.current.missing):
- msg = ('cannot apply textdelta to %s: file not found'
- % self.current.file)
- raise IOError(errno.ENOENT, msg)
-
- if self.current.file in self.current.missing:
+ if file_baton not in self._openfiles:
+ raise EditingError('trying to patch a closed file %s' % file_baton)
+ path, base, isexec, islink, copypath = self._openfiles[file_baton]
+ if not isinstance(base, basestring):
+ raise EditingError('trying to edit a file again: %s' % path)
+ if not self.meta.is_path_valid(path):
return lambda x: None
- base = self.current.files[self.current.file]
- target = NeverClosingStringIO()
+
+ target = svnwrap.SimpleStringIO(closing=False)
self.stream = target
handler = svnwrap.apply_txdelta(base, target)
@@ -352,19 +557,92 @@ class HgEditor(svnwrap.Editor):
'cannot call handler!')
def txdelt_window(window):
try:
- if not self.meta.is_path_valid(self.current.file):
+ if not self.meta.is_path_valid(path):
return
- handler(window)
+ try:
+ handler(window)
+ except AssertionError, e: # pragma: no cover
+ # Enhance the exception message
+ msg, others = e.args[0], e.args[1:]
+
+ if msg:
+ msg += '\n'
+
+ msg += _TXDELT_WINDOW_HANDLER_FAILURE_MSG
+ e.args = (msg,) + others
+ raise e
+
# window being None means commit this file
if not window:
- self.current.files[self.current.file] = target.getvalue()
+ self._openfiles[file_baton] = (
+ path, target, isexec, islink, copypath)
except svnwrap.SubversionException, e: # pragma: no cover
if e.args[1] == svnwrap.ERR_INCOMPLETE_DATA:
- self.current.missing.add(self.current.file)
+ self.addmissing(path)
else: # pragma: no cover
raise hgutil.Abort(*e.args)
except: # pragma: no cover
- print len(base), self.current.file
self._exception_info = sys.exc_info()
raise
return txdelt_window
+
+ def close(self):
+ if self._openfiles:
+ for e in self._openfiles.itervalues():
+ self.ui.debug('error: %s was not closed\n' % e[0])
+ raise EditingError('%d edited files were not closed'
+ % len(self._openfiles))
+
+ if self._opendirs:
+ raise EditingError('directory %s was not closed'
+ % self._opendirs[-1][1])
+
+ # Resolve by changelog entries to avoid extra reads
+ nodes = {}
+ for path, copy in self._svncopies.iteritems():
+ nodes.setdefault(copy.node, []).append((path, copy))
+ for node, copies in nodes.iteritems():
+ for path, copy in copies:
+ data, isexec, islink, copied = copy.resolve(self._getctx)
+ self.current.set(path, data, isexec, islink, copied)
+ self._svncopies.clear()
+
+ # Resolve missing files
+ if self._missing:
+ missing = sorted(self._missing)
+ self.ui.debug('fetching %s files that could not use replay.\n'
+ % len(missing))
+ if self.ui.configbool('hgsubversion', 'failonmissing', False):
+ raise EditingError('missing entry: %s' % missing[0])
+
+ svn = self._svn
+ rev = self.current.rev.revnum
+ root = svn.subdir and svn.subdir[1:] or ''
+ i = 1
+ for f in missing:
+ if self.ui.debugflag:
+ self.ui.debug('fetching %s\n' % f)
+ else:
+ self.ui.note('.')
+ self.ui.flush()
+ if i % 50 == 0:
+ svn.init_ra_and_client()
+ i += 1
+ data, mode = svn.get_file(f, rev)
+ self.current.set(f, data, 'x' in mode, 'l' in mode)
+ if not self.ui.debugflag:
+ self.ui.note('\n')
+
+ for f in self._deleted:
+ self.current.delete(f)
+ self._deleted.clear()
+
+_TXDELT_WINDOW_HANDLER_FAILURE_MSG = (
+ "Your SVN repository may not be supplying correct replay deltas."
+ " It is strongly"
+ "\nadvised that you repull the entire SVN repository using"
+ " hg pull --stupid."
+ "\nAlternatively, re-pull just this revision using --stupid and verify"
+ " that the"
+ "\nchangeset is correct."
+)
diff --git a/hgsubversion/help/subversion.rst b/hgsubversion/help/subversion.rst
index 0209c1c..c37977d 100644
--- a/hgsubversion/help/subversion.rst
+++ b/hgsubversion/help/subversion.rst
@@ -302,6 +302,25 @@ settings:
be included or excluded. See the documentation for ``hg convert`` for more
information on filemaps.
+ ``hgsubversion.filestoresize``
+
+ Maximum amount of temporary edited files data to be kept in memory,
+ in megabytes. The replay and stupid mode pull data by retrieving
+ delta information from the subversion repository and applying it on
+ known files data. Since the order of file edits is driven by the
+ subversion delta information order, edited files cannot be committed
+ immediately and are kept until all of them have been processed for
+ each changeset. ``filestoresize`` defines the maximum amount of
+ files data to be kept in memory before falling back to storing them
+ in a temporary directory. This setting is important with
+ repositories containing many files or large ones as both the
+ application of deltas and Mercurial commit process require the whole
+ file data to be available in memory. By limiting the amount of
+ temporary data kept in memory, larger files can be retrieved, at the
+ price of slower disk operations. Set it to a negative value to
+ disable the fallback behaviour and keep everything in memory.
+ Default to 200.
+
``hgsubversion.username``, ``hgsubversion.password``
Set the username or password for accessing Subversion repositories.
@@ -352,6 +371,24 @@ The following options only have an effect on the initial clone of a repository:
contain tags. The default is to only look in ``tags``. This option has no
effect for single-directory clones.
+ ``hgsubversion.unsafeskip``
+
+ A space or comma separated list of Subversion revision numbers to
+ skip over when pulling or cloning. This can be useful for
+ troublesome commits, such as someone accidentally deleting trunk
+ and then restoring it. (In delete-and-restore cases, you may also
+ need to clone or pull in multiple steps, to help hgsubversion
+ track history correctly.)
+
+ NOTE: this option is dangerous. Careless use can make it
+ impossible to pull later Subversion revisions cleanly, e.g. if the
+ content of a file depends on changes made in a skipped rev.
+ Skipping a rev may also prevent future invocations of ``hg svn
+ verify`` from succeeding (if the contents of the Mercurial repo
+ become out of step with the contents of the Subversion repo). If
+ you use this option, be sure to carefully check the result of a
+ pull afterwards.
+
Please note that some of these options may be specified as command line options
as well, and when done so, will override the configuration. If an authormap,
filemap or branchmap is specified, its contents will be read and stored for use
diff --git a/hgsubversion/hooks/__init__.py b/hgsubversion/hooks/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/hgsubversion/hooks/__init__.py
diff --git a/hgsubversion/hooks/updatemeta.py b/hgsubversion/hooks/updatemeta.py
new file mode 100644
index 0000000..8519e00
--- /dev/null
+++ b/hgsubversion/hooks/updatemeta.py
@@ -0,0 +1,31 @@
+# Mercurial hook to update/rebuild svn metadata if there are svn changes in
+# the incoming changegroup.
+#
+# To install, add the following to your hgrc:
+# [hooks]
+# changegroup = python:hgsubversion.hooks.updatemeta.hook
+
+from mercurial import node
+
+import hgsubversion
+import hgsubversion.util
+import hgsubversion.svncommands
+
+def hook(ui, repo, **kwargs):
+ updatemeta = False
+ startrev = repo[node.bin(kwargs["node"])].rev()
+ # Check each rev until we find one that contains svn metadata
+ for rev in xrange(startrev, len(repo)):
+ svnrev = hgsubversion.util.getsvnrev(repo[rev])
+ if svnrev and svnrev.startswith("svn:"):
+ updatemeta = True
+ break
+
+ if updatemeta:
+ try:
+ hgsubversion.svncommands.updatemeta(ui, repo, args=[])
+ ui.status("Updated svn metadata\n")
+ except Exception, e:
+ ui.warn("Failed to update svn metadata: %s" % str(e))
+
+ return False
diff --git a/hgsubversion/maps.py b/hgsubversion/maps.py
index 6bfca52..43601fd 100644
--- a/hgsubversion/maps.py
+++ b/hgsubversion/maps.py
@@ -1,5 +1,6 @@
''' Module for self-contained maps. '''
+import errno
import os
from mercurial import util as hgutil
from mercurial import node
@@ -134,8 +135,7 @@ class Tags(dict):
svncommands.rebuildmeta(repo.ui, repo, ())
return
elif ver != self.VERSION:
- print 'tagmap too new -- please upgrade'
- raise NotImplementedError
+ raise hgutil.Abort('tagmap too new -- please upgrade')
for l in f:
ha, revision, tag = l.split(' ', 2)
revision = int(revision)
@@ -182,7 +182,8 @@ class RevMap(dict):
def __init__(self, repo):
dict.__init__(self)
- self.path = os.path.join(repo.path, 'svn', 'rev_map')
+ self.path = self.mappath(repo)
+ self.repo = repo
self.ypath = os.path.join(repo.path, 'svn', 'lastpulled')
# TODO(durin42): Consider moving management of the youngest
# file to svnmeta itself rather than leaving it here.
@@ -212,13 +213,25 @@ class RevMap(dict):
check = lambda x: x[0][1] == branch and x[0][0] < rev.revnum
return sorted(filter(check, self.iteritems()), reverse=True)
- def _load(self):
- f = open(self.path)
+ @staticmethod
+ def mappath(repo):
+ return os.path.join(repo.path, 'svn', 'rev_map')
+
+ @classmethod
+ def readmapfile(cls, repo, missingok=True):
+ try:
+ f = open(cls.mappath(repo))
+ except IOError, err:
+ if not missingok or err.errno != errno.ENOENT:
+ raise
+ return iter([])
ver = int(f.readline())
- if ver != self.VERSION:
- print 'revmap too new -- please upgrade'
- raise NotImplementedError
- for l in f:
+ if ver != cls.VERSION:
+ raise hgutil.Abort('revmap too new -- please upgrade')
+ return f
+
+ def _load(self):
+ for l in self.readmapfile(self.repo):
revnum, ha, branch = l.split(' ', 2)
if branch == '\n':
branch = None
@@ -230,7 +243,6 @@ class RevMap(dict):
if revnum < self.oldest or not self.oldest:
self.oldest = revnum
dict.__setitem__(self, (revnum, branch), node.bin(ha))
- f.close()
def _write(self):
f = open(self.path, 'w')
@@ -311,7 +323,7 @@ class FileMap(object):
msg = 'duplicate %s entry in %s: "%s"\n'
self.ui.status(msg % (m, fn, path))
return
- bits = m.strip('e'), path
+ bits = m.rstrip('e'), path
self.ui.debug('%sing %s\n' % bits)
# respect rule order
mapping[path] = len(self)
@@ -347,8 +359,7 @@ class FileMap(object):
f = open(self.path)
ver = int(f.readline())
if ver != self.VERSION:
- print 'filemap too new -- please upgrade'
- raise NotImplementedError
+ raise hgutil.Abort('filemap too new -- please upgrade')
self.load_fd(f, self.path)
f.close()
diff --git a/hgsubversion/pushmod.py b/hgsubversion/pushmod.py
index 7c0a104..879ad68 100644
--- a/hgsubversion/pushmod.py
+++ b/hgsubversion/pushmod.py
@@ -82,6 +82,11 @@ def _getdirchanges(svn, branchpath, parentctx, ctx, changedfiles, extchanges):
added.append(d)
for d in olddirs:
+ if not d:
+ # Do not remove the root directory when the hg repo becomes
+ # empty. hgsubversion cannot create branches, do not remove
+ # them.
+ continue
if d not in newdirs and _isdir(svn, branchpath, d):
deleted.append(d)
@@ -133,6 +138,10 @@ def commit(ui, repo, rev_ctx, meta, base_revision, svn):
# this kind of renames: a -> b, b -> c
copies[file] = renamed[0]
base_data = parent[renamed[0]].data()
+ else:
+ autoprops = svn.autoprops_config.properties(file)
+ if autoprops:
+ props.setdefault(file, {}).update(autoprops)
action = 'add'
dirname = '/'.join(file.split('/')[:-1] + [''])
diff --git a/hgsubversion/replay.py b/hgsubversion/replay.py
index 9a59002..75012cb 100644
--- a/hgsubversion/replay.py
+++ b/hgsubversion/replay.py
@@ -30,7 +30,6 @@ def updateexternals(ui, meta, current):
branches = {}
for path, entry in current.externals.iteritems():
if not meta.is_path_valid(path):
- ui.warn('WARNING: Invalid path %s in externals\n' % path)
continue
p, b, bp = meta.split_branch_path(path)
@@ -52,11 +51,28 @@ def updateexternals(ui, meta, current):
else:
current.delete(path)
+
+def _safe_message(msg):
+ if msg:
+ try:
+ msg.decode('utf-8')
+ except UnicodeDecodeError:
+ # ancient svn failed to enforce utf8 encoding
+ return msg.decode('iso-8859-1').encode('utf-8')
+ return msg
+
def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
+ try:
+ return _convert_rev(ui, meta, svn, r, tbdelta, firstrun)
+ finally:
+ meta.editor.current.close()
+
+def _convert_rev(ui, meta, svn, r, tbdelta, firstrun):
editor = meta.editor
editor.current.clear()
editor.current.rev = r
+ editor.setsvn(svn)
if firstrun and meta.revmap.oldest <= 0:
# We know nothing about this project, so fetch everything before
@@ -65,33 +81,28 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
svn.get_revision(r.revnum, editor)
else:
svn.get_replay(r.revnum, editor, meta.revmap.oldest)
+ editor.close()
current = editor.current
- current.findmissing(svn)
updateexternals(ui, meta, current)
if current.exception is not None: # pragma: no cover
traceback.print_exception(*current.exception)
raise ReplayException()
- if current.missing:
- raise MissingPlainTextError()
-
- # paranoidly generate the list of files to commit
- files_to_commit = set(current.files.keys())
- files_to_commit.update(current.symlinks.keys())
- files_to_commit.update(current.execfiles.keys())
- files_to_commit.update(current.deleted.keys())
- # back to a list and sort so we get sane behavior
- files_to_commit = list(files_to_commit)
- files_to_commit.sort()
+
+ files_to_commit = current.files()
branch_batches = {}
rev = current.rev
date = meta.fixdate(rev.date)
# build up the branches that have files on them
+ failoninvalid = ui.configbool('hgsubversion',
+ 'failoninvalidreplayfile', False)
for f in files_to_commit:
if not meta.is_path_valid(f):
+ if failoninvalid:
+ raise hgutil.Abort('file %s should not be in commit list' % f)
continue
p, b = meta.split_branch_path(f)[:2]
if b not in branch_batches:
@@ -144,30 +155,33 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
def filectxfn(repo, memctx, path):
current_file = files[path]
- if current_file in current.deleted:
- raise IOError(errno.ENOENT, '%s is deleted' % path)
- copied = current.copies.get(current_file)
- flags = parentctx.flags(path)
- is_exec = current.execfiles.get(current_file, 'x' in flags)
- is_link = current.symlinks.get(current_file, 'l' in flags)
- if current_file in current.files:
- data = current.files[current_file]
- if is_link and data.startswith('link '):
- data = data[len('link '):]
- elif is_link:
- ui.debug('file marked as link, but may contain data: '
- '%s (%r)\n' % (current_file, flags))
+ data, isexec, islink, copied = current.pop(current_file)
+ if isexec is None or islink is None:
+ flags = parentctx.flags(path)
+ if isexec is None:
+ isexec = 'x' in flags
+ if islink is None:
+ islink = 'l' in flags
+
+ if data is not None:
+ if islink:
+ if data.startswith('link '):
+ data = data[len('link '):]
+ else:
+ ui.debug('file marked as link, but may contain data: '
+ '%s\n' % current_file)
else:
data = parentctx.filectx(path).data()
return context.memfilectx(path=path,
data=data,
- islink=is_link, isexec=is_exec,
+ islink=islink, isexec=isexec,
copied=copied)
+ message = _safe_message(rev.message)
meta.mapbranch(extra)
current_ctx = context.memctx(meta.repo,
parents,
- rev.message or util.default_commit_msg(ui),
+ message or util.default_commit_msg(ui),
files.keys(),
filectxfn,
meta.authors[rev.author],
@@ -203,7 +217,7 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
current_ctx = context.memctx(meta.repo,
(ha, node.nullid),
- rev.message or ' ',
+ _safe_message(rev.message) or ' ',
[],
del_all_files,
meta.authors[rev.author],
diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py
index cf39869..c9107c1 100644
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -22,7 +22,7 @@ import util
# a
# a
# +a
-#
+#
# Property changes on: a
# ___________________________________________________________________
# Added: svn:executable
@@ -235,15 +235,20 @@ except AttributeError:
def patchrepo(ui, meta, parentctx, patchfp):
if not svnbackend:
return patchrepoold(ui, meta, parentctx, patchfp)
- store = patch.filestore()
+ store = patch.filestore(util.getfilestoresize(ui))
try:
touched = set()
backend = svnbackend(ui, meta.repo, parentctx, store)
- ret = patch.patchbackend(ui, backend, patchfp, 0, touched)
- if ret < 0:
- raise BadPatchApply('patching failed')
- if ret > 0:
- raise BadPatchApply('patching succeeded with fuzz')
+
+ try:
+ ret = patch.patchbackend(ui, backend, patchfp, 0, touched)
+ if ret < 0:
+ raise BadPatchApply('patching failed')
+ if ret > 0:
+ raise BadPatchApply('patching succeeded with fuzz')
+ except patch.PatchError, e:
+ raise BadPatchApply(str(e))
+
files = {}
for f in touched:
try:
@@ -271,8 +276,8 @@ def diff_branchrev(ui, svn, meta, branch, branchpath, r, parentctx):
if prev is None or pbranch == branch:
# letting patch handle binaries sounded
# cool, but it breaks patch in sad ways
- d = svn.get_unified_diff(branchpath, r.revnum, deleted=False,
- ignore_type=False)
+ d = svn.get_unified_diff(branchpath, r.revnum, other_rev=prev,
+ deleted=False, ignore_type=False)
else:
d = svn.get_unified_diff(branchpath, r.revnum,
other_path=ppath, other_rev=prev,
@@ -534,7 +539,12 @@ def fetch_branchrev(svn, meta, branch, branchpath, r, parentctx):
else:
branchprefix = (branchpath and branchpath + '/') or ''
for path, e in r.paths.iteritems():
- if not path.startswith(branchprefix):
+ if path == branchpath:
+ if e.action != 'R' or branch not in meta.branches:
+ # Full-branch replacements are handled as reverts,
+ # skip everything else.
+ continue
+ elif not path.startswith(branchprefix):
continue
if not meta.is_path_valid(path):
continue
@@ -698,6 +708,20 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
branch = meta.localname(p)
if not (r.paths[p].action == 'R' and branch in meta.branches):
continue
+ # Check the branch is not being replaced by one of its
+ # ancestors, it happens a lot with project-wide reverts.
+ frompath = r.paths[p].copyfrom_path
+ frompath, frombranch = meta.split_branch_path(
+ frompath, existing=False)[:2]
+ if frompath == '':
+ fromnode = meta.get_parent_revision(
+ r.paths[p].copyfrom_rev + 1, frombranch, exact=True)
+ if fromnode != node.nullid:
+ fromctx = meta.repo[fromnode]
+ pctx = meta.repo[meta.get_parent_revision(
+ r.revnum, branch, exact=True)]
+ if util.isancestor(pctx, fromctx):
+ continue
closed = checkbranch(meta, r, branch)
if closed is not None:
deleted_branches[branch] = closed
diff --git a/hgsubversion/svncommands.py b/hgsubversion/svncommands.py
index eec9e51..91dd529 100644
--- a/hgsubversion/svncommands.py
+++ b/hgsubversion/svncommands.py
@@ -3,6 +3,7 @@ import posixpath
import cPickle as pickle
import sys
import traceback
+import urlparse
from mercurial import commands
from mercurial import hg
@@ -15,82 +16,28 @@ import svnwrap
import svnrepo
import util
import svnexternals
+import verify
-def verify(ui, repo, args=None, **opts):
- '''verify current revision against Subversion repository
- '''
+def updatemeta(ui, repo, args, **opts):
+ """Do a partial rebuild of the subversion metadata.
- if repo is None:
- raise error.RepoError("There is no Mercurial repository"
- " here (.hg not found)")
+ Assumes that the metadata that currently exists is valid, but that
+ some is missing, e.g. because you have pulled some revisions via a
+ native mercurial method.
- ctx = repo[opts.get('rev', '.')]
- if 'close' in ctx.extra():
- ui.write('cannot verify closed branch')
- return 0
- convert_revision = ctx.extra().get('convert_revision')
- if convert_revision is None or not convert_revision.startswith('svn:'):
- raise hgutil.Abort('revision %s not from SVN' % ctx)
-
- if args:
- url = repo.ui.expandpath(args[0])
- else:
- url = repo.ui.expandpath('default')
-
- svn = svnrepo.svnremoterepo(ui, url).svn
- meta = repo.svnmeta(svn.uuid, svn.subdir)
- srev, branch, branchpath = meta.get_source_rev(ctx=ctx)
-
- branchpath = branchpath[len(svn.subdir.lstrip('/')):]
- branchurl = ('%s/%s' % (url, branchpath)).strip('/')
+ """
- ui.write('verifying %s against %s@%i\n' % (ctx, branchurl, srev))
+ return _buildmeta(ui, repo, args, partial=True)
- svnfiles = set()
- result = 0
- svndata = svn.list_files(branchpath, srev)
- for i, (fn, type) in enumerate(svndata):
- util.progress(ui, 'verify', i)
- if type != 'f':
- continue
- svnfiles.add(fn)
- fp = fn
- if branchpath:
- fp = branchpath + '/' + fn
- data, mode = svn.get_file(posixpath.normpath(fp), srev)
- try:
- fctx = ctx[fn]
- except error.LookupError:
- result = 1
- continue
- dmatch = fctx.data() == data
- mmatch = fctx.flags() == mode
- if not (dmatch and mmatch):
- ui.write('difference in file %s\n' % fn)
- result = 1
-
- hgfiles = set(ctx) - util.ignoredfiles
- if hgfiles != svnfiles:
- unexpected = hgfiles - svnfiles
- if unexpected:
- ui.write('unexpected files:\n')
- for f in sorted(unexpected):
- ui.write(' %s\n' % f)
- missing = svnfiles - hgfiles
- if missing:
- ui.write('missing files:\n')
- for f in sorted(missing):
- ui.write(' %s\n' % f)
- result = 1
-
- return result
-
-
-def rebuildmeta(ui, repo, args, **opts):
+def rebuildmeta(ui, repo, args, unsafe_skip_uuid_check=False, **opts):
"""rebuild hgsubversion metadata using values stored in revisions
"""
+ return _buildmeta(ui, repo, args, partial=False,
+ skipuuid=unsafe_skip_uuid_check)
+
+def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
if repo is None:
raise error.RepoError("There is no Mercurial repository"
@@ -110,14 +57,41 @@ def rebuildmeta(ui, repo, args, **opts):
if not os.path.exists(svnmetadir):
os.makedirs(svnmetadir)
+ youngest = 0
+ startrev = 0
+ sofar = []
+ branchinfo = {}
+ if partial:
+ try:
+ youngestpath = os.path.join(svnmetadir, 'lastpulled')
+ foundpartialinfo = False
+ if os.path.exists(youngestpath):
+ youngest = int(util.load_string(youngestpath).strip())
+ sofar = list(maps.RevMap.readmapfile(repo))
+ if sofar and len(sofar[-1].split(' ', 2)) > 1:
+ lasthash = sofar[-1].split(' ', 2)[1]
+ startrev = repo[lasthash].rev() + 1
+ branchinfo = pickle.load(open(os.path.join(svnmetadir,
+ 'branch_info')))
+ foundpartialinfo = True
+ if not foundpartialinfo:
+ ui.status('missing some metadata -- doing a full rebuild\n')
+ partial = False
+ except IOError, err:
+ if err.errno != errno.ENOENT:
+ raise
+ ui.status('missing some metadata -- doing a full rebuild')
+ except AttributeError:
+ ui.status('no metadata available -- doing a full rebuild')
+
+
lastpulled = open(os.path.join(svnmetadir, 'lastpulled'), 'wb')
revmap = open(os.path.join(svnmetadir, 'rev_map'), 'w')
revmap.write('1\n')
+ revmap.writelines(sofar)
last_rev = -1
- branchinfo = {}
- noderevnums = {}
tagfile = os.path.join(svnmetadir, 'tagmap')
- if os.path.exists(maps.Tags.filepath(repo)):
+ if not partial and os.path.exists(maps.Tags.filepath(repo)) :
os.unlink(maps.Tags.filepath(repo))
tags = maps.Tags(repo)
@@ -126,7 +100,7 @@ def rebuildmeta(ui, repo, args, **opts):
skipped = set()
closed = set()
- numrevs = len(repo)
+ numrevs = len(repo) - startrev
subdirfile = open(os.path.join(svnmetadir, 'subdir'), 'w')
subdirfile.write(subdir.strip('/'))
@@ -136,34 +110,38 @@ def rebuildmeta(ui, repo, args, **opts):
# it would make us use O(revisions^2) time, so we perform an extra traversal
# of the repository instead. During this traversal, we find all converted
# changesets that close a branch, and store their first parent
- youngest = 0
- for rev in repo:
- util.progress(ui, 'prepare', rev, total=numrevs)
+ for rev in xrange(startrev, len(repo)):
+ util.progress(ui, 'prepare', rev - startrev, total=numrevs)
ctx = repo[rev]
- extra = ctx.extra()
- convinfo = extra.get('convert_revision', None)
+ convinfo = util.getsvnrev(ctx, None)
if not convinfo:
continue
svnrevnum = int(convinfo.rsplit('@', 1)[1])
youngest = max(youngest, svnrevnum)
- if extra.get('close', None) is None:
+ if ctx.extra().get('close', None) is None:
continue
droprev = lambda x: x.rsplit('@', 1)[0]
parentctx = ctx.parents()[0]
- parentinfo = parentctx.extra().get('convert_revision', '@')
+ parentinfo = util.getsvnrev(parentctx, '@')
if droprev(parentinfo) == droprev(convinfo):
- closed.add(parentctx.rev())
+ if parentctx.rev() < startrev:
+ parentbranch = parentctx.branch()
+ if parentbranch == 'default':
+ parentbranch = None
+ branchinfo.pop(parentbranch)
+ else:
+ closed.add(parentctx.rev())
lastpulled.write(str(youngest) + '\n')
util.progress(ui, 'prepare', None, total=numrevs)
- for rev in repo:
- util.progress(ui, 'rebuild', rev, total=numrevs)
+ for rev in xrange(startrev, len(repo)):
+ util.progress(ui, 'rebuild', rev-startrev, total=numrevs)
ctx = repo[rev]
- convinfo = ctx.extra().get('convert_revision', None)
+ convinfo = util.getsvnrev(ctx, None)
if not convinfo:
continue
if '.hgtags' in ctx.files():
@@ -174,7 +152,7 @@ def rebuildmeta(ui, repo, args, **opts):
newdata = ctx.filectx('.hgtags').data()
for newtag in newdata[len(parentdata):-1].split('\n'):
ha, tag = newtag.split(' ', 1)
- tagged = repo[ha].extra().get('convert_revision', None)
+ tagged = util.getsvnrev(repo[ha], None)
if tagged is None:
tagged = -1
else:
@@ -212,9 +190,12 @@ def rebuildmeta(ui, repo, args, **opts):
# write repository uuid if required
if uuid is None:
uuid = convinfo[4:40]
- assert uuid == svn.uuid, 'UUIDs did not match!'
+ if not skipuuid:
+ if uuid != svn.uuid:
+ raise hgutil.Abort('remote svn repository identifier '
+ 'does not match')
uuidfile = open(os.path.join(svnmetadir, 'uuid'), 'w')
- uuidfile.write(uuid)
+ uuidfile.write(svn.uuid)
uuidfile.close()
# don't reflect closed branches
@@ -239,7 +220,6 @@ def rebuildmeta(ui, repo, args, **opts):
revmap.write('%s %s %s\n' % (revision, ctx.hex(), commitpath))
revision = int(revision)
- noderevnums[ctx.node()] = revision
if revision > last_rev:
last_rev = revision
@@ -252,7 +232,7 @@ def rebuildmeta(ui, repo, args, **opts):
parent = ctx
while parent.node() != node.nullid:
parentextra = parent.extra()
- parentinfo = parentextra.get('convert_revision')
+ parentinfo = util.getsvnrev(parent)
assert parentinfo
parent = parent.parents()[0]
@@ -277,15 +257,19 @@ def rebuildmeta(ui, repo, args, **opts):
pass
elif branch not in branchinfo:
parent = ctx.parents()[0]
- if (parent.node() in noderevnums
+ if (parent.node() not in skipped
+ and util.getsvnrev(parent, '').startswith('svn:')
and parent.branch() != ctx.branch()):
parentbranch = parent.branch()
if parentbranch == 'default':
parentbranch = None
else:
parentbranch = None
+ # branchinfo is a map from mercurial branch to a
+ # (svn branch, svn parent revision, svn revision) tuple
+ parentrev = util.getsvnrev(parent, '@').split('@')[1] or 0
branchinfo[branch] = (parentbranch,
- noderevnums.get(parent.node(), 0),
+ int(parentrev),
revision)
util.progress(ui, 'rebuild', None, total=numrevs)
@@ -408,7 +392,7 @@ def info(ui, repo, **opts):
ui.status('Not a child of an svn revision.\n')
return 0
r, br = hashes[pn]
- subdir = parent.extra()['convert_revision'][40:].split('@')[0]
+ subdir = util.getsvnrev(parent)[40:].split('@')[0]
if meta.layout == 'single':
branchpath = ''
elif br == None:
@@ -520,8 +504,9 @@ table = {
'listauthors': listauthors,
'update': update,
'help': help_,
+ 'updatemeta': updatemeta,
'rebuildmeta': rebuildmeta,
'updateexternals': svnexternals.updateexternals,
- 'verify': verify,
+ 'verify': verify.verify,
}
svn.__doc__ = _helpgen()
diff --git a/hgsubversion/svnmeta.py b/hgsubversion/svnmeta.py
index adc8f58..f95e1e9 100644
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -292,7 +292,9 @@ class SVNMeta(object):
return ''
if path and path[0] == '/':
path = path[1:]
- if path and path.startswith(self.subdir):
+ if path == self.subdir:
+ return ''
+ if path and path.startswith(self.subdir + '/'):
path = path[len(self.subdir):]
if path and path[0] == '/':
path = path[1:]
@@ -365,7 +367,7 @@ class SVNMeta(object):
if existing:
return None, None, None
if path == 'trunk' or path.startswith('trunk/'):
- path = path.split('/')[1:]
+ path = '/'.join(path.split('/')[1:])
test = 'trunk'
elif path.startswith('branches/'):
elts = path.split('/')
@@ -391,10 +393,10 @@ class SVNMeta(object):
return {ln: (src_branch, src_rev, revnum)}
return {}
- def is_path_valid(self, path):
+ def is_path_valid(self, path, existing=True):
if path is None:
return False
- subpath = self.split_branch_path(path)[0]
+ subpath = self.split_branch_path(path, existing)[0]
if subpath is None:
return False
return subpath in self.filemap
@@ -542,7 +544,9 @@ class SVNMeta(object):
# 1. Is the file located inside any currently known
# branch? If yes, then we're done with it, this isn't
# interesting.
- # 2. Does the file have copyfrom information? If yes, then
+ # 2. Does the file have copyfrom information? If yes, and
+ # the branch is being replaced by what would be an
+ # ancestor, treat it as a regular revert. Otherwise,
# we're done: this is a new branch, and we record the
# copyfrom in added_branches if it comes from the root
# of another branch, or create it from scratch.
@@ -563,6 +567,18 @@ class SVNMeta(object):
if paths[p].action == 'D':
self.closebranches.add(br) # case 4
elif paths[p].action == 'R':
+ # Check the replacing source is not an ancestor
+ # branch of the branch being replaced, this
+ # would just be a revert.
+ cfi, cbr = self.split_branch_path(
+ paths[p].copyfrom_path, paths[p].copyfrom_rev)[:2]
+ if cfi == '':
+ cctx = self.repo[self.get_parent_revision(
+ paths[p].copyfrom_rev + 1, cbr)]
+ ctx = self.repo[self.get_parent_revision(
+ revision.revnum, br)]
+ if cctx and util.isancestor(ctx, cctx):
+ continue
parent = self._determine_parent_branch(
p, paths[p].copyfrom_path, paths[p].copyfrom_rev,
revision.revnum)
diff --git a/hgsubversion/svnrepo.py b/hgsubversion/svnrepo.py
index c471fa5..4cb967e 100644
--- a/hgsubversion/svnrepo.py
+++ b/hgsubversion/svnrepo.py
@@ -18,8 +18,13 @@ import errno
from mercurial import error
from mercurial import util as hgutil
-from mercurial import httprepo
-import mercurial.repo
+
+try:
+ from mercurial.peer import peerrepository
+ from mercurial import httppeer
+except ImportError:
+ from mercurial.repo import repository as peerrepository
+ from mercurial import httprepo as httppeer
try:
from mercurial import phases
@@ -107,12 +112,14 @@ def generate_repo_class(ui, repo):
repo.__class__ = svnlocalrepo
-class svnremoterepo(mercurial.repo.repository):
+class svnremoterepo(peerrepository):
""" the dumb wrapper for actual Subversion repositories """
def __init__(self, ui, path=None):
self.ui = ui
if path is None:
+ path = self.ui.config('paths', 'default-push')
+ if path is None:
path = self.ui.config('paths', 'default')
if not path:
raise hgutil.Abort('no Subversion URL specified')
@@ -127,6 +134,9 @@ class svnremoterepo(mercurial.repo.repository):
else:
self.password_stores = None
+ def _capabilities(self):
+ return self.capabilities
+
@propertycache
def svnauth(self):
# DO NOT default the user to hg's getuser(). If you provide
@@ -177,7 +187,7 @@ def instance(ui, url, create):
if url.startswith('http://') or url.startswith('https://'):
try:
# may yield a bogus 'real URL...' message
- return httprepo.instance(ui, url, create)
+ return httppeer.instance(ui, url, create)
except error.RepoError:
ui.traceback()
ui.note('(falling back to Subversion support)\n')
@@ -185,4 +195,102 @@ def instance(ui, url, create):
if create:
raise hgutil.Abort('cannot create new remote Subversion repository')
+ svnwrap.prompt_callback(SubversionPrompt(ui))
return svnremoterepo(ui, url)
+
+class SubversionPrompt(object):
+ def __init__(self, ui):
+ self.ui = ui
+
+ def maybe_print_realm(self, realm):
+ if realm:
+ self.ui.write('Authentication realm: %s\n' % (realm,))
+ self.ui.flush()
+
+ def username(self, realm, may_save, pool=None):
+ self.maybe_print_realm(realm)
+ username = self.ui.prompt('Username: ', default='')
+ return (username, bool(may_save))
+
+ def simple(self, realm, default_username, may_save, pool=None):
+ self.maybe_print_realm(realm)
+ if default_username:
+ username = default_username
+ else:
+ username = self.ui.prompt('Username: ', default='')
+ password = self.ui.getpass('Password for \'%s\': ' % (username,), default='')
+ return (username, password, bool(may_save))
+
+ def ssl_client_cert(self, realm, may_save, pool=None):
+ self.maybe_print_realm(realm)
+ cert_file = self.ui.prompt('Client certificate filename: ', default='')
+ return (cert_file, bool(may_save))
+
+ def ssl_client_cert_pw(self, realm, may_save, pool=None):
+ password = self.ui.getpass('Passphrase for \'%s\': ' % (realm,), default='')
+ return (password, bool(may_save))
+
+ def insecure(fn):
+ def fun(self, *args, **kwargs):
+ failures = args[1]
+ cert_info = args[2]
+ # cert_info[0] is hostname
+ # cert_info[1] is fingerprint
+
+ fingerprint = self.ui.config('hostfingerprints', cert_info[0])
+ if fingerprint and fingerprint.lower() == cert_info[1].lower():
+ # same as the acceptance temporarily
+ return (failures, False)
+
+ cacerts = self.ui.config('web', 'cacerts')
+ if not cacerts:
+ # same as the acceptance temporarily
+ return (failures, False)
+
+ return fn(self, *args, **kwargs)
+ return fun
+
+ @insecure
+ def ssl_server_trust(self, realm, failures, cert_info, may_save, pool=None):
+ msg = 'Error validating server certificate for \'%s\':\n' % (realm,)
+ if failures & svnwrap.SSL_UNKNOWNCA:
+ msg += (
+ ' - The certificate is not issued by a trusted authority. Use the\n'
+ ' fingerprint to validate the certificate manually!\n'
+ )
+ if failures & svnwrap.SSL_CNMISMATCH:
+ msg += ' - The certificate hostname does not match.\n'
+ if failures & svnwrap.SSL_NOTYETVALID:
+ msg += ' - The certificate is not yet valid.\n'
+ if failures & svnwrap.SSL_EXPIRED:
+ msg += ' - The certificate has expired.\n'
+ if failures & svnwrap.SSL_OTHER:
+ msg += ' - The certificate has an unknown error.\n'
+ msg += (
+ 'Certificate information:\n'
+ '- Hostname: %s\n'
+ '- Valid: from %s until %s\n'
+ '- Issuer: %s\n'
+ '- Fingerprint: %s\n'
+ ) % (
+ cert_info[0], # hostname
+ cert_info[2], # valid_from
+ cert_info[3], # valid_until
+ cert_info[4], # issuer_dname
+ cert_info[1], # fingerprint
+ )
+ if may_save:
+ msg += '(R)eject, accept (t)emporarily or accept (p)ermanently? '
+ choices = (('&Reject'), ('&Temporarily'), ('&Permanently'))
+ else:
+ msg += '(R)eject or accept (t)emporarily? '
+ choices = (('&Reject'), ('&Temporarily'))
+ choice = self.ui.promptchoice(msg, choices, default=0)
+ if choice == 1:
+ creds = (failures, False)
+ elif may_save and choice == 2:
+ creds = (failures, True)
+ else:
+ creds = None
+ return creds
+
diff --git a/hgsubversion/svnwrap/common.py b/hgsubversion/svnwrap/common.py
index 211ff6d..1157b21 100644
--- a/hgsubversion/svnwrap/common.py
+++ b/hgsubversion/svnwrap/common.py
@@ -8,6 +8,9 @@ import tempfile
import urlparse
import urllib
import collections
+import fnmatch
+import ConfigParser
+import sys
class SubversionRepoCanNotReplay(Exception):
"""Exception raised when the svn server is too old to have replay.
@@ -78,3 +81,91 @@ class Revision(tuple):
def __str__(self):
return 'r%d by %s' % (self.revnum, self.author)
+
+
+_svn_config_dir = None
+
+
+class AutoPropsConfig(object):
+ """Provides the subversion auto-props functionality
+ when pushing new files.
+ """
+ def __init__(self, config_dir=None):
+ config_file = config_file_path(config_dir)
+ self.config = ConfigParser.RawConfigParser()
+ self.config.read([config_file])
+
+ def properties(self, file):
+ """Returns a dictionary of the auto-props applicable for file.
+ Takes enable-auto-props into account.
+ """
+ properties = {}
+ if self.autoprops_enabled():
+ for pattern,prop_list in self.config.items('auto-props'):
+ if fnmatch.fnmatchcase(os.path.basename(file), pattern):
+ properties.update(parse_autoprops(prop_list))
+ return properties
+
+ def autoprops_enabled(self):
+ return (self.config.has_option('miscellany', 'enable-auto-props')
+ and self.config.getboolean( 'miscellany', 'enable-auto-props')
+ and self.config.has_section('auto-props'))
+
+
+def config_file_path(config_dir):
+ if config_dir == None:
+ global _svn_config_dir
+ config_dir = _svn_config_dir
+ if config_dir == None:
+ if sys.platform == 'win32':
+ config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
+ else:
+ config_dir = os.path.join(os.environ['HOME'], '.subversion')
+ return os.path.join(config_dir, 'config')
+
+
+def parse_autoprops(prop_list):
+ """Parses a string of autoprops and returns a dictionary of
+ the results.
+ Emulates the parsing of core.auto_props_enumerator.
+ """
+ def unquote(s):
+ if len(s)>1 and s[0] in ['"', "'"] and s[0]==s[-1]:
+ return s[1:-1]
+ return s
+
+ properties = {}
+ for prop in prop_list.split(';'):
+ if '=' in prop:
+ prop, value = prop.split('=',1)
+ value = unquote(value.strip())
+ else:
+ value = ''
+ properties[prop.strip()] = value
+ return properties
+
+class SimpleStringIO(object):
+ """SimpleStringIO can replace a StringIO in write mode.
+
+ cStringIO reallocates and doubles the size of its internal buffer
+ when it needs to append new data which requires two large blocks for
+ large inputs. SimpleStringIO stores each individual blocks and joins
+ them once done. This might cause more memory fragmentation but
+ requires only one large block. In practice, ra.get_file() seems to
+ write in 16kB blocks (svn 1.7.5) which should be friendly to memory
+ allocators.
+ """
+ def __init__(self, closing=True):
+ self._blocks = []
+ self._closing = closing
+
+ def write(self, s):
+ self._blocks.append(s)
+
+ def getvalue(self):
+ return ''.join(self._blocks)
+
+ def close(self):
+ if self._closing:
+ del self._blocks
+
diff --git a/hgsubversion/svnwrap/subvertpy_wrapper.py b/hgsubversion/svnwrap/subvertpy_wrapper.py
index d85ee80..76cd05c 100644
--- a/hgsubversion/svnwrap/subvertpy_wrapper.py
+++ b/hgsubversion/svnwrap/subvertpy_wrapper.py
@@ -1,5 +1,4 @@
import cStringIO
-import getpass
import errno
import os
import shutil
@@ -59,6 +58,11 @@ ERR_FS_TXN_OUT_OF_DATE = subvertpy.ERR_FS_TXN_OUT_OF_DATE
ERR_INCOMPLETE_DATA = subvertpy.ERR_INCOMPLETE_DATA
ERR_RA_DAV_PATH_NOT_FOUND = subvertpy.ERR_RA_DAV_PATH_NOT_FOUND
ERR_RA_DAV_REQUEST_FAILED = subvertpy.ERR_RA_DAV_REQUEST_FAILED
+SSL_UNKNOWNCA = subvertpy.SSL_UNKNOWNCA
+SSL_CNMISMATCH = subvertpy.SSL_CNMISMATCH
+SSL_NOTYETVALID = subvertpy.SSL_NOTYETVALID
+SSL_EXPIRED = subvertpy.SSL_EXPIRED
+SSL_OTHER = subvertpy.SSL_OTHER
SubversionException = subvertpy.SubversionException
apply_txdelta = delta.apply_txdelta_handler
# superclass for editor.HgEditor
@@ -73,6 +77,11 @@ def ieditor(fn):
return fn
+_prompt = None
+def prompt_callback(callback):
+ global _prompt
+ _prompt = callback
+
_svntypes = {
subvertpy.NODE_DIR: 'd',
subvertpy.NODE_FILE: 'f',
@@ -87,38 +96,18 @@ class PathAdapter(object):
self.copyfrom_path = intern(self.copyfrom_path)
class AbstractEditor(object):
- __slots__ = ('editor',)
+ __slots__ = ('editor', 'baton')
- def __init__(self, editor):
+ def __init__(self, editor, baton=None):
self.editor = editor
+ self.baton = baton
def set_target_revision(self, rev):
pass
def open_root(self, base_revnum):
- return self.open_directory('', base_revnum)
-
- def open_directory(self, path, base_revnum):
- self.editor.open_directory(path, None, base_revnum)
- return DirectoryEditor(self.editor, path)
-
- def open_file(self, path, base_revnum):
- self.editor.open_file(path, None, base_revnum)
- return FileEditor(self.editor, path)
-
- def add_directory(self, path, copyfrom_path=None, copyfrom_rev=-1):
- self.editor.add_directory(path, None, copyfrom_path, copyfrom_rev)
- return DirectoryEditor(self.editor, path)
-
- def add_file(self, path, copyfrom_path=None, copyfrom_rev=-1):
- self.editor.add_file(path, None, copyfrom_path, copyfrom_rev)
- return FileEditor(self.editor, path)
-
- def apply_textdelta(self, base_checksum):
- return self.editor.apply_textdelta(self, None, base_checksum)
-
- def change_prop(self, name, value):
- raise NotImplementedError()
+ baton = self.editor.open_root(None, base_revnum)
+ return DirectoryEditor(self.editor, baton)
def abort(self):
# TODO: should we do something special here?
@@ -127,37 +116,51 @@ class AbstractEditor(object):
def close(self):
del self.editor
- def delete_entry(self, path, revnum):
- self.editor.delete_entry(path, revnum, None)
-
class FileEditor(AbstractEditor):
- __slots__ = ('path',)
-
- def __init__(self, editor, path):
- super(FileEditor, self).__init__(editor)
- self.path = path
+ def __init__(self, editor, baton):
+ super(FileEditor, self).__init__(editor, baton)
def change_prop(self, name, value):
- self.editor.change_file_prop(self.path, name, value, pool=None)
+ self.editor.change_file_prop(self.baton, name, value, pool=None)
+
+ def apply_textdelta(self, base_checksum):
+ return self.editor.apply_textdelta(self.baton, base_checksum)
def close(self, checksum=None):
+ self.editor.close_file(self.baton, checksum)
super(FileEditor, self).close()
- del self.path
class DirectoryEditor(AbstractEditor):
- __slots__ = ('path',)
+ def __init__(self, editor, baton):
+ super(DirectoryEditor, self).__init__(editor, baton)
- def __init__(self, editor, path):
- super(DirectoryEditor, self).__init__(editor)
- self.path = path
+ def delete_entry(self, path, revnum):
+ self.editor.delete_entry(path, revnum, self.baton)
+
+ def open_directory(self, path, base_revnum):
+ baton = self.editor.open_directory(path, self.baton, base_revnum)
+ return DirectoryEditor(self.editor, baton)
+
+ def add_directory(self, path, copyfrom_path=None, copyfrom_rev=-1):
+ baton = self.editor.add_directory(
+ path, self.baton, copyfrom_path, copyfrom_rev)
+ return DirectoryEditor(self.editor, baton)
+
+ def open_file(self, path, base_revnum):
+ baton = self.editor.open_file(path, self.baton, base_revnum)
+ return FileEditor(self.editor, baton)
+
+ def add_file(self, path, copyfrom_path=None, copyfrom_rev=-1):
+ baton = self.editor.add_file(
+ path, self.baton, copyfrom_path, copyfrom_rev)
+ return FileEditor(self.editor, baton)
def change_prop(self, name, value):
- self.editor.change_dir_prop(self.path, name, value, pool=None)
+ self.editor.change_dir_prop(self.baton, name, value, pool=None)
def close(self):
- self.editor.close_directory(self.path)
+ self.editor.close_directory(self.baton)
super(DirectoryEditor, self).close()
- del self.path
class SubversionRepo(object):
"""Wrapper for a Subversion repository.
@@ -191,6 +194,7 @@ class SubversionRepo(object):
# expects unquoted paths
self.subdir = urllib.unquote(self.subdir)
self.hasdiff3 = True
+ self.autoprops_config = common.AutoPropsConfig()
def init_ra_and_client(self):
"""
@@ -202,11 +206,28 @@ class SubversionRepo(object):
"""
def getclientstring():
return 'hgsubversion'
- # TODO: handle certificate authentication, Mercurial style
- def getpass(realm, username, may_save):
- return self.username or username, self.password or '', False
- def getuser(realm, may_save):
- return self.username or '', False
+
+ def simple(realm, username, may_save):
+ return _prompt.simple(realm, username, may_save)
+
+ def username(realm, may_save):
+ return _prompt.username(realm, may_save)
+
+ def ssl_client_cert(realm, may_save):
+ return _prompt.ssl_client_cert(realm, may_save)
+
+ def ssl_client_cert_pw(realm, may_save):
+ return _prompt.ssl_client_cert_pw(realm, may_save)
+
+ def ssl_server_trust(realm, failures, cert_info, may_save):
+ creds = _prompt.ssl_server_trust(realm, failures, cert_info, may_save)
+ if creds is None:
+ # We need to reject the certificate, but subvertpy doesn't
+ # handle None as a return value here, and requires
+ # we instead return a tuple of (int, bool). Because of that,
+ # we return (0, False) instead.
+ creds = (0, False)
+ return creds
providers = ra.get_platform_specific_client_providers()
providers += [
@@ -215,9 +236,15 @@ class SubversionRepo(object):
ra.get_ssl_client_cert_file_provider(),
ra.get_ssl_client_cert_pw_file_provider(),
ra.get_ssl_server_trust_file_provider(),
- ra.get_username_prompt_provider(getuser, 0),
- ra.get_simple_prompt_provider(getpass, 0),
]
+ if _prompt:
+ providers += [
+ ra.get_simple_prompt_provider(simple, 2),
+ ra.get_username_prompt_provider(username, 2),
+ ra.get_ssl_client_cert_prompt_provider(ssl_client_cert, 2),
+ ra.get_ssl_client_cert_pw_prompt_provider(ssl_client_cert_pw, 2),
+ ra.get_ssl_server_trust_prompt_provider(ssl_server_trust),
+ ]
auth = ra.Auth(providers)
if self.username:
@@ -225,9 +252,20 @@ class SubversionRepo(object):
if self.password:
auth.set_parameter(subvertpy.AUTH_PARAM_DEFAULT_PASSWORD, self.password)
- self.remote = ra.RemoteAccess(url=self.svn_url,
- client_string_func=getclientstring,
- auth=auth)
+ try:
+ self.remote = ra.RemoteAccess(url=self.svn_url,
+ client_string_func=getclientstring,
+ auth=auth)
+ except SubversionException, e:
+ # e.child contains a detailed error messages
+ msglist = []
+ svn_exc = e
+ while svn_exc:
+ if svn_exc.args[0]:
+ msglist.append(svn_exc.args[0])
+ svn_exc = svn_exc.child
+ msg = '\n'.join(msglist)
+ raise common.SubversionConnectionException(msg)
self.client = client.Client()
self.client.auth = auth
@@ -411,10 +449,14 @@ class SubversionRepo(object):
return pathidx
- rooteditor = commiteditor.open_root()
- visitdir(rooteditor, '', paths, 0)
- rooteditor.close()
- commiteditor.close()
+ try:
+ rooteditor = commiteditor.open_root()
+ visitdir(rooteditor, '', paths, 0)
+ rooteditor.close()
+ commiteditor.close()
+ except:
+ commiteditor.abort()
+ raise
def get_replay(self, revision, editor, oldestrev=0):
@@ -467,7 +509,7 @@ class SubversionRepo(object):
"""
mode = ''
try:
- out = cStringIO.StringIO()
+ out = common.SimpleStringIO()
rev, info = self.remote.get_file(path, out, revision)
data = out.getvalue()
out.close()
diff --git a/hgsubversion/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py
index d232f03..51373b8 100644
--- a/hgsubversion/svnwrap/svn_swig_wrapper.py
+++ b/hgsubversion/svnwrap/svn_swig_wrapper.py
@@ -1,5 +1,4 @@
import cStringIO
-import getpass
import errno
import os
import shutil
@@ -43,6 +42,11 @@ ERR_FS_NOT_FOUND = core.SVN_ERR_FS_NOT_FOUND
ERR_FS_TXN_OUT_OF_DATE = core.SVN_ERR_FS_TXN_OUT_OF_DATE
ERR_INCOMPLETE_DATA = core.SVN_ERR_INCOMPLETE_DATA
ERR_RA_DAV_REQUEST_FAILED = core.SVN_ERR_RA_DAV_REQUEST_FAILED
+SSL_UNKNOWNCA = core.SVN_AUTH_SSL_UNKNOWNCA
+SSL_CNMISMATCH = core.SVN_AUTH_SSL_CNMISMATCH
+SSL_NOTYETVALID = core.SVN_AUTH_SSL_NOTYETVALID
+SSL_EXPIRED = core.SVN_AUTH_SSL_EXPIRED
+SSL_OTHER = core.SVN_AUTH_SSL_OTHER
SubversionException = core.SubversionException
Editor = delta.Editor
@@ -57,6 +61,7 @@ def optrev(revnum):
optrev.value.number = revnum
return optrev
+core.svn_config_ensure(None)
svn_config = core.svn_config_get_config(None)
class RaCallbacks(ra.Callbacks):
@staticmethod
@@ -88,19 +93,49 @@ def ieditor(fn):
raise
return fun
-def user_pass_prompt(realm, default_username, ms, pool): # pragma: no cover
- # FIXME: should use getpass() and username() from mercurial.ui
+_prompt = None
+def prompt_callback(callback):
+ global _prompt
+ _prompt = callback
+
+def _simple(realm, default_username, ms, pool):
+ ret = _prompt.simple(realm, default_username, ms, pool)
creds = core.svn_auth_cred_simple_t()
- creds.may_save = ms
- if default_username:
- sys.stderr.write('Auth realm: %s\n' % (realm,))
- creds.username = default_username
+ (creds.username, creds.password, creds.may_save) = ret
+ return creds
+
+def _username(realm, ms, pool):
+ ret = _prompt.username(realm, ms, pool)
+ creds = core.svn_auth_cred_username_t()
+ (creds.username, creds.may_save) = ret
+ return creds
+
+def _ssl_client_cert(realm, may_save, pool):
+ ret = _prompt.ssl_client_cert(realm, may_save, pool)
+ creds = core.svn_auth_cred_ssl_client_cert_t()
+ (creds.cert_file, creds.may_save) = ret
+ return creds
+
+def _ssl_client_cert_pw(realm, may_save, pool):
+ ret = _prompt.ssl_client_cert_pw(realm, may_save, pool)
+ creds = core.svn_auth_cred_ssl_client_cert_pw_t()
+ (creds.password, creds.may_save) = ret
+ return creds
+
+def _ssl_server_trust(realm, failures, cert_info, may_save, pool):
+ cert = [
+ cert_info.hostname,
+ cert_info.fingerprint,
+ cert_info.valid_from,
+ cert_info.valid_until,
+ cert_info.issuer_dname,
+ ]
+ ret = _prompt.ssl_server_trust(realm, failures, cert, may_save, pool)
+ if ret:
+ creds = core.svn_auth_cred_ssl_server_trust_t()
+ (creds.accepted_failures, creds.may_save) = ret
else:
- sys.stderr.write('Auth realm: %s\n' % (realm,))
- sys.stderr.write('Username: ')
- sys.stderr.flush()
- creds.username = sys.stdin.readline().strip()
- creds.password = getpass.getpass('Password for %s: ' % creds.username)
+ creds = None
return creds
def _create_auth_baton(pool, password_stores):
@@ -145,9 +180,17 @@ def _create_auth_baton(pool, password_stores):
client.get_ssl_client_cert_file_provider(),
client.get_ssl_client_cert_pw_file_provider(),
client.get_ssl_server_trust_file_provider(),
- client.get_simple_prompt_provider(user_pass_prompt, 2),
]
+ if _prompt:
+ providers += [
+ client.get_simple_prompt_provider(_simple, 2),
+ client.get_username_prompt_provider(_username, 2),
+ client.get_ssl_client_cert_prompt_provider(_ssl_client_cert, 2),
+ client.get_ssl_client_cert_pw_prompt_provider(_ssl_client_cert_pw, 2),
+ client.get_ssl_server_trust_prompt_provider(_ssl_server_trust),
+ ]
+
return core.svn_auth_open(providers, pool)
_svntypes = {
@@ -165,7 +208,7 @@ class SubversionRepo(object):
# --username and --password override URL credentials
self.username = parsed[0]
self.password = parsed[1]
- self.svn_url = parsed[2]
+ self.svn_url = core.svn_path_canonicalize(parsed[2])
self.auth_baton_pool = core.Pool()
self.auth_baton = _create_auth_baton(self.auth_baton_pool, password_stores)
# self.init_ra_and_client() assumes that a pool already exists
@@ -184,6 +227,7 @@ class SubversionRepo(object):
# expects unquoted paths
self.subdir = urllib.unquote(self.subdir)
self.hasdiff3 = True
+ self.autoprops_config = common.AutoPropsConfig()
def init_ra_and_client(self):
"""Initializes the RA and client layers, because sometimes getting
@@ -210,19 +254,14 @@ class SubversionRepo(object):
self.ra = ra.open2(self.svn_url, callbacks,
svn_config, self.pool)
except SubversionException, e:
- if e.apr_err == core.SVN_ERR_RA_SERF_SSL_CERT_UNTRUSTED:
- msg = ('Subversion does not trust the SSL certificate for this '
- 'site; please try running \'svn ls %s\' first.'
- % self.svn_url)
- elif e.apr_err == core.SVN_ERR_RA_DAV_REQUEST_FAILED:
- msg = ('Failed to open Subversion repository; please try '
- 'running \'svn ls %s\' for details.' % self.svn_url)
- else:
- msg = e.args[0]
- for k, v in vars(core).iteritems():
- if k.startswith('SVN_ERR_') and v == e.apr_err:
- msg = '%s (%s)' % (msg, k)
- break
+ # e.child contains a detailed error messages
+ msglist = []
+ svn_exc = e
+ while svn_exc:
+ if svn_exc.args[0]:
+ msglist.append(svn_exc.args[0])
+ svn_exc = svn_exc.child
+ msg = '\n'.join(msglist)
raise common.SubversionConnectionException(msg)
@property
@@ -394,9 +433,15 @@ class SubversionRepo(object):
# TODO pass md5(new_text) instead of None
editor.close_file(baton, None, pool)
- delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb,
- self.pool)
- editor.close_edit(edit_baton, self.pool)
+ try:
+ delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb,
+ self.pool)
+ editor.close_edit(edit_baton, self.pool)
+ except:
+ # If anything went wrong on the preceding lines, we should
+ # abort the in-progress transaction.
+ editor.abort_edit(edit_baton, self.pool)
+ raise
def get_replay(self, revision, editor, oldest_rev_i_have=0):
# this method has a tendency to chew through RAM if you don't re-init
@@ -415,6 +460,19 @@ class SubversionRepo(object):
else:
raise
+ # if we're not pulling the whole repo, svn fails to report
+ # file properties for files merged from subtrees outside ours
+ if self.svn_url != self.root:
+ links, execs = editor.current.symlinks, editor.current.execfiles
+ l = len(self.subdir) - 1
+ for f in editor.current.added:
+ sf = f[l:]
+ if links[f] or execs[f]:
+ continue
+ props = self.list_props(sf, revision)
+ links[f] = props.get('svn:special') == '*'
+ execs[f] = props.get('svn:executable') == '*'
+
def get_revision(self, revision, editor):
''' feed the contents of the given revision to the given editor '''
@@ -485,7 +543,7 @@ class SubversionRepo(object):
assert not path.startswith('/')
mode = ''
try:
- out = cStringIO.StringIO()
+ out = common.SimpleStringIO()
info = ra.get_file(self.ra, path, revision, out)
data = out.getvalue()
out.close()
@@ -559,6 +617,9 @@ class SubversionRepo(object):
if not path or path == '.':
return self.svn_url
assert path[0] != '/', path
- return '/'.join((self.svn_url,
- urllib.quote(path).rstrip('/'),
- ))
+ path = path.rstrip('/')
+ try:
+ # new in svn 1.7
+ return core.svn_uri_canonicalize(self.svn_url + '/' + path)
+ except AttributeError:
+ return self.svn_url + '/' + urllib.quote(path)
diff --git a/hgsubversion/util.py b/hgsubversion/util.py
index f55893e..8648349 100644
--- a/hgsubversion/util.py
+++ b/hgsubversion/util.py
@@ -1,6 +1,8 @@
+import errno
import re
import os
import urllib
+from collections import deque
from mercurial import cmdutil
from mercurial import error
@@ -13,6 +15,8 @@ try:
except ImportError:
pass
+import maps
+
ignoredfiles = set(['.hgtags', '.hgsvnexternals', '.hgsub', '.hgsubstate'])
b_re = re.compile(r'^\+\+\+ b\/([^\n]*)', re.MULTILINE)
@@ -208,6 +212,14 @@ def swap_out_encoding(new_encoding="UTF-8"):
encoding.encoding = new_encoding
return old
+def isancestor(ctx, ancestorctx):
+ """Return True if ancestorctx is equal or an ancestor of ctx."""
+ if ctx == ancestorctx:
+ return True
+ for actx in ctx.ancestors():
+ if actx == ancestorctx:
+ return True
+ return False
def issamefile(parentctx, childctx, f):
"""Return True if f exists and is the same in childctx and parentctx"""
@@ -231,11 +243,17 @@ def issamefile(parentctx, childctx, f):
# parentctx is not an ancestor of childctx, files are unrelated
return False
+
+def getsvnrev(ctx, defval=None):
+ '''Extract SVN revision from commit metadata'''
+ return ctx.extra().get('convert_revision', defval)
+
+
def _templatehelper(ctx, kw):
'''
Helper function for displaying information about converted changesets.
'''
- convertinfo = ctx.extra().get('convert_revision', '')
+ convertinfo = getsvnrev(ctx, '')
if not convertinfo or not convertinfo.startswith('svn:'):
return ''
@@ -273,11 +291,17 @@ def revset_fromsvn(repo, subset, x):
'''
args = revset.getargs(x, 0, 0, "fromsvn takes no arguments")
- def matches(r):
- convertinfo = repo[r].extra().get('convert_revision', '')
- return convertinfo[:4] == 'svn:'
-
- return [r for r in subset if matches(r)]
+ rev = repo.changelog.rev
+ bin = node.bin
+ try:
+ svnrevs = set(rev(bin(l.split(' ', 2)[1]))
+ for l in maps.RevMap.readmapfile(repo, missingok=False))
+ return filter(svnrevs.__contains__, subset)
+ except IOError, err:
+ if err.errno != errno.ENOENT:
+ raise
+ raise hgutil.Abort("svn metadata is missing - "
+ "run 'hg svn rebuildmeta' to reconstruct it")
def revset_svnrev(repo, subset, x):
'''``svnrev(number)``
@@ -288,19 +312,65 @@ def revset_svnrev(repo, subset, x):
rev = revset.getstring(args[0],
"the argument to svnrev() must be a number")
try:
- rev = int(rev)
+ revnum = int(rev)
except ValueError:
raise error.ParseError("the argument to svnrev() must be a number")
- def matches(r):
- convertinfo = repo[r].extra().get('convert_revision', '')
- if convertinfo[:4] != 'svn:':
- return False
- return int(convertinfo[40:].rsplit('@', 1)[-1]) == rev
-
- return [r for r in subset if matches(r)]
+ rev = rev + ' '
+ revs = []
+ try:
+ for l in maps.RevMap.readmapfile(repo, missingok=False):
+ if l.startswith(rev):
+ n = l.split(' ', 2)[1]
+ r = repo[node.bin(n)].rev()
+ if r in subset:
+ revs.append(r)
+ return revs
+ except IOError, err:
+ if err.errno != errno.ENOENT:
+ raise
+ raise hgutil.Abort("svn metadata is missing - "
+ "run 'hg svn rebuildmeta' to reconstruct it")
revsets = {
'fromsvn': revset_fromsvn,
'svnrev': revset_svnrev,
}
+
+def getfilestoresize(ui):
+ """Return the replay or stupid file memory store size in megabytes or -1"""
+ size = ui.configint('hgsubversion', 'filestoresize', 200)
+ if size >= 0:
+ size = size*(2**20)
+ else:
+ size = -1
+ return size
+
+# Copy-paste from mercurial.util to avoid having to deal with backward
+# compatibility, plus the cache size is configurable.
+def lrucachefunc(func, size):
+ '''cache most recent results of function calls'''
+ cache = {}
+ order = deque()
+ if func.func_code.co_argcount == 1:
+ def f(arg):
+ if arg not in cache:
+ if len(cache) > size:
+ del cache[order.popleft()]
+ cache[arg] = func(arg)
+ else:
+ order.remove(arg)
+ order.append(arg)
+ return cache[arg]
+ else:
+ def f(*args):
+ if args not in cache:
+ if len(cache) > size:
+ del cache[order.popleft()]
+ cache[args] = func(*args)
+ else:
+ order.remove(args)
+ order.append(args)
+ return cache[args]
+
+ return f
diff --git a/hgsubversion/verify.py b/hgsubversion/verify.py
new file mode 100644
index 0000000..2e5a1ae
--- /dev/null
+++ b/hgsubversion/verify.py
@@ -0,0 +1,196 @@
+import posixpath
+
+from mercurial import util as hgutil
+from mercurial import error
+
+import svnwrap
+import svnrepo
+import util
+import editor
+
+def verify(ui, repo, args=None, **opts):
+ '''verify current revision against Subversion repository
+ '''
+
+ if repo is None:
+ raise error.RepoError("There is no Mercurial repository"
+ " here (.hg not found)")
+
+ ctx = repo[opts.get('rev', '.')]
+ if 'close' in ctx.extra():
+ ui.write('cannot verify closed branch')
+ return 0
+ convert_revision = ctx.extra().get('convert_revision')
+ if convert_revision is None or not convert_revision.startswith('svn:'):
+ raise hgutil.Abort('revision %s not from SVN' % ctx)
+
+ if args:
+ url = repo.ui.expandpath(args[0])
+ else:
+ url = repo.ui.expandpath('default')
+
+ svn = svnrepo.svnremoterepo(ui, url).svn
+ meta = repo.svnmeta(svn.uuid, svn.subdir)
+ srev, branch, branchpath = meta.get_source_rev(ctx=ctx)
+
+ branchpath = branchpath[len(svn.subdir.lstrip('/')):]
+ branchurl = ('%s/%s' % (url, branchpath)).strip('/')
+
+ ui.write('verifying %s against %s@%i\n' % (ctx, branchurl, srev))
+
+ if opts.get('stupid', ui.configbool('hgsubversion', 'stupid')):
+ svnfiles = set()
+ result = 0
+
+ hgfiles = set(ctx) - util.ignoredfiles
+
+ svndata = svn.list_files(branchpath, srev)
+ for i, (fn, type) in enumerate(svndata):
+ util.progress(ui, 'verify', i, total=len(hgfiles))
+
+ if type != 'f':
+ continue
+ svnfiles.add(fn)
+ fp = fn
+ if branchpath:
+ fp = branchpath + '/' + fn
+ data, mode = svn.get_file(posixpath.normpath(fp), srev)
+ try:
+ fctx = ctx[fn]
+ except error.LookupError:
+ result = 1
+ continue
+ if not fctx.data() == data:
+ ui.write('difference in: %s\n' % fn)
+ result = 1
+ if not fctx.flags() == mode:
+ ui.write('wrong flags for: %s\n' % fn)
+ result = 1
+
+ if hgfiles != svnfiles:
+ unexpected = hgfiles - svnfiles
+ for f in sorted(unexpected):
+ ui.write('unexpected file: %s\n' % f)
+ missing = svnfiles - hgfiles
+ for f in sorted(missing):
+ ui.write('missing file: %s\n' % f)
+ result = 1
+
+ util.progress(ui, 'verify', None, total=len(hgfiles))
+
+ else:
+ class VerifyEditor(svnwrap.Editor):
+ """editor that verifies a repository against the given context."""
+ def __init__(self, ui, ctx):
+ self.ui = ui
+ self.ctx = ctx
+ self.unexpected = set(ctx) - util.ignoredfiles
+ self.missing = set()
+ self.failed = False
+
+ self.total = len(self.unexpected)
+ self.seen = 0
+
+ def open_root(self, base_revnum, pool=None):
+ pass
+
+ def add_directory(self, path, parent_baton, copyfrom_path,
+ copyfrom_revision, pool=None):
+ self.file = None
+ self.props = None
+
+ def open_directory(self, path, parent_baton, base_revision, pool=None):
+ self.file = None
+ self.props = None
+
+ def add_file(self, path, parent_baton=None, copyfrom_path=None,
+ copyfrom_revision=None, file_pool=None):
+
+ if path in self.unexpected:
+ self.unexpected.remove(path)
+ self.file = path
+ self.props = {}
+ else:
+ self.total += 1
+ self.missing.add(path)
+ self.failed = True
+ self.file = None
+ self.props = None
+
+ self.seen += 1
+ util.progress(self.ui, 'verify', self.seen, total=self.total)
+
+ def open_file(self, path, base_revnum):
+ raise NotImplementedError()
+
+ def apply_textdelta(self, file_baton, base_checksum, pool=None):
+ stream = svnwrap.SimpleStringIO(closing=False)
+ handler = svnwrap.apply_txdelta('', stream)
+ if not callable(handler):
+ raise hgutil.Abort('Error in Subversion bindings: '
+ 'cannot call handler!')
+ def txdelt_window(window):
+ handler(window)
+ # window being None means we're done
+ if window:
+ return
+
+ fctx = self.ctx[self.file]
+ hgdata = fctx.data()
+ svndata = stream.getvalue()
+
+ if 'svn:executable' in self.props:
+ if fctx.flags() != 'x':
+ self.ui.warn('wrong flags for: %s\n' % self.file)
+ self.failed = True
+ elif 'svn:special' in self.props:
+ hgdata = 'link ' + hgdata
+ if fctx.flags() != 'l':
+ self.ui.warn('wrong flags for: %s\n' % self.file)
+ self.failed = True
+ elif fctx.flags():
+ self.ui.warn('wrong flags for: %s\n' % self.file)
+ self.failed = True
+
+ if hgdata != svndata:
+ self.ui.warn('difference in: %s\n' % self.file)
+ self.failed = True
+
+ if self.file is not None:
+ return txdelt_window
+
+ def change_dir_prop(self, dir_baton, name, value, pool=None):
+ pass
+
+ def change_file_prop(self, file_baton, name, value, pool=None):
+ if self.props is not None:
+ self.props[name] = value
+
+ def close_file(self, file_baton, checksum, pool=None):
+ pass
+
+ def close_directory(self, dir_baton, pool=None):
+ pass
+
+ def delete_entry(self, path, revnum, pool=None):
+ raise NotImplementedError()
+
+ def check(self):
+ util.progress(self.ui, 'verify', None, total=self.total)
+
+ for f in self.unexpected:
+ self.ui.warn('unexpected file: %s\n' % f)
+ self.failed = True
+ for f in self.missing:
+ self.ui.warn('missing file: %s\n' % f)
+ self.failed = True
+ return not self.failed
+
+ v = VerifyEditor(ui, ctx)
+ svnrepo.svnremoterepo(ui, branchurl).svn.get_revision(srev, v)
+ if v.check():
+ result = 0
+ else:
+ result = 1
+
+ return result
diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py
index e255569..a9df527 100644
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -12,6 +12,7 @@ from mercurial import util as hgutil
from mercurial import node
from mercurial import i18n
from mercurial import extensions
+from mercurial import repair
import replay
import pushmod
@@ -65,13 +66,32 @@ def parents(orig, ui, repo, *args, **opts):
return 0
+def getpeer(ui, opts, source):
+ # Since 2.3 (1ac628cd7113)
+ peer = getattr(hg, 'peer', None)
+ if peer:
+ return peer(ui, opts, source)
+ return hg.repository(ui, source)
+
+def getlocalpeer(ui, opts, source):
+ peer = getpeer(ui, opts, source)
+ repo = getattr(peer, 'local', lambda: peer)()
+ if isinstance(repo, bool):
+ repo = peer
+ return repo
+
+def getcaps(other):
+ return (getattr(other, 'caps', None) or
+ getattr(other, 'capabilities', None) or set())
+
+
def incoming(orig, ui, repo, origsource='default', **opts):
"""show incoming revisions from Subversion
"""
source, revs, checkout = util.parseurl(ui.expandpath(origsource))
- other = hg.repository(ui, source)
- if 'subversion' not in other.capabilities:
+ other = getpeer(ui, opts, source)
+ if 'subversion' not in getcaps(other):
return orig(ui, repo, origsource, **opts)
svn = other.svn
@@ -159,95 +179,121 @@ def push(repo, dest, force, revs):
checkpush(force, revs)
ui = repo.ui
old_encoding = util.swap_out_encoding()
- # TODO: implement --rev/#rev support
- # TODO: do credentials specified in the URL still work?
- svnurl = repo.ui.expandpath(dest.svnurl)
- svn = dest.svn
- meta = repo.svnmeta(svn.uuid, svn.subdir)
+ try:
+ # TODO: implement --rev/#rev support
+ # TODO: do credentials specified in the URL still work?
+ svn = dest.svn
+ meta = repo.svnmeta(svn.uuid, svn.subdir)
- # Strategy:
- # 1. Find all outgoing commits from this head
- if len(repo.parents()) != 1:
- ui.status('Cowardly refusing to push branch merge\n')
- return 0 # results in nonzero exit status, see hg's commands.py
- workingrev = repo.parents()[0]
- ui.status('searching for changes\n')
- hashes = meta.revmap.hashes()
- outgoing = util.outgoing_revisions(repo, hashes, workingrev.node())
- if not (outgoing and len(outgoing)):
- ui.status('no changes found\n')
- return 1 # so we get a sane exit status, see hg's commands.push
- while outgoing:
-
- # 2. Commit oldest revision that needs to be pushed
- oldest = outgoing.pop(-1)
- old_ctx = repo[oldest]
- old_pars = old_ctx.parents()
- if len(old_pars) != 1:
- ui.status('Found a branch merge, this needs discussion and '
- 'implementation.\n')
+ # Strategy:
+ # 1. Find all outgoing commits from this head
+ if len(repo.parents()) != 1:
+ ui.status('Cowardly refusing to push branch merge\n')
return 0 # results in nonzero exit status, see hg's commands.py
- # We will commit to svn against this node's parent rev. Any file-level
- # conflicts here will result in an error reported by svn.
- base_ctx = old_pars[0]
- base_revision = hashes[base_ctx.node()][0]
- svnbranch = base_ctx.branch()
- # Find most recent svn commit we have on this branch.
- # This node will become the nearest known ancestor of the pushed rev.
- oldtipctx = base_ctx
- old_children = oldtipctx.descendants()
- seen = set(c.node() for c in old_children)
- samebranchchildren = [c for c in old_children if c.branch() == svnbranch
- and c.node() in hashes]
- if samebranchchildren:
- # The following relies on descendants being sorted by rev.
- oldtipctx = samebranchchildren[-1]
- # All set, so commit now.
- try:
- pushmod.commit(ui, repo, old_ctx, meta, base_revision, svn)
- except pushmod.NoFilesException:
- ui.warn("Could not push revision %s because it had no changes in svn.\n" %
- old_ctx)
- return 1
-
- # 3. Fetch revisions from svn
- # TODO: this probably should pass in the source explicitly - rev too?
- r = repo.pull(dest, force=force)
- assert not r or r == 0
-
- # 4. Find the new head of the target branch
- # We expect to get our own new commit back, but we might also get other
- # commits that happened since our last pull, or even right after our own
- # commit (race).
- for c in oldtipctx.descendants():
- if c.node() not in seen and c.branch() == svnbranch:
- newtipctx = c
-
- # 5. Rebase all children of the currently-pushing rev to the new head
- heads = repo.heads(old_ctx.node())
- for needs_transplant in heads:
+ workingrev = repo.parents()[0]
+ ui.status('searching for changes\n')
+ hashes = meta.revmap.hashes()
+ outgoing = util.outgoing_revisions(repo, hashes, workingrev.node())
+ to_strip=[]
+ if not (outgoing and len(outgoing)):
+ ui.status('no changes found\n')
+ return 1 # so we get a sane exit status, see hg's commands.push
+ while outgoing:
+
+ # 2. Commit oldest revision that needs to be pushed
+ oldest = outgoing.pop(-1)
+ old_ctx = repo[oldest]
+ old_pars = old_ctx.parents()
+ if len(old_pars) != 1:
+ ui.status('Found a branch merge, this needs discussion and '
+ 'implementation.\n')
+ # results in nonzero exit status, see hg's commands.py
+ return 0
+ # We will commit to svn against this node's parent rev. Any
+ # file-level conflicts here will result in an error reported
+ # by svn.
+ base_ctx = old_pars[0]
+ base_revision = hashes[base_ctx.node()][0]
+ svnbranch = base_ctx.branch()
+ # Find most recent svn commit we have on this branch. This
+ # node will become the nearest known ancestor of the pushed
+ # rev.
+ oldtipctx = base_ctx
+ old_children = oldtipctx.descendants()
+ seen = set(c.node() for c in old_children)
+ samebranchchildren = [c for c in old_children
+ if c.branch() == svnbranch and c.node() in hashes]
+ if samebranchchildren:
+ # The following relies on descendants being sorted by rev.
+ oldtipctx = samebranchchildren[-1]
+ # All set, so commit now.
+ try:
+ pushmod.commit(ui, repo, old_ctx, meta, base_revision, svn)
+ except pushmod.NoFilesException:
+ ui.warn("Could not push revision %s because it had no changes "
+ "in svn.\n" % old_ctx)
+ return 1
+
+ # 3. Fetch revisions from svn
+ # TODO: this probably should pass in the source explicitly -
+ # rev too?
+ r = repo.pull(dest, force=force)
+ assert not r or r == 0
+
+ # 4. Find the new head of the target branch
+ # We expect to get our own new commit back, but we might
+ # also get other commits that happened since our last pull,
+ # or even right after our own commit (race).
+ for c in oldtipctx.descendants():
+ if c.node() not in seen and c.branch() == svnbranch:
+ newtipctx = c
+
+ # 5. Rebase all children of the currently-pushing rev to the
+ # new head
+ #
+ # there may be commits descended from the one we just
+ # pushed to svn that we aren't going to push to svn in
+ # this operation
+ oldhex = node.hex(old_ctx.node())
+ needs_rebase_set = "%s:: and not(%s)" % (oldhex, oldhex)
def extrafn(ctx, extra):
- if ctx.node() == oldest:
- return
extra['branch'] = ctx.branch()
- # TODO: can we avoid calling our own rebase wrapper here?
- rebase(hgrebase.rebase, ui, repo, svn=True, svnextrafn=extrafn,
- svnsourcerev=needs_transplant)
- # Reload the repo after the rebase. Do not reuse contexts across this.
+
+ util.swap_out_encoding(old_encoding)
+ try:
+ hgrebase.rebase(ui, repo, dest=node.hex(newtipctx.node()),
+ rev=[needs_rebase_set],
+ extrafn=extrafn,
+ # We actually want to strip one more rev than
+ # we're rebasing
+ keep=True)
+ finally:
+ util.swap_out_encoding()
+
+ to_strip.append(old_ctx.node())
+ # don't trust the pre-rebase repo. Do not reuse
+ # contexts across this.
newtip = newtipctx.node()
- repo = hg.repository(ui, meta.path)
+ repo = getlocalpeer(ui, {}, meta.path)
newtipctx = repo[newtip]
- # Rewrite the node ids in outgoing to their rebased versions.
+
rebasemap = dict()
for child in newtipctx.descendants():
rebasesrc = child.extra().get('rebase_source')
if rebasesrc:
rebasemap[node.bin(rebasesrc)] = child.node()
outgoing = [rebasemap.get(n) or n for n in outgoing]
- # TODO: stop constantly creating the SVNMeta instances.
- meta = repo.svnmeta(svn.uuid, svn.subdir)
- hashes = meta.revmap.hashes()
- util.swap_out_encoding(old_encoding)
+
+ meta = repo.svnmeta(svn.uuid, svn.subdir)
+ hashes = meta.revmap.hashes()
+ util.swap_out_encoding(old_encoding)
+ try:
+ hg.update(repo, repo['tip'].node())
+ finally:
+ util.swap_out_encoding()
+ repair.strip(ui, repo, to_strip, "all")
+ finally:
+ util.swap_out_encoding(old_encoding)
return 1 # so we get a sane exit status, see hg's commands.push
@@ -259,75 +305,90 @@ def pull(repo, source, heads=[], force=False):
# Split off #rev
svn_url, heads, checkout = util.parseurl(svn_url, heads)
old_encoding = util.swap_out_encoding()
-
+ total = None
try:
- stopat_rev = int(checkout or 0)
- except ValueError:
- raise hgutil.Abort('unrecognised Subversion revision %s: '
- 'only numbers work.' % checkout)
+ try:
+ stopat_rev = int(checkout or 0)
+ except ValueError:
+ raise hgutil.Abort('unrecognised Subversion revision %s: '
+ 'only numbers work.' % checkout)
- have_replay = not repo.ui.configbool('hgsubversion', 'stupid')
- if not have_replay:
- repo.ui.note('fetching stupidly...\n')
+ have_replay = not repo.ui.configbool('hgsubversion', 'stupid')
+ if not have_replay:
+ repo.ui.note('fetching stupidly...\n')
- svn = source.svn
- meta = repo.svnmeta(svn.uuid, svn.subdir)
+ svn = source.svn
+ meta = repo.svnmeta(svn.uuid, svn.subdir)
- layout = repo.ui.config('hgsubversion', 'layout', 'auto')
- if layout == 'auto':
- rootlist = svn.list_dir('', revision=(stopat_rev or None))
- if sum(map(lambda x: x in rootlist, ('branches', 'tags', 'trunk'))):
- layout = 'standard'
+ layout = repo.ui.config('hgsubversion', 'layout', 'auto')
+ if layout == 'auto':
+ try:
+ rootlist = svn.list_dir('', revision=(stopat_rev or None))
+ except svnwrap.SubversionException, e:
+ err = "%s (subversion error: %d)" % (e.args[0], e.args[1])
+ raise hgutil.Abort(err)
+ if sum(map(lambda x: x in rootlist, ('branches', 'tags', 'trunk'))):
+ layout = 'standard'
+ else:
+ layout = 'single'
+ repo.ui.setconfig('hgsubversion', 'layout', layout)
+ repo.ui.note('using %s layout\n' % layout)
+
+ branch = repo.ui.config('hgsubversion', 'branch')
+ if branch:
+ if layout != 'single':
+ msg = ('branch cannot be specified for Subversion clones using '
+ 'standard directory layout')
+ raise hgutil.Abort(msg)
+
+ meta.branchmap['default'] = branch
+
+ ui = repo.ui
+ start = meta.revmap.youngest
+ origrevcount = len(meta.revmap)
+
+ if start <= 0:
+ # we are initializing a new repository
+ start = repo.ui.config('hgsubversion', 'startrev', 0)
+ if isinstance(start, str) and start.upper() == 'HEAD':
+ start = svn.last_changed_rev
+ else:
+ start = int(start)
+
+ if start > 0:
+ if layout == 'standard':
+ raise hgutil.Abort('non-zero start revisions are only '
+ 'supported for single-directory clones.')
+ ui.note('starting at revision %d; any prior will be ignored\n'
+ % start)
+ # fetch all revisions *including* the one specified...
+ start -= 1
+
+ # anything less than zero makes no sense
+ if start < 0:
+ start = 0
+
+ skiprevs = repo.ui.configlist('hgsubversion', 'unsafeskip', '')
+ try:
+ skiprevs = set(map(int, skiprevs))
+ except ValueError:
+ raise hgutil.Abort('unrecognised Subversion revisions %r: '
+ 'only numbers work.' % checkout)
+
+ oldrevisions = len(meta.revmap)
+ if stopat_rev:
+ total = stopat_rev - start
else:
- layout = 'single'
- repo.ui.setconfig('hgsubversion', 'layout', layout)
- repo.ui.note('using %s layout\n' % layout)
+ total = svn.HEAD - start
+ lastpulled = None
- branch = repo.ui.config('hgsubversion', 'branch')
- if branch:
- if layout != 'single':
- msg = ('branch cannot be specified for Subversion clones using '
- 'standard directory layout')
- raise hgutil.Abort(msg)
-
- meta.branchmap['default'] = branch
-
- ui = repo.ui
- start = meta.revmap.youngest
- origrevcount = len(meta.revmap)
-
- if start <= 0:
- # we are initializing a new repository
- start = repo.ui.config('hgsubversion', 'startrev', 0)
- if isinstance(start, str) and start.upper() == 'HEAD':
- start = svn.last_changed_rev
- else:
- start = int(start)
-
- if start > 0:
- if layout == 'standard':
- raise hgutil.Abort('non-zero start revisions are only '
- 'supported for single-directory clones.')
- ui.note('starting at revision %d; any prior will be ignored\n'
- % start)
- # fetch all revisions *including* the one specified...
- start -= 1
-
- # anything less than zero makes no sense
- if start < 0:
- start = 0
-
- oldrevisions = len(meta.revmap)
- if stopat_rev:
- total = stopat_rev - start
- else:
- total = svn.HEAD - start
- lastpulled = None
- try:
try:
# start converting revisions
firstrun = True
for r in svn.revisions(start=start, stop=stopat_rev):
+ if r.revnum in skiprevs:
+ ui.status('[r%d SKIPPED]\n' % r.revnum)
+ continue
lastpulled = r.revnum
if (r.author is None and
r.message == 'This is an empty revision for padding.'):
@@ -380,9 +441,10 @@ def pull(repo, source, heads=[], force=False):
ui.traceback()
raise hgutil.Abort(*e.args)
except KeyboardInterrupt:
- pass
+ ui.traceback()
finally:
- util.progress(ui, 'pull', None, total=total)
+ if total is not None:
+ util.progress(ui, 'pull', None, total=total)
util.swap_out_encoding(old_encoding)
if lastpulled is not None:
@@ -476,7 +538,7 @@ def clone(orig, ui, source, dest=None, **opts):
if isinstance(origsource, str):
source, branch, checkout = util.parseurl(ui.expandpath(origsource),
opts.get('branch'))
- srcrepo = hg.repository(ui, source)
+ srcrepo = getpeer(ui, opts, source)
else:
srcrepo = origsource
@@ -508,7 +570,12 @@ def clone(orig, ui, source, dest=None, **opts):
srcrepo = data.get('srcrepo')
if dstrepo.local() and srcrepo.capable('subversion'):
- fd = dstrepo.opener("hgrc", "a", text=True)
+ dst = dstrepo.local()
+ if isinstance(dst, bool):
+ # Apparently <= hg@1.9
+ fd = dstrepo.opener("hgrc", "a", text=True)
+ else:
+ fd = dst.opener("hgrc", "a", text=True)
for section in set(s for s, v in optionmap.itervalues()):
config = dict(ui.configitems(section))
for name in dontretain[section]:
diff --git a/setup.py b/setup.py
index dec8c0c..226ee92 100755
--- a/setup.py
+++ b/setup.py
@@ -118,7 +118,7 @@ setup(
long_description=open(os.path.join(os.path.dirname(__file__),
'README')).read(),
keywords='mercurial',
- packages=('hgsubversion', 'hgsubversion.svnwrap'),
+ packages=('hgsubversion', 'hgsubversion.hooks', 'hgsubversion.svnwrap'),
package_data={ 'hgsubversion': ['help/subversion.rst'] },
platforms='any',
install_requires=requires,
diff --git a/tests/comprehensive/test_stupid_pull.py b/tests/comprehensive/test_stupid_pull.py
index 17e8122..fcc1ed6 100644
--- a/tests/comprehensive/test_stupid_pull.py
+++ b/tests/comprehensive/test_stupid_pull.py
@@ -45,7 +45,9 @@ def buildmethod(case, name, layout):
attrs = {'_do_case': _do_case,
}
for case in (f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')):
- name = 'test_' + case[:-len('.svndump')]
+ if case == 'corrupt.svndump':
+ continue
+ name = 'test_' + case[:-len('.svndump')].replace('-', '_')
# Automatic layout branchtag collision exposes a minor defect
# here, but since it isn't a regression we suppress the test case.
if case != 'branchtagcollision.svndump':
diff --git a/tests/comprehensive/test_verify.py b/tests/comprehensive/test_verify.py
deleted file mode 100644
index 65bdfa9..0000000
--- a/tests/comprehensive/test_verify.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import os
-import pickle
-import sys
-import unittest
-
-# wrapped in a try/except because of weirdness in how
-# run.py works as compared to nose.
-try:
- import test_util
-except ImportError:
- sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
- import test_util
-
-from mercurial import hg
-from mercurial import ui
-
-from hgsubversion import svncommands
-
-def _do_case(self, name, stupid, layout):
- subdir = test_util.subdir.get(name, '')
- repo = self._load_fixture_and_fetch(name, subdir=subdir, stupid=stupid, layout=layout)
- assert len(self.repo) > 0
- for i in repo:
- ctx = repo[i]
- self.assertEqual(svncommands.verify(repo.ui, repo, rev=ctx.node()), 0)
-
-def buildmethod(case, name, stupid, layout):
- m = lambda self: self._do_case(case, stupid, layout)
- m.__name__ = name
- bits = case, stupid and 'stupid' or 'real', layout
- m.__doc__ = 'Test verify on %s with %s replay. (%s)' % bits
- return m
-
-attrs = {'_do_case': _do_case}
-fixtures = [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]
-for case in fixtures:
- # this fixture results in an empty repository, don't use it
- if case == 'project_root_not_repo_root.svndump':
- continue
- bname = 'test_' + case[:-len('.svndump')]
- attrs[bname] = buildmethod(case, bname, False, 'standard')
- name = bname + '_stupid'
- attrs[name] = buildmethod(case, name, True, 'standard')
- name = bname + '_single'
- attrs[name] = buildmethod(case, name, False, 'single')
- # Disabled because the "stupid and real are the same" tests
- # verify this plus even more.
- # name = bname + '_single_stupid'
- # attrs[name] = buildmethod(case, name, True, 'single')
-
-VerifyTests = type('VerifyTests', (test_util.TestBase,), attrs)
-
-def suite():
- all_tests = [unittest.TestLoader().loadTestsFromTestCase(VerifyTests)]
- return unittest.TestSuite(all_tests)
diff --git a/tests/comprehensive/test_verify_and_startrev.py b/tests/comprehensive/test_verify_and_startrev.py
new file mode 100644
index 0000000..0834caf
--- /dev/null
+++ b/tests/comprehensive/test_verify_and_startrev.py
@@ -0,0 +1,116 @@
+import os
+import pickle
+import sys
+import unittest
+
+# wrapped in a try/except because of weirdness in how
+# run.py works as compared to nose.
+try:
+ import test_util
+except ImportError:
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+ import test_util
+
+from mercurial import hg
+from mercurial import ui
+
+from hgsubversion import verify
+
+# these fixtures contain no files at HEAD and would result in empty clones
+_skipshallow = set([
+ 'binaryfiles.svndump',
+ 'binaryfiles-broken.svndump',
+ 'emptyrepo.svndump',
+ 'correct.svndump',
+ 'corrupt.svndump',
+])
+
+_skipall = set([
+ 'project_root_not_repo_root.svndump',
+ 'movetotrunk.svndump',
+])
+
+_skipstandard = set([
+ 'subdir_is_file_prefix.svndump',
+ 'correct.svndump',
+ 'corrupt.svndump',
+ 'emptyrepo2.svndump',
+])
+
+def _do_case(self, name, stupid, layout):
+ subdir = test_util.subdir.get(name, '')
+ repo, svnpath = self.load_and_fetch(name, subdir=subdir, stupid=stupid,
+ layout=layout)
+ assert len(self.repo) > 0
+ for i in repo:
+ ctx = repo[i]
+ self.assertEqual(verify.verify(repo.ui, repo, rev=ctx.node(),
+ stupid=True), 0)
+ self.assertEqual(verify.verify(repo.ui, repo, rev=ctx.node(),
+ stupid=False), 0)
+
+ # check a startrev clone
+ if layout == 'single' and name not in _skipshallow:
+ self.wc_path += '_shallow'
+ shallowrepo = self.fetch(svnpath, subdir=subdir, stupid=stupid,
+ layout='single', startrev='HEAD')
+
+ self.assertEqual(len(shallowrepo), 1,
+ "shallow clone should have just one revision, not %d"
+ % len(shallowrepo))
+
+ fulltip = repo['tip']
+ shallowtip = shallowrepo['tip']
+
+ repo.ui.pushbuffer()
+ self.assertEqual(0, verify.verify(repo.ui, shallowrepo,
+ rev=shallowtip.node(),
+ stupid=True))
+ self.assertEqual(0, verify.verify(repo.ui, shallowrepo,
+ rev=shallowtip.node(),
+ stupid=False))
+
+ stupidui = ui.ui(repo.ui)
+ stupidui.config('hgsubversion', 'stupid', True)
+ self.assertEqual(verify.verify(stupidui, repo, rev=ctx.node(),
+ stupid=True), 0)
+ self.assertEqual(verify.verify(stupidui, repo, rev=ctx.node(),
+ stupid=False), 0)
+
+ # viewing diff's of lists of files is easier on the eyes
+ self.assertMultiLineEqual('\n'.join(fulltip), '\n'.join(shallowtip),
+ repo.ui.popbuffer())
+
+ for f in fulltip:
+ self.assertMultiLineEqual(fulltip[f].data(), shallowtip[f].data())
+
+
+def buildmethod(case, name, stupid, layout):
+ m = lambda self: self._do_case(case, stupid, layout)
+ m.__name__ = name
+ bits = case, stupid and 'stupid' or 'real', layout
+ m.__doc__ = 'Test verify on %s with %s replay. (%s)' % bits
+ return m
+
+attrs = {'_do_case': _do_case}
+fixtures = [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]
+for case in fixtures:
+ if case in _skipall:
+ continue
+ bname = 'test_' + case[:-len('.svndump')]
+ if case not in _skipstandard:
+ attrs[bname] = buildmethod(case, bname, False, 'standard')
+ name = bname + '_stupid'
+ attrs[name] = buildmethod(case, name, True, 'standard')
+ name = bname + '_single'
+ attrs[name] = buildmethod(case, name, False, 'single')
+ # Disabled because the "stupid and real are the same" tests
+ # verify this plus even more.
+ # name = bname + '_single_stupid'
+ # attrs[name] = buildmethod(case, name, True, 'single')
+
+VerifyTests = type('VerifyTests', (test_util.TestBase,), attrs)
+
+def suite():
+ all_tests = [unittest.TestLoader().loadTestsFromTestCase(VerifyTests)]
+ return unittest.TestSuite(all_tests)
diff --git a/tests/fixtures/addspecial.sh b/tests/fixtures/addspecial.sh
new file mode 100644
index 0000000..1d242bc
--- /dev/null
+++ b/tests/fixtures/addspecial.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+mkdir temp
+cd temp
+
+svnadmin create repo
+svn co file://`pwd`/repo wc
+cd wc
+
+mkdir -p trunk branches
+svn add trunk branches
+svn ci -m'initial structure'
+cd trunk
+echo a>a
+svn add a
+svn ci -mci1 a
+cd ..
+svn up
+svn cp trunk branches/foo
+svn ci -m'branch foo'
+cd branches/foo
+ln -s a fnord
+svn add fnord
+svn ci -msymlink fnord
+mkdir 'spacy name'
+echo a > 'spacy name/spacy file'
+svn add 'spacy name'
+svn ci -mspacy 'spacy name'
+svn up
+echo b > 'spacy name/surprise ~'
+svn add 'spacy name/surprise ~'
+svn ci -mtilde 'spacy name'
+svn up ../..
+echo foo > exe
+chmod +x exe
+svn add exe
+svn ci -mexecutable exe
+svn up ../..
+cd ../../trunk
+svn merge ../branches/foo
+svn ci -mmerge
+svn up
+
+pwd
+cd ../../..
+svnadmin dump temp/repo > addspecial.svndump
+echo
+echo 'Complete.'
+echo 'You probably want to clean up temp now.'
+echo 'Dump in addspecial.svndump'
+exit 0
diff --git a/tests/fixtures/addspecial.svndump b/tests/fixtures/addspecial.svndump
new file mode 100644
index 0000000..6cc3e4c
--- /dev/null
+++ b/tests/fixtures/addspecial.svndump
@@ -0,0 +1,302 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 01df53ad-5d72-4756-8742-f669dc98f791
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2012-05-13T22:22:43.218190Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 118
+Content-length: 118
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:44.112163Z
+K 7
+svn:log
+V 17
+initial structure
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 103
+Content-length: 103
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:45.111247Z
+K 7
+svn:log
+V 3
+ci1
+PROPS-END
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Revision-number: 3
+Prop-content-length: 111
+Content-length: 111
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:48.110257Z
+K 7
+svn:log
+V 10
+branch foo
+PROPS-END
+
+Node-path: branches/foo
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk
+
+
+Revision-number: 4
+Prop-content-length: 107
+Content-length: 107
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:49.115096Z
+K 7
+svn:log
+V 7
+symlink
+PROPS-END
+
+Node-path: branches/foo/fnord
+Node-kind: file
+Node-action: add
+Prop-content-length: 33
+Text-content-length: 6
+Text-content-md5: c118dba188202a1efc975bef6064180b
+Text-content-sha1: 41f94e4692313bf7f7c92aa600002f1dff93d6bf
+Content-length: 39
+
+K 11
+svn:special
+V 1
+*
+PROPS-END
+link a
+
+Revision-number: 5
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:50.119266Z
+K 7
+svn:log
+V 5
+spacy
+PROPS-END
+
+Node-path: branches/foo/spacy name
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: branches/foo/spacy name/spacy file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Revision-number: 6
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:52.123367Z
+K 7
+svn:log
+V 5
+tilde
+PROPS-END
+
+Node-path: branches/foo/spacy name/surprise ~
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 3b5d5c3712955042212316173ccf37be
+Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+Content-length: 12
+
+PROPS-END
+b
+
+
+Revision-number: 7
+Prop-content-length: 111
+Content-length: 111
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:54.129462Z
+K 7
+svn:log
+V 10
+executable
+PROPS-END
+
+Node-path: branches/foo/exe
+Node-kind: file
+Node-action: add
+Prop-content-length: 36
+Text-content-length: 4
+Text-content-md5: d3b07384d113edec49eaa6238ad5ff00
+Text-content-sha1: f1d2d2f924e986ac86fdf7b36c94bcdf32beec15
+Content-length: 40
+
+K 14
+svn:executable
+V 1
+*
+PROPS-END
+foo
+
+
+Revision-number: 8
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-13T22:22:57.111370Z
+K 7
+svn:log
+V 5
+merge
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: change
+Prop-content-length: 52
+Content-length: 52
+
+K 13
+svn:mergeinfo
+V 17
+/branches/foo:3-7
+PROPS-END
+
+
+Node-path: trunk/exe
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 7
+Node-copyfrom-path: branches/foo/exe
+Text-copy-source-md5: d3b07384d113edec49eaa6238ad5ff00
+Text-copy-source-sha1: f1d2d2f924e986ac86fdf7b36c94bcdf32beec15
+
+
+Node-path: trunk/fnord
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 7
+Node-copyfrom-path: branches/foo/fnord
+Text-copy-source-md5: c118dba188202a1efc975bef6064180b
+Text-copy-source-sha1: 41f94e4692313bf7f7c92aa600002f1dff93d6bf
+
+
+Node-path: trunk/spacy name
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 7
+Node-copyfrom-path: branches/foo/spacy name
+
+
diff --git a/tests/fixtures/copies.sh b/tests/fixtures/copies.sh
new file mode 100755
index 0000000..4709bc9
--- /dev/null
+++ b/tests/fixtures/copies.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+#
+# Generate copies.svndump
+#
+
+rm -rf temp
+mkdir temp
+cd temp
+mkdir -p import/trunk/dir
+echo a > import/trunk/dir/a
+
+svnadmin create testrepo
+svnurl=file://`pwd`/testrepo
+svn import import $svnurl -m init
+
+svn co $svnurl project
+cd project
+svn cp trunk/dir trunk/dir2
+echo b >> trunk/dir2/a
+svn ci -m 'copy/edit trunk/dir/a'
+svn up
+svn cp trunk/dir2 trunk/dir3
+svn ci -m 'copy dir2 to dir3'
+svn rm trunk/dir3/a
+svn cp trunk/dir2/a trunk/dir3/a
+svn ci -m 'copy and remove'
+cd ..
+
+svnadmin dump testrepo > ../copies.svndump
diff --git a/tests/fixtures/copies.svndump b/tests/fixtures/copies.svndump
new file mode 100644
index 0000000..76cfd97
--- /dev/null
+++ b/tests/fixtures/copies.svndump
@@ -0,0 +1,158 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 6f377846-a035-4244-a154-e87a9351a653
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2012-10-15T19:02:56.936694Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-15T19:02:56.958201Z
+K 7
+svn:log
+V 4
+init
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/dir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/dir/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Revision-number: 2
+Prop-content-length: 123
+Content-length: 123
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-15T19:02:58.046478Z
+K 7
+svn:log
+V 21
+copy/edit trunk/dir/a
+PROPS-END
+
+Node-path: trunk/dir2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk/dir
+
+
+Node-path: trunk/dir2/a
+Node-kind: file
+Node-action: change
+Text-content-length: 4
+Text-content-md5: dd8c6a395b5dd36c56d23275028f526c
+Text-content-sha1: 05dec960e24d918b8a73a1c53bcbbaac2ee5c2e0
+Content-length: 4
+
+a
+b
+
+
+Revision-number: 3
+Prop-content-length: 119
+Content-length: 119
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-15T19:03:01.045897Z
+K 7
+svn:log
+V 17
+copy dir2 to dir3
+PROPS-END
+
+Node-path: trunk/dir3
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/dir2
+
+
+Revision-number: 4
+Prop-content-length: 117
+Content-length: 117
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-15T19:03:03.046654Z
+K 7
+svn:log
+V 15
+copy and remove
+PROPS-END
+
+Node-path: trunk/dir3/a
+Node-kind: file
+Node-action: delete
+
+Node-path: trunk/dir3/a
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/dir2/a
+Text-copy-source-md5: dd8c6a395b5dd36c56d23275028f526c
+Text-copy-source-sha1: 05dec960e24d918b8a73a1c53bcbbaac2ee5c2e0
+
+
+
+
diff --git a/tests/fixtures/correct.svndump b/tests/fixtures/correct.svndump
new file mode 100644
index 0000000..9e24c3c
--- /dev/null
+++ b/tests/fixtures/correct.svndump
@@ -0,0 +1,103 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 00000000-0000-0000-0000-000000000000
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2010-11-30T15:10:25.898546Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 100
+Content-length: 100
+
+K 7
+svn:log
+V 0
+
+K 10
+svn:author
+V 6
+danchr
+K 8
+svn:date
+V 27
+2010-11-30T15:16:01.077550Z
+PROPS-END
+
+Node-path: empty-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: executable-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 36
+Text-content-length: 11
+Text-content-md5: 01839ba8c81c3b2c7486607e0c683e62
+Text-content-sha1: 5e70f8a25fe8ad4ad971bfd3388c258b019268d4
+Content-length: 47
+
+K 14
+svn:executable
+V 1
+*
+PROPS-END
+Executable
+
+
+Node-path: regular-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 10
+Text-content-md5: 2e01b7f4ab0c18c05a3059eb2e2420d9
+Text-content-sha1: 6e530e985be313a43dc9734251656be8f0c94ab8
+Content-length: 20
+
+PROPS-END
+Contents.
+
+
+Node-path: another-regular-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 10
+Text-content-md5: 2e01b7f4ab0c18c05a3059eb2e2420d9
+Text-content-sha1: 6e530e985be313a43dc9734251656be8f0c94ab8
+Content-length: 20
+
+PROPS-END
+Contents.
+
+
+Node-path: symlink
+Node-kind: file
+Node-action: add
+Prop-content-length: 33
+Text-content-length: 6
+Text-content-md5: 654580f41818cd6f51408c7cbd313728
+Text-content-sha1: 130b8faaf3e1acc1b95f77ac835e9c8b6eee5c96
+Content-length: 39
+
+K 11
+svn:special
+V 1
+*
+PROPS-END
+link A
+
diff --git a/tests/fixtures/corrupt.svndump b/tests/fixtures/corrupt.svndump
new file mode 100644
index 0000000..bcc0941
--- /dev/null
+++ b/tests/fixtures/corrupt.svndump
@@ -0,0 +1,97 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 00000000-0000-0000-0000-000000000000
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2010-11-30T15:10:25.898546Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 100
+Content-length: 100
+
+K 10
+svn:author
+V 6
+danchr
+K 8
+svn:date
+V 27
+2010-11-30T15:16:01.077550Z
+K 7
+svn:log
+V 0
+
+PROPS-END
+
+Node-path: another-regular-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: executable-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 11
+Text-content-md5: 01839ba8c81c3b2c7486607e0c683e62
+Text-content-sha1: 5e70f8a25fe8ad4ad971bfd3388c258b019268d4
+Content-length: 21
+
+PROPS-END
+Executable
+
+
+Node-path: missing-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: regular-file
+Node-kind: file
+Node-action: add
+Prop-content-length: 33
+Text-content-length: 18
+Text-content-md5: adf66a0cec83e25644c63f3c3007ae7c
+Text-content-sha1: 047e6e482d0c9cb812f89d18a9f07a43caab76bb
+Content-length: 51
+
+K 11
+svn:special
+V 1
+*
+PROPS-END
+link Bad contents.
+
+Node-path: symlink
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 1
+Text-content-md5: 7fc56270e7a70fa81a5935b72eacbe29
+Text-content-sha1: 6dcd4ce23d88e2ee9568ba546c007c63d9131c1b
+Content-length: 11
+
+PROPS-END
+A
+
diff --git a/tests/fixtures/delete_restore_trunk.sh b/tests/fixtures/delete_restore_trunk.sh
new file mode 100755
index 0000000..340b5ef
--- /dev/null
+++ b/tests/fixtures/delete_restore_trunk.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -e
+mkdir temp
+cd temp
+svnadmin create repo
+svn co file://`pwd`/repo wc
+cd wc
+mkdir branches trunk tags
+svn add *
+svn ci -m 'btt'
+echo foo > trunk/foo
+svn add trunk/foo
+svn ci -m 'add file'
+svn up
+svn rm trunk
+svn ci -m 'delete trunk'
+svn up
+cd ..
+svn cp -m 'restore trunk' file://`pwd`/repo/trunk@2 file://`pwd`/repo/trunk
+cd wc
+svn up
+echo bar >> trunk/foo
+svn ci -m 'append to file'
+svn up
+cd ../..
+svnadmin dump temp/repo > delete_restore_trunk.svndump
+echo
+echo 'Complete.'
+echo 'You probably want to clean up temp now.'
+echo 'Dump in branch_delete_parent_dir.svndump'
+exit 0
diff --git a/tests/fixtures/delete_restore_trunk.svndump b/tests/fixtures/delete_restore_trunk.svndump
new file mode 100644
index 0000000..2d95813
--- /dev/null
+++ b/tests/fixtures/delete_restore_trunk.svndump
@@ -0,0 +1,167 @@
+SVN-fs-dump-format-version: 2
+
+UUID: fca176f4-a346-479b-ae2c-78c8442c3809
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2012-05-16T22:55:55.613464Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 103
+Content-length: 103
+
+K 7
+svn:log
+V 3
+btt
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-16T22:55:56.081065Z
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 108
+Content-length: 108
+
+K 7
+svn:log
+V 8
+add file
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-16T22:55:57.071178Z
+PROPS-END
+
+Node-path: trunk/foo
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 4
+Text-content-md5: d3b07384d113edec49eaa6238ad5ff00
+Text-content-sha1: f1d2d2f924e986ac86fdf7b36c94bcdf32beec15
+Content-length: 14
+
+PROPS-END
+foo
+
+
+Revision-number: 3
+Prop-content-length: 113
+Content-length: 113
+
+K 7
+svn:log
+V 12
+delete trunk
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-16T22:55:59.058026Z
+PROPS-END
+
+Node-path: trunk
+Node-action: delete
+
+
+Revision-number: 4
+Prop-content-length: 114
+Content-length: 114
+
+K 7
+svn:log
+V 13
+restore trunk
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-16T22:56:01.055887Z
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk
+
+
+Revision-number: 5
+Prop-content-length: 115
+Content-length: 115
+
+K 7
+svn:log
+V 14
+append to file
+K 10
+svn:author
+V 6
+bryano
+K 8
+svn:date
+V 27
+2012-05-16T22:56:02.060991Z
+PROPS-END
+
+Node-path: trunk/foo
+Node-kind: file
+Node-action: change
+Text-content-length: 8
+Text-content-md5: f47c75614087a8dd938ba4acff252494
+Text-content-sha1: 4e48e2c9a3d2ca8a708cb0cc545700544efb5021
+Content-length: 8
+
+foo
+bar
+
+
diff --git a/tests/fixtures/emptyrepo2.sh b/tests/fixtures/emptyrepo2.sh
new file mode 100755
index 0000000..a29afdd
--- /dev/null
+++ b/tests/fixtures/emptyrepo2.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+#
+# Create emptyrepo2.svndump
+#
+# The generated repository contains a sequence of empty revisions
+# created with a combination of svnsync and filtering
+
+mkdir temp
+cd temp
+
+mkdir project-orig
+cd project-orig
+mkdir -p sub/trunk other
+echo a > other/a
+cd ..
+
+svnadmin create testrepo
+svnurl=file://`pwd`/testrepo
+svn import project-orig $svnurl -m init
+
+svn co $svnurl project
+cd project
+echo a >> other/a
+svn ci -m othera
+echo a >> other/a
+svn ci -m othera2
+echo b > sub/trunk/a
+svn add sub/trunk/a
+svn ci -m adda
+cd ..
+
+svnadmin create testrepo2
+cat > testrepo2/hooks/pre-revprop-change <<EOF
+#!/bin/sh
+exit 0
+EOF
+chmod +x testrepo2/hooks/pre-revprop-change
+
+svnurl2=file://`pwd`/testrepo2
+svnsync init --username svnsync $svnurl2 $svnurl/sub
+svnsync sync $svnurl2
+
+svnadmin dump testrepo2 > ../emptyrepo2.svndump
+
diff --git a/tests/fixtures/emptyrepo2.svndump b/tests/fixtures/emptyrepo2.svndump
new file mode 100644
index 0000000..3f6e553
--- /dev/null
+++ b/tests/fixtures/emptyrepo2.svndump
@@ -0,0 +1,129 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 293d1f29-635d-48b8-9cdf-468fd987067a
+
+Revision-number: 0
+Prop-content-length: 261
+Content-length: 261
+
+K 8
+svn:date
+V 27
+2012-10-03T18:58:42.535317Z
+K 17
+svn:sync-from-url
+V 74
+file:///Users/pmezard/dev/hg/hgsubversion/tests/fixtures/temp/testrepo/sub
+K 18
+svn:sync-from-uuid
+V 36
+241badf9-093f-4e71-8a58-1028abf52758
+K 24
+svn:sync-last-merged-rev
+V 1
+4
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-03T18:58:42.556405Z
+K 7
+svn:log
+V 4
+init
+PROPS-END
+
+Node-path: sub
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: sub/trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 107
+Content-length: 107
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-03T18:58:43.040912Z
+K 7
+svn:log
+V 6
+othera
+PROPS-END
+
+Revision-number: 3
+Prop-content-length: 108
+Content-length: 108
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-03T18:58:44.042124Z
+K 7
+svn:log
+V 7
+othera2
+PROPS-END
+
+Revision-number: 4
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-03T18:58:45.053459Z
+K 7
+svn:log
+V 4
+adda
+PROPS-END
+
+Node-path: sub/trunk/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 3b5d5c3712955042212316173ccf37be
+Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+Content-length: 12
+
+PROPS-END
+b
+
+
diff --git a/tests/fixtures/invalid_utf8.sh b/tests/fixtures/invalid_utf8.sh
new file mode 100755
index 0000000..76dd9f8
--- /dev/null
+++ b/tests/fixtures/invalid_utf8.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+#-*- coding: utf-8 -*-
+#
+# Generate invalid_utf8.svndump
+#
+
+#check svnadmin version, must be >= 1.7
+SVNVERSION=$(svnadmin --version | head -n 1 | cut -d \ -f 3)
+if [[ "$SVNVERSION" < '1.7' ]] ; then
+ echo "You MUST have svn 1.7 or above to use this script"
+ exit 1
+fi
+
+set -x
+
+TMPDIR=$(mktemp -d)
+WD=$(pwd)
+
+cd $TMPDIR
+
+svnadmin create failrepo
+svn co file://$PWD/failrepo fail
+(
+ cd fail
+ touch A
+ svn add A
+ svn ci -m blabargrod
+)
+svnadmin --pre-1.6-compatible create invalid_utf8
+svnadmin dump failrepo | \
+ sed "s/blabargrod/$(echo blåbærgrød | iconv -f utf-8 -t latin1)/g" | \
+ svnadmin load --bypass-prop-validation invalid_utf8
+
+tar cz -C invalid_utf8 -f "$WD"/invalid_utf8.tar.gz .
diff --git a/tests/fixtures/invalid_utf8.tar.gz b/tests/fixtures/invalid_utf8.tar.gz
new file mode 100644
index 0000000..537021f
--- /dev/null
+++ b/tests/fixtures/invalid_utf8.tar.gz
Binary files differ
diff --git a/tests/fixtures/movetotrunk.sh b/tests/fixtures/movetotrunk.sh
new file mode 100755
index 0000000..b27572c
--- /dev/null
+++ b/tests/fixtures/movetotrunk.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+#
+# Generate movetotrunk.svndump
+#
+
+mkdir temp
+cd temp
+
+mkdir project-orig
+cd project-orig
+cd ..
+
+svnadmin create testrepo
+svnurl=file://`pwd`/testrepo
+svn mkdir --parents $svnurl/sub1/sub2 -m subpaths
+svn import project-orig $svnurl/sub1/sub2 -m "init project"
+
+svn co $svnurl/sub1/sub2 project
+cd project
+echo a > a
+svn add a
+mkdir dir
+echo b > dir/b
+svn add dir
+svn ci -m adda
+svn up
+mkdir trunk
+svn add trunk
+svn mv a trunk/a
+svn mv dir trunk/dir
+svn ci -m 'move to trunk'
+cd ..
+
+svnadmin dump testrepo > ../movetotrunk.svndump
diff --git a/tests/fixtures/movetotrunk.svndump b/tests/fixtures/movetotrunk.svndump
new file mode 100644
index 0000000..928a8f3
--- /dev/null
+++ b/tests/fixtures/movetotrunk.svndump
@@ -0,0 +1,154 @@
+SVN-fs-dump-format-version: 2
+
+UUID: bb3f8dfd-83a8-4fe0-b57e-00a3838532ab
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2012-10-20T20:23:15.254324Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 109
+Content-length: 109
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-20T20:23:15.271492Z
+K 7
+svn:log
+V 8
+subpaths
+PROPS-END
+
+Node-path: sub1
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: sub1/sub2
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-20T20:23:16.068226Z
+K 7
+svn:log
+V 4
+adda
+PROPS-END
+
+Node-path: sub1/sub2/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Node-path: sub1/sub2/dir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: sub1/sub2/dir/b
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 3b5d5c3712955042212316173ccf37be
+Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+Content-length: 12
+
+PROPS-END
+b
+
+
+Revision-number: 3
+Prop-content-length: 115
+Content-length: 115
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-20T20:23:20.043626Z
+K 7
+svn:log
+V 13
+move to trunk
+PROPS-END
+
+Node-path: sub1/sub2/trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: sub1/sub2/trunk/a
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: sub1/sub2/a
+Text-copy-source-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-copy-source-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+
+
+Node-path: sub1/sub2/trunk/dir
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: sub1/sub2/dir
+
+
+Node-path: sub1/sub2/dir
+Node-action: delete
+
+
+Node-path: sub1/sub2/a
+Node-action: delete
+
+
diff --git a/tests/fixtures/revert.sh b/tests/fixtures/revert.sh
new file mode 100755
index 0000000..aaab485
--- /dev/null
+++ b/tests/fixtures/revert.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# Generate revert.svndump
+#
+
+rm -rf temp
+mkdir temp
+cd temp
+mkdir -p import/trunk/dir
+cd import/trunk
+echo a > a
+echo b > dir/b
+cd ../..
+
+svnadmin create testrepo
+svnurl=file://`pwd`/testrepo
+svn import import $svnurl -m init
+
+svn co $svnurl project
+cd project
+echo a >> trunk/a
+echo b >> trunk/dir/b
+svn ci -m changefiles
+svn up
+# Test directory revert
+svn rm trunk
+svn cp $svnurl/trunk@1 trunk
+svn st
+svn ci -m revert
+svn up
+# Test file revert
+svn rm trunk/a
+svn rm trunk/dir/b
+svn cp $svnurl/trunk/a@2 trunk/a
+svn cp $svnurl/trunk/dir/b@2 trunk/dir/b
+svn ci -m revert2
+cd ..
+
+svnadmin dump testrepo > ../revert.svndump
diff --git a/tests/fixtures/revert.svndump b/tests/fixtures/revert.svndump
new file mode 100644
index 0000000..2a368ae
--- /dev/null
+++ b/tests/fixtures/revert.svndump
@@ -0,0 +1,197 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 307f02f4-2d74-44cb-98a4-4e162241d396
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2012-10-06T08:50:46.559327Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 105
+Content-length: 105
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-06T08:50:46.581582Z
+K 7
+svn:log
+V 4
+init
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Node-path: trunk/dir
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/dir/b
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 3b5d5c3712955042212316173ccf37be
+Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+Content-length: 12
+
+PROPS-END
+b
+
+
+Revision-number: 2
+Prop-content-length: 113
+Content-length: 113
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-06T08:50:47.048033Z
+K 7
+svn:log
+V 11
+changefiles
+PROPS-END
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: change
+Text-content-length: 4
+Text-content-md5: 0d227f1abf8c2932d342e9b99cc957eb
+Text-content-sha1: d7c8127a20a396cff08af086a1c695b0636f0c29
+Content-length: 4
+
+a
+a
+
+
+Node-path: trunk/dir/b
+Node-kind: file
+Node-action: change
+Text-content-length: 4
+Text-content-md5: 06ac26ed8b614fc0b141e4542aa067c2
+Text-content-sha1: f6980469e74f7125178e88ec571e06fe6ce86e95
+Content-length: 4
+
+b
+b
+
+
+Revision-number: 3
+Prop-content-length: 107
+Content-length: 107
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-06T08:50:50.058224Z
+K 7
+svn:log
+V 6
+revert
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: delete
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+
+
+Revision-number: 4
+Prop-content-length: 108
+Content-length: 108
+
+K 10
+svn:author
+V 7
+pmezard
+K 8
+svn:date
+V 27
+2012-10-06T08:50:54.047396Z
+K 7
+svn:log
+V 7
+revert2
+PROPS-END
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: delete
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/a
+Text-copy-source-md5: 0d227f1abf8c2932d342e9b99cc957eb
+Text-copy-source-sha1: d7c8127a20a396cff08af086a1c695b0636f0c29
+
+
+
+
+Node-path: trunk/dir/b
+Node-kind: file
+Node-action: delete
+
+Node-path: trunk/dir/b
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/dir/b
+Text-copy-source-md5: 06ac26ed8b614fc0b141e4542aa067c2
+Text-copy-source-sha1: f6980469e74f7125178e88ec571e06fe6ce86e95
+
+
+
+
diff --git a/tests/fixtures/subdir_is_file_prefix.svndump b/tests/fixtures/subdir_is_file_prefix.svndump
new file mode 100644
index 0000000..c09b45a
--- /dev/null
+++ b/tests/fixtures/subdir_is_file_prefix.svndump
@@ -0,0 +1,72 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 924a052a-5e5a-4a8e-a677-da5565bec340
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2011-03-04T12:33:29.342045Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 123
+Content-length: 123
+
+K 7
+svn:log
+V 22
+Create directory flaf.
+K 10
+svn:author
+V 6
+danchr
+K 8
+svn:date
+V 27
+2011-03-04T12:34:00.349950Z
+PROPS-END
+
+Node-path: flaf
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 138
+Content-length: 138
+
+K 7
+svn:log
+V 37
+Create the file flaf.txt within flaf.
+K 10
+svn:author
+V 6
+danchr
+K 8
+svn:date
+V 27
+2011-03-04T12:45:01.701033Z
+PROPS-END
+
+Node-path: flaf/flaf.txt
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 15
+Text-content-md5: 8c0059c8f7998e8003836b8e8fcb74d7
+Text-content-sha1: b7d680bc5411f46395c4ef267001e1a307d7b0d5
+Content-length: 25
+
+PROPS-END
+Goodbye world.
+
+
diff --git a/tests/run.py b/tests/run.py
index 98cf0a6..3832633 100644
--- a/tests/run.py
+++ b/tests/run.py
@@ -18,26 +18,29 @@ def tests():
import test_fetch_renames
import test_fetch_symlinks
import test_fetch_truncated
+ import test_hooks
import test_pull
+ import test_pull_fallback
import test_push_command
import test_push_renames
import test_push_dirs
import test_push_eol
+ import test_push_autoprops
import test_rebuildmeta
import test_single_dir_clone
- import test_startrev
import test_svnwrap
import test_tags
import test_template_keywords
import test_utility_commands
import test_unaffected_core
+ import test_updatemeta
import test_urls
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.dirname(__file__), 'comprehensive'))
import test_stupid_pull
- import test_verify
+ import test_verify_and_startrev
return locals()
diff --git a/tests/test_fetch_branches.py b/tests/test_fetch_branches.py
index 7a9287c..18fdd40 100644
--- a/tests/test_fetch_branches.py
+++ b/tests/test_fetch_branches.py
@@ -58,8 +58,9 @@ class TestFetchBranches(test_util.TestBase):
self.test_unorderedbranch(True)
def test_renamed_branch_to_trunk(self, stupid=False):
+ config = {'hgsubversion.failonmissing': 'true'}
repo = self._load_fixture_and_fetch('branch_rename_to_trunk.svndump',
- stupid=stupid)
+ stupid=stupid, config=config)
self.assertEqual(repo['default'].parents()[0].branch(), 'dev_branch')
self.assert_('iota' in repo['default'])
self.assertEqual(repo['old_trunk'].parents()[0].branch(), 'default')
diff --git a/tests/test_fetch_command.py b/tests/test_fetch_command.py
index b4e3fce..b1683fc 100644
--- a/tests/test_fetch_command.py
+++ b/tests/test_fetch_command.py
@@ -8,6 +8,7 @@ from mercurial import commands
from mercurial import hg
from mercurial import node
from mercurial import ui
+from mercurial import encoding
class TestBasicRepoLayout(test_util.TestBase):
@@ -94,13 +95,16 @@ class TestBasicRepoLayout(test_util.TestBase):
assert 'README' not in repo
assert '../branches' not in repo
- def test_files_copied_from_outside_btt(self):
+ def test_files_copied_from_outside_btt(self, stupid=False):
repo = self._load_fixture_and_fetch(
- 'test_files_copied_from_outside_btt.svndump')
+ 'test_files_copied_from_outside_btt.svndump', stupid=stupid)
self.assertEqual(node.hex(repo['tip'].node()),
'3c78170e30ddd35f2c32faa0d8646ab75bba4f73')
self.assertEqual(len(repo.changelog), 2)
+ def test_files_copied_from_outside_btt_stupid(self):
+ self.test_files_copied_from_outside_btt(stupid=True)
+
def test_file_renamed_in_from_outside_btt(self):
repo = self._load_fixture_and_fetch(
'file_renamed_in_from_outside_btt.svndump')
@@ -175,7 +179,8 @@ class TestBasicRepoLayout(test_util.TestBase):
self.assertEqual(repo[r].hex(), repo2[r].hex())
def test_path_quoting_stupid(self):
- self.test_path_quoting(True)
+ repo = self.test_path_quoting(True)
+
def test_identical_fixtures(self):
'''ensure that the non_ascii_path_N fixtures are identical'''
@@ -186,6 +191,14 @@ class TestBasicRepoLayout(test_util.TestBase):
self.assertMultiLineEqual(open(fixturepaths[0]).read(),
open(fixturepaths[1]).read())
+ def test_invalid_message(self):
+ repo = self._load_fixture_and_fetch('invalid_utf8.tar.gz')
+ # changelog returns descriptions in local encoding
+ desc = encoding.fromlocal(repo[0].description())
+ self.assertEqual(desc.decode('utf8'),
+ u'bl\xe5b\xe6rgr\xf8d')
+
+
class TestStupidPull(test_util.TestBase):
def test_stupid(self):
repo = self._load_fixture_and_fetch('two_heads.svndump', stupid=True)
@@ -216,6 +229,63 @@ class TestStupidPull(test_util.TestBase):
self.assertEqual(node.hex(repo['tip'].node()),
'1a6c3f30911d57abb67c257ec0df3e7bc44786f7')
+ def test_empty_repo(self, stupid=False):
+ # This used to crash HgEditor because it could be closed without
+ # having been initialized again.
+ self._load_fixture_and_fetch('emptyrepo2.svndump', stupid=stupid)
+
+ def test_empty_repo_stupid(self):
+ self.test_empty_repo(stupid=True)
+
+ def test_fetch_revert(self, stupid=False):
+ repo = self._load_fixture_and_fetch('revert.svndump', stupid=stupid)
+ graph = self.getgraph(repo)
+ refgraph = """\
+o changeset: 3:937dcd1206d4
+| branch:
+| tags: tip
+| summary: revert2
+| files: a dir/b
+|
+o changeset: 2:9317a748b7c3
+| branch:
+| tags:
+| summary: revert
+| files: a dir/b
+|
+o changeset: 1:243259a4138a
+| branch:
+| tags:
+| summary: changefiles
+| files: a dir/b
+|
+o changeset: 0:ab86791fc857
+ branch:
+ tags:
+ summary: init
+ files: a dir/b
+"""
+ self.assertEqual(refgraph.strip(), graph.strip())
+
+ def test_fetch_revert_stupid(self):
+ self.test_fetch_revert(stupid=True)
+
+ def test_fetch_movetotrunk(self, stupid=False):
+ repo = self._load_fixture_and_fetch('movetotrunk.svndump',
+ stupid=stupid, subdir='sub1/sub2')
+ graph = self.getgraph(repo)
+ refgraph = """\
+o changeset: 0:02996a5980ba
+ branch:
+ tags: tip
+ summary: move to trunk
+ files: a dir/b
+"""
+ self.assertEqual(refgraph.strip(), graph.strip())
+
+ def test_fetch_movetotrunk_stupid(self):
+ self.test_fetch_movetotrunk(stupid=True)
+
def suite():
all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestBasicRepoLayout),
unittest.TestLoader().loadTestsFromTestCase(TestStupidPull),
diff --git a/tests/test_fetch_mappings.py b/tests/test_fetch_mappings.py
index 02727f6..267515c 100644
--- a/tests/test_fetch_mappings.py
+++ b/tests/test_fetch_mappings.py
@@ -13,6 +13,7 @@ from mercurial import util as hgutil
from hgsubversion import maps
from hgsubversion import svncommands
from hgsubversion import util
+from hgsubversion import verify
class MapTests(test_util.TestBase):
@property
@@ -100,49 +101,43 @@ class MapTests(test_util.TestBase):
all_tests = set(test)
self.assertEqual(fromself.symmetric_difference(all_tests), set())
- def test_file_map(self, stupid=False):
- repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
+ def _loadwithfilemap(self, svndump, filemapcontent, stupid=False,
+ failonmissing=True):
+ repo_path = self.load_svndump(svndump)
filemap = open(self.filemap, 'w')
- filemap.write("include alpha\n")
+ filemap.write(filemapcontent)
filemap.close()
ui = self.ui(stupid)
ui.setconfig('hgsubversion', 'filemap', self.filemap)
+ ui.setconfig('hgsubversion', 'failoninvalidreplayfile', 'true')
+ ui.setconfig('hgsubversion', 'failonmissing', failonmissing)
commands.clone(ui, test_util.fileurl(repo_path),
self.wc_path, filemap=self.filemap)
- self.assertEqual(node.hex(self.repo[0].node()), '88e2c7492d83e4bf30fbb2dcbf6aa24d60ac688d')
- self.assertEqual(node.hex(self.repo['default'].node()), 'e524296152246b3837fe9503c83b727075835155')
+ return self.repo
+
+ def test_file_map(self, stupid=False):
+ repo = self._loadwithfilemap('replace_trunk_with_branch.svndump',
+ "include alpha\n", stupid)
+ self.assertEqual(node.hex(repo[0].node()), '88e2c7492d83e4bf30fbb2dcbf6aa24d60ac688d')
+ self.assertEqual(node.hex(repo['default'].node()), 'e524296152246b3837fe9503c83b727075835155')
def test_file_map_stupid(self):
# TODO: re-enable test if we ever reinstate this feature
self.assertRaises(hgutil.Abort, self.test_file_map, True)
def test_file_map_exclude(self, stupid=False):
- repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
- filemap = open(self.filemap, 'w')
- filemap.write("exclude alpha\n")
- filemap.close()
- ui = self.ui(stupid)
- ui.setconfig('hgsubversion', 'filemap', self.filemap)
- commands.clone(ui, test_util.fileurl(repo_path),
- self.wc_path, filemap=self.filemap)
- self.assertEqual(node.hex(self.repo[0].node()), '2c48f3525926ab6c8b8424bcf5eb34b149b61841')
- self.assertEqual(node.hex(self.repo['default'].node()), 'b37a3c0297b71f989064d9b545b5a478bbed7cc1')
+ repo = self._loadwithfilemap('replace_trunk_with_branch.svndump',
+ "exclude alpha\n", stupid)
+ self.assertEqual(node.hex(repo[0].node()), '2c48f3525926ab6c8b8424bcf5eb34b149b61841')
+ self.assertEqual(node.hex(repo['default'].node()), 'b37a3c0297b71f989064d9b545b5a478bbed7cc1')
def test_file_map_exclude_stupid(self):
# TODO: re-enable test if we ever reinstate this feature
self.assertRaises(hgutil.Abort, self.test_file_map_exclude, True)
def test_file_map_rule_order(self):
- repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
- filemap = open(self.filemap, 'w')
- filemap.write("exclude alpha\n")
- filemap.write("include .\n")
- filemap.write("exclude gamma\n")
- filemap.close()
- ui = self.ui(False)
- ui.setconfig('hgsubversion', 'filemap', self.filemap)
- commands.clone(ui, test_util.fileurl(repo_path),
- self.wc_path, filemap=self.filemap)
+ repo = self._loadwithfilemap('replace_trunk_with_branch.svndump',
+ "exclude alpha\ninclude .\nexclude gamma\n")
# The exclusion of alpha is overridden by the later rule to
# include all of '.', whereas gamma should remain excluded
# because it's excluded after the root directory.
@@ -151,6 +146,33 @@ class MapTests(test_util.TestBase):
self.assertEqual(self.repo['default'].manifest().keys(),
['alpha', 'beta'])
+ def test_file_map_copy(self):
+ # Exercise excluding files copied from a non-excluded directory.
+ # There will be missing files as we are copying from an excluded
+ # directory.
+ repo = self._loadwithfilemap('copies.svndump', "exclude dir2\n",
+ failonmissing=False)
+ self.assertEqual(['dir/a', 'dir3/a'], list(repo[2]))
+
+ def test_file_map_exclude_copy_source_and_dest(self):
+ # dir3 is excluded and copied from dir2 which is also excluded.
+ # dir3 files should not be marked as missing and fetched.
+ repo = self._loadwithfilemap('copies.svndump',
+ "exclude dir2\nexclude dir3\n")
+ self.assertEqual(['dir/a'], list(repo[2]))
+
+ def test_file_map_include_file_exclude_dir(self):
+ # dir3 is excluded but we want dir3/a, which is also copied from
+ # an exluded dir2. dir3/a should be fetched.
+ repo = self._loadwithfilemap('copies.svndump',
+ "include .\nexclude dir2\nexclude dir3\ninclude dir3/a\n",
+ failonmissing=False)
+ self.assertEqual(['dir/a', 'dir3/a'], list(repo[2]))
+
+ def test_file_map_delete_dest(self):
+ repo = self._loadwithfilemap('copies.svndump', 'exclude dir3\n')
+ self.assertEqual(['dir/a', 'dir2/a'], list(repo[3]))
+
def test_branchmap(self, stupid=False):
repo_path = self.load_svndump('branchmap.svndump')
branchmap = open(self.branchmap, 'w')
@@ -244,6 +266,8 @@ class MapTests(test_util.TestBase):
ui = self.ui(stupid)
src, dest = test_util.hgclone(ui, self.wc_path, self.wc_path + '_clone',
update=False)
+ src = test_util.getlocalpeer(src)
+ dest = test_util.getlocalpeer(dest)
svncommands.rebuildmeta(ui, dest,
args=[test_util.fileurl(repo_path)])
@@ -270,7 +294,7 @@ class MapTests(test_util.TestBase):
repo = self.repo
for r in repo:
- self.assertEquals(svncommands.verify(ui, repo, rev=r), 0)
+ self.assertEquals(verify.verify(ui, repo, rev=r), 0)
def test_branchmap_verify_stupid(self):
'''test verify on a branchmapped clone (stupid)'''
diff --git a/tests/test_fetch_renames.py b/tests/test_fetch_renames.py
index 64b69d3..625e48d 100644
--- a/tests/test_fetch_renames.py
+++ b/tests/test_fetch_renames.py
@@ -14,8 +14,11 @@ class TestFetchRenames(test_util.TestBase):
w('%s: %r %r\n' % (f, fctx.data(), fctx.renamed()))
def _test_rename(self, stupid):
- repo = self._load_fixture_and_fetch('renames.svndump', stupid=stupid)
- # self._debug_print_copies(repo)
+ config = {
+ 'hgsubversion.filestoresize': '0',
+ }
+ repo = self._load_fixture_and_fetch('renames.svndump', stupid=stupid,
+ config=config)
# Map revnum to mappings of dest name to (source name, dest content)
copies = {
diff --git a/tests/test_fetch_symlinks.py b/tests/test_fetch_symlinks.py
index 7f9252b..42d5518 100644
--- a/tests/test_fetch_symlinks.py
+++ b/tests/test_fetch_symlinks.py
@@ -50,7 +50,17 @@ class TestFetchSymlinks(test_util.TestBase):
def test_symlinks_stupid(self):
self.test_symlinks(True)
+class TestMergeSpecial(test_util.TestBase):
+ def test_special(self):
+ repo = self._load_fixture_and_fetch('addspecial.svndump',
+ subdir='trunk')
+ ctx = repo['tip']
+ self.assertEqual(ctx['fnord'].flags(), 'l')
+ self.assertEqual(ctx['exe'].flags(), 'x')
+
def suite():
- all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchSymlinks),
- ]
+ all_tests = [
+ unittest.TestLoader().loadTestsFromTestCase(TestFetchSymlinks),
+ unittest.TestLoader().loadTestsFromTestCase(TestMergeSpecial),
+ ]
return unittest.TestSuite(all_tests)
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
new file mode 100644
index 0000000..ebcb381
--- /dev/null
+++ b/tests/test_helpers.py
@@ -0,0 +1,37 @@
+import os, sys, unittest
+
+_rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, _rootdir)
+
+from hgsubversion import editor
+
+class TestHelpers(unittest.TestCase):
+ def test_filestore(self):
+ fs = editor.FileStore(2)
+ fs.setfile('a', 'a')
+ fs.setfile('b', 'b')
+ self.assertEqual('a', fs._data.get('a'))
+ self.assertEqual('b', fs._data.get('b'))
+
+ fs.delfile('b')
+ self.assertRaises(IOError, lambda: fs.getfile('b'))
+ fs.setfile('bb', 'bb')
+ self.assertTrue('bb' in fs._files)
+ self.assertTrue('bb' not in fs._data)
+ self.assertEqual('bb', fs.getfile('bb'))
+
+ fs.delfile('bb')
+ self.assertTrue('bb' not in fs._files)
+ self.assertEqual([], os.listdir(fs._tempdir))
+ self.assertRaises(IOError, lambda: fs.getfile('bb'))
+
+ fs.setfile('bb', 'bb')
+ self.assertEqual(1, len(os.listdir(fs._tempdir)))
+ fs.popfile('bb')
+ self.assertEqual([], os.listdir(fs._tempdir))
+ self.assertRaises(editor.EditingError, lambda: fs.getfile('bb'))
+
+def suite():
+ return unittest.TestSuite([
+ unittest.TestLoader().loadTestsFromTestCase(TestHelpers),
+ ])
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
new file mode 100644
index 0000000..bff3730
--- /dev/null
+++ b/tests/test_hooks.py
@@ -0,0 +1,48 @@
+import sys
+import test_util
+import unittest
+
+from mercurial import hg
+from mercurial import commands
+
+class TestHooks(test_util.TestBase):
+ def setUp(self):
+ super(TestHooks, self).setUp()
+
+ def _loadupdate(self, fixture_name, *args, **kwargs):
+ kwargs = kwargs.copy()
+ kwargs.update(stupid=False, noupdate=False)
+ repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs)
+ return repo, repo_path
+
+ def test_updatemetahook(self):
+ repo, repo_path = self._loadupdate('single_rev.svndump')
+ state = repo.parents()
+ self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
+ commands.pull(self.repo.ui, self.repo)
+
+ # Clone to a new repository and add a hook
+ new_wc_path = "%s-2" % self.wc_path
+ commands.clone(self.repo.ui, self.wc_path, new_wc_path)
+ newrepo = hg.repository(test_util.testui(), new_wc_path)
+ newrepo.ui.setconfig('hooks', 'changegroup.meta',
+ 'python:hgsubversion.hooks.updatemeta.hook')
+
+ # Commit a rev that should trigger svn meta update
+ self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed Again'})
+ commands.pull(self.repo.ui, self.repo)
+
+ self.called = False
+ import hgsubversion.svncommands
+ oldupdatemeta = hgsubversion.svncommands.updatemeta
+ def _updatemeta(ui, repo, args=[]):
+ self.called = True
+ hgsubversion.svncommands.updatemeta = _updatemeta
+
+ # Pull and make sure our updatemeta function gets called
+ commands.pull(newrepo.ui, newrepo)
+ hgsubversion.svncommands.updatemeta = oldupdatemeta
+ self.assertTrue(self.called)
+
+def suite():
+ return unittest.findTestCases(sys.modules[__name__])
diff --git a/tests/test_pull.py b/tests/test_pull.py
index 556fbf1..ebfe85a 100644
--- a/tests/test_pull.py
+++ b/tests/test_pull.py
@@ -6,14 +6,16 @@ from mercurial import node
from mercurial import ui
from mercurial import util as hgutil
from mercurial import commands
+from hgsubversion import verify
class TestPull(test_util.TestBase):
def setUp(self):
super(TestPull, self).setUp()
- def _loadupdate(self, fixture_name):
- repo, repo_path = self.load_and_fetch(fixture_name, stupid=False,
- noupdate=False)
+ def _loadupdate(self, fixture_name, *args, **kwargs):
+ kwargs = kwargs.copy()
+ kwargs.update(stupid=False, noupdate=False)
+ repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs)
return repo, repo_path
def test_nochanges(self):
@@ -58,6 +60,26 @@ class TestPull(test_util.TestBase):
commands.pull(repo.ui, repo)
self.assertEqual(oldheads, map(node.hex, repo.heads()))
+ def test_skip_basic(self):
+ repo, repo_path = self._loadupdate('single_rev.svndump')
+ self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
+ self.add_svn_rev(repo_path, {'trunk/beta': 'More changed'})
+ self.add_svn_rev(repo_path, {'trunk/gamma': 'Even more changeder'})
+ repo.ui.setconfig('hgsubversion', 'unsafeskip', '3 4')
+ commands.pull(repo.ui, repo)
+ tip = repo['tip'].rev()
+ self.assertEqual(tip, 1)
+ self.assertEquals(verify.verify(repo.ui, repo, rev=tip), 1)
+
+ def test_skip_delete_restore(self):
+ repo, repo_path = self._loadupdate('delete_restore_trunk.svndump',
+ rev=2)
+ repo.ui.setconfig('hgsubversion', 'unsafeskip', '3 4')
+ commands.pull(repo.ui, repo)
+ tip = repo['tip'].rev()
+ self.assertEqual(tip, 1)
+ self.assertEquals(verify.verify(repo.ui, repo, rev=tip), 0)
+
def suite():
import unittest, sys
return unittest.findTestCases(sys.modules[__name__])
diff --git a/tests/test_pull_fallback.py b/tests/test_pull_fallback.py
new file mode 100644
index 0000000..e543d4e
--- /dev/null
+++ b/tests/test_pull_fallback.py
@@ -0,0 +1,106 @@
+import test_util
+
+import re
+import mercurial
+from mercurial import commands
+from hgsubversion import stupid
+from hgsubversion import svnwrap
+from hgsubversion import wrappers
+
+class TestPullFallback(test_util.TestBase):
+ def setUp(self):
+ super(TestPullFallback, self).setUp()
+
+ def _loadupdate(self, fixture_name, *args, **kwargs):
+ kwargs = kwargs.copy()
+ kwargs.update(noupdate=False)
+ repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs)
+ return repo, repo_path
+
+ def test_stupid_fallback_to_stupid_fullrevs(self):
+ return
+ to_patch = {
+ 'mercurial.patch.patchbackend': _patchbackend_raise,
+ 'stupid.diff_branchrev': stupid.diff_branchrev,
+ 'stupid.fetch_branchrev': stupid.fetch_branchrev,
+ }
+
+ expected_calls = {
+ 'mercurial.patch.patchbackend': 1,
+ 'stupid.diff_branchrev': 1,
+ 'stupid.fetch_branchrev': 1,
+ }
+
+ repo, repo_path = self._loadupdate(
+ 'single_rev.svndump', stupid=True)
+
+ # Passing stupid=True doesn't seem to be working - force it
+ repo.ui.setconfig('hgsubversion', 'stupid', "true")
+ state = repo.parents()
+
+ calls, replaced = _monkey_patch(to_patch)
+
+ try:
+ self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
+ commands.pull(self.repo.ui, repo, update=True)
+ self.failIfEqual(state, repo.parents())
+ self.assertTrue('tip' in repo[None].tags())
+ self.assertEqual(expected_calls, calls)
+
+ finally:
+ _monkey_unpatch(replaced)
+
+def _monkey_patch(to_patch, start=None):
+ if start is None:
+ import sys
+ start = sys.modules[__name__]
+
+ calls = {}
+ replaced = {}
+
+ for path, replacement in to_patch.iteritems():
+ obj = start
+ owner, attr = path.rsplit('.', 1)
+
+ for a in owner.split('.', -1):
+ obj = getattr(obj, a)
+
+ replaced[path] = getattr(obj, attr)
+ calls[path] = 0
+
+ def outer(path=path, calls=calls, replacement=replacement):
+ def wrapper(*p, **kw):
+ calls[path] += 1
+ return replacement(*p, **kw)
+
+ return wrapper
+
+ setattr(obj, attr, outer())
+
+ return calls, replaced
+
+def _monkey_unpatch(to_patch, start=None):
+ if start is None:
+ import sys
+ start = sys.modules[__name__]
+
+ replaced = {}
+
+ for path, replacement in to_patch.iteritems():
+ obj = start
+ owner, attr = path.rsplit('.', 1)
+
+ for a in owner.split('.', -1):
+ obj = getattr(obj, a)
+
+ replaced[path] = getattr(obj, attr)
+ setattr(obj, attr, replacement)
+
+ return replaced
+
+def _patchbackend_raise(*p, **kw):
+ raise mercurial.patch.PatchError("patch failed")
+
+def suite():
+ import unittest, sys
+ return unittest.findTestCases(sys.modules[__name__])
diff --git a/tests/test_push_autoprops.py b/tests/test_push_autoprops.py
new file mode 100644
index 0000000..3dfba47
--- /dev/null
+++ b/tests/test_push_autoprops.py
@@ -0,0 +1,107 @@
+import subprocess
+import sys
+import unittest
+import os
+
+import test_util
+
+from hgsubversion import svnwrap
+
+class PushAutoPropsTests(test_util.TestBase):
+ def setUp(self):
+ test_util.TestBase.setUp(self)
+ repo, self.repo_path = self.load_and_fetch('emptyrepo.svndump')
+
+ def test_push_honors_svn_autoprops(self):
+ self.setup_svn_config(
+ "[miscellany]\n"
+ "enable-auto-props = yes\n"
+ "[auto-props]\n"
+ "*.py = test:prop=success\n")
+ changes = [('test.py', 'test.py', 'echo hallo')]
+ self.commitchanges(changes)
+ self.pushrevisions(True)
+ prop_val = test_util.svnpropget(
+ self.repo_path, "trunk/test.py", 'test:prop')
+ self.assertEqual('success', prop_val)
+
+
+class AutoPropsConfigTest(test_util.TestBase):
+ def test_use_autoprops_for_matching_file_when_enabled(self):
+ self.setup_svn_config(
+ "[miscellany]\n"
+ "enable-auto-props = yes\n"
+ "[auto-props]\n"
+ "*.py = test:prop=success\n")
+ props = self.new_autoprops_config().properties('xxx/test.py')
+ self.assertEqual({ 'test:prop': 'success'}, props)
+
+ def new_autoprops_config(self):
+ return svnwrap.AutoPropsConfig(self.config_dir)
+
+ def test_ignore_nonexisting_config(self):
+ config_file = os.path.join(self.config_dir, 'config')
+ os.remove(config_file)
+ self.assertTrue(not os.path.exists(config_file))
+ props = self.new_autoprops_config().properties('xxx/test.py')
+ self.assertEqual({}, props)
+
+ def test_ignore_autoprops_when_file_doesnt_match(self):
+ self.setup_svn_config(
+ "[miscellany]\n"
+ "enable-auto-props = yes\n"
+ "[auto-props]\n"
+ "*.py = test:prop=success\n")
+ props = self.new_autoprops_config().properties('xxx/test.sh')
+ self.assertEqual({}, props)
+
+ def test_ignore_autoprops_when_disabled(self):
+ self.setup_svn_config(
+ "[miscellany]\n"
+ "#enable-auto-props = yes\n"
+ "[auto-props]\n"
+ "*.py = test:prop=success\n")
+ props = self.new_autoprops_config().properties('xxx/test.py')
+ self.assertEqual({}, props)
+
+ def test_combine_properties_of_multiple_matches(self):
+ self.setup_svn_config(
+ "[miscellany]\n"
+ "enable-auto-props = yes\n"
+ "[auto-props]\n"
+ "*.py = test:prop=success\n"
+ "test.* = test:prop2=success\n")
+ props = self.new_autoprops_config().properties('xxx/test.py')
+ self.assertEqual({
+ 'test:prop': 'success', 'test:prop2': 'success'}, props)
+
+
+class ParseAutoPropsTests(test_util.TestBase):
+ def test_property_value_is_optional(self):
+ props = svnwrap.parse_autoprops("svn:executable")
+ self.assertEqual({'svn:executable': ''}, props)
+ props = svnwrap.parse_autoprops("svn:executable=")
+ self.assertEqual({'svn:executable': ''}, props)
+
+ def test_property_value_may_be_quoted(self):
+ props = svnwrap.parse_autoprops("svn:eol-style=\" native \"")
+ self.assertEqual({'svn:eol-style': ' native '}, props)
+ props = svnwrap.parse_autoprops("svn:eol-style=' native '")
+ self.assertEqual({'svn:eol-style': ' native '}, props)
+
+ def test_surrounding_whitespaces_are_ignored(self):
+ props = svnwrap.parse_autoprops(" svn:eol-style = native ")
+ self.assertEqual({'svn:eol-style': 'native'}, props)
+
+ def test_multiple_properties_are_separated_by_semicolon(self):
+ props = svnwrap.parse_autoprops(
+ "svn:eol-style=native;svn:executable=true\n")
+ self.assertEqual({
+ 'svn:eol-style': 'native',
+ 'svn:executable': 'true'},
+ props)
+
+
+def suite():
+ return unittest.findTestCases(sys.modules[__name__])
+
diff --git a/tests/test_push_command.py b/tests/test_push_command.py
index 8342100..1c41596 100644
--- a/tests/test_push_command.py
+++ b/tests/test_push_command.py
@@ -134,7 +134,7 @@ class PushTests(test_util.TestBase):
finally:
if sys.version_info >= (2,6):
svnserve.kill()
- else:
+ else:
test_util.kill_process(svnserve)
def test_push_over_svnserve(self):
@@ -487,7 +487,40 @@ class PushTests(test_util.TestBase):
'Outgoing changesets parent is not at subversion '
'HEAD\n'
'(pull again and rebase on a newer revision)')
+ # verify that any pending transactions on the server got cleaned up
+ self.assertEqual([], os.listdir(
+ os.path.join(self.tmpdir, 'testrepo-1', 'db', 'transactions')))
+ def test_push_encoding(self):
+ self.test_push_two_revs()
+ # Writing then rebasing UTF-8 filenames in a cp1252 windows console
+ # used to fail because hg internal encoding was being changed during
+ # the interactions with subversion, *and during the rebase*, which
+ # confused the dirstate and made it believe the file was deleted.
+ fn = 'pi\xc3\xa8ce/test'
+ changes = [(fn, fn, 'a')]
+ par = self.repo['tip'].rev()
+ self.commitchanges(changes, parent=par)
+ self.pushrevisions()
+
+ def test_push_emptying_changeset(self):
+ r = self.repo['tip']
+ changes = [
+ ('alpha', None, None),
+ ('beta', None, None),
+ ]
+ parent = self.repo['tip'].rev()
+ self.commitchanges(changes, parent=parent)
+ self.pushrevisions()
+ self.assertEqual({}, self.repo['tip'].manifest())
+
+ # Try to re-add a file after emptying the branch
+ changes = [
+ ('alpha', 'alpha', 'alpha'),
+ ]
+ self.commitchanges(changes, parent=self.repo['tip'].rev())
+ self.pushrevisions()
+ self.assertEqual(['alpha'], list(self.repo['tip'].manifest()))
def suite():
test_classes = [PushTests, ]
diff --git a/tests/test_rebuildmeta.py b/tests/test_rebuildmeta.py
index bee8e37..5e2de4b 100644
--- a/tests/test_rebuildmeta.py
+++ b/tests/test_rebuildmeta.py
@@ -34,6 +34,8 @@ def _do_case(self, name, stupid, single):
wc2_path = self.wc_path + '_clone'
u = ui.ui()
src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False)
+ src = test_util.getlocalpeer(src)
+ dest = test_util.getlocalpeer(dest)
# insert a wrapper that prevents calling changectx.children()
def failfn(orig, ctx):
@@ -51,9 +53,48 @@ def _do_case(self, name, stupid, single):
# remove the wrapper
context.changectx.children = origchildren
+ self._run_assertions(name, stupid, single, src, dest, u)
+
+ wc3_path = self.wc_path + '_partial'
+ src, dest = test_util.hgclone(u,
+ self.wc_path,
+ wc3_path,
+ update=False,
+ rev=[0])
+ srcrepo = test_util.getlocalpeer(src)
+ dest = test_util.getlocalpeer(dest)
+
+ # insert a wrapper that prevents calling changectx.children()
+ extensions.wrapfunction(context.changectx, 'children', failfn)
+
+ try:
+ svncommands.rebuildmeta(u, dest,
+ args=[test_util.fileurl(repo_path +
+ subdir), ])
+ finally:
+ # remove the wrapper
+ context.changectx.children = origchildren
+
+ dest.pull(src)
+
+ # insert a wrapper that prevents calling changectx.children()
+ extensions.wrapfunction(context.changectx, 'children', failfn)
+ try:
+ svncommands.updatemeta(u, dest,
+ args=[test_util.fileurl(repo_path +
+ subdir), ])
+ finally:
+ # remove the wrapper
+ context.changectx.children = origchildren
+
+ self._run_assertions(name, stupid, single, srcrepo, dest, u)
+
+
+def _run_assertions(self, name, stupid, single, src, dest, u):
+
self.assertTrue(os.path.isdir(os.path.join(src.path, 'svn')),
'no .hg/svn directory in the source!')
- self.assertTrue(os.path.isdir(os.path.join(src.path, 'svn')),
+ self.assertTrue(os.path.isdir(os.path.join(dest.path, 'svn')),
'no .hg/svn directory in the destination!')
dest = hg.repository(u, os.path.dirname(dest.path))
for tf in ('lastpulled', 'rev_map', 'uuid', 'tagmap', 'layout', 'subdir',):
@@ -68,7 +109,7 @@ def _do_case(self, name, stupid, single):
self.assertNotEqual(old, new,
'rebuildmeta unexpected match on youngest rev!')
continue
- self.assertMultiLineEqual(old, new)
+ self.assertMultiLineEqual(old, new, tf + ' differs')
self.assertEqual(src.branchtags(), dest.branchtags())
srcbi = pickle.load(open(os.path.join(src.path, 'svn', 'branch_info')))
destbi = pickle.load(open(os.path.join(dest.path, 'svn', 'branch_info')))
@@ -101,11 +142,17 @@ def buildmethod(case, name, stupid, single):
return m
+skip = set([
+ 'project_root_not_repo_root.svndump',
+ 'corrupt.svndump',
+])
+
attrs = {'_do_case': _do_case,
+ '_run_assertions': _run_assertions,
}
for case in [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]:
# this fixture results in an empty repository, don't use it
- if case == 'project_root_not_repo_root.svndump':
+ if case in skip:
continue
bname = 'test_' + case[:-len('.svndump')]
attrs[bname] = buildmethod(case, bname, False, False)
diff --git a/tests/test_single_dir_clone.py b/tests/test_single_dir_clone.py
index 52ebe0f..7c3cb3e 100644
--- a/tests/test_single_dir_clone.py
+++ b/tests/test_single_dir_clone.py
@@ -44,6 +44,15 @@ class TestSingleDir(test_util.TestBase):
self.assertEqual(repo.branchtags().keys(), ['default', ])
self.assertEqual(repo['default'].manifest().keys(), oldmanifest)
+ def test_clone_subdir_is_file_prefix(self, stupid=False):
+ FIXTURE = 'subdir_is_file_prefix.svndump'
+ repo = self._load_fixture_and_fetch(FIXTURE,
+ stupid=stupid,
+ layout='single',
+ subdir=test_util.subdir[FIXTURE])
+ self.assertEqual(repo.branchtags().keys(), ['default'])
+ self.assertEqual(repo['tip'].manifest().keys(), ['flaf.txt'])
+
def test_externals_single(self):
repo = self._load_fixture_and_fetch('externals.svndump',
stupid=False,
diff --git a/tests/test_startrev.py b/tests/test_startrev.py
deleted file mode 100644
index e544bff..0000000
--- a/tests/test_startrev.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import test_util
-
-import os
-import unittest
-
-def _do_case(self, name, subdir, stupid):
- wc_base = self.wc_path
- self.wc_path = wc_base + '_full'
- headclone = self._load_fixture_and_fetch(name, subdir=subdir, stupid=stupid,
- layout='single', startrev='HEAD')
- self.wc_path = wc_base + '_head'
- fullclone = self._load_fixture_and_fetch(name, subdir=subdir, stupid=stupid,
- layout='single')
-
- fulltip = fullclone['tip']
- headtip = headclone['tip']
- # viewing diff's of lists of files is easier on the eyes
- self.assertMultiLineEqual('\n'.join(fulltip), '\n'.join(headtip))
-
- for f in fulltip:
- self.assertMultiLineEqual(fulltip[f].data(), headtip[f].data())
-
- self.assertNotEqual(len(fullclone), 0, "full clone shouldn't be empty")
- self.assertEqual(len(headclone), 1,
- "shallow clone should have just one revision, not %d"
- % len(headclone))
-
-def buildmethod(case, name, subdir, stupid):
- m = lambda self: self._do_case(case, subdir.strip('/'), stupid)
- m.__name__ = name
- m.__doc__ = ('Test clone with startrev on %s%s with %s replay.' %
- (case, subdir, (stupid and 'stupid') or 'real'))
- return m
-
-
-# these fixtures contain no files at HEAD and would result in empty clones
-nofiles = set([
- 'binaryfiles.svndump',
- 'binaryfiles-broken.svndump',
- 'emptyrepo.svndump',
-])
-
-# these fixtures contain no files in trunk at HEAD and would result in an empty
-# shallow clone if cloning trunk, so we use another subdirectory
-subdirmap = {
- 'commit-to-tag.svndump': '/branches/magic',
- 'pushexternals.svndump': '',
- 'tag_name_same_as_branch.svndump': '/branches/magic',
-}
-
-attrs = {'_do_case': _do_case,
- }
-
-for case in [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]:
- if case in nofiles:
- continue
-
- subdir = test_util.subdir.get(case, '') + subdirmap.get(case, '/trunk')
-
- bname = 'test_' + case[:-len('.svndump')]
- attrs[bname] = buildmethod(case, bname, subdir, False)
- name = bname + '_stupid'
- attrs[name] = buildmethod(case, name, subdir, True)
-
-StartRevTests = type('StartRevTests', (test_util.TestBase,), attrs)
-
-
-def suite():
- all_tests = [unittest.TestLoader().loadTestsFromTestCase(StartRevTests),
- ]
- return unittest.TestSuite(all_tests)
diff --git a/tests/test_tags.py b/tests/test_tags.py
index 630e039..29afcdb 100644
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -112,6 +112,7 @@ rename a tag
"You should check that before assuming issues with this test.\n")
wc2_path = self.wc_path + '2'
src, dest = test_util.hgclone(repo.ui, self.wc_path, wc2_path, update=False)
+ dest = test_util.getlocalpeer(dest)
svncommands.rebuildmeta(repo.ui,
dest,
args=[test_util.fileurl(repo_path), ])
diff --git a/tests/test_updatemeta.py b/tests/test_updatemeta.py
new file mode 100644
index 0000000..37bb1f8
--- /dev/null
+++ b/tests/test_updatemeta.py
@@ -0,0 +1,81 @@
+import test_util
+
+import os
+import pickle
+import unittest
+import test_rebuildmeta
+
+from mercurial import context
+from mercurial import extensions
+from mercurial import hg
+from mercurial import ui
+
+from hgsubversion import svncommands
+from hgsubversion import svnmeta
+
+
+
+def _do_case(self, name, stupid, single):
+ subdir = test_util.subdir.get(name, '')
+ layout = 'auto'
+ if single:
+ layout = 'single'
+ repo, repo_path = self.load_and_fetch(name, subdir=subdir, stupid=stupid,
+ layout=layout)
+ assert len(self.repo) > 0
+ wc2_path = self.wc_path + '_clone'
+ u = ui.ui()
+ src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False)
+ src = test_util.getlocalpeer(src)
+ dest = test_util.getlocalpeer(dest)
+
+ # insert a wrapper that prevents calling changectx.children()
+ def failfn(orig, ctx):
+ self.fail('calling %s is forbidden; it can cause massive slowdowns '
+ 'when rebuilding large repositories' % orig)
+
+ origchildren = getattr(context.changectx, 'children')
+ extensions.wrapfunction(context.changectx, 'children', failfn)
+
+ # test updatemeta on an empty repo
+ try:
+ svncommands.updatemeta(u, dest,
+ args=[test_util.fileurl(repo_path +
+ subdir), ])
+ finally:
+ # remove the wrapper
+ context.changectx.children = origchildren
+
+ self._run_assertions(name, stupid, single, src, dest, u)
+
+
+def _run_assertions(self, name, stupid, single, src, dest, u):
+ test_rebuildmeta._run_assertions(self, name, stupid, single, src, dest, u)
+
+
+skip = set([
+ 'project_root_not_repo_root.svndump',
+ 'corrupt.svndump',
+])
+
+attrs = {'_do_case': _do_case,
+ '_run_assertions': _run_assertions,
+ }
+for case in [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]:
+ # this fixture results in an empty repository, don't use it
+ if case in skip:
+ continue
+ bname = 'test_' + case[:-len('.svndump')]
+ attrs[bname] = test_rebuildmeta.buildmethod(case, bname, False, False)
+ name = bname + '_stupid'
+ attrs[name] = test_rebuildmeta.buildmethod(case, name, True, False)
+ name = bname + '_single'
+ attrs[name] = test_rebuildmeta.buildmethod(case, name, False, True)
+
+UpdateMetaTests = type('UpdateMetaTests', (test_util.TestBase,), attrs)
+
+
+def suite():
+ all_tests = [unittest.TestLoader().loadTestsFromTestCase(UpdateMetaTests),
+ ]
+ return unittest.TestSuite(all_tests)
diff --git a/tests/test_util.py b/tests/test_util.py
index 0343fe2..c2c67de 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -7,6 +7,7 @@ import shutil
import stat
import subprocess
import sys
+import tarfile
import tempfile
import unittest
import urllib
@@ -37,6 +38,7 @@ except AttributeError:
SkipTest = None
from hgsubversion import util
+from hgsubversion import svnwrap
# Documentation for Subprocess.Popen() says:
# "Note that on Windows, you cannot set close_fds to true and
@@ -96,11 +98,17 @@ subdir = {'truncatedhistory.svndump': '/project2',
'project_name_with_space.svndump': '/project name',
'non_ascii_path_1.svndump': '/b\xC3\xB8b',
'non_ascii_path_2.svndump': '/b%C3%B8b',
+ 'subdir_is_file_prefix.svndump': '/flaf',
}
FIXTURES = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'fixtures')
+def getlocalpeer(repo):
+ localrepo = getattr(repo, 'local', lambda: repo)()
+ if isinstance(localrepo, bool):
+ localrepo = repo
+ return localrepo
def _makeskip(name, message):
if SkipTest:
@@ -143,7 +151,10 @@ def fileurl(path):
path = os.path.abspath(path).replace(os.sep, '/')
drive, path = os.path.splitdrive(path)
if drive:
- drive = '/' + drive
+ # In svn 1.7, the swig svn wrapper returns local svn URLs
+ # with an uppercase drive letter, try to match that to
+ # simplify svn info tests.
+ drive = '/' + drive.upper()
url = 'file://%s%s' % (drive, path)
return url
@@ -158,11 +169,8 @@ def testui(stupid=False, layout='auto', startrev=0):
return u
def dispatch(cmd):
- try:
- req = dispatchmod.request(cmd)
- dispatchmod.dispatch(req)
- except AttributeError, e:
- dispatchmod.dispatch(cmd)
+ cmd = getattr(dispatchmod, 'request', lambda x: x)(cmd)
+ dispatchmod.dispatch(cmd)
def rmtree(path):
# Read-only files cannot be removed under Windows
@@ -198,12 +206,12 @@ def _verify_our_modules():
'from the wrong path!'
)
-def hgclone(ui, source, dest, update=True):
+def hgclone(ui, source, dest, update=True, rev=None):
if getattr(hg, 'peer', None):
# Since 1.9 (d976542986d2)
- src, dest = hg.clone(ui, {}, source, dest, update=update)
+ src, dest = hg.clone(ui, {}, source, dest, update=update, rev=rev)
else:
- src, dest = hg.clone(ui, source, dest, update=update)
+ src, dest = hg.clone(ui, source, dest, update=update, rev=rev)
return src, dest
def svnls(repo_path, path, rev='HEAD'):
@@ -236,6 +244,9 @@ class TestBase(unittest.TestCase):
def setUp(self):
_verify_our_modules()
+ # the Python 2.7 default of 640 is obnoxiously low
+ self.maxDiff = 4096
+
self.oldenv = dict([(k, os.environ.get(k, None),) for k in
('LANG', 'LC_ALL', 'HGRCPATH',)])
self.oldt = i18n.t
@@ -255,6 +266,10 @@ class TestBase(unittest.TestCase):
self.wc_path = '%s/testrepo_wc' % self.tmpdir
self.svn_wc = None
+ self.config_dir = self.tmpdir
+ svnwrap.common._svn_config_dir = self.config_dir
+ self.setup_svn_config('')
+
# Previously, we had a MockUI class that wrapped ui, and giving access
# to the stream. The ui.pushbuffer() and ui.popbuffer() can be used
# instead. Using the regular UI class, with all stderr redirected to
@@ -263,6 +278,10 @@ class TestBase(unittest.TestCase):
self.patch = (ui.ui.write_err, ui.ui.write)
setattr(ui.ui, self.patch[0].func_name, self.patch[1])
+ def setup_svn_config(self, config):
+ with open(self.config_dir + '/config', 'w') as c:
+ c.write(config)
+
def _makerepopath(self):
self.repocount += 1
return '%s/testrepo-%d' % (self.tmpdir, self.repocount)
@@ -297,15 +316,31 @@ class TestBase(unittest.TestCase):
proc.communicate()
return path
- def load_and_fetch(self, fixture_name, subdir=None, stupid=False,
- layout='auto', startrev=0, externals=None,
- noupdate=True):
+ def load_repo_tarball(self, fixture_name):
+ '''Extracts a tarball of an svn repo and returns the svn repo path.'''
+ path = self._makerepopath()
+ assert not os.path.exists(path)
+ os.mkdir(path)
+ tarball = tarfile.open(os.path.join(FIXTURES, fixture_name))
+ # This is probably somewhat fragile, but I'm not sure how to
+ # do better in particular, I think it assumes that the tar
+ # entries are in the right order and that directories appear
+ # before their contents. This is a valid assummption for sane
+ # tarballs, from what I can tell. In particular, for a simple
+ # tarball of a svn repo with paths relative to the repo root,
+ # it seems to work
+ for entry in tarball:
+ tarball.extract(entry, path)
+ return path
+
+ def fetch(self, repo_path, subdir=None, stupid=False, layout='auto',
+ startrev=0, externals=None, noupdate=True, dest=None, rev=None,
+ config=None):
if layout == 'single':
if subdir is None:
subdir = 'trunk'
elif subdir is None:
subdir = ''
- repo_path = self.load_svndump(fixture_name)
projectpath = repo_path
if subdir:
projectpath += '/' + subdir
@@ -321,12 +356,27 @@ class TestBase(unittest.TestCase):
cmd.append('--stupid')
if noupdate:
cmd.append('--noupdate')
+ if rev is not None:
+ cmd.append('--rev=%s' % rev)
+ config = dict(config or {})
if externals:
- cmd[:0] = ['--config', 'hgsubversion.externals=%s' % externals]
+ config['hgsubversion.externals'] = str(externals)
+ for k,v in reversed(sorted(config.iteritems())):
+ cmd[:0] = ['--config', '%s=%s' % (k, v)]
dispatch(cmd)
- return hg.repository(testui(), self.wc_path), repo_path
+ return hg.repository(testui(), self.wc_path)
+
+ def load_and_fetch(self, fixture_name, *args, **opts):
+ if fixture_name.endswith('.svndump'):
+ repo_path = self.load_svndump(fixture_name)
+ elif fixture_name.endswith('tar.gz'):
+ repo_path = self.load_repo_tarball(fixture_name)
+ else:
+ assert False, 'Unknown fixture type'
+
+ return self.fetch(repo_path, *args, **opts), repo_path
def _load_fixture_and_fetch(self, *args, **kwargs):
repo, repo_path = self.load_and_fetch(*args, **kwargs)
@@ -470,7 +520,7 @@ class TestBase(unittest.TestCase):
msg = '%s\n%s' % (msg or '', diff)
raise self.failureException, msg
- def draw(self, repo):
+ def getgraph(self, repo):
"""Helper function displaying a repository graph, especially
useful when debugging comprehensive tests.
"""
@@ -487,4 +537,10 @@ summary: {desc|firstline}
files: {files}
"""
+ _ui.pushbuffer()
graphlog.graphlog(_ui, repo, rev=None, template=templ)
+ return _ui.popbuffer()
+
+ def draw(self, repo):
+ sys.stdout.write(self.getgraph(repo))
+
diff --git a/tests/test_utility_commands.py b/tests/test_utility_commands.py
index 10926e2..338dc82 100644
--- a/tests/test_utility_commands.py
+++ b/tests/test_utility_commands.py
@@ -14,6 +14,7 @@ from mercurial import util as hgutil
from hgsubversion import util
from hgsubversion import svncommands
+from hgsubversion import verify
from hgsubversion import wrappers
expected_info_output = '''URL: %(repourl)s/%(branch)s
@@ -66,6 +67,23 @@ class UtilityTests(test_util.TestBase):
'rev': 5,
})
self.assertMultiLineEqual(actual, expected)
+ destpath = self.wc_path + '_clone'
+ test_util.hgclone(u, self.repo, destpath)
+ repo2 = hg.repository(u, destpath)
+ repo2.ui.setconfig('paths', 'default-push',
+ self.repo.ui.config('paths', 'default'))
+ hg.update(repo2, 'default')
+ svncommands.rebuildmeta(u, repo2, [])
+ u.pushbuffer()
+ svncommands.info(u, repo2)
+ actual = u.popbuffer()
+ expected = (expected_info_output %
+ {'date': '2008-10-08 01:39:29 +0000 (Wed, 08 Oct 2008)',
+ 'repourl': repourl(repo_path),
+ 'branch': 'trunk',
+ 'rev': 6,
+ })
+ self.assertMultiLineEqual(actual, expected)
def test_info_single(self):
repo, repo_path = self.load_and_fetch('two_heads.svndump', subdir='trunk')
@@ -245,28 +263,82 @@ class UtilityTests(test_util.TestBase):
authors=author_path)
self.assertMultiLineEqual(open(author_path).read(), 'Augie=\nevil=\n')
- def test_svnverify(self):
+ def test_svnverify(self, stupid=False):
repo, repo_path = self.load_and_fetch('binaryfiles.svndump',
- noupdate=False)
- ret = svncommands.verify(self.ui(), repo, [], rev=1)
+ noupdate=False, stupid=stupid)
+ ret = verify.verify(self.ui(), repo, [], rev=1, stupid=stupid)
self.assertEqual(0, ret)
repo_path = self.load_svndump('binaryfiles-broken.svndump')
u = self.ui()
u.pushbuffer()
- ret = svncommands.verify(u, repo, [test_util.fileurl(repo_path)],
- rev=1)
+ ret = verify.verify(u, repo, [test_util.fileurl(repo_path)],
+ rev=1, stupid=stupid)
output = u.popbuffer()
self.assertEqual(1, ret)
output = re.sub(r'file://\S+', 'file://', output)
- self.assertEqual("""\
+ self.assertMultiLineEqual("""\
verifying d51f46a715a1 against file://
-difference in file binary2
-unexpected files:
- binary1
-missing files:
- binary3
+difference in: binary2
+unexpected file: binary1
+missing file: binary3
""", output)
+ def test_svnverify_stupid(self):
+ self.test_svnverify(True)
+
+ def test_corruption(self, stupid=False):
+ SUCCESS = 0
+ FAILURE = 1
+
+ repo, repo_path = self.load_and_fetch('correct.svndump', layout='single',
+ subdir='', stupid=stupid)
+
+ ui = self.ui()
+
+ self.assertEqual(SUCCESS, verify.verify(ui, self.repo, rev='tip',
+ stupid=stupid))
+
+ corrupt_source = test_util.fileurl(self.load_svndump('corrupt.svndump'))
+
+ repo.ui.setconfig('paths', 'default', corrupt_source)
+
+ ui.pushbuffer()
+ code = verify.verify(ui, repo, rev='tip')
+ actual = ui.popbuffer()
+
+ actual = actual.replace(corrupt_source, '$REPO')
+ actual = set(actual.splitlines())
+
+ expected = set([
+ 'verifying 78e965230a13 against $REPO@1',
+ 'missing file: missing-file',
+ 'wrong flags for: executable-file',
+ 'wrong flags for: symlink',
+ 'wrong flags for: regular-file',
+ 'difference in: another-regular-file',
+ 'difference in: regular-file',
+ 'unexpected file: empty-file',
+ ])
+
+ self.assertEqual((FAILURE, expected), (code, actual))
+
+ def test_corruption_stupid(self):
+ self.test_corruption(True)
+
+ def test_svnrebuildmeta(self):
+ otherpath = self.load_svndump('binaryfiles-broken.svndump')
+ otherurl = test_util.fileurl(otherpath)
+ self.load_and_fetch('replace_trunk_with_branch.svndump')
+ # rebuildmeta with original repo
+ svncommands.rebuildmeta(self.ui(), repo=self.repo, args=[])
+ # rebuildmeta with unrelated repo
+ self.assertRaises(hgutil.Abort,
+ svncommands.rebuildmeta,
+ self.ui(), repo=self.repo, args=[otherurl])
+ # rebuildmeta --unsafe-skip-uuid-check with unrelated repo
+ svncommands.rebuildmeta(self.ui(), repo=self.repo, args=[otherurl],
+ unsafe_skip_uuid_check=True)
+
def suite():
all_tests = [unittest.TestLoader().loadTestsFromTestCase(UtilityTests),
]