changeset 1106:5cb6c95e0283 stable

Merge default and stable so I can do stable releases again.
author Augie Fackler <raf@durin42.com>
date Tue, 11 Feb 2014 12:48:49 -0500
parents b5b1fce26f1f (current diff) 7d47a0f73135 (diff)
children 0f16e11b2c2b
files tests/test_rebuildmeta.py tests/test_updatemeta.py
diffstat 69 files changed, 3201 insertions(+), 1339 deletions(-) [+]
line wrap: on
line diff
--- a/README
+++ b/README
@@ -16,7 +16,7 @@ Installation
 ------------
 You need to have either have Subversion 1.5 (or later) installed along with
 either Subvertpy 0.7.4 (or later) or the Subversion SWIG Python bindings. You
-need Mercurial 1.3 or later.
+need Mercurial 2.0 or later.
 
 .. _mercurial: http://selenic.com/repo/hg
 .. _mercurial-stable: http://selenic.com/repo/hg-stable
--- a/hgsubversion/__init__.py
+++ b/hgsubversion/__init__.py
@@ -84,6 +84,10 @@ wrapcmds = { # cmd: generic, target, fix
     'clone': (False, 'sources', True, True, [
         ('T', 'tagpaths', '',
          'list of paths to search for tags in Subversion repositories'),
+        ('', 'branchdir', '',
+         'path to search for branches in subversion repositories'),
+        ('', 'infix', '',
+         'path relative to trunk, branch an tag dirs to import'),
         ('A', 'authors', '',
          'file mapping Subversion usernames to Mercurial authors'),
         ('', 'filemap', '',
@@ -180,6 +184,9 @@ def reposetup(ui, repo):
         for tunnel in ui.configlist('hgsubversion', 'tunnels'):
             hg.schemes['svn+' + tunnel] = svnrepo
 
+    if revset and ui.configbool('hgsubversion', 'nativerevs'):
+        extensions.wrapfunction(revset, 'stringset', util.revset_stringset)
+
 _old_local = hg.schemes['file']
 def _lookup(url):
     if util.islocalrepo(url):
new file mode 100644
--- /dev/null
+++ b/hgsubversion/compathacks.py
@@ -0,0 +1,11 @@
+"""Functions to work around API changes inside Mercurial."""
+
+def branchset(repo):
+  """Return the set of branches present in a repo.
+
+  Works around branchtags() vanishing between 2.8 and 2.9.
+  """
+  try:
+    return set(repo.branchmap())
+  except AttributeError:
+    return set(repo.branchtags())
--- a/hgsubversion/editor.py
+++ b/hgsubversion/editor.py
@@ -191,7 +191,7 @@ class HgEditor(svnwrap.Editor):
         # A mapping of file paths to batons
         self._openpaths = {}
         self._deleted = set()
-        self._getctx = util.lrucachefunc(self.repo.changectx, 3)
+        self._getctx = hgutil.lrucachefunc(self.repo.changectx)
         # A stack of opened directory (baton, path) pairs.
         self._opendirs = []
         self._missing = set()
@@ -401,6 +401,10 @@ class HgEditor(svnwrap.Editor):
         br_path, branch = self.meta.split_branch_path(path)[:2]
         if br_path is not None:
             if not copyfrom_path and not br_path:
+                # This handles the case where a branch root is
+                # replaced without copy info.  It will show up as a
+                # deletion and then an add.
+                self.meta.closebranches.discard(branch)
                 self.current.emptybranches[branch] = True
             else:
                 self.current.emptybranches[branch] = False
@@ -570,13 +574,16 @@ class HgEditor(svnwrap.Editor):
 
                     msg += _TXDELT_WINDOW_HANDLER_FAILURE_MSG
                     e.args = (msg,) + others
-                    raise e
+
+                    # re-raising ensures that we show the full stack trace
+                    raise
 
                 # window being None means commit this file
                 if not window:
                     self._openfiles[file_baton] = (
                         path, target, isexec, islink, copypath)
             except svnwrap.SubversionException, e: # pragma: no cover
+                self.ui.traceback()
                 if e.args[1] == svnwrap.ERR_INCOMPLETE_DATA:
                     self.addmissing(path)
                 else: # pragma: no cover
--- a/hgsubversion/help/subversion.rst
+++ b/hgsubversion/help/subversion.rst
@@ -32,7 +32,12 @@ all its tags and branches. In such cases
 trunk, as in the example above. This is known as `standard layout`, and works
 with repositories that use the conventional ``trunk``, ``tags`` and ``branches``
 directories. By default, hgsubversion will use this layout whenever it finds any
-of these directories at the specified directory on the server.
+of these directories at the specified directory on the server.  Standard layout
+also supports alternate names for the ``branches`` directory and multiple tags
+locations.  Finally, Standard Layout supports selecting a subdirectory relative
+to ``trunk``, and each branch and tag dir.  This is useful if you have a single
+``trunk``, ``branches``, and ``tags`` with several projects inside, and you wish
+to import only a single project.
 
 If you instead want to clone just a single directory or branch, clone the
 specific directory path. In the example above, to get *only* trunk, you would
@@ -45,6 +50,10 @@ hgsubversion.layout option (see below fo
 
  $ hg clone --layout single svn+http://python-nose.googlecode.com/svn nose-hg
 
+Finally, if you want to clone two or more directores as separate
+branches, use the custom layout.  See the documentation below for the
+``hgsubversionbranch.*`` configuration for detailed help.
+
 Pulling new revisions into an already-converted repository is the same
 as from any other Mercurial source. Within the first example above,
 the following three commands are all equivalent::
@@ -296,6 +305,19 @@ settings:
     Path to a file for changing branch names during the conversion from
     Subversion to Mercurial.
 
+  ``hgsubversion.branchdir``
+
+    Specifies the subdirectory to look for branches under.  The
+    default is ``branches``.  This option has no effect for
+    single-directory clones.
+
+  ``hgsubversion.infix``
+
+    Specifies a path to strip between relative to the trunk/branch/tag
+    root as the mercurial root.  This can be used to import a single
+    sub-project when you have several sub-projects under a single
+    trunk/branches/tags layout in subversion.
+
   ``hgsubversion.filemap``
 
     Path to a file for filtering files during the conversion. Files may either
@@ -357,7 +379,9 @@ The following options only have an effec
     repository is converted into a single branch. The default,
     ``auto``, causes hgsubversion to assume a standard layout if any
     of trunk, branches, or tags exist within the specified directory
-    on the server.
+    on the server.  ``custom`` causes hgsubversion to read the
+    ``hgsubversionbranch`` config section to determine the repository
+    layout.
 
   ``hgsubversion.startrev``
 
@@ -389,6 +413,27 @@ The following options only have an effec
     you use this option, be sure to carefully check the result of a
     pull afterwards.
 
+    ``hgsubversionbranch.*``
+
+    Use this config section with the custom layout to specify a cusomt
+    mapping of subversion path to Mercurial branch.  This is useful if
+    your layout is substantially different from the standard
+    trunk/branches/tags layout and/or you are only interested in a few
+    branches.
+
+    Example config that pulls in trunk as the default branch,
+    personal/alice as the alice branch, and releases/2.0/2.7 as
+    release-2.7::
+
+        [hgsubversionbranch]
+            default = trunk
+            alice = personal/alice
+            release-2.7 = releases/2.0/2.7
+
+    Note that it is an error to specify more than one branch for a
+    given path, or to sepecify nested paths (e.g. releases/2.0 and
+    releases/2.0/2.7)
+
 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
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/__init__.py
@@ -0,0 +1,46 @@
+"""Code for dealing with subversion layouts
+
+This package is intended to encapsulate everything about subversion
+layouts.  This includes detecting the layout based on looking at
+subversion, mapping subversion paths to hg branches, and doing any
+other path translation necessary.
+
+NB: this has a long way to go before it does everything it claims to
+
+"""
+
+from mercurial import util as hgutil
+
+import custom
+import detect
+import persist
+import single
+import standard
+
+__all__ = [
+    "detect",
+    "layout_from_name",
+    "persist",
+    ]
+
+# This is the authoritative store of what layouts are available.
+# The intention is for extension authors who wish to build their own
+# layout to add it to this dict.
+NAME_TO_CLASS = {
+    "custom": custom.CustomLayout,
+    "single": single.SingleLayout,
+    "standard": standard.StandardLayout,
+}
+
+
+def layout_from_name(name, ui):
+    """Returns a layout module given the layout name
+
+    You should use one of the layout.detect.* functions to get the
+    name to pass to this function.
+
+    """
+
+    if name not in NAME_TO_CLASS:
+        raise hgutil.Abort('Unknown hgsubversion layout: %s' %name)
+    return NAME_TO_CLASS[name](ui)
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/base.py
@@ -0,0 +1,92 @@
+"""Module to hold the base API for layout classes.
+
+This module should not contain any implementation, just a definition
+of the API concrete layouts are expected to implement.
+
+"""
+
+from mercurial import util as hgutil
+
+class BaseLayout(object):
+
+    def __init__(self, ui):
+        self.ui = ui
+
+    def __unimplemented(self, method_name):
+        raise NotImplementedError(
+            "Incomplete layout implementation: %s.%s doesn't implement %s" %
+            (self.__module__, self.__name__, method_name))
+
+    def localname(self, path):
+        """Compute the local name for a branch located at path.
+
+        path should be relative to the repo url.
+
+        """
+        self.__unimplemented('localname')
+
+    def remotename(self, branch):
+        """Compute a subversion path for a mercurial branch name
+
+        This should return a path relative to the repo url
+
+        Implementations may indicate that no mapping is possible for
+        the given branch by raising a KeyError.
+
+        """
+        self.__unimplemented('remotename')
+
+    def remotepath(self, branch, subdir='/'):
+        """Compute a  subversion path for a mercurial branch name.
+
+        This should return an absolute path, assuming our repo root is at subdir
+        A false subdir shall be taken to mean /.
+
+        Implementations may indicate that no mapping is possible for
+        the given branch by raising a KeyError.
+
+        """
+        self.__unimplemented('remotepath')
+
+    def taglocations(self, meta_data_dir):
+        """Return a list of locations within svn to search for tags
+
+        Should be returned in reverse-sorted order.
+
+        """
+        self.__unimplemented('tagpaths')
+
+    def get_path_tag(self, path, taglocations):
+        """Get the tag name for the given svn path, if it is a possible tag.
+
+        This function should return None if the path cannot be a tag.
+        Returning a non-empty sring does not imply that the path is a
+        tag, only that it is a candidate to be a tag.  Returning an
+        empty string is an error.
+
+        Path should be relative to the repo url.
+        taglocations should be as returned by self.taglocations()
+
+        """
+        self.__unimplemented('get_path_tag')
+
+    def split_remote_name(self, path, known_branches):
+        """Split the path into a branch component and a local component.
+
+        path should be relative to our repo url
+
+        returns (branch_path, local_path)
+
+        branch_path should be suitable to pass into localname,
+        i.e. branch_path should NOT have a leading or trailing /
+
+        local_path should be relative to the root of the Mercurial working dir
+
+        Note that it is permissible to return a longer branch_path
+        than is passed in iff the path that is passed in is a parent
+        directory of exactly one branch.  This is intended to handle
+        the case where we are importing a particular subdirectory of
+        asubversion branch structure.
+
+        """
+        self.__unimplemented('split_remote_name')
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/custom.py
@@ -0,0 +1,105 @@
+"""Layout that allows you to define arbitrary subversion to mercurial mappings.
+
+This is the simplest layout to use if your layout is just plain weird.
+Also useful if your layout is pretty normal, but you personally only
+want a couple of branches.
+
+
+"""
+
+import base
+
+
+class CustomLayout(base.BaseLayout):
+
+    def __init__(self, ui):
+        base.BaseLayout.__init__(self, ui)
+
+        self.svn_to_hg = {}
+        self.hg_to_svn = {}
+
+        for hg_branch, svn_path in ui.configitems('hgsubversionbranch'):
+
+            hg_branch = hg_branch.strip()
+            if hg_branch == 'default' or not hg_branch:
+                hg_branch = None
+            svn_path = svn_path.strip('/')
+
+            for other_svn in self.svn_to_hg:
+                if other_svn == svn_path:
+                    msg = 'specified two hg branches for svn path %s: %s and %s'
+                    raise hgutil.Abort(msg % (svn_path, other_hg, hg_branch))
+
+                if (other_svn.startswith(svn_path + '/') or
+                    svn_path.startswith(other_svn + '/')):
+                    msg = 'specified mappings for nested svn paths: %s and %s'
+                    raise hgutl.Abort(msg % (svn_path, other_svn))
+
+            self.svn_to_hg[svn_path] = hg_branch
+            self.hg_to_svn[hg_branch] = svn_path
+
+    def localname(self, path):
+        if path in self.svn_to_hg:
+            return self.svn_to_hg[path]
+        children = []
+        for svn_path in self.svn_to_hg:
+            if svn_path.startswith(path + '/'):
+                children.append(svn_path)
+        if len(children) == 1:
+            return self.svn_to_hg[children[0]]
+
+        return '../%s' % path
+
+    def remotename(self, branch):
+        if branch =='default':
+            branch = None
+        if branch and branch.startswith('../'):
+            return branch[3:]
+        if branch not in self.hg_to_svn:
+            raise KeyError('Unknown mercurial branch: %s' % branch)
+        return self.hg_to_svn[branch]
+
+    def remotepath(self, branch, subdir='/'):
+        if not subdir.endswith('/'):
+            subdir += '/'
+        return subdir + self.remotename(branch)
+
+    def taglocations(self, meta_data_dir):
+        return []
+
+    def get_path_tag(self, path, taglocations):
+        return None
+
+    def split_remote_name(self, path, known_branches):
+        if path in self.svn_to_hg:
+            return path, ''
+        children = []
+        for svn_path in self.svn_to_hg:
+            if path.startswith(svn_path + '/'):
+                return svn_path, path[len(svn_path)+1:]
+            if svn_path.startswith(path + '/'):
+                children.append(svn_path)
+
+        # if the path represents the parent of exactly one of our svn
+        # branches, treat it as though it were that branch, because
+        # that means we are probably pulling in a subproject of an svn
+        # project, and someone copied the parent svn project.
+        if len(children) == 1:
+            return children[0], ''
+
+        for branch in known_branches:
+            if branch and branch.startswith('../'):
+                if path.startswith(branch[3:] + '/'):
+                    # -3 for the leading ../, plus one for the trailing /
+                    return branch[3:], path[len(branch) - 2:]
+                if branch[3:].startswith(path + '/'):
+                    children.append(branch[3:])
+
+        if len(children) == 1:
+            return children[0], ''
+
+
+        # this splits on the rightmost '/' but considers the entire
+        # string to be the branch component of the path if there is no '/'
+        components = path.rsplit('/', 1)
+        return components[0], '/'.join(components[1:])
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/detect.py
@@ -0,0 +1,111 @@
+""" Layout detection for subversion repos.
+
+Figure out what layout we should be using, based on config, command
+line flags, subversion contents, and anything else we decide to base
+it on.
+
+"""
+
+import os.path
+
+from mercurial import util as hgutil
+
+import __init__ as layouts
+
+def layout_from_subversion(svn, revision=None, ui=None):
+    """ Guess what layout to use based on directories under the svn root.
+
+    This is intended for use during bootstrapping.  It guesses which
+    layout to use based on the presence or absence of the conventional
+    trunk, branches, tags dirs immediately under the path your are
+    cloning.
+
+    Additionally, this will write the layout in use to the ui object
+    passed, if any.
+
+    """
+    # import late to avoid trouble when running the test suite
+    try:
+        from hgext_hgsubversion import svnwrap
+    except ImportError:
+        from hgsubversion import svnwrap
+
+    try:
+        rootlist = svn.list_dir('', revision=revision)
+    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'
+    ui.setconfig('hgsubversion', 'layout', layout)
+    return layout
+
+def layout_from_config(ui, allow_auto=False):
+    """ Load the layout we are using based on config
+
+    We will read the config from the ui object.  Pass allow_auto=True
+    if you are doing bootstrapping and can detect the layout in
+    another manner if you get auto.  Otherwise, we will abort if we
+    detect the layout as auto.
+    """
+
+    layout = ui.config('hgsubversion', 'layout', default='auto')
+    if layout == 'auto' and not allow_auto:
+        raise hgutil.Abort('layout not yet determined')
+    elif layout not in layouts.NAME_TO_CLASS and layout != 'auto':
+        raise hgutil.Abort("unknown layout '%s'" % layout)
+    return layout
+
+def layout_from_file(meta_data_dir, ui=None):
+    """ Load the layout in use from the metadata file.
+
+    If you pass the ui arg, we will also write the layout to the
+    config for that ui.
+
+    """
+
+    layout = None
+    layoutfile = os.path.join(meta_data_dir, 'layout')
+    if os.path.exists(layoutfile):
+        f = open(layoutfile)
+        layout = f.read().strip()
+        f.close()
+        if ui:
+            ui.setconfig('hgsubversion', 'layout', layout)
+    return layout
+
+def layout_from_commit(subdir, revpath, branch, ui):
+    """ Guess what the layout is based existing commit info
+
+    Specifically, this compares the subdir for the repository and the
+    revpath as extracted from the convinfo in the commit.  If they
+    match, the layout is assumed to be single.  Otherwise, it tries
+    the available layouts and selects the first one that would
+    translate the given branch to the given revpath.
+
+    """
+
+    subdir = subdir or '/'
+    if subdir == revpath:
+        return 'single'
+
+    candidates = set()
+    for layout in layouts.NAME_TO_CLASS:
+        layoutobj = layouts.layout_from_name(layout, ui)
+        try:
+            remotepath = layoutobj.remotepath(branch, subdir)
+        except KeyError:
+            continue
+        if  remotepath == revpath:
+            candidates.add(layout)
+
+    if len(candidates) == 1:
+        return candidates.pop()
+    elif candidates:
+        config_layout = layout_from_config(ui, allow_auto=True)
+        if config_layout in candidates:
+            return config_layout
+
+    return 'standard'
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/persist.py
@@ -0,0 +1,16 @@
+"""Code for persisting the layout config in various locations.
+
+Basically, if you want to save the layout, this is where you should go
+to do it.
+
+"""
+
+import os.path
+
+def layout_to_file(meta_data_dir, layout):
+    """Save the given layout to a file under the given meta_data_dir"""
+
+    layoutfile = os.path.join(meta_data_dir, 'layout')
+    f = open(layoutfile, 'w')
+    f.write(layout)
+    f.close()
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/single.py
@@ -0,0 +1,24 @@
+
+
+import base
+
+class SingleLayout(base.BaseLayout):
+    """A layout with only the default branch"""
+
+    def localname(self, path):
+        return None
+
+    def remotename(self, branch):
+        return ''
+
+    def remotepath(self, branch, subdir='/'):
+        return subdir or '/'
+
+    def taglocations(self, meta_data_dir):
+        return []
+
+    def get_path_tag(self, path, taglocations):
+        return None
+
+    def split_remote_name(self, path, known_branches):
+        return '', path
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/standard.py
@@ -0,0 +1,130 @@
+import os.path
+import pickle
+
+import base
+
+class StandardLayout(base.BaseLayout):
+    """The standard trunk, branches, tags layout"""
+
+    def __init__(self, ui):
+        base.BaseLayout.__init__(self, ui)
+
+        self._tag_locations = None
+
+        self._branch_dir = ui.config('hgsubversion', 'branchdir', 'branches')
+        if self._branch_dir[0] == '/':
+            self._branch_dir = self._branch_dir[1:]
+        if self._branch_dir[-1] != '/':
+            self._branch_dir += '/'
+
+        self._infix = ui.config('hgsubversion', 'infix', '').strip('/')
+        if self._infix:
+            self._infix = '/' + self._infix
+
+        self._trunk = 'trunk%s' % self._infix
+
+    def localname(self, path):
+        if path == self._trunk:
+            return None
+        elif path.startswith(self._branch_dir) and path.endswith(self._infix):
+            path = path[len(self._branch_dir):]
+            if self._infix:
+                path = path[:-len(self._infix)]
+            return path
+        return  '../%s' % path
+
+    def remotename(self, branch):
+        if branch == 'default' or branch is None:
+            path = self._trunk
+        elif branch.startswith('../'):
+            path =  branch[3:]
+        else:
+            path = ''.join((self._branch_dir, branch, self._infix))
+
+        return path
+
+    def remotepath(self, branch, subdir='/'):
+        if subdir == '/':
+            subdir = ''
+        branchpath = self._trunk
+        if branch and branch != 'default':
+            if branch.startswith('../'):
+                branchpath = branch[3:]
+            else:
+                branchpath = ''.join((self._branch_dir, branch, self._infix))
+
+        return '%s/%s' % (subdir or '', branchpath)
+
+    def taglocations(self, meta_data_dir):
+        # import late to avoid trouble when running the test suite
+        from hgext_hgsubversion import util
+
+        if self._tag_locations is None:
+
+            tag_locations_file = os.path.join(meta_data_dir, 'tag_locations')
+
+            if os.path.exists(tag_locations_file):
+                f = open(tag_locations_file)
+                self._tag_locations = pickle.load(f)
+                f.close()
+            else:
+                self._tag_locations = self.ui.configlist('hgsubversion',
+                                                        'tagpaths',
+                                                        ['tags'])
+            util.pickle_atomic(self._tag_locations, tag_locations_file)
+
+            # ensure nested paths are handled properly
+            self._tag_locations.sort()
+            self._tag_locations.reverse()
+
+        return self._tag_locations
+
+    def get_path_tag(self, path, taglocations):
+        for tagspath in taglocations:
+            if path.startswith(tagspath + '/'):
+                    tag = path[len(tagspath) + 1:]
+                    if tag:
+                        return tag
+        return None
+
+    def split_remote_name(self, path, known_branches):
+
+        # this odd evolution is how we deal with people doing things like
+        # creating brances (note the typo), committing to a branch under it,
+        # and then moving it to branches
+
+        # we need to find the ../foo branch names, if they exist, before
+        # trying to create a normally-named branch
+
+        components = path.split('/')
+        candidate = ''
+        while self.localname(candidate) not in known_branches and components:
+            if not candidate:
+                candidate = components.pop(0)
+            else:
+                candidate += '/'
+                candidate += components.pop(0)
+        if self.localname(candidate) in known_branches:
+            return candidate, '/'.join(components)
+
+        if path == 'trunk' or path.startswith('trunk/'):
+            return self._trunk, path[len(self._trunk) + 1:]
+
+        if path.startswith(self._branch_dir):
+            path = path[len(self._branch_dir):]
+            components = path.split('/', 1)
+            branch_path = ''.join((self._branch_dir, components[0]))
+            if len(components) == 1:
+                local_path = ''
+            else:
+                local_path = components[1]
+
+            if local_path == '':
+                branch_path += self._infix
+            elif local_path.startswith(self._infix[1:] + '/'):
+                branch_path += self._infix
+                local_path = local_path[len(self._infix):]
+            return branch_path, local_path
+
+        components = path.split('/')
+        return '/'.join(components[:-1]), components[-1]
--- a/hgsubversion/maps.py
+++ b/hgsubversion/maps.py
@@ -14,6 +14,9 @@ class AuthorMap(dict):
 
     If the 'hgsubversion.defaultauthors' configuration option is set to false,
     attempting to obtain an unknown author will fail with an Abort.
+    
+    If the 'hgsubversion.caseignoreauthors' configuration option is set to true,
+    the userid from Subversion is always compared lowercase.
     '''
 
     def __init__(self, ui, path, defaulthost=None):
@@ -26,6 +29,8 @@ class AuthorMap(dict):
         '''
         self.ui = ui
         self.path = path
+        self.use_defaultauthors = self.ui.configbool('hgsubversion', 'defaultauthors', True)
+        self.caseignoreauthors = self.ui.configbool('hgsubversion', 'caseignoreauthors', False)
         if defaulthost:
             self.defaulthost = '@%s' % defaulthost.lstrip('@')
         else:
@@ -45,7 +50,7 @@ class AuthorMap(dict):
         if path != self.path:
             writing = open(self.path, 'a')
 
-        self.ui.note('reading authormap from %s\n' % path)
+        self.ui.debug('reading authormap from %s\n' % path)
         f = open(path, 'r')
         for number, line_org in enumerate(f):
 
@@ -63,6 +68,9 @@ class AuthorMap(dict):
             src = src.strip()
             dst = dst.strip()
 
+            if self.caseignoreauthors:
+                src = src.lower()
+
             if writing:
                 if not src in self:
                     self.ui.debug('adding author %s to author map\n' % src)
@@ -83,12 +91,18 @@ class AuthorMap(dict):
         as well as the backing store. '''
         if author is None:
             author = '(no author)'
-        if author in self:
-            result = self.super.__getitem__(author)
-        elif self.ui.configbool('hgsubversion', 'defaultauthors', True):
+
+        if self.caseignoreauthors:
+            search_author = author.lower()
+        else:
+            search_author = author
+
+        if search_author in self:
+            result = self.super.__getitem__(search_author)
+        elif self.use_defaultauthors:
             self[author] = result = '%s%s' % (author, self.defaulthost)
             msg = 'substituting author "%s" for default "%s"\n'
-            self.ui.note(msg % (author, result))
+            self.ui.debug(msg % (author, result))
         else:
             msg = 'author %s has no entry in the author map!'
             raise hgutil.Abort(msg % author)
@@ -333,7 +347,7 @@ class FileMap(object):
             f.close()
 
     def load(self, fn):
-        self.ui.note('reading file map from %s\n' % fn)
+        self.ui.debug('reading file map from %s\n' % fn)
         f = open(fn, 'r')
         self.load_fd(f, fn)
         f.close()
@@ -355,7 +369,7 @@ class FileMap(object):
                 self.ui.warn(msg % (fn, line.rstrip()))
 
     def _load(self):
-        self.ui.note('reading in-repo file map from %s\n' % self.path)
+        self.ui.debug('reading in-repo file map from %s\n' % self.path)
         f = open(self.path)
         ver = int(f.readline())
         if ver != self.VERSION:
@@ -394,7 +408,7 @@ class BranchMap(dict):
         if path != self.path:
             writing = open(self.path, 'a')
 
-        self.ui.note('reading branchmap from %s\n' % path)
+        self.ui.debug('reading branchmap from %s\n' % path)
         f = open(path, 'r')
         for number, line in enumerate(f):
 
@@ -456,7 +470,7 @@ class TagMap(dict):
         if path != self.path:
             writing = open(self.path, 'a')
 
-        self.ui.note('reading tag renames from %s\n' % path)
+        self.ui.debug('reading tag renames from %s\n' % path)
         f = open(path, 'r')
         for number, line in enumerate(f):
 
--- a/hgsubversion/pushmod.py
+++ b/hgsubversion/pushmod.py
@@ -99,12 +99,7 @@ def commit(ui, repo, rev_ctx, meta, base
     file_data = {}
     parent = rev_ctx.parents()[0]
     parent_branch = rev_ctx.parents()[0].branch()
-    branch_path = 'trunk'
-
-    if meta.layout == 'single':
-        branch_path = ''
-    elif parent_branch and parent_branch != 'default':
-        branch_path = 'branches/%s' % parent_branch
+    branch_path = meta.layoutobj.remotename(parent_branch)
 
     extchanges = svnexternals.diff(svnexternals.parse(ui, parent),
                                    svnexternals.parse(ui, rev_ctx))
@@ -139,7 +134,7 @@ def commit(ui, repo, rev_ctx, meta, base
                     copies[file] = renamed[0]
                     base_data = parent[renamed[0]].data()
                 else:
-                    autoprops = svn.autoprops_config.properties(file) 
+                    autoprops = svn.autoprops_config.properties(file)
                     if autoprops:
                         props.setdefault(file, {}).update(autoprops)
 
@@ -201,16 +196,20 @@ def commit(ui, repo, rev_ctx, meta, base
     if not new_target_files:
         raise NoFilesException()
     try:
-        svn.commit(new_target_files, rev_ctx.description(), file_data,
-                   base_revision, set(addeddirs), set(deleteddirs),
-                   props, newcopies)
+        return svn.commit(new_target_files, rev_ctx.description(), file_data,
+                          base_revision, set(addeddirs), set(deleteddirs),
+                          props, newcopies)
     except svnwrap.SubversionException, e:
+        ui.traceback()
+
         if len(e.args) > 0 and e.args[1] in (svnwrap.ERR_FS_TXN_OUT_OF_DATE,
-                                             svnwrap.ERR_FS_CONFLICT):
+                                             svnwrap.ERR_FS_CONFLICT,
+                                             svnwrap.ERR_FS_ALREADY_EXISTS):
             raise hgutil.Abort('Outgoing changesets parent is not at '
                                'subversion HEAD\n'
                                '(pull again and rebase on a newer revision)')
+        elif len(e.args) > 0 and e.args[1] == svnwrap.ERR_REPOS_HOOK_FAILURE:
+            # Special handling for svn hooks blocking error
+            raise hgutil.Abort(e.args[0])
         else:
             raise
-
-    return True
--- a/hgsubversion/replay.py
+++ b/hgsubversion/replay.py
@@ -6,6 +6,7 @@ from mercurial import node
 from mercurial import context
 from mercurial import util as hgutil
 
+import compathacks
 import svnexternals
 import util
 
@@ -51,16 +52,6 @@ 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)
@@ -107,7 +98,8 @@ def _convert_rev(ui, meta, svn, r, tbdel
         p, b = meta.split_branch_path(f)[:2]
         if b not in branch_batches:
             branch_batches[b] = []
-        branch_batches[b].append((p, f))
+        if p:
+            branch_batches[b].append((p, f))
 
     closebranches = {}
     for branch in tbdelta['branches'][1]:
@@ -142,7 +134,7 @@ def _convert_rev(ui, meta, svn, r, tbdel
             tag = meta.get_path_tag(meta.remotename(branch))
             if (tag and tag not in meta.tags
                 and branch not in meta.branches
-                and branch not in meta.repo.branchtags()
+                and branch not in compathacks.branchset(meta.repo)
                 and not files):
                 continue
 
@@ -177,11 +169,10 @@ def _convert_rev(ui, meta, svn, r, tbdel
                                       islink=islink, isexec=isexec,
                                       copied=copied)
 
-        message = _safe_message(rev.message)
         meta.mapbranch(extra)
         current_ctx = context.memctx(meta.repo,
                                      parents,
-                                     message or util.default_commit_msg(ui),
+                                     util.getmessage(ui, rev),
                                      files.keys(),
                                      filectxfn,
                                      meta.authors[rev.author],
@@ -204,21 +195,22 @@ def _convert_rev(ui, meta, svn, r, tbdel
             continue
 
         parent_ctx = meta.repo.changectx(ha)
+        files = []
         def del_all_files(*args):
             raise IOError(errno.ENOENT, 'deleting all files')
 
-        # True here meant nuke all files, shouldn't happen with branch closing
-        if current.emptybranches[branch]: # pragma: no cover
-            raise hgutil.Abort('Empty commit to an open branch attempted. '
-                               'Please report this issue.')
+        # True here means nuke all files.  This happens when you
+        # replace a branch root with an empty directory
+        if current.emptybranches[branch]:
+            files = meta.repo[ha].files()
 
         extra = meta.genextra(rev.revnum, branch)
         meta.mapbranch(extra)
 
         current_ctx = context.memctx(meta.repo,
                                      (ha, node.nullid),
-                                     _safe_message(rev.message) or ' ',
-                                     [],
+                                     util.getmessage(ui, rev),
+                                     files,
                                      del_all_files,
                                      meta.authors[rev.author],
                                      date,
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -8,6 +8,7 @@ from mercurial import patch
 from mercurial import revlog
 from mercurial import util as hgutil
 
+import compathacks
 import svnwrap
 import svnexternals
 import util
@@ -606,12 +607,18 @@ def checkbranch(meta, r, branch):
             return None
     return branchtip
 
-def branches_in_paths(meta, tbdelta, paths, revnum, checkpath, listdir):
+def branches_in_paths(meta, tbdelta, paths, revnum, checkpath, listdir,
+                      firstrun):
     '''Given a list of paths, return mapping of all branches touched
     to their branch path.
     '''
     branches = {}
-    paths_need_discovery = []
+    if firstrun:
+        paths_need_discovery = [p for (p, t) in listdir('', revnum)
+                                if t == 'f']
+    else:
+        paths_need_discovery = []
+
     for p in paths:
         relpath, branch, branchpath = meta.split_branch_path(p)
         if relpath is not None:
@@ -628,50 +635,27 @@ def branches_in_paths(meta, tbdelta, pat
     if not paths_need_discovery:
         return branches
 
-    paths_need_discovery = [(len(p), p) for p in paths_need_discovery]
-    paths_need_discovery.sort()
-    paths_need_discovery = [p[1] for p in paths_need_discovery]
     actually_files = []
     while paths_need_discovery:
         p = paths_need_discovery.pop(0)
-        path_could_be_file = True
-        ind = 0
-        while ind < len(paths_need_discovery) and not paths_need_discovery:
-            if op.startswith(p):
-                path_could_be_file = False
-            ind += 1
-        if path_could_be_file:
-            if checkpath(p, revnum) == 'f':
-                actually_files.append(p)
-            # if there's a copyfrom_path and there were files inside that copyfrom,
-            # we need to detect those branches. It's a little thorny and slow, but
-            # seems to be the best option.
-            elif paths[p].copyfrom_path and not p.startswith('tags/'):
-                paths_need_discovery.extend(['%s/%s' % (p, x[0])
-                                             for x in listdir(p, revnum)
-                                             if x[1] == 'f'])
-
-        if not actually_files:
+        if checkpath(p, revnum) == 'f':
+            actually_files.append(p)
+        # if there's a copyfrom_path and there were files inside that copyfrom,
+        # we need to detect those branches. It's a little thorny and slow, but
+        # seems to be the best option.
+        elif paths[p].copyfrom_path and not meta.get_path_tag(p):
+            paths_need_discovery.extend(['%s/%s' % (p, x[0])
+                                         for x in listdir(p, revnum)
+                                         if x[1] == 'f'])
+
+    for path in actually_files:
+        if meta.get_path_tag(path):
+            continue
+        fpath, branch, bpath = meta.split_branch_path(path, existing=False)
+        if bpath is None:
             continue
+        branches[branch] = bpath
 
-        filepaths = [p.split('/') for p in actually_files]
-        filepaths = [(len(p), p) for p in filepaths]
-        filepaths.sort()
-        filepaths = [p[1] for p in filepaths]
-        while filepaths:
-            path = filepaths.pop(0)
-            parentdir = '/'.join(path[:-1])
-            filepaths = [p for p in filepaths if not '/'.join(p).startswith(parentdir)]
-            branchpath = meta.normalize(parentdir)
-            if branchpath.startswith('tags/'):
-                continue
-            branchname = meta.localname(branchpath)
-            if branchpath.startswith('trunk/'):
-                branches[meta.localname('trunk')] = 'trunk'
-                continue
-            if branchname and branchname.startswith('../'):
-                continue
-            branches[branchname] = branchpath
     return branches
 
 def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
@@ -681,7 +665,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
         raise hgutil.Abort('filemaps currently unsupported with stupid replay.')
 
     branches = branches_in_paths(meta, tbdelta, r.paths, r.revnum,
-                                 svn.checkpath, svn.list_files)
+                                 svn.checkpath, svn.list_files, firstrun)
     brpaths = branches.values()
     bad_branch_paths = {}
     for br, bp in branches.iteritems():
@@ -722,9 +706,9 @@ def convert_rev(ui, meta, svn, r, tbdelt
                     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
+            closed = checkbranch(meta, r, branch)
+            if closed is not None:
+                deleted_branches[branch] = closed
 
     date = meta.fixdate(r.date)
     check_deleted_branches = set(tbdelta['branches'][1])
@@ -745,7 +729,8 @@ def convert_rev(ui, meta, svn, r, tbdelt
         # it, or we can force the existing fetch_branchrev() path. Do
         # the latter for now.
         incremental = (meta.revmap.oldest > 0 and
-                       parentctx.rev() != node.nullrev)
+                       parentctx.rev() != node.nullrev and
+                       not firstrun)
 
         if incremental:
             try:
@@ -789,7 +774,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
             # svnmeta.committag(), we can skip the whole branch for now
             if (tag and tag not in meta.tags and
                 b not in meta.branches
-                and b not in meta.repo.branchtags()
+                and b not in compathacks.branchset(meta.repo)
                 and not files_touched):
                 continue
 
@@ -814,7 +799,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
         meta.mapbranch(extra)
         current_ctx = context.memctx(meta.repo,
                                      [parentctx.node(), revlog.nullid],
-                                     r.message or util.default_commit_msg(ui),
+                                     util.getmessage(ui, r),
                                      files_touched,
                                      filectxfn,
                                      meta.authors[r.author],
--- a/hgsubversion/svncommands.py
+++ b/hgsubversion/svncommands.py
@@ -4,6 +4,7 @@ import cPickle as pickle
 import sys
 import traceback
 import urlparse
+import errno
 
 from mercurial import commands
 from mercurial import hg
@@ -11,6 +12,7 @@ from mercurial import node
 from mercurial import util as hgutil
 from mercurial import error
 
+import layouts
 import maps
 import svnwrap
 import svnrepo
@@ -37,6 +39,22 @@ def rebuildmeta(ui, repo, args, unsafe_s
     return _buildmeta(ui, repo, args, partial=False,
                       skipuuid=unsafe_skip_uuid_check)
 
+def read_if_exists(path):
+     try:
+        fp = open(path, 'rb')
+        d = fp.read()
+        fp.close()
+        return d
+     except IOError, err:
+         if err.errno != errno.ENOENT:
+             raise
+
+def write_if_needed(path, content):
+    if read_if_exists(path) != content:
+        fp = open(path, 'wb')
+        fp.write(content)
+        fp.close()
+
 def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
 
     if repo is None:
@@ -44,18 +62,27 @@ def _buildmeta(ui, repo, args, partial=F
                               " here (.hg not found)")
 
     dest = None
+    validateuuid = False
     if len(args) == 1:
         dest = args[0]
+        validateuuid = True
     elif len(args) > 1:
         raise hgutil.Abort('rebuildmeta takes 1 or no arguments')
-    uuid = None
     url = repo.ui.expandpath(dest or repo.ui.config('paths', 'default-push') or
                              repo.ui.config('paths', 'default') or '')
-    svn = svnrepo.svnremoterepo(ui, url).svn
-    subdir = svn.subdir
     svnmetadir = os.path.join(repo.path, 'svn')
     if not os.path.exists(svnmetadir):
         os.makedirs(svnmetadir)
+    uuidpath = os.path.join(svnmetadir, 'uuid')
+    uuid = read_if_exists(uuidpath)
+
+    subdirpath = os.path.join(svnmetadir, 'subdir')
+    subdir = read_if_exists(subdirpath)
+    svn = None
+    if subdir is None:
+        svn = svnrepo.svnremoterepo(ui, url).svn
+        subdir = svn.subdir
+        open(subdirpath, 'wb').write(subdir.strip('/'))
 
     youngest = 0
     startrev = 0
@@ -80,9 +107,9 @@ def _buildmeta(ui, repo, args, partial=F
         except IOError, err:
             if err.errno != errno.ENOENT:
                 raise
-            ui.status('missing some metadata -- doing a full rebuild')
+            ui.status('missing some metadata -- doing a full rebuild\n')
         except AttributeError:
-            ui.status('no metadata available -- doing a full rebuild')
+            ui.status('no metadata available -- doing a full rebuild\n')
 
 
     lastpulled = open(os.path.join(svnmetadir, 'lastpulled'), 'wb')
@@ -96,22 +123,19 @@ def _buildmeta(ui, repo, args, partial=F
     tags = maps.Tags(repo)
 
     layout = None
+    layoutobj = None
 
     skipped = set()
     closed = set()
 
     numrevs = len(repo) - startrev
 
-    subdirfile = open(os.path.join(svnmetadir, 'subdir'), 'w')
-    subdirfile.write(subdir.strip('/'))
-    subdirfile.close()
-
     # ctx.children() visits all revisions in the repository after ctx. Calling
     # 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
     for rev in xrange(startrev, len(repo)):
-        util.progress(ui, 'prepare', rev - startrev, total=numrevs)
+        ui.progress('prepare', rev - startrev, total=numrevs)
         try:
             ctx = repo[rev]
         except error.RepoError:
@@ -141,10 +165,10 @@ def _buildmeta(ui, repo, args, partial=F
                 closed.add(parentctx.rev())
 
     lastpulled.write(str(youngest) + '\n')
-    util.progress(ui, 'prepare', None, total=numrevs)
+    ui.progress('prepare', None, total=numrevs)
 
     for rev in xrange(startrev, len(repo)):
-        util.progress(ui, 'rebuild', rev-startrev, total=numrevs)
+        ui.progress('rebuild', rev-startrev, total=numrevs)
         try:
             ctx = repo[rev]
         except error.RepoError:
@@ -186,27 +210,27 @@ def _buildmeta(ui, repo, args, partial=F
                                             'right location in the repo.')
 
         if layout is None:
-            if (subdir or '/') == revpath:
-                layout = 'single'
-            else:
-                layout = 'standard'
-            f = open(os.path.join(svnmetadir, 'layout'), 'w')
-            f.write(layout)
-            f.close()
+            layout = layouts.detect.layout_from_commit(subdir, revpath,
+                                                       ctx.branch(), ui)
+            existing_layout = layouts.detect.layout_from_file(svnmetadir)
+            if layout != existing_layout:
+                layouts.persist.layout_to_file(svnmetadir, layout)
+            layoutobj = layouts.layout_from_name(layout, ui)
         elif layout == 'single':
             assert (subdir or '/') == revpath, ('Possible layout detection'
                                                 ' defect in replay')
 
         # write repository uuid if required
-        if uuid is None:
+        if uuid is None or validateuuid:
+            validateuuid = False
             uuid = convinfo[4:40]
             if not skipuuid:
+                if svn is None:
+                    svn = svnrepo.svnremoterepo(ui, url).svn
                 if uuid != svn.uuid:
                     raise hgutil.Abort('remote svn repository identifier '
                                        'does not match')
-            uuidfile = open(os.path.join(svnmetadir, 'uuid'), 'w')
-            uuidfile.write(svn.uuid)
-            uuidfile.close()
+            write_if_needed(uuidpath, uuid)
 
         # don't reflect closed branches
         if (ctx.extra().get('close') and not ctx.files() or
@@ -216,29 +240,25 @@ def _buildmeta(ui, repo, args, partial=F
 
         # find commitpath, write to revmap
         commitpath = revpath[len(subdir)+1:]
-        if layout == 'standard':
-            if commitpath.startswith('branches/'):
-                commitpath = commitpath[len('branches/'):]
-            elif commitpath == 'trunk':
-                commitpath = ''
-            else:
-                if commitpath.startswith('tags/') and ctx.extra().get('close'):
-                    continue
-                commitpath = '../' + commitpath
-        else:
-            commitpath = ''
-        revmap.write('%s %s %s\n' % (revision, ctx.hex(), commitpath))
+
+        tag_locations = layoutobj.taglocations(svnmetadir)
+        found_tag = False
+        for location in tag_locations:
+            if commitpath.startswith(location + '/'):
+                found_tag = True
+                break
+        if found_tag and ctx.extra().get('close'):
+            continue
+
+        branch = layoutobj.localname(commitpath)
+        revmap.write('%s %s %s\n' % (revision, ctx.hex(), branch or ''))
 
         revision = int(revision)
         if revision > last_rev:
             last_rev = revision
 
         # deal with branches
-        if not commitpath:
-            branch = None
-        elif not commitpath.startswith('../'):
-            branch = commitpath
-        elif ctx.parents()[0].node() != node.nullid:
+        if branch and branch.startswith('../'):
             parent = ctx
             while parent.node() != node.nullid:
                 parentextra = parent.extra()
@@ -248,17 +268,16 @@ def _buildmeta(ui, repo, args, partial=F
 
                 parentpath = parentinfo[40:].split('@')[0][len(subdir) + 1:]
 
-                if parentpath.startswith('tags/') and parentextra.get('close'):
+                found_tag = False
+                for location in tag_locations:
+                    if parentpath.startswith(location + '/'):
+                        found_tag = True
+                        break
+                if found_tag and parentextra.get('close'):
                     continue
-                elif parentpath.startswith('branches/'):
-                    branch = parentpath[len('branches/'):]
-                elif parentpath == 'trunk':
-                    branch = None
-                else:
-                    branch = '../' + parentpath
+
+                branch = layoutobj.localname(parentpath)
                 break
-        else:
-            branch = commitpath
 
         if rev in closed:
             # a direct child of this changeset closes the branch; drop it
@@ -282,7 +301,7 @@ def _buildmeta(ui, repo, args, partial=F
                                   int(parentrev),
                                   revision)
 
-    util.progress(ui, 'rebuild', None, total=numrevs)
+    ui.progress('rebuild', None, total=numrevs)
 
     # save off branch info
     branchinfofile = open(os.path.join(svnmetadir, 'branch_info'), 'w')
@@ -363,10 +382,9 @@ def genignore(ui, repo, force=False, **o
     hashes = meta.revmap.hashes()
     parent = util.parentrev(ui, repo, meta, hashes)
     r, br = hashes[parent.node()]
-    if meta.layout == 'single':
-        branchpath = ''
-    else:
-        branchpath = br and ('branches/%s/' % br) or 'trunk/'
+    branchpath = meta.layoutobj.remotename(br)
+    if branchpath:
+        branchpath += '/'
     ignorelines = ['.hgignore', 'syntax:glob']
     dirs = [''] + [d[0] for d in svn.list_files(branchpath, r)
                    if d[1] == 'd']
@@ -403,17 +421,8 @@ def info(ui, repo, **opts):
         return 0
     r, br = hashes[pn]
     subdir = util.getsvnrev(parent)[40:].split('@')[0]
-    if meta.layout == 'single':
-        branchpath = ''
-    elif br == None:
-        branchpath = '/trunk'
-    elif br.startswith('../'):
-        branchpath = '/%s' % br[3:]
-        subdir = subdir.replace('branches/../', '')
-    else:
-        branchpath = '/branches/%s' % br
     remoterepo = svnrepo.svnremoterepo(repo.ui)
-    url = '%s%s' % (remoterepo.svnurl, branchpath)
+    url = meta.layoutobj.remotepath(br, remoterepo.svnurl)
     author = meta.authors.reverselookup(parent.user())
     # cleverly figure out repo root w/o actually contacting the server
     reporoot = url[:len(url)-len(subdir)]
--- a/hgsubversion/svnexternals.py
+++ b/hgsubversion/svnexternals.py
@@ -16,9 +16,13 @@ passpegrev = True # see svnsubrepo below
 try:
     canonpath = hgutil.canonpath
 except (ImportError, AttributeError):
-    from mercurial import scmutil
-    canonpath = scmutil.canonpath
     passpegrev = False
+    try:
+        from mercurial import scmutil
+        canonpath = scmutil.canonpath
+    except (ImportError, AttributeError):
+        from mercurial import pathutil
+        canonpath = pathutil.canonpath
 
 import util
 
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -10,25 +10,10 @@ from mercurial import node
 
 import util
 import maps
+import layouts
 import editor
 
 
-def pickle_atomic(data, file_path):
-    """pickle some data to a path atomically.
-
-    This is present because I kept corrupting my revmap by managing to hit ^C
-    during the pickle of that file.
-    """
-    f = hgutil.atomictempfile(file_path, 'w+b', 0644)
-    pickle.dump(data, f)
-    # Older versions of hg have .rename() instead of .close on
-    # atomictempfile.
-    if getattr(hgutil.atomictempfile, 'rename', False):
-        f.rename()
-    else:
-        f.close()
-
-
 class SVNMeta(object):
 
     def __init__(self, repo, uuid=None, subdir=None):
@@ -49,7 +34,6 @@ class SVNMeta(object):
 
         author_host = self.ui.config('hgsubversion', 'defaulthost', uuid)
         authors = util.configpath(self.ui, 'authormap')
-        tag_locations = self.ui.configlist('hgsubversion', 'tagpaths', ['tags'])
         self.usebranchnames = self.ui.configbool('hgsubversion',
                                                  'usebranchnames', True)
         branchmap = util.configpath(self.ui, 'branchmap')
@@ -63,23 +47,9 @@ class SVNMeta(object):
             f.close()
         self.prevbranches = dict(self.branches)
         self.tags = maps.Tags(repo)
-        if os.path.exists(self.tag_locations_file):
-            f = open(self.tag_locations_file)
-            self.tag_locations = pickle.load(f)
-            f.close()
-        else:
-            self.tag_locations = tag_locations
-        if os.path.exists(self.layoutfile):
-            f = open(self.layoutfile)
-            self._layout = f.read().strip()
-            f.close()
-            self.repo.ui.setconfig('hgsubversion', 'layout', self._layout)
-        else:
-            self._layout = None
-        pickle_atomic(self.tag_locations, self.tag_locations_file)
-        # ensure nested paths are handled properly
-        self.tag_locations.sort()
-        self.tag_locations.reverse()
+        self._layout = layouts.detect.layout_from_file(self.meta_data_dir,
+                                                       ui=self.repo.ui)
+        self._layoutobj = None
 
         self.authors = maps.AuthorMap(self.ui, self.authors_file,
                                  defaulthost=author_host)
@@ -107,15 +77,16 @@ class SVNMeta(object):
         # resolved into something other than auto before this ever
         # gets called
         if not self._layout or self._layout == 'auto':
-            lo = self.repo.ui.config('hgsubversion', 'layout', default='auto')
-            if lo == 'auto':
-                raise hgutil.Abort('layout not yet determined')
-            self._layout = lo
-            f = open(self.layoutfile, 'w')
-            f.write(self._layout)
-            f.close()
+            self._layout = layouts.detect.layout_from_config(self.repo.ui)
+            layouts.persist.layout_to_file(self.meta_data_dir, self._layout)
         return self._layout
 
+    @property
+    def layoutobj(self):
+        if not self._layoutobj:
+            self._layoutobj = layouts.layout_from_name(self.layout, self.ui)
+        return self._layoutobj
+
     @property
     def editor(self):
         if not hasattr(self, '_editor'):
@@ -185,10 +156,6 @@ class SVNMeta(object):
     def branch_info_file(self):
         return os.path.join(self.meta_data_dir, 'branch_info')
 
-    @property
-    def tag_locations_file(self):
-        return os.path.join(self.meta_data_dir, 'tag_locations')
-
     @property
     def authors_file(self):
         return os.path.join(self.meta_data_dir, 'authors')
@@ -206,10 +173,6 @@ class SVNMeta(object):
         # called tag-renames for backwards compatibility
         return os.path.join(self.meta_data_dir, 'tag-renames')
 
-    @property
-    def layoutfile(self):
-        return os.path.join(self.meta_data_dir, 'layout')
-
     def fixdate(self, date):
         if date is not None:
             date = date.replace('T', ' ').replace('Z', '').split('.')[0]
@@ -223,27 +186,15 @@ class SVNMeta(object):
         '''Save the Subversion metadata. This should really be called after
         every revision is created.
         '''
-        pickle_atomic(self.branches, self.branch_info_file)
+        util.pickle_atomic(self.branches, self.branch_info_file)
 
     def localname(self, path):
         """Compute the local name for a branch located at path.
         """
-        if self.layout == 'single':
-            return 'default'
-        if path == 'trunk':
-            return None
-        elif path.startswith('branches/'):
-            return path[len('branches/'):]
-        return  '../%s' % path
+        return self.layoutobj.localname(path)
 
     def remotename(self, branch):
-        if self.layout == 'single':
-            return ''
-        if branch == 'default' or branch is None:
-            return 'trunk'
-        elif branch.startswith('../'):
-            return branch[3:]
-        return 'branches/%s' % branch
+        return self.layoutobj.remotename(branch)
 
     def genextra(self, revnum, branch):
         extra = {}
@@ -253,17 +204,10 @@ class SVNMeta(object):
         if subdir and subdir[0] != '/':
             subdir = '/' + subdir
 
-        if self.layout == 'single':
-            path = subdir or '/'
-        else:
-            branchpath = 'trunk'
-            if branch:
-                extra['branch'] = branch
-                if branch.startswith('../'):
-                    branchpath = branch[3:]
-                else:
-                    branchpath = 'branches/%s' % branch
-            path = '%s/%s' % (subdir, branchpath)
+        path = self.layoutobj.remotepath(branch, subdir)
+
+        if branch:
+            extra['branch'] = branch
 
         extra['convert_revision'] = 'svn:%(uuid)s%(path)s@%(rev)s' % {
             'uuid': self.uuid,
@@ -300,6 +244,10 @@ class SVNMeta(object):
             path = path[1:]
         return path
 
+    @property
+    def taglocations(self):
+        return self.layoutobj.taglocations(self.meta_data_dir)
+
     def get_path_tag(self, path):
         """If path could represent the path to a tag, returns the
         potential (non-empty) tag name. Otherwise, returns None
@@ -307,14 +255,8 @@ class SVNMeta(object):
         Note that it's only a tag if it was copied from the path '' in a branch
         (or tag) we have, for our purposes.
         """
-        if self.layout != 'single':
-            path = self.normalize(path)
-            for tagspath in self.tag_locations:
-                if path.startswith(tagspath + '/'):
-                    tag = path[len(tagspath) + 1:]
-                    if tag:
-                        return tag
-        return None
+        path = self.normalize(path)
+        return self.layoutobj.get_path_tag(path, self.taglocations)
 
     def split_branch_path(self, path, existing=True):
         """Figure out which branch inside our repo this path represents, and
@@ -335,8 +277,6 @@ class SVNMeta(object):
         relative to our subdirectory.
         """
         path = self.normalize(path)
-        if self.layout == 'single':
-            return (path, None, '')
         tag = self.get_path_tag(path)
         if tag:
             # consider the new tags when dispatching entries
@@ -355,38 +295,34 @@ class SVNMeta(object):
                 svrpath = path[:-(len(brpath)+1)]
             ln = self.localname(svrpath)
             return brpath, ln, svrpath
-        test = ''
-        path_comps = path.split('/')
-        while self.localname(test) not in self.branches and len(path_comps):
-            if not test:
-                test = path_comps.pop(0)
-            else:
-                test += '/%s' % path_comps.pop(0)
-        if self.localname(test) in self.branches:
-            return path[len(test)+1:], self.localname(test), test
-        if existing:
+
+        branch_path, local_path = self.layoutobj.split_remote_name(path,
+                                                                   self.branches)
+        branch_name = self.layoutobj.localname(branch_path)
+
+        if branch_name in self.branches:
+            return local_path, branch_name, branch_path
+        elif existing or (branch_name and branch_name.startswith('../')):
             return None, None, None
-        if path == 'trunk' or path.startswith('trunk/'):
-            path = '/'.join(path.split('/')[1:])
-            test = 'trunk'
-        elif path.startswith('branches/'):
-            elts = path.split('/')
-            test = '/'.join(elts[:2])
-            path = '/'.join(elts[2:])
         else:
-            path = test.split('/')[-1]
-            test = '/'.join(test.split('/')[:-1])
-        ln = self.localname(test)
-        if ln and ln.startswith('../'):
-            return None, None, None
-        return path, ln, test
+            return local_path, branch_name, branch_path
 
     def _determine_parent_branch(self, p, src_path, src_rev, revnum):
         if src_path is not None:
             src_file, src_branch = self.split_branch_path(src_path)[:2]
             src_tag = self.get_path_tag(src_path)
             if src_tag or src_file == '':
-                ln = self.localname(p)
+                brpath, fpath = self.layoutobj.split_remote_name(p,
+                                                                 self.branches)
+                # we'll sometimes get a different path out of
+                # split_remate_name than the one we passed in, but
+                # only for the root of a branch, since the svn copies
+                # of those will sometimes be of parent directories of
+                # our root
+                if fpath == '':
+                    ln = self.localname(brpath)
+                else:
+                    ln = self.localname(p)
                 if src_tag in self.tags:
                     changeid = self.tags[src_tag]
                     src_rev, src_branch = self.get_source_rev(changeid)[:2]
@@ -480,8 +416,6 @@ class SVNMeta(object):
             raise KeyError('%s has no conversion record' % ctx)
         branchpath, revnum = extra['convert_revision'][40:].rsplit('@', 1)
         branch = self.localname(self.normalize(branchpath))
-        if self.layout == 'single':
-            branchpath = ''
         if branchpath and branchpath[0] == '/':
             branchpath = branchpath[1:]
         return int(revnum), branch, branchpath
@@ -495,9 +429,6 @@ class SVNMeta(object):
         values are the place the branch came from. The deletions are
         sets of the deleted branches.
         """
-        if self.layout == 'single':
-            return {'branches': ({None: (None, 0, -1), }, set()),
-                    }
         paths = revision.paths
         added_branches = {}
         # Reset the tags delta before detecting the new one, and take
@@ -559,8 +490,8 @@ class SVNMeta(object):
             #    action of 'D'. We mark the branch as deleted.
             # 5. It's the parent directory of one or more
             #    already-known branches, so we mark them as deleted.
-            # 6. It's a branch being replaced by another branch - the
-            #    action will be 'R'.
+            # 6. It's a branch being replaced by another branch or a new
+            #    directory - the action will be 'R'.
             fi, br = self.split_branch_path(p)[:2]
             if fi is not None:
                 if fi == '':
@@ -570,15 +501,16 @@ class SVNMeta(object):
                         # 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
+                        if paths[p].copyfrom_path:
+                            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)
@@ -655,7 +587,7 @@ class SVNMeta(object):
             revnum, branch = self.get_source_rev(ctx=parentctx)[:2]
         ctx = context.memctx(self.repo,
                              (parentctx.node(), node.nullid),
-                             rev.message or util.default_commit_msg(self.ui),
+                             util.getmessage(self.ui, rev),
                              ['.hgtags', ],
                              hgtagsfn,
                              self.authors[rev.author],
@@ -720,7 +652,7 @@ class SVNMeta(object):
 
             ctx = context.memctx(self.repo,
                                  (parent.node(), node.nullid),
-                                 rev.message or ' ',
+                                 util.getmessage(self.ui, rev),
                                  ['.hgtags'],
                                  fctxfun,
                                  self.authors[rev.author],
@@ -742,7 +674,7 @@ class SVNMeta(object):
         self.mapbranch(extra, True)
         ctx = context.memctx(self.repo,
                              (node, revlog.nullid),
-                             rev.message or util.default_commit_msg(self.ui),
+                             util.getmessage(self.ui, rev),
                              [],
                              lambda x, y, z: None,
                              self.authors[rev.author],
--- a/hgsubversion/svnrepo.py
+++ b/hgsubversion/svnrepo.py
@@ -122,7 +122,7 @@ class svnremoterepo(peerrepository):
         if path is None:
             path = self.ui.config('paths', 'default')
         if not path:
-            raise hgutil.Abort('no Subversion URL specified')
+            raise hgutil.Abort('no Subversion URL specified. Expect[path] default= or [path] default-push= SVN URL entries in hgrc.')
         self.path = path
         self.capabilities = set(['lookup', 'subversion'])
         pws = self.ui.config('hgsubversion', 'password_stores', None)
@@ -156,7 +156,8 @@ class svnremoterepo(peerrepository):
     @propertycache
     def svn(self):
         try:
-            return svnwrap.SubversionRepo(*self.svnauth, password_stores=self.password_stores)
+            auth = self.svnauth
+            return svnwrap.SubversionRepo(auth[0], auth[1], auth[2], password_stores=self.password_stores)
         except svnwrap.SubversionConnectionException, e:
             self.ui.traceback()
             raise hgutil.Abort(e)
@@ -218,7 +219,7 @@ class SubversionPrompt(object):
             username = default_username
         else:
             username = self.ui.prompt('Username: ', default='')
-        password = self.ui.getpass('Password for \'%s\': ' % (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):
@@ -227,7 +228,7 @@ class SubversionPrompt(object):
         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='')
+        password = self.ui.getpass("Passphrase for '%s': " % (realm,), default='')
         return (password, bool(may_save))
 
     def insecure(fn):
@@ -252,7 +253,7 @@ class SubversionPrompt(object):
 
     @insecure
     def ssl_server_trust(self, realm, failures, cert_info, may_save, pool=None):
-        msg = 'Error validating server certificate for \'%s\':\n' % (realm,)
+        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'
@@ -293,4 +294,3 @@ class SubversionPrompt(object):
         else:
             creds = None
         return creds
-
--- a/hgsubversion/svnwrap/common.py
+++ b/hgsubversion/svnwrap/common.py
@@ -51,7 +51,7 @@ class Revision(tuple):
 
     Derives from tuple in an attempt to minimise the memory footprint.
     """
-    def __new__(self, revnum, author, message, date, paths, strip_path=''):
+    def __new__(self, revnum, author, message, date, paths=None, strip_path=''):
         _paths = {}
         if paths:
             for p in paths:
--- a/hgsubversion/svnwrap/subvertpy_wrapper.py
+++ b/hgsubversion/svnwrap/subvertpy_wrapper.py
@@ -52,17 +52,19 @@ def version():
     return (svnvers, 'Subvertpy ' + _versionstr(subvertpy.__version__))
 
 # exported values
+ERR_FS_ALREADY_EXISTS = subvertpy.ERR_FS_ALREADY_EXISTS
 ERR_FS_CONFLICT = subvertpy.ERR_FS_CONFLICT
 ERR_FS_NOT_FOUND = subvertpy.ERR_FS_NOT_FOUND
 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
+ERR_REPOS_HOOK_FAILURE = subvertpy.ERR_REPOS_HOOK_FAILURE
 SSL_CNMISMATCH = subvertpy.SSL_CNMISMATCH
-SSL_NOTYETVALID = subvertpy.SSL_NOTYETVALID
 SSL_EXPIRED = subvertpy.SSL_EXPIRED
+SSL_NOTYETVALID = subvertpy.SSL_NOTYETVALID
 SSL_OTHER = subvertpy.SSL_OTHER
+SSL_UNKNOWNCA = subvertpy.SSL_UNKNOWNCA
 SubversionException = subvertpy.SubversionException
 apply_txdelta = delta.apply_txdelta_handler
 # superclass for editor.HgEditor
@@ -90,12 +92,20 @@ def prompt_callback(callback):
 class PathAdapter(object):
     __slots__ = ('action', 'copyfrom_path', 'copyfrom_rev')
 
-    def __init__(self, path):
-        self.action, self.copyfrom_path, self.copyfrom_rev = path
+    def __init__(self, action, copyfrom_path, copyfrom_rev):
+        self.action = action
+        self.copyfrom_path = copyfrom_path
+        self.copyfrom_rev = copyfrom_rev
+
         if self.copyfrom_path:
             self.copyfrom_path = intern(self.copyfrom_path)
 
-class AbstractEditor(object):
+    def __repr__(self):
+        return '%s(%r, %r, %r)' % (type(self).__name__, self.action,
+                                     self.copyfrom_path, self.copyfrom_rev)
+
+
+class BaseEditor(object):
     __slots__ = ('editor', 'baton')
 
     def __init__(self, editor, baton=None):
@@ -116,7 +126,9 @@ class AbstractEditor(object):
     def close(self):
         del self.editor
 
-class FileEditor(AbstractEditor):
+class FileEditor(BaseEditor):
+    __slots__ = ()
+
     def __init__(self, editor, baton):
         super(FileEditor, self).__init__(editor, baton)
 
@@ -130,7 +142,9 @@ class FileEditor(AbstractEditor):
         self.editor.close_file(self.baton, checksum)
         super(FileEditor, self).close()
 
-class DirectoryEditor(AbstractEditor):
+class DirectoryEditor(BaseEditor):
+    __slots__ = ()
+
     def __init__(self, editor, baton):
         super(DirectoryEditor, self).__init__(editor, baton)
 
@@ -332,7 +346,7 @@ class SubversionRepo(object):
                              props.get(properties.PROP_REVISION_AUTHOR),
                              props.get(properties.PROP_REVISION_LOG),
                              props.get(properties.PROP_REVISION_DATE),
-                             dict([(k, PathAdapter(v))
+                             dict([(k, PathAdapter(*v))
                                    for k, v in paths.iteritems()]),
                              strip_path=self.subdir)
                 revisions.append(r)
@@ -374,10 +388,14 @@ class SubversionRepo(object):
     def commit(self, paths, message, file_data, base_revision, addeddirs,
                deleteddirs, props, copies):
         """Commits the appropriate targets from revision in editor's store.
+
+        Return the committed revision as a common.Revision instance.
         """
-        def commitcb(*args):
-            commit_info.append(args)
-        commit_info = []
+        def commitcb(rev, date, author):
+            r = common.Revision(rev, author, message, date)
+            committedrev.append(r)
+
+        committedrev = []
         revprops = { properties.PROP_REVISION_LOG: message }
         # revprops.update(props)
         commiteditor = self.remote.get_commit_editor(revprops, commitcb)
@@ -417,7 +435,7 @@ class SubversionRepo(object):
                         editor.delete_entry(path, base_revision)
                         continue
                     else:
-                        assert False, 'invalid action \'%s\'' % action
+                        assert False, "invalid action '%s'" % action
 
                     if path in props:
                         if props[path].get('svn:special', None):
@@ -453,15 +471,17 @@ class SubversionRepo(object):
             rooteditor = commiteditor.open_root()
             visitdir(rooteditor, '', paths, 0)
             rooteditor.close()
-            commiteditor.close()
         except:
             commiteditor.abort()
             raise
+        commiteditor.close()
+
+        return committedrev.pop()
 
     def get_replay(self, revision, editor, oldestrev=0):
 
         try:
-            self.remote.replay(revision, oldestrev, AbstractEditor(editor))
+            self.remote.replay(revision, oldestrev, BaseEditor(editor))
         except (SubversionException, NotImplementedError), e: # pragma: no cover
             # can I depend on this number being constant?
             if (isinstance(e, NotImplementedError) or
@@ -476,7 +496,7 @@ class SubversionRepo(object):
     def get_revision(self, revision, editor):
         ''' feed the contents of the given revision to the given editor '''
         reporter = self.remote.do_update(revision, '', True,
-                                         AbstractEditor(editor))
+                                         BaseEditor(editor))
         reporter.set_path('', revision, True)
         reporter.finish()
 
--- a/hgsubversion/svnwrap/svn_swig_wrapper.py
+++ b/hgsubversion/svnwrap/svn_swig_wrapper.py
@@ -37,16 +37,18 @@ def version():
     return '%d.%d.%d' % current_bindings, 'SWIG'
 
 # exported values
+ERR_FS_ALREADY_EXISTS = core.SVN_ERR_FS_ALREADY_EXISTS
 ERR_FS_CONFLICT = core.SVN_ERR_FS_CONFLICT
 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
+ERR_REPOS_HOOK_FAILURE = core.SVN_ERR_REPOS_HOOK_FAILURE
 SSL_CNMISMATCH = core.SVN_AUTH_SSL_CNMISMATCH
-SSL_NOTYETVALID = core.SVN_AUTH_SSL_NOTYETVALID
 SSL_EXPIRED = core.SVN_AUTH_SSL_EXPIRED
+SSL_NOTYETVALID = core.SVN_AUTH_SSL_NOTYETVALID
 SSL_OTHER = core.SVN_AUTH_SSL_OTHER
+SSL_UNKNOWNCA = core.SVN_AUTH_SSL_UNKNOWNCA
 SubversionException = core.SubversionException
 Editor = delta.Editor
 
@@ -366,11 +368,20 @@ class SubversionRepo(object):
     def commit(self, paths, message, file_data, base_revision, addeddirs,
                deleteddirs, properties, copies):
         """Commits the appropriate targets from revision in editor's store.
+
+        Return the committed revision as a common.Revision instance.
         """
         self.init_ra_and_client()
-        commit_info = []
-        def commit_cb(_commit_info, pool):
-            commit_info.append(_commit_info)
+
+        def commit_cb(commit_info, pool):
+            # disregard commit_info.post_commit_err for now
+            r = common.Revision(commit_info.revision, commit_info.author,
+                                message, commit_info.date)
+
+            committedrev.append(r)
+
+        committedrev = []
+
         editor, edit_baton = ra.get_commit_editor2(self.ra,
                                                    message,
                                                    commit_cb,
@@ -436,13 +447,16 @@ class SubversionRepo(object):
         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
 
+        editor.close_edit(edit_baton, self.pool)
+
+        return committedrev.pop()
+
     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
         self.init_ra_and_client()
@@ -469,9 +483,14 @@ class SubversionRepo(object):
                 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') == '*'
+                # The list_props API creates a new connection and then
+                # calls get_file for the remote file case.  It also
+                # creates a new connection to the subversion server
+                # every time it's called.  As a result, it's actually
+                # *cheaper* to call get_file than list_props here
+                data, mode = self.get_file(sf, revision)
+                links[f] = mode == 'l'
+                execs[f] = mode == 'x'
 
     def get_revision(self, revision, editor):
         ''' feed the contents of the given revision to the given editor '''
--- a/hgsubversion/util.py
+++ b/hgsubversion/util.py
@@ -1,15 +1,21 @@
+import cPickle as pickle
 import errno
 import re
 import os
 import urllib
-from collections import deque
 
 from mercurial import cmdutil
 from mercurial import error
 from mercurial import hg
 from mercurial import node
+from mercurial import repair
 from mercurial import util as hgutil
 
+try:
+    from collections import deque
+except:
+    from mercurial.util import deque
+
 try:
     from mercurial import revset
 except ImportError:
@@ -79,6 +85,14 @@ def islocalrepo(url):
         path = path.rsplit('/', 1)[0]
     return False
 
+def strip(ui, repo, changesets, *args , **opts):
+    try:
+        repair.strip(ui, repo, changesets, *args, **opts)
+    except TypeError:
+        # only 2.1.2 and later allow strip to take a list of nodes
+        for changeset in changesets:
+            repair.strip(ui, repo, changeset, *args, **opts)
+
 
 def version(ui):
     """Return version information if available."""
@@ -128,16 +142,20 @@ def save_string(file_path, string):
     f.write(str(string))
     f.close()
 
+def pickle_atomic(data, file_path):
+    """pickle some data to a path atomically.
 
-# TODO remove when we drop 1.3 support
-def progress(ui, *args, **kwargs):
-    if getattr(ui, 'progress', False):
-        return ui.progress(*args, **kwargs)
-
-# TODO remove when we drop 1.5 support
-remoteui = getattr(cmdutil, 'remoteui', getattr(hg, 'remoteui', False))
-if not remoteui:
-    raise ImportError('Failed to import remoteui')
+    This is present because I kept corrupting my revmap by managing to hit ^C
+    during the pickle of that file.
+    """
+    f = hgutil.atomictempfile(file_path, 'w+b', 0644)
+    pickle.dump(data, f)
+    # Older versions of hg have .rename() instead of .close on
+    # atomictempfile.
+    if getattr(hgutil.atomictempfile, 'rename', False):
+        f.rename()
+    else:
+        f.close()
 
 def parseurl(url, heads=[]):
     parsed = hg.parseurl(url, heads)
@@ -202,8 +220,19 @@ def outgoing_common_and_heads(repo, reve
         return ([sourcecx.node()], [sourcerev])
     return ([sourcerev], [sourcerev]) # nothing outgoing
 
-def default_commit_msg(ui):
-    return ui.config('hgsubversion', 'defaultmessage', '')
+def getmessage(ui, rev):
+    msg = rev.message
+
+    if msg:
+        try:
+            msg.decode('utf-8')
+            return msg
+
+        except UnicodeDecodeError:
+            # ancient svn failed to enforce utf8 encoding
+            return msg.decode('iso-8859-1').encode('utf-8')
+    else:
+        return ui.config('hgsubversion', 'defaultmessage', '')
 
 def describe_commit(ui, h, b):
     ui.note(' committed to "%s" as %s\n' % ((b or 'default'), node.short(h)))
@@ -340,6 +369,11 @@ revsets = {
     'svnrev': revset_svnrev,
 }
 
+def revset_stringset(orig, repo, subset, x):
+    if x.startswith('r') and x[1:].isdigit():
+        return revset_svnrev(repo, subset, ('string', x[1:]))
+    return orig(repo, subset, x)
+
 def getfilestoresize(ui):
     """Return the replay or stupid file memory store size in megabytes or -1"""
     size = ui.configint('hgsubversion', 'filestoresize', 200)
@@ -349,35 +383,6 @@ def getfilestoresize(ui):
         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
-
 def parse_revnum(svnrepo, r):
     try:
         return int(r or 0)
@@ -385,5 +390,4 @@ def parse_revnum(svnrepo, r):
         if isinstance(r, str) and r.lower() in ('head', 'tip'):
             return svnrepo.last_changed_rev
         else:
-            # TODO: use error.RepoLookupError when we drop 1.3?
-            raise hgutil.Abort("unknown Subversion revision %r" % r)
+            raise error.RepoLookupError("unknown Subversion revision %r" % r)
--- a/hgsubversion/verify.py
+++ b/hgsubversion/verify.py
@@ -1,7 +1,9 @@
+import difflib
 import posixpath
 
 from mercurial import util as hgutil
 from mercurial import error
+from mercurial import worker
 
 import svnwrap
 import svnrepo
@@ -38,34 +40,68 @@ def verify(ui, repo, args=None, **opts):
 
     ui.write('verifying %s against %s@%i\n' % (ctx, branchurl, srev))
 
+    def diff_file(path, svndata):
+        fctx = ctx[path]
+
+        if ui.verbose and not fctx.isbinary():
+            svndesc = '%s/%s/%s@%d' % (svn.svn_url, branchpath, path, srev)
+            hgdesc = '%s@%s' % (path, ctx)
+
+            for c in difflib.unified_diff(svndata.splitlines(True),
+                                          fctx.data().splitlines(True),
+                                          svndesc, hgdesc):
+                ui.note(c)
+
     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))
+        def verifydata(svndata):
+            svnworker = svnrepo.svnremoterepo(ui, url).svn
+
+            i = 0
+            res = True
+            for fn, type in svndata:
+                i += 1
+                if type != 'f':
+                    continue
+
+                fp = fn
+                if branchpath:
+                    fp = branchpath + '/' + fn
+                data, mode = svnworker.get_file(posixpath.normpath(fp), srev)
+                try:
+                    fctx = ctx[fn]
+                except error.LookupError:
+                    yield i, "%s\0%r" % (fn, res)
+                    continue
+
+                if not fctx.data() == data:
+                    ui.write('difference in: %s\n' % fn)
+                    diff_file(fn, data)
+                    res = False
+                if not fctx.flags() == mode:
+                    ui.write('wrong flags for: %s\n' % fn)
+                    res = False
+                yield i, "%s\0%r" % (fn, res)
+
+        if url.startswith('file://'):
+            perarg = 0.00001
+        else:
+            perarg = 0.000001
 
-            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)
+        svndata = svn.list_files(branchpath, srev)
+        w = worker.worker(repo.ui, perarg, verifydata, (), tuple(svndata))
+        i = 0
+        for _, t in w:
+            ui.progress('verify', i, total=len(hgfiles))
+            i += 1
+            fn, ok = t.split('\0', 2)
+            if not bool(ok):
                 result = 1
+            svnfiles.add(fn)
 
         if hgfiles != svnfiles:
             unexpected = hgfiles - svnfiles
@@ -76,7 +112,7 @@ def verify(ui, repo, args=None, **opts):
                 ui.write('missing file: %s\n' % f)
             result = 1
 
-        util.progress(ui, 'verify', None, total=len(hgfiles))
+        ui.progress('verify', None, total=len(hgfiles))
 
     else:
         class VerifyEditor(svnwrap.Editor):
@@ -118,7 +154,7 @@ def verify(ui, repo, args=None, **opts):
                     self.props = None
 
                 self.seen += 1
-                util.progress(self.ui, 'verify', self.seen, total=self.total)
+                self.ui.progress('verify', self.seen, total=self.total)
 
             def open_file(self, path, base_revnum):
                 raise NotImplementedError()
@@ -154,6 +190,7 @@ def verify(ui, repo, args=None, **opts):
 
                     if hgdata != svndata:
                         self.ui.warn('difference in: %s\n' % self.file)
+                        diff_file(self.file, svndata)
                         self.failed = True
 
                 if self.file is not None:
@@ -176,7 +213,7 @@ def verify(ui, repo, args=None, **opts):
                 raise NotImplementedError()
 
             def check(self):
-                util.progress(self.ui, 'verify', None, total=self.total)
+                self.ui.progress('verify', None, total=self.total)
 
                 for f in self.unexpected:
                     self.ui.warn('unexpected file: %s\n' % f)
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -1,11 +1,7 @@
 from hgext import rebase as hgrebase
 
 from mercurial import cmdutil
-try:
-    from mercurial import discovery
-    discovery.nullid  # force demandimport to import the module
-except ImportError:
-    discovery = None
+from mercurial import discovery
 from mercurial import patch
 from mercurial import hg
 from mercurial import util as hgutil
@@ -13,7 +9,11 @@ from mercurial import node
 from mercurial import i18n
 from mercurial import extensions
 from mercurial import repair
+from mercurial import revset
+from mercurial import scmutil
 
+import layouts
+import os
 import replay
 import pushmod
 import stupid as stupidmod
@@ -22,10 +22,9 @@ import svnrepo
 import util
 
 try:
-    from mercurial import scmutil
-    revpair = scmutil.revpair
+    from mercurial import obsolete
 except ImportError:
-    revpair = cmdutil.revpair
+    obsolete = None
 
 pullfuns = {
     True: replay.convert_rev,
@@ -98,7 +97,15 @@ def incoming(orig, ui, repo, origsource=
     meta = repo.svnmeta(svn.uuid, svn.subdir)
 
     ui.status('incoming changes from %s\n' % other.svnurl)
-    for r in svn.revisions(start=meta.revmap.youngest):
+    svnrevisions = list(svn.revisions(start=meta.revmap.youngest))
+    if opts.get('newest_first'):
+        svnrevisions.reverse()
+    # Returns 0 if there are incoming changes, 1 otherwise.
+    if len(svnrevisions) > 0:
+        ret = 0
+    else:
+        ret = 1
+    for r in svnrevisions:
         ui.status('\n')
         for label, attr in revmeta:
             l1 = label + ':'
@@ -106,9 +113,11 @@ def incoming(orig, ui, repo, origsource=
             if not ui.verbose:
                 val = val.split('\n')[0]
             ui.status('%s%s\n' % (l1.ljust(13), val))
+    return ret
 
 
-def findcommonoutgoing(repo, other, onlyheads=None, force=False, commoninc=None):
+def findcommonoutgoing(repo, other, onlyheads=None, force=False,
+                       commoninc=None, portable=False):
     assert other.capable('subversion')
     # split off #rev; TODO implement --revision/#rev support
     svn = other.svn
@@ -116,11 +125,10 @@ def findcommonoutgoing(repo, other, only
     parent = repo.parents()[0].node()
     hashes = meta.revmap.hashes()
     common, heads = util.outgoing_common_and_heads(repo, hashes, parent)
-    if discovery is not None:
-        outobj = getattr(discovery, 'outgoing', None)
-        if outobj is not None:
-            # Mercurial 2.1 and later
-            return outobj(repo.changelog, common, heads)
+    outobj = getattr(discovery, 'outgoing', None)
+    if outobj is not None:
+        # Mercurial 2.1 and later
+        return outobj(repo.changelog, common, heads)
     # Mercurial 2.0 and earlier
     return common, heads
 
@@ -151,7 +159,7 @@ def diff(orig, ui, repo, *args, **opts):
         if o_r:
             parent = repo[o_r[-1]].parents()[0]
         opts['rev'] = ['%s:.' % node.hex(parent.node()), ]
-    node1, node2 = revpair(repo, opts['rev'])
+    node1, node2 = scmutil.revpair(repo, opts['rev'])
     baserev, _junk = hashes.get(node1, (-1, 'junk'))
     newrev, _junk = hashes.get(node2, (-1, 'junk'))
     it = patch.diff(repo, node1, node2,
@@ -169,16 +177,16 @@ def push(repo, dest, force, revs):
     """push revisions starting at a specified head back to Subversion.
     """
     assert not revs, 'designated revisions for push remains unimplemented.'
-    if hasattr(cmdutil, 'bail_if_changed'):
-        cmdutil.bail_if_changed(repo)
-    else:
-        # Since 1.9 (d68ddccf276b)
-        cmdutil.bailifchanged(repo)
+    cmdutil.bailifchanged(repo)
     checkpush = getattr(repo, 'checkpush', None)
     if checkpush:
         checkpush(force, revs)
     ui = repo.ui
     old_encoding = util.swap_out_encoding()
+
+    hasobsolete = obsolete and obsolete._enabled
+
+    temporary_commits = []
     try:
         # TODO: implement --rev/#rev support
         # TODO: do credentials specified in the URL still work?
@@ -194,106 +202,145 @@ def push(repo, dest, force, revs):
         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:
+        tip_ctx = repo[outgoing[-1]].p1()
+        svnbranch = tip_ctx.branch()
+        modified_files = {}
+        for i in range(len(outgoing) - 1, -1, -1):
+            # 2. Pick the oldest changeset that needs to be pushed
+            current_ctx = repo[outgoing[i]]
+            original_ctx = current_ctx
+
+            if len(current_ctx.parents()) != 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.
+
+            # 3. Move the changeset to the tip of the branch if necessary
+            conflicts = False
+            for file in current_ctx.files():
+                if file in modified_files:
+                    conflicts = True
+                    break
+
+            if conflicts or current_ctx.branch() != svnbranch:
+                util.swap_out_encoding(old_encoding)
+                try:
+                    def extrafn(ctx, extra):
+                        extra['branch'] = ctx.branch()
+
+                    ui.note('rebasing %s onto %s \n' % (current_ctx, tip_ctx))
+                    hgrebase.rebase(ui, repo,
+                                    dest=node.hex(tip_ctx.node()),
+                                    rev=[node.hex(current_ctx.node())],
+                                    extrafn=extrafn, keep=True)
+                finally:
+                    util.swap_out_encoding()
+
+                # Don't trust the pre-rebase repo and context.
+                repo = getlocalpeer(ui, {}, meta.path)
+                tip_ctx = repo[tip_ctx.node()]
+                for c in tip_ctx.descendants():
+                    rebasesrc = c.extra().get('rebase_source')
+                    if rebasesrc and node.bin(rebasesrc) == current_ctx.node():
+                        current_ctx = c
+                        temporary_commits.append(c.node())
+                        break
+
+            # 4. Push the changeset to subversion
+            tip_hash = hashes[tip_ctx.node()][0]
             try:
-                pushmod.commit(ui, repo, old_ctx, meta, base_revision, svn)
+                ui.status('committing %s\n' % current_ctx)
+                pushedrev = pushmod.commit(ui, repo, current_ctx, meta,
+                                           tip_hash, svn)
             except pushmod.NoFilesException:
                 ui.warn("Could not push revision %s because it had no changes "
-                        "in svn.\n" % old_ctx)
-                return 1
+                        "in svn.\n" % current_ctx)
+                return
 
-            # 3. Fetch revisions from svn
-            # TODO: this probably should pass in the source explicitly -
-            # rev too?
+            # 5. Pull the latest changesets from subversion, which will
+            # include the one we just committed (and possibly others).
             r = repo.pull(dest, force=force)
             assert not r or r == 0
+            meta = repo.svnmeta(svn.uuid, svn.subdir)
+            hashes = meta.revmap.hashes()
 
-            # 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):
-                extra['branch'] = ctx.branch()
+            # 6. Move our tip to the latest pulled tip
+            for c in tip_ctx.descendants():
+                if c.node() in hashes and c.branch() == svnbranch:
+                    if meta.get_source_rev(ctx=c)[0] == pushedrev.revnum:
+                        # This is corresponds to the changeset we just pushed
+                        if hasobsolete:
+                            ui.note('marking %s as obsoleted by %s\n' %
+                                    (original_ctx.hex(), c.hex()))
+                            obsolete.createmarkers(repo, [(original_ctx, [c])])
+
+                    tip_ctx = c
+
+                    # Remember what files have been modified since the
+                    # whole push started.
+                    for file in c.files():
+                        modified_files[file] = True
+
+            # 7. Rebase any children of the commit we just pushed
+            # that are not in the outgoing set
+            for c in original_ctx.children():
+                if not c.node() in hashes and not c.node() in outgoing:
+                    util.swap_out_encoding(old_encoding)
+                    try:
+                        # Path changed as subdirectories were getting
+                        # deleted during push.
+                        saved_path = os.getcwd()
+                        os.chdir(repo.root)
+
+                        def extrafn(ctx, extra):
+                            extra['branch'] = ctx.branch()
+
+                        ui.status('rebasing non-outgoing %s onto %s\n' % (c, tip_ctx))
+                        needs_rebase_set = "%s::" % node.hex(c.node())
+                        hgrebase.rebase(ui, repo,
+                                        dest=node.hex(tip_ctx.node()),
+                                        rev=[needs_rebase_set],
+                                        extrafn=extrafn,
+                                        keep=not hasobsolete)
+                    finally:
+                        os.chdir(saved_path)
+                        util.swap_out_encoding()
 
-            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 = getlocalpeer(ui, {}, meta.path)
-            newtipctx = repo[newtip]
-
-            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]
 
-            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")
+
+        if not hasobsolete:
+            # strip the original changesets since the push was
+            # successful and changeset obsolescence is unavailable
+            util.strip(ui, repo, outgoing, "all")
     finally:
-        util.swap_out_encoding(old_encoding)
+        try:
+            # It's always safe to delete the temporary commits.
+            # The originals are not deleted unless the push
+            # completely succeeded.
+            if temporary_commits:
+                # If the repo is on a temporary commit, get off before
+                # the strip.
+                parent = repo[None].p1()
+                if parent.node() in temporary_commits:
+                    hg.update(repo, parent.p1().node())
+                if hasobsolete:
+                    relations = ((repo[n], ()) for n in temporary_commits)
+                    obsolete.createmarkers(repo, relations)
+                else:
+                    util.strip(ui, repo, temporary_commits, backup=None)
+
+        finally:
+            util.swap_out_encoding(old_encoding)
     return 1 # so we get a sane exit status, see hg's commands.push
 
 
@@ -316,18 +363,11 @@ def pull(repo, source, heads=[], force=F
 
         stopat_rev = util.parse_revnum(svn, checkout)
 
-        layout = repo.ui.config('hgsubversion', 'layout', 'auto')
+        layout = layouts.detect.layout_from_config(repo.ui, allow_auto=True)
         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)
+            layout = layouts.detect.layout_from_subversion(svn,
+                                                           (stopat_rev or None),
+                                                           repo.ui)
             repo.ui.note('using %s layout\n' % layout)
 
         branch = repo.ui.config('hgsubversion', 'branch')
@@ -375,12 +415,10 @@ def pull(repo, source, heads=[], force=F
             # 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.'):
+                if (r.revnum in skiprevs or
+                    (r.author is None and
+                     r.message == 'This is an empty revision for padding.')):
+                    lastpulled = r.revnum
                     continue
                 tbdelta = meta.update_branch_tag_map_for_rev(r)
                 # got a 502? Try more than once!
@@ -388,12 +426,8 @@ def pull(repo, source, heads=[], force=F
                 converted = False
                 while not converted:
                     try:
-                        msg = ''
-                        if r.message:
-                            msg = r.message.strip()
-                        if not msg:
-                            msg = util.default_commit_msg(ui)
-                        else:
+                        msg = util.getmessage(ui, r).strip()
+                        if msg:
                             msg = [s.strip() for s in msg.splitlines() if s][0]
                         if getattr(ui, 'termwidth', False):
                             w = ui.termwidth()
@@ -401,7 +435,7 @@ def pull(repo, source, heads=[], force=F
                             w = hgutil.termwidth()
                         bits = (r.revnum, r.author, msg)
                         ui.status(('[r%d] %s: %s' % bits)[:w] + '\n')
-                        util.progress(ui, 'pull', r.revnum - start, total=total)
+                        ui.progress('pull', r.revnum - start, total=total)
 
                         meta.save_tbdelta(tbdelta)
                         close = pullfuns[have_replay](ui, meta, svn, r, tbdelta,
@@ -429,11 +463,14 @@ def pull(repo, source, heads=[], force=F
                         else:
                             ui.traceback()
                             raise hgutil.Abort(*e.args)
+
+                lastpulled = r.revnum
+
         except KeyboardInterrupt:
             ui.traceback()
     finally:
         if total is not None:
-            util.progress(ui, 'pull', None, total=total)
+            ui.progress('pull', None, total=total)
         util.swap_out_encoding(old_encoding)
 
     if lastpulled is not None:
@@ -468,10 +505,10 @@ def rebase(orig, ui, repo, **opts):
     hashes = meta.revmap.hashes()
     o_r = util.outgoing_revisions(repo, hashes, sourcerev=sourcerev)
     if not o_r:
-        ui.status('Nothing to rebase!\n')
+        ui.note('nothing to rebase\n')
         return 0
     if len(repo[sourcerev].children()):
-        ui.status('Refusing to rebase non-head commit like a coward\n')
+        ui.status('refusing to rebase non-head commit like a coward\n')
         return 0
     parent_rev = repo[o_r[-1]].parents()[0]
     target_rev = parent_rev
@@ -486,7 +523,7 @@ def rebase(orig, ui, repo, **opts):
                 exhausted_choices = False
                 break
     if parent_rev == target_rev:
-        ui.status('Already up to date!\n')
+        ui.status('already up to date!\n')
         return 0
     return orig(ui, repo, dest=node.hex(target_rev.node()),
                 base=node.hex(sourcerev),
@@ -496,6 +533,8 @@ def rebase(orig, ui, repo, **opts):
 optionmap = {
     'tagpaths': ('hgsubversion', 'tagpaths'),
     'authors': ('hgsubversion', 'authormap'),
+    'branchdir': ('hgsubversion', 'branchdir'),
+    'infix': ('hgsubversion', 'infix'),
     'filemap': ('hgsubversion', 'filemap'),
     'branchmap': ('hgsubversion', 'branchmap'),
     'tagmap': ('hgsubversion', 'tagmap'),
@@ -507,7 +546,13 @@ optionmap = {
     'startrev': ('hgsubversion', 'startrev'),
 }
 
-dontretain = { 'hgsubversion': set(['authormap', 'filemap', 'layout', ]) }
+extrasections = set(['hgsubversionbranch'])
+
+
+dontretain = {
+    'hgsubversion': set(['authormap', 'filemap', 'layout', ]),
+    'hgsubversionbranch': set(),
+    }
 
 def clone(orig, ui, source, dest=None, **opts):
     """
@@ -565,7 +610,9 @@ def clone(orig, ui, source, dest=None, *
             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()):
+        preservesections = set(s for s, v in optionmap.itervalues())
+        preservesections |= extrasections
+        for section in preservesections:
             config = dict(ui.configitems(section))
             for name in dontretain[section]:
                 config.pop(name, None)
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,8 @@ def runcmd(cmd, env):
     # trust warnings since the .hg/hgrc file is untrusted. That is
     # fine, we don't want to load it anyway.
     err = [e for e in err.splitlines()
-           if not e.startswith('Not trusting file')]
+           if not (e.startswith('Not trusting file')
+                   or e.startswith('obsolete feature not enabled'))]
     if err:
         return ''
     return out
@@ -118,7 +119,8 @@ setup(
     long_description=open(os.path.join(os.path.dirname(__file__),
                                          'README')).read(),
     keywords='mercurial',
-    packages=('hgsubversion', 'hgsubversion.hooks', 'hgsubversion.svnwrap'),
+    packages=('hgsubversion', 'hgsubversion.hooks', 'hgsubversion.layouts',
+              'hgsubversion.svnwrap'),
     package_data={ 'hgsubversion': ['help/subversion.rst'] },
     platforms='any',
     install_requires=requires,
new file mode 100644
--- /dev/null
+++ b/tests/comprehensive/test_custom_layout.py
@@ -0,0 +1,65 @@
+import os
+import pickle
+import sys
+import unittest
+
+from mercurial import hg
+from mercurial import ui
+
+# 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 hgsubversion import wrappers
+
+
+def _do_case(self, name, stupid):
+    subdir = test_util.subdir.get(name, '')
+    config = {
+        'hgsubversion.stupid': stupid and '1' or '0',
+        }
+    repo, repo_path = self.load_and_fetch(name,
+                                          subdir=subdir,
+                                          layout='auto',
+                                          config=config)
+    assert test_util.repolen(self.repo) > 0, \
+        'Repo had no changes, maybe you need to add a subdir entry in test_util?'
+    wc2_path = self.wc_path + '_custom'
+    checkout_path = repo_path
+    if subdir:
+        checkout_path += '/' + subdir
+    u = ui.ui()
+    if stupid:
+        u.setconfig('hgsubversion', 'stupid', '1')
+    u.setconfig('hgsubversion', 'layout', 'custom')
+    for branch, path in test_util.custom.get(name, {}).iteritems():
+        u.setconfig('hgsubversionbranch', branch, path)
+    test_util.hgclone(u,
+                      test_util.fileurl(checkout_path),
+                      wc2_path,
+                      update=False)
+    self.repo2 = hg.repository(ui.ui(), wc2_path)
+    self.assertEqual(self.repo.heads(), self.repo2.heads())
+
+
+def buildmethod(case, name, stupid):
+    m = lambda self: self._do_case(case, stupid)
+    m.__name__ = name
+    replay = stupid and 'stupid' or 'regular'
+    m.__doc__ = 'Test custom produces same as standard on %s. (%s)' % (case,
+                                                                       replay)
+    return m
+
+attrs = {'_do_case': _do_case,
+         }
+for case in test_util.custom:
+    name = 'test_' + case[:-len('.svndump')].replace('-', '_')
+    attrs[name] = buildmethod(case, name, stupid=False)
+    name += '_stupid'
+    attrs[name] = buildmethod(case, name, stupid=True)
+
+CustomPullTests = type('CustomPullTests', (test_util.TestBase,), attrs)
rename from tests/test_rebuildmeta.py
rename to tests/comprehensive/test_rebuildmeta.py
--- a/tests/test_rebuildmeta.py
+++ b/tests/comprehensive/test_rebuildmeta.py
@@ -1,14 +1,22 @@
-import test_util
-
 import os
 import pickle
 import unittest
+import sys
+
+# 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 context
 from mercurial import extensions
 from mercurial import hg
 from mercurial import ui
 
+from hgsubversion import compathacks
 from hgsubversion import svncommands
 from hgsubversion import svnmeta
 
@@ -23,16 +31,18 @@ expect_youngest_skew = [('file_mixed_wit
 
 
 
-def _do_case(self, name, stupid, single):
+def _do_case(self, name, layout):
     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'
+    single = layout == 'single'
     u = ui.ui()
+    config = {}
+    if layout == 'custom':
+        for branch, path in test_util.custom.get(name, {}).iteritems():
+            config['hgsubversionbranch.%s' % branch] = path
+            u.setconfig('hgsubversionbranch', branch, path)
+    repo, repo_path = self.load_and_fetch(name, subdir=subdir, layout=layout)
+    assert test_util.repolen(self.repo) > 0
+    wc2_path = self.wc_path + '_clone'
     src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False)
     src = test_util.getlocalpeer(src)
     dest = test_util.getlocalpeer(dest)
@@ -53,7 +63,7 @@ def _do_case(self, name, stupid, single)
         # remove the wrapper
         context.changectx.children = origchildren
 
-    self._run_assertions(name, stupid, single, src, dest, u)
+    self._run_assertions(name, single, src, dest, u)
 
     wc3_path = self.wc_path + '_partial'
     src, dest = test_util.hgclone(u,
@@ -87,10 +97,10 @@ def _do_case(self, name, stupid, single)
         # remove the wrapper
         context.changectx.children = origchildren
 
-    self._run_assertions(name, stupid, single, srcrepo, dest, u)
+    self._run_assertions(name, single, srcrepo, dest, u)
 
 
-def _run_assertions(self, name, stupid, single, src, dest, u):
+def _run_assertions(self, name, single, src, dest, u):
 
     self.assertTrue(os.path.isdir(os.path.join(src.path, 'svn')),
                     'no .hg/svn directory in the source!')
@@ -105,12 +115,16 @@ def _run_assertions(self, name, stupid, 
         self.assertTrue(os.path.isfile(dtf), '%r is missing!' % tf)
         old, new = open(stf).read(), open(dtf).read()
         if tf == 'lastpulled' and (name,
-                                   stupid, single) in expect_youngest_skew:
+                                   self.stupid, single) in expect_youngest_skew:
             self.assertNotEqual(old, new,
                                 'rebuildmeta unexpected match on youngest rev!')
             continue
         self.assertMultiLineEqual(old, new, tf + ' differs')
-        self.assertEqual(src.branchtags(), dest.branchtags())
+        try:
+          self.assertEqual(src.branchmap(), dest.branchmap())
+        except AttributeError:
+          # hg 2.8 and earlier
+          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')))
     self.assertEqual(sorted(srcbi.keys()), sorted(destbi.keys()))
@@ -130,15 +144,11 @@ def _run_assertions(self, name, stupid, 
             self.assertEqual(srcinfo[2], destinfo[2])
 
 
-def buildmethod(case, name, stupid, single):
-    m = lambda self: self._do_case(case, stupid, single)
+def buildmethod(case, name, layout):
+    m = lambda self: self._do_case(case, layout)
     m.__name__ = name
-    m.__doc__ = ('Test rebuildmeta on %s with %s replay. (%s)' %
-                 (case,
-                  (stupid and 'stupid') or 'real',
-                  (single and 'single') or 'standard',
-                  )
-                 )
+    m.__doc__ = ('Test rebuildmeta on %s (%s)' %
+                 (case, layout))
     return m
 
 
@@ -149,22 +159,18 @@ skip = set([
 
 attrs = {'_do_case': _do_case,
          '_run_assertions': _run_assertions,
+         'stupid_mode_tests': True,
          }
 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] = buildmethod(case, bname, False, False)
-    name = bname + '_stupid'
-    attrs[name] = buildmethod(case, name, True, False)
-    name = bname + '_single'
-    attrs[name] = buildmethod(case, name, False, True)
+    attrs[bname] = buildmethod(case, bname, 'auto')
+    attrs[bname + '_single'] = buildmethod(case, bname + '_single', 'single')
+    if case in test_util.custom:
+            attrs[bname + '_custom'] = buildmethod(case,
+                                                   bname + '_custom',
+                                                   'single')
 
 RebuildMetaTests = type('RebuildMetaTests', (test_util.TestBase,), attrs)
-
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(RebuildMetaTests),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/comprehensive/test_stupid_pull.py
+++ b/tests/comprehensive/test_stupid_pull.py
@@ -19,11 +19,18 @@ from hgsubversion import wrappers
 
 def _do_case(self, name, layout):
     subdir = test_util.subdir.get(name, '')
-    repo, repo_path = self.load_and_fetch(name, subdir=subdir, stupid=False,
-                                          layout=layout)
-    assert len(self.repo) > 0, 'Repo had no changes, maybe you need to add a subdir entry in test_util?'
-    wc2_path = self.wc_path + '_stupid'
+    config = {}
     u = ui.ui()
+    for branch, path in test_util.custom.get(name, {}).iteritems():
+        config['hgsubversionbranch.%s' % branch] = path
+        u.setconfig('hgsubversionbranch', branch, path)
+    repo, repo_path = self.load_and_fetch(name,
+                                          subdir=subdir,
+                                          layout=layout,
+                                          config=config)
+    assert test_util.repolen(self.repo) > 0, \
+        'Repo had no changes, maybe you need to add a subdir entry in test_util?'
+    wc2_path = self.wc_path + '_stupid'
     checkout_path = repo_path
     if subdir:
         checkout_path += '/' + subdir
@@ -52,13 +59,8 @@ for case in (f for f in os.listdir(test_
     # here, but since it isn't a regression we suppress the test case.
     if case != 'branchtagcollision.svndump':
         attrs[name] = buildmethod(case, name, 'auto')
-    name += '_single'
-    attrs[name] = buildmethod(case, name, 'single')
+    attrs[name + '_single'] = buildmethod(case, name + '_single', 'single')
+    if case in test_util.custom:
+        attrs[name + '_custom'] = buildmethod(case, name + '_custom', 'custom')
 
 StupidPullTests = type('StupidPullTests', (test_util.TestBase,), attrs)
-
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(StupidPullTests),
-          ]
-    return unittest.TestSuite(all_tests)
rename from tests/test_updatemeta.py
rename to tests/comprehensive/test_updatemeta.py
--- a/tests/test_updatemeta.py
+++ b/tests/comprehensive/test_updatemeta.py
@@ -1,8 +1,16 @@
-import test_util
-
 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
+
 import test_rebuildmeta
 
 from mercurial import context
@@ -15,16 +23,24 @@ from hgsubversion import svnmeta
 
 
 
-def _do_case(self, name, stupid, single):
+def _do_case(self, name, layout):
     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'
+    single = layout == 'single'
     u = ui.ui()
+    config = {}
+    if layout == 'custom':
+        config['hgsubversion.layout'] = 'custom'
+        u.setconfig('hgsubversion', 'layout', 'custom')
+        for branch, path in test_util.custom.get(name, {}).iteritems():
+            config['hgsubversionbranch.%s' % branch] = path
+            u.setconfig('hgsubversionbranch', branch, path)
+
+    repo, repo_path = self.load_and_fetch(name,
+                                          subdir=subdir,
+                                          layout=layout,
+                                          config=config)
+    assert test_util.repolen(self.repo) > 0
+    wc2_path = self.wc_path + '_clone'
     src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False)
     src = test_util.getlocalpeer(src)
     dest = test_util.getlocalpeer(dest)
@@ -46,11 +62,11 @@ def _do_case(self, name, stupid, single)
         # remove the wrapper
         context.changectx.children = origchildren
 
-    self._run_assertions(name, stupid, single, src, dest, u)
+    self._run_assertions(name, 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)
+def _run_assertions(self, name, single, src, dest, u):
+    test_rebuildmeta._run_assertions(self, name, single, src, dest, u)
 
 
 skip = set([
@@ -60,22 +76,21 @@ skip = set([
 
 attrs = {'_do_case': _do_case,
          '_run_assertions': _run_assertions,
+         'stupid_mode_tests': True,
          }
 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)
+    attrs[bname] = test_rebuildmeta.buildmethod(case, bname, 'auto')
+    attrs[bname + '_single'] = test_rebuildmeta.buildmethod(case,
+                                                            bname + '_single',
+                                                            'single')
+    if case in test_util.custom:
+        attrs[bname + '_custom'] = test_rebuildmeta.buildmethod(case,
+                                                                bname + '_custom',
+                                                                'custom')
 
 
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(UpdateMetaTests),
-          ]
-    return unittest.TestSuite(all_tests)
+UpdateMetaTests = type('UpdateMetaTests', (test_util.TestBase,), attrs)
--- a/tests/comprehensive/test_verify_and_startrev.py
+++ b/tests/comprehensive/test_verify_and_startrev.py
@@ -37,11 +37,16 @@ from hgsubversion import verify
     'emptyrepo2.svndump',
 ])
 
-def _do_case(self, name, stupid, layout):
+def _do_case(self, name, 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
+    config = {}
+    for branch, path in test_util.custom.get(name, {}).iteritems():
+        config['hgsubversionbranch.%s' % branch] = path
+    repo, svnpath = self.load_and_fetch(name,
+                                        subdir=subdir,
+                                        layout=layout,
+                                        config=config)
+    assert test_util.repolen(self.repo) > 0
     for i in repo:
         ctx = repo[i]
         self.assertEqual(verify.verify(repo.ui, repo, rev=ctx.node(),
@@ -52,12 +57,12 @@ def _do_case(self, name, stupid, layout)
     # check a startrev clone
     if layout == 'single' and name not in _skipshallow:
         self.wc_path += '_shallow'
-        shallowrepo = self.fetch(svnpath, subdir=subdir, stupid=stupid,
+        shallowrepo = self.fetch(svnpath, subdir=subdir,
                                  layout='single', startrev='HEAD')
 
-        self.assertEqual(len(shallowrepo), 1,
+        self.assertEqual(test_util.repolen(shallowrepo), 1,
                          "shallow clone should have just one revision, not %d"
-                         % len(shallowrepo))
+                         % test_util.repolen(shallowrepo))
 
         fulltip = repo['tip']
         shallowtip = shallowrepo['tip']
@@ -85,32 +90,22 @@ def _do_case(self, name, stupid, layout)
             self.assertMultiLineEqual(fulltip[f].data(), shallowtip[f].data())
 
 
-def buildmethod(case, name, stupid, layout):
-    m = lambda self: self._do_case(case, stupid, layout)
+def buildmethod(case, name, layout):
+    m = lambda self: self._do_case(case, layout)
     m.__name__ = name
-    bits = case, stupid and 'stupid' or 'real', layout
-    m.__doc__ = 'Test verify on %s with %s replay. (%s)' % bits
+    m.__doc__ = 'Test verify on %s (%s)' % (case, layout)
     return m
 
-attrs = {'_do_case': _do_case}
+attrs = {'_do_case': _do_case, 'stupid_mode_tests': True}
 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')
+        attrs[bname] = buildmethod(case, bname, 'standard')
+    attrs[bname + '_single'] = buildmethod(case, bname + '_single', 'single')
+    if case in test_util.custom:
+        attrs[bname + '_custom'] = buildmethod(case, bname + '_custom', 'custom')
 
 VerifyTests = type('VerifyTests', (test_util.TestBase,), attrs)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(VerifyTests)]
-    return unittest.TestSuite(all_tests)
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/misspelled_branches_tags.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+set -e
+
+mkdir temp
+cd temp
+
+svnadmin create testrepo
+svn checkout file://`pwd`/testrepo client
+
+cd client
+mkdir trunk
+mkdir branchez
+mkdir tagz
+
+svn add trunk branchez tagz
+svn commit -m "Initial commit"
+
+echo "trunk" >> trunk/file
+svn add trunk/file
+svn commit -m "Added file in trunk"
+
+svn cp trunk tagz/tag_from_trunk
+svn ci -m 'created tag from trunk'
+
+svn cp trunk branchez/branch
+svn ci -m 'created branch from trunk'
+
+echo "branch" > branchez/branch/file
+svn ci -m "committed to the branch"
+
+svn cp branchez/branch tagz/tag_from_branch
+svn ci -m "create tag from branch"
+
+cd ..
+svnadmin dump testrepo > ../misspelled_branches_tags.svndump
+
+echo "Created misspelled_branches_tags.svndump"
+echo "You might want to clean up ${PWD} now"
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/misspelled_branches_tags.svndump
@@ -0,0 +1,227 @@
+SVN-fs-dump-format-version: 2
+
+UUID: a4f285b8-14d5-4bc0-92c8-0e5438624f2e
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2013-06-13T00:26:00.303912Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 120
+Content-length: 120
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:26:00.666275Z
+K 7
+svn:log
+V 14
+Initial commit
+PROPS-END
+
+Node-path: branchez
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: tagz
+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: 125
+Content-length: 125
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:26:00.997106Z
+K 7
+svn:log
+V 19
+Added file in trunk
+PROPS-END
+
+Node-path: trunk/file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 6
+Text-content-md5: edf45fe5c98c5367733b39bbb2bb20d9
+Text-content-sha1: 7361d1685e5c86dfc523620cfaf598f196f86239
+Content-length: 16
+
+PROPS-END
+trunk
+
+
+Revision-number: 3
+Prop-content-length: 128
+Content-length: 128
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:26:01.435764Z
+K 7
+svn:log
+V 22
+created tag from trunk
+PROPS-END
+
+Node-path: tagz/tag_from_trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Node-path: tagz/tag_from_trunk/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/file
+Text-copy-source-md5: edf45fe5c98c5367733b39bbb2bb20d9
+Text-copy-source-sha1: 7361d1685e5c86dfc523620cfaf598f196f86239
+
+
+Revision-number: 4
+Prop-content-length: 131
+Content-length: 131
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:26:01.816716Z
+K 7
+svn:log
+V 25
+created branch from trunk
+PROPS-END
+
+Node-path: branchez/branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Node-path: branchez/branch/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/file
+Text-copy-source-md5: edf45fe5c98c5367733b39bbb2bb20d9
+Text-copy-source-sha1: 7361d1685e5c86dfc523620cfaf598f196f86239
+
+
+Revision-number: 5
+Prop-content-length: 129
+Content-length: 129
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:26:02.232496Z
+K 7
+svn:log
+V 23
+committed to the branch
+PROPS-END
+
+Node-path: branchez/branch/file
+Node-kind: file
+Node-action: change
+Text-content-length: 7
+Text-content-md5: 99df69f80e72a660346459fa63c31fd4
+Text-content-sha1: f49390feacc0a7fb2b36ad16dc0bc44036193402
+Content-length: 7
+
+branch
+
+
+Revision-number: 6
+Prop-content-length: 128
+Content-length: 128
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:26:02.596105Z
+K 7
+svn:log
+V 22
+create tag from branch
+PROPS-END
+
+Node-path: tagz/tag_from_branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: branchez/branch
+
+
+Node-path: tagz/tag_from_branch/file
+Node-kind: file
+Node-action: delete
+
+Node-path: tagz/tag_from_branch/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 5
+Node-copyfrom-path: branchez/branch/file
+Text-copy-source-md5: 99df69f80e72a660346459fa63c31fd4
+Text-copy-source-sha1: f49390feacc0a7fb2b36ad16dc0bc44036193402
+
+
+
+
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/subdir_branches_tags.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+set -e
+
+mkdir temp
+cd temp
+
+svnadmin create testrepo
+svn checkout file://`pwd`/testrepo client
+
+cd client
+mkdir trunk
+mkdir -p bran/ches
+mkdir -p ta/gs
+
+svn add trunk bran ta
+svn commit -m "Initial commit"
+
+echo "trunk" >> trunk/file
+svn add trunk/file
+svn commit -m "Added file in trunk"
+
+svn cp trunk ta/gs/tag_from_trunk
+svn ci -m 'created tag from trunk'
+
+svn cp trunk bran/ches/branch
+svn ci -m 'created branch from trunk'
+
+echo "branch" > bran/ches/branch/file
+svn ci -m "committed to the branch"
+
+svn cp bran/ches/branch ta/gs/tag_from_branch
+svn ci -m "create tag from branch"
+
+cd ..
+svnadmin dump testrepo > ../subdir_branches_tags.svndump
+
+echo "Created subdir_branches_tags.svndump"
+echo "You might want to clean up ${PWD} now"
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/subdir_branches_tags.svndump
@@ -0,0 +1,245 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 591b9313-8b8d-45af-bb0f-4d8efe82f2b0
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2013-06-13T00:25:39.145214Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 120
+Content-length: 120
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:25:39.542218Z
+K 7
+svn:log
+V 14
+Initial commit
+PROPS-END
+
+Node-path: bran
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: bran/ches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: ta
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: ta/gs
+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: 125
+Content-length: 125
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:25:39.963701Z
+K 7
+svn:log
+V 19
+Added file in trunk
+PROPS-END
+
+Node-path: trunk/file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 6
+Text-content-md5: edf45fe5c98c5367733b39bbb2bb20d9
+Text-content-sha1: 7361d1685e5c86dfc523620cfaf598f196f86239
+Content-length: 16
+
+PROPS-END
+trunk
+
+
+Revision-number: 3
+Prop-content-length: 128
+Content-length: 128
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:25:40.344923Z
+K 7
+svn:log
+V 22
+created tag from trunk
+PROPS-END
+
+Node-path: ta/gs/tag_from_trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Node-path: ta/gs/tag_from_trunk/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/file
+Text-copy-source-md5: edf45fe5c98c5367733b39bbb2bb20d9
+Text-copy-source-sha1: 7361d1685e5c86dfc523620cfaf598f196f86239
+
+
+Revision-number: 4
+Prop-content-length: 131
+Content-length: 131
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:25:40.700526Z
+K 7
+svn:log
+V 25
+created branch from trunk
+PROPS-END
+
+Node-path: bran/ches/branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Node-path: bran/ches/branch/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk/file
+Text-copy-source-md5: edf45fe5c98c5367733b39bbb2bb20d9
+Text-copy-source-sha1: 7361d1685e5c86dfc523620cfaf598f196f86239
+
+
+Revision-number: 5
+Prop-content-length: 129
+Content-length: 129
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:25:41.081165Z
+K 7
+svn:log
+V 23
+committed to the branch
+PROPS-END
+
+Node-path: bran/ches/branch/file
+Node-kind: file
+Node-action: change
+Text-content-length: 7
+Text-content-md5: 99df69f80e72a660346459fa63c31fd4
+Text-content-sha1: f49390feacc0a7fb2b36ad16dc0bc44036193402
+Content-length: 7
+
+branch
+
+
+Revision-number: 6
+Prop-content-length: 128
+Content-length: 128
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-06-13T00:25:41.512313Z
+K 7
+svn:log
+V 22
+create tag from branch
+PROPS-END
+
+Node-path: ta/gs/tag_from_branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: bran/ches/branch
+
+
+Node-path: ta/gs/tag_from_branch/file
+Node-kind: file
+Node-action: delete
+
+Node-path: ta/gs/tag_from_branch/file
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 5
+Node-copyfrom-path: bran/ches/branch/file
+Text-copy-source-md5: 99df69f80e72a660346459fa63c31fd4
+Text-copy-source-sha1: f49390feacc0a7fb2b36ad16dc0bc44036193402
+
+
+
+
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/subprojects.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+
+set -e
+
+mkdir temp
+cd temp
+
+svnadmin create testrepo
+svn checkout file://`pwd`/testrepo client
+
+cd client
+mkdir trunk
+mkdir -p branches
+mkdir -p tags
+
+svn add trunk branches tags
+svn commit -m "Initial commit"
+
+mkdir trunk/project trunk/other
+echo "project trunk" > trunk/project/file
+echo "other trunk" > trunk/other/phile
+svn add trunk/project trunk/other
+svn commit -m "Added file and phile in trunk"
+
+svn up
+
+svn cp trunk tags/tag_from_trunk
+svn ci -m 'created tag from trunk'
+
+svn up
+
+svn cp trunk branches/branch
+svn ci -m 'created branch from trunk'
+
+svn up
+
+echo "project branch" > branches/branch/project/file
+svn ci -m "committed to the project branch"
+
+svn up
+
+echo "trunk2" > trunk/project/file
+svn ci -m "committed to trunk again"
+
+svn up
+
+echo "other branch" > branches/branch/other/phile
+svn ci -m "committed to the other branch"
+
+svn up
+
+svn cp branches/branch tags/tag_from_branch
+svn ci -m "create tag from branch"
+
+cd ..
+svnadmin dump testrepo > ../subprojects.svndump
+
+echo "Created subprojects.svndump"
+echo "You might want to clean up ${PWD} now"
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/subprojects.svndump
@@ -0,0 +1,283 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 03c99a5f-42f9-43e0-bb0d-03549a88a7e4
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2013-07-23T22:47:56.963334Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 120
+Content-length: 120
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:47:57.401454Z
+K 7
+svn:log
+V 14
+Initial commit
+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: 135
+Content-length: 135
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:47:57.849874Z
+K 7
+svn:log
+V 29
+Added file and phile in trunk
+PROPS-END
+
+Node-path: trunk/other
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/other/phile
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 12
+Text-content-md5: fe5279547ba9d8c257b67c1938853896
+Text-content-sha1: 6c94bf284aa7bc931c358ae3dfcfb4fc9f335579
+Content-length: 22
+
+PROPS-END
+other trunk
+
+
+Node-path: trunk/project
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/project/file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 14
+Text-content-md5: d61b3a5935cb974e41082d9eb8eb912e
+Text-content-sha1: 1e7f7740062dc540ab20fb6cf395cad3c55f396f
+Content-length: 24
+
+PROPS-END
+project trunk
+
+
+Revision-number: 3
+Prop-content-length: 128
+Content-length: 128
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:47:58.281764Z
+K 7
+svn:log
+V 22
+created tag from trunk
+PROPS-END
+
+Node-path: tags/tag_from_trunk
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk
+
+
+Revision-number: 4
+Prop-content-length: 131
+Content-length: 131
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:47:59.456625Z
+K 7
+svn:log
+V 25
+created branch from trunk
+PROPS-END
+
+Node-path: branches/branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 3
+Node-copyfrom-path: trunk
+
+
+Revision-number: 5
+Prop-content-length: 137
+Content-length: 137
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:47:59.862054Z
+K 7
+svn:log
+V 31
+committed to the project branch
+PROPS-END
+
+Node-path: branches/branch/project/file
+Node-kind: file
+Node-action: change
+Text-content-length: 15
+Text-content-md5: 64cdb38c10361681c4c2918a222a3102
+Text-content-sha1: 545ef3bb672a1dd01fb9bd2a2eb7621882a4c701
+Content-length: 15
+
+project branch
+
+
+Revision-number: 6
+Prop-content-length: 130
+Content-length: 130
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:48:00.345069Z
+K 7
+svn:log
+V 24
+committed to trunk again
+PROPS-END
+
+Node-path: trunk/project/file
+Node-kind: file
+Node-action: change
+Text-content-length: 7
+Text-content-md5: 28d0a7e7ef2864416b7a9398623e4d09
+Text-content-sha1: 91454e2d3487f712490f17481157e389c11a6fe0
+Content-length: 7
+
+trunk2
+
+
+Revision-number: 7
+Prop-content-length: 135
+Content-length: 135
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:48:00.751804Z
+K 7
+svn:log
+V 29
+committed to the other branch
+PROPS-END
+
+Node-path: branches/branch/other/phile
+Node-kind: file
+Node-action: change
+Text-content-length: 13
+Text-content-md5: 7c133b867f55c0ba8688e1f111ddebaf
+Text-content-sha1: aee59a1c349cedc1ab035263bd7f14d58c6ab33b
+Content-length: 13
+
+other branch
+
+
+Revision-number: 8
+Prop-content-length: 128
+Content-length: 128
+
+K 10
+svn:author
+V 10
+dschleimer
+K 8
+svn:date
+V 27
+2013-07-23T22:48:01.199203Z
+K 7
+svn:log
+V 22
+create tag from branch
+PROPS-END
+
+Node-path: tags/tag_from_branch
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 7
+Node-copyfrom-path: branches/branch
+
+
old mode 100644
new mode 100755
--- a/tests/run.py
+++ b/tests/run.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python
+
 import optparse
 import os
 import sys
@@ -19,6 +21,7 @@ def tests():
     import test_fetch_symlinks
     import test_fetch_truncated
     import test_hooks
+    import test_svn_pre_commit_hooks
     import test_pull
     import test_pull_fallback
     import test_push_command
@@ -26,20 +29,21 @@ def tests():
     import test_push_dirs
     import test_push_eol
     import test_push_autoprops
-    import test_rebuildmeta
     import test_single_dir_clone
+    import test_single_dir_push
     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_rebuildmeta
     import test_stupid_pull
+    import test_updatemeta
     import test_verify_and_startrev
 
     return locals()
@@ -100,21 +104,24 @@ if __name__ == '__main__':
 
     args = [i.split('.py')[0].replace('-', '_') for i in args]
 
+    loader = unittest.TestLoader()
+    suite = unittest.TestSuite()
+
     if not args:
         check = lambda x: options.comprehensive or not comprehensive(x)
-        mods = [m for (n, m) in sorted(all_tests.iteritems()) if check(m)]
-        suite = [m.suite() for m in mods]
+        suite.addTests(loader.loadTestsFromModule(m)
+                       for (n, m) in sorted(all_tests.iteritems())
+                       if check(m))
     else:
-        suite = []
         for arg in args:
             if arg == 'test_util':
                 continue
             elif arg not in all_tests:
                 print >> sys.stderr, 'test module %s not available' % arg
             else:
-                suite.append(all_tests[arg].suite())
+                suite.addTest(loader.loadTestsFromModule(all_tests[arg]))
 
     runner = unittest.TextTestRunner(**testargs)
-    result = runner.run(unittest.TestSuite(suite))
+    result = runner.run(suite)
     if not result.wasSuccessful():
         sys.exit(1)
--- a/tests/test_binaryfiles.py
+++ b/tests/test_binaryfiles.py
@@ -3,14 +3,8 @@ import test_util
 import unittest
 
 class TestFetchBinaryFiles(test_util.TestBase):
-    def test_binaryfiles(self, stupid=False):
-        repo = self._load_fixture_and_fetch('binaryfiles.svndump', stupid=stupid)
-        self.assertEqual('cce7fe400d8d', str(repo['tip']))
-
-    def test_binaryfiles_stupid(self):
-        self.test_binaryfiles(True)
+    stupid_mode_tests = True
 
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchBinaryFiles),
-          ]
-    return unittest.TestSuite(all_tests)
+    def test_binaryfiles(self):
+        repo = self._load_fixture_and_fetch('binaryfiles.svndump')
+        self.assertEqual('cce7fe400d8d', str(repo['tip']))
--- a/tests/test_diff.py
+++ b/tests/test_diff.py
@@ -35,9 +35,3 @@ class DiffTests(test_util.TestBase):
         u.pushbuffer()
         wrappers.diff(lambda x, y, z: None, u, self.repo, svn=True)
         self.assertEqual(u.popbuffer(), expected_diff_output)
-
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(DiffTests),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_externals.py
+++ b/tests/test_externals.py
@@ -16,6 +16,8 @@ except (ImportError, AttributeError), e:
 from hgsubversion import svnexternals
 
 class TestFetchExternals(test_util.TestBase):
+    stupid_mode_tests = True
+
     def test_externalsfile(self):
         f = svnexternals.externalsfile()
         f['t1'] = 'dir1 -r10 svn://foobar'
@@ -74,8 +76,8 @@ class TestFetchExternals(test_util.TestB
         for line, expected in samples:
             self.assertEqual(expected, svnexternals.parsedefinition(line))
 
-    def test_externals(self, stupid=False):
-        repo = self._load_fixture_and_fetch('externals.svndump', stupid=stupid)
+    def test_externals(self):
+        repo = self._load_fixture_and_fetch('externals.svndump')
 
         ref0 = """[.]
  ^/externals/project1 deps/project1
@@ -125,9 +127,6 @@ class TestFetchExternals(test_util.TestB
 """
         self.assertEqual(ref6, repo[6]['.hgsvnexternals'].data())
 
-    def test_externals_stupid(self):
-        self.test_externals(True)
-
     def test_updateexternals(self):
         def checkdeps(deps, nodeps, repo, rev=None):
             svnexternals.updateexternals(ui, [rev], repo)
@@ -141,7 +140,7 @@ class TestFetchExternals(test_util.TestB
                                 'unexpected: %s@%r' % (d, rev))
 
         ui = self.ui()
-        repo = self._load_fixture_and_fetch('externals.svndump', stupid=0)
+        repo = self._load_fixture_and_fetch('externals.svndump')
         commands.update(ui, repo)
         checkdeps(['deps/project1'], [], repo, 0)
         checkdeps(['deps/project1', 'deps/project2'], [], repo, 1)
@@ -152,12 +151,11 @@ class TestFetchExternals(test_util.TestB
                   ['subdir2/deps/project1'], repo, 3)
         checkdeps(['subdir/deps/project1'], ['deps/project2'], repo, 4)
 
-    def test_hgsub(self, stupid=False):
+    def test_hgsub(self):
         if subrepo is None:
             return
         repo = self._load_fixture_and_fetch('externals.svndump',
-                                            externals='subrepos',
-                                            stupid=stupid)
+                                            externals='subrepos')
         self.assertEqual("""\
 deps/project1 = [hgsubversion] :^/externals/project1 deps/project1
 """, repo[0]['.hgsub'].data())
@@ -217,9 +215,6 @@ deps/project2 = [hgsubversion] :-r{REV} 
 2 deps/project2
 """, repo[6]['.hgsubstate'].data())
 
-    def test_hgsub_stupid(self):
-        self.test_hgsub(True)
-
     def test_ignore(self):
         repo = self._load_fixture_and_fetch('externals.svndump',
                                             externals='ignore')
@@ -246,7 +241,7 @@ 2 deps/project2
 
         ui = self.ui()
         repo = self._load_fixture_and_fetch('externals.svndump',
-                                            stupid=0, externals='subrepos')
+                                            externals='subrepos')
         checkdeps(ui, repo, 0, ['deps/project1'], [])
         checkdeps(ui, repo, 1, ['deps/project1', 'deps/project2'], [])
         checkdeps(ui, repo, 2, ['subdir/deps/project1', 'subdir2/deps/project1',
@@ -260,12 +255,11 @@ 2 deps/project2
         repo.wwrite('subdir/deps/project1/a', 'foobar', '')
         commands.update(ui, repo, node='4', clean=True)
 
-    def test_mergeexternals(self, stupid=False):
+    def test_mergeexternals(self):
         if subrepo is None:
             return
         repo = self._load_fixture_and_fetch('mergeexternals.svndump',
-                                            externals='subrepos',
-                                            stupid=stupid)
+                                            externals='subrepos')
         # Check merged directories externals are fine
         self.assertEqual("""\
 d1/ext = [hgsubversion] d1:^/trunk/common/ext ext
@@ -273,11 +267,11 @@ d2/ext = [hgsubversion] d2:^/trunk/commo
 d3/ext3 = [hgsubversion] d3:^/trunk/common/ext ext3
 """, repo['tip']['.hgsub'].data())
 
-    def test_mergeexternals_stupid(self):
-        self.test_mergeexternals(True)
-
 class TestPushExternals(test_util.TestBase):
-    def test_push_externals(self, stupid=False):
+    stupid_mode_tests = True
+    obsolete_mode_tests = True
+
+    def test_push_externals(self):
         repo = self._load_fixture_and_fetch('pushexternals.svndump')
         # Add a new reference on an existing and non-existing directory
         changes = [
@@ -293,7 +287,7 @@ class TestPushExternals(test_util.TestBa
             ('subdir2/a', 'subdir2/a', 'a'),
             ]
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
 
         # Remove all references from one directory, add a new one
@@ -308,7 +302,7 @@ class TestPushExternals(test_util.TestBa
             ('subdir1/a', None, None),
             ]
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
         # Check subdir2/a is still there even if the externals were removed
         self.assertTrue('subdir2/a' in self.repo['tip'])
@@ -319,13 +313,10 @@ class TestPushExternals(test_util.TestBa
             ('.hgsvnexternals', None, None),
             ]
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
 
-    def test_push_externals_stupid(self):
-        self.test_push_externals(True)
-
-    def test_push_hgsub(self, stupid=False):
+    def test_push_hgsub(self):
         if subrepo is None:
             return
 
@@ -350,7 +341,7 @@ HEAD subdir2/deps/project2
         self.svnco(repo_path, 'externals/project1', '2', 'subdir1/deps/project1')
         self.svnco(repo_path, 'externals/project2', '2', 'subdir2/deps/project2')
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
 
         # Check .hgsub and .hgsubstate were not pushed
@@ -374,7 +365,7 @@ HEAD subdir1/deps/project2
         self.svnco(repo_path, 'externals/project1', '2', 'subdir1/deps/project1')
         self.svnco(repo_path, 'externals/project2', '2', 'subdir1/deps/project2')
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
         # Check subdir2/a is still there even if the externals were removed
         self.assertTrue('subdir2/a' in self.repo['tip'])
@@ -391,7 +382,7 @@ HEAD subdir1/deps/project1
 """),
             ]
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
 
         # Test externals removal
@@ -400,11 +391,5 @@ HEAD subdir1/deps/project1
             ('.hgsubstate', None, None),
             ]
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchExternals),
-           unittest.TestLoader().loadTestsFromTestCase(TestPushExternals),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_fetch_branches.py
+++ b/tests/test_fetch_branches.py
@@ -2,11 +2,15 @@ import test_util
 
 import unittest
 
+from mercurial import error
 from mercurial import hg
 from mercurial import node
-from mercurial import util as hgutil
+
+from hgsubversion import compathacks
 
 class TestFetchBranches(test_util.TestBase):
+    stupid_mode_tests = True
+
     def _load_fixture_and_fetch_with_anchor(self, fixture_name, anchor):
         repo_path = self.load_svndump(fixture_name)
         source = '%s#%s' % (test_util.fileurl(repo_path), anchor)
@@ -24,43 +28,31 @@ class TestFetchBranches(test_util.TestBa
     def openbranches(self, repo):
         return self.branches(repo)[0]
 
-    def test_rename_branch_parent(self, stupid=False):
-        repo = self._load_fixture_and_fetch('rename_branch_parent_dir.svndump',
-                                            stupid=stupid)
+    def test_rename_branch_parent(self):
+        repo = self._load_fixture_and_fetch('rename_branch_parent_dir.svndump')
         heads = [repo[n] for n in repo.heads()]
         heads = dict([(ctx.branch(), ctx) for ctx in heads])
         # Let these tests disabled yet as the fix is not obvious
         self.assertEqual(['dev_branch'], self.openbranches(repo))
 
-    def test_rename_branch_parent_stupid(self):
-        self.test_rename_branch_parent(stupid=True)
-
-    def test_unrelatedbranch(self, stupid=False):
-        repo = self._load_fixture_and_fetch('unrelatedbranch.svndump',
-                                            stupid=stupid)
+    def test_unrelatedbranch(self):
+        repo = self._load_fixture_and_fetch('unrelatedbranch.svndump')
         heads = [repo[n] for n in repo.heads()]
         heads = dict([(ctx.branch(), ctx) for ctx in heads])
         # Let these tests disabled yet as the fix is not obvious
         self.assertEqual(heads['branch1'].manifest().keys(), ['b'])
         self.assertEqual(heads['branch2'].manifest().keys(), ['a', 'b'])
 
-    def test_unrelatedbranch_stupid(self):
-        self.test_unrelatedbranch(True)
-
-    def test_unorderedbranch(self, stupid=False):
-        repo = self._load_fixture_and_fetch('unorderedbranch.svndump',
-                                            stupid=stupid)
+    def test_unorderedbranch(self):
+        repo = self._load_fixture_and_fetch('unorderedbranch.svndump')
         r = repo['branch']
         self.assertEqual(0, r.parents()[0].rev())
         self.assertEqual(['a', 'c', 'z'], sorted(r.manifest()))
 
-    def test_unorderedbranch_stupid(self):
-        self.test_unorderedbranch(True)
-
-    def test_renamed_branch_to_trunk(self, stupid=False):
+    def test_renamed_branch_to_trunk(self):
         config = {'hgsubversion.failonmissing': 'true'}
         repo = self._load_fixture_and_fetch('branch_rename_to_trunk.svndump',
-                                            stupid=stupid, config=config)
+                                            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')
@@ -68,56 +60,40 @@ class TestFetchBranches(test_util.TestBa
         expected = ['default', 'old_trunk']
         self.assertEqual(self.openbranches(repo), expected)
 
-    def test_renamed_branch_to_trunk_stupid(self):
-        self.test_renamed_branch_to_trunk(stupid=True)
-
-    def test_replace_trunk_with_branch(self, stupid=False):
-        repo = self._load_fixture_and_fetch('replace_trunk_with_branch.svndump',
-                                            stupid=stupid)
+    def test_replace_trunk_with_branch(self):
+        repo = self._load_fixture_and_fetch('replace_trunk_with_branch.svndump')
         self.assertEqual(repo['default'].parents()[0].branch(), 'test')
         self.assertEqual(repo['tip'].branch(), 'default')
         self.assertEqual(repo['tip'].extra().get('close'), '1')
         self.assertEqual(self.openbranches(repo), ['default'])
 
-    def test_copybeforeclose(self, stupid=False):
-        repo = self._load_fixture_and_fetch('copybeforeclose.svndump',
-                                            stupid=stupid)
+    def test_copybeforeclose(self):
+        repo = self._load_fixture_and_fetch('copybeforeclose.svndump')
         self.assertEqual(repo['tip'].branch(), 'test')
         self.assertEqual(repo['test'].extra().get('close'), '1')
         self.assertEqual(repo['test']['b'].data(), 'a\n')
 
-    def test_copybeforeclose_stupid(self):
-        self.test_copybeforeclose(True)
-
-    def test_replace_trunk_with_branch_stupid(self):
-        self.test_replace_trunk_with_branch(stupid=True)
-
-    def test_branch_create_with_dir_delete_works(self, stupid=False):
-        repo = self._load_fixture_and_fetch('branch_create_with_dir_delete.svndump',
-                                            stupid=stupid)
+    def test_branch_create_with_dir_delete_works(self):
+        repo = self._load_fixture_and_fetch('branch_create_with_dir_delete.svndump')
         self.assertEqual(repo['tip'].manifest().keys(),
                          ['alpha', 'beta', 'iota', 'gamma', ])
 
-    def test_branch_tip_update_to_default(self, stupid=False):
+    def test_branch_tip_update_to_default(self):
         repo = self._load_fixture_and_fetch('unorderedbranch.svndump',
-                                            stupid=stupid, noupdate=False)
+                                            noupdate=False)
         self.assertEqual(repo[None].branch(), 'default')
         self.assertTrue('tip' not in repo[None].tags())
 
-    def test_branch_tip_update_to_default_stupid(self):
-        self.test_branch_tip_update_to_default(True)
-
     def test_branch_pull_anchor(self):
-        self.assertRaises(hgutil.Abort,
+        self.assertRaises(error.RepoLookupError,
                           self._load_fixture_and_fetch_with_anchor,
                           'unorderedbranch.svndump', 'NaN')
         repo = self._load_fixture_and_fetch_with_anchor(
             'unorderedbranch.svndump', '4')
-        self.assertTrue('c' not in repo.branchtags())
+        self.assertTrue('c' not in compathacks.branchset(repo))
 
-    def test_branches_weird_moves(self, stupid=False):
+    def test_branches_weird_moves(self):
         repo = self._load_fixture_and_fetch('renamedproject.svndump',
-                                            stupid=stupid,
                                             subdir='project')
         heads = [repo[n] for n in repo.heads()]
         heads = dict((ctx.branch(), ctx) for ctx in heads)
@@ -126,21 +102,16 @@ class TestFetchBranches(test_util.TestBa
         self.assertEqual(mdefault, ['a', 'b', 'd/a'])
         self.assertEqual(mbranch, ['a'])
 
-    def test_branches_weird_moves_stupid(self):
-        self.test_branches_weird_moves(True)
-
-    def test_branch_delete_parent_dir(self, stupid=False):
-        repo = self._load_fixture_and_fetch('branch_delete_parent_dir.svndump',
-                                            stupid=stupid)
+    def test_branch_delete_parent_dir(self):
+        repo = self._load_fixture_and_fetch('branch_delete_parent_dir.svndump')
         openb, closedb = self.branches(repo)
         self.assertEqual(openb, [])
         self.assertEqual(closedb, ['dev_branch'])
         self.assertEqual(list(repo['dev_branch']), ['foo'])
 
-    def test_replace_branch_with_branch(self, stupid=False):
-        repo = self._load_fixture_and_fetch('replace_branch_with_branch.svndump',
-                                            stupid=stupid)
-        self.assertEqual(7, len(repo))
+    def test_replace_branch_with_branch(self):
+        repo = self._load_fixture_and_fetch('replace_branch_with_branch.svndump')
+        self.assertEqual(7, test_util.repolen(repo))
         # tip is former topological branch1 being closed
         ctx = repo['tip']
         self.assertEqual('1', ctx.extra().get('close', '0'))
@@ -159,8 +130,67 @@ class TestFetchBranches(test_util.TestBa
         for f in ctx:
             self.assertTrue(not ctx[f].renamed())
 
-    def test_replace_branch_with_branch_stupid(self, stupid=False):
-        self.test_replace_branch_with_branch(True)
+    def test_misspelled_branches_tags(self):
+        config = {
+            'hgsubversion.branchdir': 'branchez',
+            'hgsubversion.tagpaths': 'tagz',
+            }
+        '''Tests using the tags dir for branches and the branches dir for tags'''
+        repo = self._load_fixture_and_fetch('misspelled_branches_tags.svndump',
+                                            layout='standard',
+                                            config=config)
+
+        heads = set([repo[n].branch() for n in repo.heads()])
+        expected_heads = set(['default', 'branch'])
+
+        self.assertEqual(heads, expected_heads)
+
+        tags = set(repo.tags())
+        expected_tags = set(['tip', 'tag_from_trunk', 'tag_from_branch'])
+        self.assertEqual(tags, expected_tags)
+
+    def test_subdir_branches_tags(self):
+        '''Tests using the tags dir for branches and the branches dir for tags'''
+        config = {
+            'hgsubversion.branchdir': 'bran/ches',
+            'hgsubversion.tagpaths': 'ta/gs',
+            }
+        repo = self._load_fixture_and_fetch('subdir_branches_tags.svndump',
+                                            layout='standard',
+                                            config=config)
+
+        heads = set([repo[n].branch() for n in repo.heads()])
+        expected_heads = set(['default', 'branch'])
+
+        self.assertEqual(heads, expected_heads)
+
+        tags = set(repo.tags())
+        expected_tags = set(['tip', 'tag_from_trunk', 'tag_from_branch'])
+        self.assertEqual(tags, expected_tags)
+
+    def test_subproject_fetch(self):
+        config = {
+            'hgsubversion.infix': 'project',
+            }
+        repo = self._load_fixture_and_fetch('subprojects.svndump',
+                                            layout='standard',
+                                            config=config)
+
+        heads = set([repo[n].branch() for n in repo.heads()])
+        expected_heads = set(['default', 'branch'])
+        self.assertEqual(heads, expected_heads)
+
+        tags = set(repo.tags())
+        expected_tags = set(['tip', 'tag_from_trunk', 'tag_from_branch'])
+        self.assertEqual(tags, expected_tags)
+
+        for head in repo.heads():
+            ctx = repo[head]
+            self.assertFalse('project/file' in ctx, 'failed to strip infix')
+            self.assertTrue('file' in ctx, 'failed to track a simple file')
+            self.assertFalse('other/phile' in ctx, 'pulled in other project')
+            self.assertFalse('phile' in ctx, 'merged other project in repo')
+
 
 def suite():
     all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchBranches),
--- a/tests/test_fetch_command.py
+++ b/tests/test_fetch_command.py
@@ -11,6 +11,7 @@ from mercurial import ui
 from mercurial import encoding
 
 class TestBasicRepoLayout(test_util.TestBase):
+    stupid_mode_tests = True
 
     def test_no_dates(self):
         repo = self._load_fixture_and_fetch('test_no_dates.svndump')
@@ -62,17 +63,9 @@ class TestBasicRepoLayout(test_util.Test
         self.assertEqual(repo['tip'], repo['default'])
         self.assertEqual(len(repo.heads()), 2)
 
-    def test_many_special_cases_replay(self):
+    def test_many_special_cases(self):
         repo = self._load_fixture_and_fetch('many_special_cases.svndump')
-        self._many_special_cases_checks(repo)
 
-
-    def test_many_special_cases_diff(self):
-        repo = self._load_fixture_and_fetch('many_special_cases.svndump',
-                                            stupid=True)
-        self._many_special_cases_checks(repo)
-
-    def _many_special_cases_checks(self, repo):
         self.assertEquals(node.hex(repo[0].node()),
                          '434ed487136c1b47c1e8f952edb4dc5a8e6328df')
         # two possible hashes for bw compat to hg < 1.5, since hg 1.5
@@ -95,15 +88,12 @@ class TestBasicRepoLayout(test_util.Test
         assert 'README' not in repo
         assert '../branches' not in repo
 
-    def test_files_copied_from_outside_btt(self, stupid=False):
+    def test_files_copied_from_outside_btt(self):
         repo = self._load_fixture_and_fetch(
-            'test_files_copied_from_outside_btt.svndump', stupid=stupid)
+            'test_files_copied_from_outside_btt.svndump')
         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)
+        self.assertEqual(test_util.repolen(repo.changelog), 2)
 
     def test_file_renamed_in_from_outside_btt(self):
         repo = self._load_fixture_and_fetch(
@@ -130,32 +120,21 @@ class TestBasicRepoLayout(test_util.Test
         self.assertEqual(node.hex(repo['tip'].node()),
                          '1a6c3f30911d57abb67c257ec0df3e7bc44786f7')
 
-    def test_propedit_with_nothing_else(self, stupid=False):
-        repo = self._load_fixture_and_fetch('branch_prop_edit.svndump',
-                                            stupid=stupid)
+    def test_propedit_with_nothing_else(self):
+        repo = self._load_fixture_and_fetch('branch_prop_edit.svndump')
         self.assertEqual(repo['tip'].description(), 'Commit bogus propchange.')
         self.assertEqual(repo['tip'].branch(), 'dev_branch')
 
-    def test_propedit_with_nothing_else_stupid(self):
-        self.test_propedit_with_nothing_else(stupid=True)
-
-    def test_entry_deletion(self, stupid=False):
-        repo = self._load_fixture_and_fetch('delentries.svndump',
-                                            stupid=stupid)
+    def test_entry_deletion(self):
+        repo = self._load_fixture_and_fetch('delentries.svndump')
         files = list(sorted(repo['tip'].manifest()))
         self.assertEqual(['aa', 'd1/c', 'd1/d2prefix'], files)
 
-    def test_entry_deletion_stupid(self):
-        self.test_entry_deletion(stupid=True)
-
-    def test_fetch_when_trunk_has_no_files(self, stupid=False):
-        repo = self._load_fixture_and_fetch('file_not_in_trunk_root.svndump', stupid=stupid)
+    def test_fetch_when_trunk_has_no_files(self):
+        repo = self._load_fixture_and_fetch('file_not_in_trunk_root.svndump')
         self.assertEqual(repo['tip'].branch(), 'default')
 
-    def test_fetch_when_trunk_has_no_files_stupid(self):
-        self.test_fetch_when_trunk_has_no_files(stupid=True)
-
-    def test_path_quoting(self, stupid=False):
+    def test_path_quoting(self):
         repo_path = self.load_svndump('non_ascii_path_1.svndump')
         subdir = '/b\xC3\xB8b'
         quoted_subdir = urllib.quote(subdir)
@@ -164,7 +143,7 @@ class TestBasicRepoLayout(test_util.Test
         wc_path = self.wc_path
         wc2_path = wc_path + '-2'
 
-        ui = self.ui(stupid=stupid)
+        ui = self.ui()
 
         commands.clone(ui, repo_url + subdir, wc_path)
         commands.clone(ui, repo_url + quoted_subdir, wc2_path)
@@ -173,15 +152,11 @@ class TestBasicRepoLayout(test_util.Test
 
         self.assertEqual(repo['tip'].extra()['convert_revision'],
                          repo2['tip'].extra()['convert_revision'])
-        self.assertEqual(len(repo), len(repo2))
+        self.assertEqual(test_util.repolen(repo), test_util.repolen(repo2))
 
         for r in repo:
             self.assertEqual(repo[r].hex(), repo2[r].hex())
 
-    def test_path_quoting_stupid(self):
-        repo = self.test_path_quoting(True)
-
-
     def test_identical_fixtures(self):
         '''ensure that the non_ascii_path_N fixtures are identical'''
         fixturepaths = [
@@ -200,8 +175,10 @@ class TestBasicRepoLayout(test_util.Test
 
 
 class TestStupidPull(test_util.TestBase):
+    stupid_mode_tests = True
+
     def test_stupid(self):
-        repo = self._load_fixture_and_fetch('two_heads.svndump', stupid=True)
+        repo = self._load_fixture_and_fetch('two_heads.svndump')
         self.assertEqual(node.hex(repo[0].node()),
                          '434ed487136c1b47c1e8f952edb4dc5a8e6328df')
         self.assertEqual(node.hex(repo['tip'].node()),
@@ -220,8 +197,7 @@ class TestStupidPull(test_util.TestBase)
 
     def test_oldest_not_trunk_and_tag_vendor_branch(self):
         repo = self._load_fixture_and_fetch(
-            'tagged_vendor_and_oldest_not_trunk.svndump',
-            stupid=True)
+            'tagged_vendor_and_oldest_not_trunk.svndump')
         self.assertEqual(node.hex(repo['oldest'].node()),
                          '926671740dec045077ab20f110c1595f935334fa')
         self.assertEqual(repo['tip'].parents()[0].parents()[0],
@@ -229,65 +205,53 @@ class TestStupidPull(test_util.TestBase)
         self.assertEqual(node.hex(repo['tip'].node()),
                          '1a6c3f30911d57abb67c257ec0df3e7bc44786f7')
 
-    def test_empty_repo(self, stupid=False):
+    def test_empty_repo(self):
         # 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)
+        self._load_fixture_and_fetch('emptyrepo2.svndump')
 
-    def test_fetch_revert(self, stupid=False):
-        repo = self._load_fixture_and_fetch('revert.svndump', stupid=stupid)
+    def test_fetch_revert(self):
+        repo = self._load_fixture_and_fetch('revert.svndump')
         graph = self.getgraph(repo)
         refgraph = """\
-o  changeset: 3:937dcd1206d4
+o  changeset: 3:937dcd1206d4 (r4)
 |  branch:
 |  tags:      tip
 |  summary:   revert2
 |  files:     a dir/b
 |
-o  changeset: 2:9317a748b7c3
+o  changeset: 2:9317a748b7c3 (r3)
 |  branch:
 |  tags:
 |  summary:   revert
 |  files:     a dir/b
 |
-o  changeset: 1:243259a4138a
+o  changeset: 1:243259a4138a (r2)
 |  branch:
 |  tags:
 |  summary:   changefiles
 |  files:     a dir/b
 |
-o  changeset: 0:ab86791fc857
+o  changeset: 0:ab86791fc857 (r1)
    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)
+"""
+        self.assertMultiLineEqual(refgraph, graph)
 
-    def test_fetch_movetotrunk(self, stupid=False):
+    def test_fetch_movetotrunk(self):
         repo = self._load_fixture_and_fetch('movetotrunk.svndump',
-                stupid=stupid, subdir='sub1/sub2')
+                subdir='sub1/sub2')
         graph = self.getgraph(repo)
         refgraph = """\
-o  changeset: 0:02996a5980ba
+o  changeset: 0:02996a5980ba (r3)
    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)
+"""
+        self.assertMultiLineEqual(refgraph, graph)
 
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestBasicRepoLayout),
-           unittest.TestLoader().loadTestsFromTestCase(TestStupidPull),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_fetch_command_regexes.py
+++ b/tests/test_fetch_command_regexes.py
@@ -1,5 +1,7 @@
 import unittest
 
+import test_util
+
 from hgsubversion import stupid
 
 
@@ -141,6 +143,3 @@ Added: svn:executable
         changed = stupid.parsediff(data)
         self.assertEqual(['empty1', 'empty2', 'binary1', 'text1', 'binary2', 'text2'],
                          [f.name for f in changed])
-
-def suite():
-    return unittest.TestLoader().loadTestsFromTestCase(RegexTests)
--- a/tests/test_fetch_exec.py
+++ b/tests/test_fetch_exec.py
@@ -5,31 +5,21 @@ import unittest
 from mercurial import node
 
 class TestFetchExec(test_util.TestBase):
+    stupid_mode_tests = True
+
     def assertexec(self, ctx, files, isexec=True):
         for f in files:
             self.assertEqual(isexec, 'x' in ctx[f].flags())
 
-    def test_exec(self, stupid=False):
-        repo = self._load_fixture_and_fetch('executebit.svndump', stupid=stupid)
+    def test_exec(self):
+        repo = self._load_fixture_and_fetch('executebit.svndump')
         self.assertexec(repo[0], ['text1', 'binary1', 'empty1'], True)
         self.assertexec(repo[0], ['text2', 'binary2', 'empty2'], False)
         self.assertexec(repo[1], ['text1', 'binary1', 'empty1'], False)
         self.assertexec(repo[1], ['text2', 'binary2', 'empty2'], True)
 
-    def test_exec_stupid(self):
-        self.test_exec(True)
-
-    def test_empty_prop_val_executable(self, stupid=False):
-        repo = self._load_fixture_and_fetch('executable_file_empty_prop.svndump',
-                                            stupid=stupid)
+    def test_empty_prop_val_executable(self):
+        repo = self._load_fixture_and_fetch('executable_file_empty_prop.svndump')
         self.assertEqual(node.hex(repo['tip'].node()),
                          '08e6b380bf291b361a418203a1cb9427213cd1fd')
         self.assertEqual(repo['tip']['foo'].flags(), 'x')
-
-    def test_empty_prop_val_executable_stupid(self):
-        self.test_empty_prop_val_executable(True)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchExec),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_fetch_mappings.py
+++ b/tests/test_fetch_mappings.py
@@ -16,6 +16,8 @@ from hgsubversion import util
 from hgsubversion import verify
 
 class MapTests(test_util.TestBase):
+    stupid_mode_tests = True
+
     @property
     def authors(self):
         return os.path.join(self.tmpdir, 'authormap')
@@ -32,13 +34,13 @@ class MapTests(test_util.TestBase):
     def tagmap(self):
         return os.path.join(self.tmpdir, 'tagmap')
 
-    def test_author_map(self, stupid=False):
+    def test_author_map(self):
         repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
         authormap = open(self.authors, 'w')
         authormap.write('Augie=Augie Fackler <durin42@gmail.com> # stuffy\n')
         authormap.write("Augie Fackler <durin42@gmail.com>\n")
         authormap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'authormap', self.authors)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, authors=self.authors)
@@ -47,15 +49,12 @@ class MapTests(test_util.TestBase):
         self.assertEqual(self.repo['tip'].user(),
                         'evil@5b65bade-98f3-4993-a01f-b7a6710da339')
 
-    def test_author_map_stupid(self):
-        self.test_author_map(True)
-
-    def test_author_map_closing_author(self, stupid=False):
+    def test_author_map_closing_author(self):
         repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
         authormap = open(self.authors, 'w')
         authormap.write("evil=Testy <test@test>")
         authormap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'authormap', self.authors)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, authors=self.authors)
@@ -64,12 +63,8 @@ class MapTests(test_util.TestBase):
         self.assertEqual(self.repo['tip'].user(),
                         'Testy <test@test>')
 
-    def test_author_map_closing_author_stupid(self):
-        self.test_author_map_closing_author(True)
-
-    def test_author_map_no_author(self, stupid=False):
-        repo, repo_path = self.load_and_fetch('no-author.svndump',
-                                              stupid=stupid)
+    def test_author_map_no_author(self):
+        repo, repo_path = self.load_and_fetch('no-author.svndump')
         users = set(self.repo[r].user() for r in self.repo)
         expected_users = ['(no author)@%s' % self.repo.svnmeta().uuid]
         self.assertEqual(sorted(users), expected_users)
@@ -78,7 +73,7 @@ class MapTests(test_util.TestBase):
         authormap = open(self.authors, 'w')
         authormap.write("(no author)=Testy <test@example.com>")
         authormap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'authormap', self.authors)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, authors=self.authors)
@@ -86,9 +81,6 @@ class MapTests(test_util.TestBase):
         expected_users = ['Testy <test@example.com>']
         self.assertEqual(sorted(users), expected_users)
 
-    def test_author_map_no_author_stupid(self):
-        self.test_author_map_no_author(True)
-
     def test_author_map_no_overwrite(self):
         cwd = os.path.dirname(__file__)
         orig = os.path.join(cwd, 'fixtures', 'author-map-test.txt')
@@ -101,13 +93,29 @@ class MapTests(test_util.TestBase):
         all_tests = set(test)
         self.assertEqual(fromself.symmetric_difference(all_tests), set())
 
-    def _loadwithfilemap(self, svndump, filemapcontent, stupid=False,
+    def test_author_map_caseignore(self):
+        repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
+        authormap = open(self.authors, 'w')
+        authormap.write('augie=Augie Fackler <durin42@gmail.com> # stuffy\n')
+        authormap.write("Augie Fackler <durin42@gmail.com>\n")
+        authormap.close()
+        ui = self.ui()
+        ui.setconfig('hgsubversion', 'authormap', self.authors)
+        ui.setconfig('hgsubversion', 'caseignoreauthors', True)
+        commands.clone(ui, test_util.fileurl(repo_path),
+                       self.wc_path, authors=self.authors)
+        self.assertEqual(self.repo[0].user(),
+                         'Augie Fackler <durin42@gmail.com>')
+        self.assertEqual(self.repo['tip'].user(),
+                        'evil@5b65bade-98f3-4993-a01f-b7a6710da339')
+
+    def _loadwithfilemap(self, svndump, filemapcontent,
             failonmissing=True):
         repo_path = self.load_svndump(svndump)
         filemap = open(self.filemap, 'w')
         filemap.write(filemapcontent)
         filemap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'filemap', self.filemap)
         ui.setconfig('hgsubversion', 'failoninvalidreplayfile', 'true')
         ui.setconfig('hgsubversion', 'failonmissing', failonmissing)
@@ -115,26 +123,21 @@ class MapTests(test_util.TestBase):
                        self.wc_path, filemap=self.filemap)
         return self.repo
 
-    def test_file_map(self, stupid=False):
+    @test_util.requiresreplay
+    def test_file_map(self):
         repo = self._loadwithfilemap('replace_trunk_with_branch.svndump',
-            "include alpha\n", stupid)
+            "include alpha\n")
         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):
+    @test_util.requiresreplay
+    def test_file_map_exclude(self):
         repo = self._loadwithfilemap('replace_trunk_with_branch.svndump',
-            "exclude alpha\n", stupid)
+            "exclude alpha\n")
         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)
-
+    @test_util.requiresreplay
     def test_file_map_rule_order(self):
         repo = self._loadwithfilemap('replace_trunk_with_branch.svndump',
             "exclude alpha\ninclude .\nexclude gamma\n")
@@ -146,6 +149,7 @@ class MapTests(test_util.TestBase):
         self.assertEqual(self.repo['default'].manifest().keys(),
                          ['alpha', 'beta'])
 
+    @test_util.requiresreplay
     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
@@ -154,6 +158,7 @@ class MapTests(test_util.TestBase):
                 failonmissing=False)
         self.assertEqual(['dir/a', 'dir3/a'], list(repo[2]))
 
+    @test_util.requiresreplay
     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.
@@ -161,6 +166,7 @@ class MapTests(test_util.TestBase):
                 "exclude dir2\nexclude dir3\n")
         self.assertEqual(['dir/a'], list(repo[2]))
 
+    @test_util.requiresreplay
     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.
@@ -169,17 +175,18 @@ class MapTests(test_util.TestBase):
                 failonmissing=False)
         self.assertEqual(['dir/a', 'dir3/a'], list(repo[2]))
 
+    @test_util.requiresreplay
     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):
+    def test_branchmap(self):
         repo_path = self.load_svndump('branchmap.svndump')
         branchmap = open(self.branchmap, 'w')
         branchmap.write("badname = good-name # stuffy\n")
         branchmap.write("feature = default\n")
         branchmap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, branchmap=self.branchmap)
@@ -188,50 +195,40 @@ class MapTests(test_util.TestBase):
         self.assert_('good-name' in branches)
         self.assertEquals(self.repo[2].branch(), 'default')
 
-    def test_branchmap_stupid(self):
-        self.test_branchmap(True)
-
-    def test_branchmap_tagging(self, stupid=False):
+    def test_branchmap_tagging(self):
         '''test tagging a renamed branch, which used to raise an exception'''
         repo_path = self.load_svndump('commit-to-tag.svndump')
         branchmap = open(self.branchmap, 'w')
         branchmap.write("magic = art\n")
         branchmap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, branchmap=self.branchmap)
         branches = set(self.repo[i].branch() for i in self.repo)
         self.assertEquals(sorted(branches), ['art', 'closeme'])
 
-    def test_branchmap_tagging_stupid(self):
-        self.test_branchmap_tagging(True)
-
-    def test_branchmap_empty_commit(self, stupid=False):
+    def test_branchmap_empty_commit(self):
         '''test mapping an empty commit on a renamed branch'''
         repo_path = self.load_svndump('propset-branch.svndump')
         branchmap = open(self.branchmap, 'w')
         branchmap.write("the-branch = bob\n")
         branchmap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, branchmap=self.branchmap)
         branches = set(self.repo[i].branch() for i in self.repo)
         self.assertEquals(sorted(branches), ['bob', 'default'])
 
-    def test_branchmap_empty_commit_stupid(self):
-        '''test mapping an empty commit on a renamed branch (stupid)'''
-        self.test_branchmap_empty_commit(True)
-
-    def test_branchmap_combine(self, stupid=False):
+    def test_branchmap_combine(self):
         '''test combining two branches, but retaining heads'''
         repo_path = self.load_svndump('branchmap.svndump')
         branchmap = open(self.branchmap, 'w')
         branchmap.write("badname = default\n")
         branchmap.write("feature = default\n")
         branchmap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, branchmap=self.branchmap)
@@ -245,25 +242,21 @@ class MapTests(test_util.TestBase):
         self.assertEquals(sorted(branches.keys()),
                           [None, 'badname', 'feature'])
 
-    def test_branchmap_combine_stupid(self):
-        '''test combining two branches, but retaining heads (stupid)'''
-        self.test_branchmap_combine(True)
-
-    def test_branchmap_rebuildmeta(self, stupid=False):
+    def test_branchmap_rebuildmeta(self):
         '''test rebuildmeta on a branchmapped clone'''
         repo_path = self.load_svndump('branchmap.svndump')
         branchmap = open(self.branchmap, 'w')
         branchmap.write("badname = dit\n")
         branchmap.write("feature = dah\n")
         branchmap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, branchmap=self.branchmap)
         originfo = self.repo.svnmeta().branches
 
         # clone & rebuild
-        ui = self.ui(stupid)
+        ui = self.ui()
         src, dest = test_util.hgclone(ui, self.wc_path, self.wc_path + '_clone',
                                       update=False)
         src = test_util.getlocalpeer(src)
@@ -276,18 +269,14 @@ class MapTests(test_util.TestBase):
         self.assertEquals(sorted(src.svnmeta().branches),
                           sorted(dest.svnmeta().branches))
 
-    def test_branchmap_rebuildmeta_stupid(self):
-        '''test rebuildmeta on a branchmapped clone (stupid)'''
-        self.test_branchmap_rebuildmeta(True)
-
-    def test_branchmap_verify(self, stupid=False):
+    def test_branchmap_verify(self):
         '''test verify on a branchmapped clone'''
         repo_path = self.load_svndump('branchmap.svndump')
         branchmap = open(self.branchmap, 'w')
         branchmap.write("badname = dit\n")
         branchmap.write("feature = dah\n")
         branchmap.close()
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, branchmap=self.branchmap)
@@ -296,10 +285,6 @@ class MapTests(test_util.TestBase):
         for r in repo:
             self.assertEquals(verify.verify(ui, repo, rev=r), 0)
 
-    def test_branchmap_verify_stupid(self):
-        '''test verify on a branchmapped clone (stupid)'''
-        self.test_branchmap_verify(True)
-
     def test_branchmap_no_replacement(self):
         '''
         test that empty mappings are rejected
@@ -315,14 +300,14 @@ class MapTests(test_util.TestBase):
         self.assertRaises(hgutil.Abort,
                           maps.BranchMap, self.ui(), self.branchmap)
 
-    def test_tagmap(self, stupid=False):
+    def test_tagmap(self):
         repo_path = self.load_svndump('basic_tag_tests.svndump')
         tagmap = open(self.tagmap, 'w')
         tagmap.write("tag_r3 = 3.x # stuffy\n")
         tagmap.write("copied_tag = \n")
         tagmap.close()
 
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'tagmap', self.tagmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, tagmap=self.tagmap)
@@ -331,10 +316,7 @@ class MapTests(test_util.TestBase):
         assert '3.x' in tags
         assert 'copied_tag' not in tags
 
-    def test_tagmap_stupid(self):
-        self.test_tagmap(True)
-
-    def test_tagren_changed(self, stupid=False):
+    def test_tagren_changed(self):
         repo_path = self.load_svndump('commit-to-tag.svndump')
         tagmap = open(self.tagmap, 'w')
         tagmap.write("edit-at-create = edit-past\n")
@@ -342,32 +324,21 @@ class MapTests(test_util.TestBase):
         tagmap.write("will-edit = edit-future\n")
         tagmap.close()
 
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'tagmap', self.tagmap)
         commands.clone(ui, test_util.fileurl(repo_path),
                        self.wc_path, tagmap=self.tagmap)
         tags = self.repo.tags()
 
-    def test_tagren_changed_stupid(self):
-        self.test_tagren_changed(True)
-
-    def test_empty_log_message(self, stupid=False):
-        repo, repo_path = self.load_and_fetch('empty-log-message.svndump',
-                                              stupid=stupid)
+    def test_empty_log_message(self):
+        repo, repo_path = self.load_and_fetch('empty-log-message.svndump')
 
         self.assertEqual(repo['tip'].description(), '')
 
         test_util.rmtree(self.wc_path)
 
-        ui = self.ui(stupid)
+        ui = self.ui()
         ui.setconfig('hgsubversion', 'defaultmessage', 'blyf')
         commands.clone(ui, test_util.fileurl(repo_path), self.wc_path)
 
         self.assertEqual(self.repo['tip'].description(), 'blyf')
-
-
-    def test_empty_log_message_stupid(self):
-        self.test_empty_log_message(True)
-
-def suite():
-    return unittest.TestLoader().loadTestsFromTestCase(MapTests)
--- a/tests/test_fetch_renames.py
+++ b/tests/test_fetch_renames.py
@@ -4,6 +4,8 @@ import sys
 import unittest
 
 class TestFetchRenames(test_util.TestBase):
+    stupid_mode_tests = True
+
     def _debug_print_copies(self, repo):
         w = sys.stderr.write
         for rev in repo:
@@ -13,12 +15,11 @@ class TestFetchRenames(test_util.TestBas
                 fctx = ctx[f]
                 w('%s: %r %r\n' % (f, fctx.data(), fctx.renamed()))
 
-    def _test_rename(self, stupid):
+    def test_rename(self):
         config = {
             'hgsubversion.filestoresize': '0',
             }
-        repo = self._load_fixture_and_fetch('renames.svndump', stupid=stupid,
-                config=config)
+        repo = self._load_fixture_and_fetch('renames.svndump', config=config)
 
         # Map revnum to mappings of dest name to (source name, dest content)
         copies = {
@@ -56,28 +57,11 @@ class TestFetchRenames(test_util.TestBas
 
         self.assertEqual(repo['tip']['changed3'].data(), 'changed\nchanged3\n')
 
-    def test_rename(self):
-        self._test_rename(False)
-
-    def test_rename_stupid(self):
-        self._test_rename(True)
-
-    def _test_case(self, stupid):
-        repo = self._load_fixture_and_fetch('filecase.svndump', stupid=stupid)
+    def test_case(self):
+        repo = self._load_fixture_and_fetch('filecase.svndump')
         files = {
             0: ['A', 'a', 'e/a', 'b', 'd/a', 'D/a', 'f/a', 'F'],
             1: ['A', 'a', 'E/a', 'B', 'd/A', 'D/a', 'f/a', 'F'],
             }
         for rev in repo:
             self.assertEqual(sorted(files[rev]), sorted(repo[rev].manifest()))
-
-    def test_case(self):
-        self._test_case(False)
-
-    def test_case_stupid(self):
-        self._test_case(True)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchRenames),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_fetch_symlinks.py
+++ b/tests/test_fetch_symlinks.py
@@ -3,8 +3,10 @@ import test_util
 import unittest
 
 class TestFetchSymlinks(test_util.TestBase):
-    def test_symlinks(self, stupid=False):
-        repo = self._load_fixture_and_fetch('symlinks.svndump', stupid=stupid)
+    stupid_mode_tests = True
+
+    def test_symlinks(self):
+        repo = self._load_fixture_and_fetch('symlinks.svndump')
         # Check symlinks throughout history
         links = {
             0: {
@@ -47,20 +49,12 @@ class TestFetchSymlinks(test_util.TestBa
             for f in links[rev]:
                 self.assertTrue(f in ctx)
 
-    def test_symlinks_stupid(self):
-        self.test_symlinks(True)
-
 class TestMergeSpecial(test_util.TestBase):
+    stupid_mode_tests = True
+
     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),
-        unittest.TestLoader().loadTestsFromTestCase(TestMergeSpecial),
-        ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_fetch_truncated.py
+++ b/tests/test_fetch_truncated.py
@@ -6,12 +6,14 @@ from mercurial import commands
 from mercurial import hg
 
 class TestFetchTruncatedHistory(test_util.TestBase):
-    def test_truncated_history(self, stupid=False):
+    stupid_mode_tests = True
+
+    def test_truncated_history(self):
         # Test repository does not follow the usual layout
         repo_path = self.load_svndump('truncatedhistory.svndump')
         svn_url = test_util.fileurl(repo_path + '/project2')
-        commands.clone(self.ui(stupid), svn_url, self.wc_path, noupdate=True)
-        repo = hg.repository(self.ui(stupid), self.wc_path)
+        commands.clone(self.ui(), svn_url, self.wc_path, noupdate=True)
+        repo = hg.repository(self.ui(), self.wc_path)
 
         # We are converting /project2/trunk coming from:
         #
@@ -26,11 +28,3 @@ class TestFetchTruncatedHistory(test_uti
         files.sort()
         self.assertEqual(files, ['a', 'b'])
         self.assertEqual(repo['tip']['a'].data(), 'a\n')
-
-    def test_truncated_history_stupid(self):
-        self.test_truncated_history(True)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchTruncatedHistory),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -30,8 +30,3 @@ class TestHelpers(unittest.TestCase):
         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),
-        ])
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -11,7 +11,7 @@ class TestHooks(test_util.TestBase):
 
     def _loadupdate(self, fixture_name, *args, **kwargs):
         kwargs = kwargs.copy()
-        kwargs.update(stupid=False, noupdate=False)
+        kwargs.update(noupdate=False)
         repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs)
         return repo, repo_path
 
@@ -43,6 +43,3 @@ class TestHooks(test_util.TestBase):
         commands.pull(newrepo.ui, newrepo)
         hgsubversion.svncommands.updatemeta = oldupdatemeta
         self.assertTrue(self.called)
-
-def suite():
-    return unittest.findTestCases(sys.modules[__name__])
--- a/tests/test_pull.py
+++ b/tests/test_pull.py
@@ -14,7 +14,7 @@ class TestPull(test_util.TestBase):
 
     def _loadupdate(self, fixture_name, *args, **kwargs):
         kwargs = kwargs.copy()
-        kwargs.update(stupid=False, noupdate=False)
+        kwargs.update(noupdate=False)
         repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs)
         return repo, repo_path
 
@@ -79,7 +79,3 @@ class TestPull(test_util.TestBase):
         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__])
--- a/tests/test_pull_fallback.py
+++ b/tests/test_pull_fallback.py
@@ -31,8 +31,9 @@ class TestPullFallback(test_util.TestBas
             'stupid.fetch_branchrev': 1,
         }
 
-        repo, repo_path = self._loadupdate(
-            'single_rev.svndump', stupid=True)
+        self.stupid = True
+        repo, repo_path = self._loadupdate('single_rev.svndump')
+        self.stupid = False
 
         # Passing stupid=True doesn't seem to be working - force it
         repo.ui.setconfig('hgsubversion', 'stupid', "true")
@@ -100,7 +101,3 @@ def _monkey_unpatch(to_patch, start=None
 
 def _patchbackend_raise(*p, **kw):
     raise mercurial.patch.PatchError("patch failed")
-
-def suite():
-    import unittest, sys
-    return unittest.findTestCases(sys.modules[__name__])
--- a/tests/test_push_autoprops.py
+++ b/tests/test_push_autoprops.py
@@ -8,6 +8,8 @@ import test_util
 from hgsubversion import svnwrap
 
 class PushAutoPropsTests(test_util.TestBase):
+    obsolete_mode_tests = True
+
     def setUp(self):
         test_util.TestBase.setUp(self)
         repo, self.repo_path = self.load_and_fetch('emptyrepo.svndump')
@@ -20,7 +22,7 @@ class PushAutoPropsTests(test_util.TestB
             "*.py = test:prop=success\n")
         changes = [('test.py', 'test.py', 'echo hallo')]
         self.commitchanges(changes)
-        self.pushrevisions(True)
+        self.pushrevisions()
         prop_val = test_util.svnpropget(
             self.repo_path, "trunk/test.py", 'test:prop')
         self.assertEqual('success', prop_val)
@@ -100,8 +102,3 @@ class ParseAutoPropsTests(test_util.Test
             'svn:eol-style': 'native',
             'svn:executable': 'true'},
             props)
-
-
-def suite():
-    return unittest.findTestCases(sys.modules[__name__])
-
--- a/tests/test_push_command.py
+++ b/tests/test_push_command.py
@@ -17,10 +17,14 @@ from mercurial import node
 from mercurial import revlog
 from mercurial import util as hgutil
 
+from hgsubversion import util
+
 import time
 
 
 class PushTests(test_util.TestBase):
+    obsolete_mode_tests = True
+
     def setUp(self):
         test_util.TestBase.setUp(self)
         self.repo_path = self.load_and_fetch('simple_branch.svndump')[1]
@@ -50,6 +54,54 @@ class PushTests(test_util.TestBase):
         tip = self.repo['tip']
         self.assertEqual(tip.node(), old_tip)
 
+    def test_push_add_of_added_upstream_gives_sane_error(self):
+        repo = self.repo
+        def file_callback(repo, memctx, path):
+            if path == 'adding_file':
+                return context.memfilectx(path=path,
+                                          data='foo',
+                                          islink=False,
+                                          isexec=False,
+                                          copied=False)
+            raise IOError()
+        p1 = repo['default'].node()
+        ctx = context.memctx(repo,
+                             (p1, node.nullid),
+                             'automated test',
+                             ['adding_file'],
+                             file_callback,
+                             'an_author',
+                             '2008-10-07 20:59:48 -0500',
+                             {'branch': 'default', })
+        new_hash = repo.commitctx(ctx)
+        hg.update(repo, repo['tip'].node())
+        old_tip = repo['tip'].node()
+        self.pushrevisions()
+        tip = self.repo['tip']
+        self.assertNotEqual(tip.node(), old_tip)
+
+        # This node adds the same file as the first one we added, and
+        # will be refused by the server for adding a file that already
+        # exists. We should respond with an error suggesting the user
+        # rebase.
+        ctx = context.memctx(repo,
+                             (p1, node.nullid),
+                             'automated test',
+                             ['adding_file'],
+                             file_callback,
+                             'an_author',
+                             '2008-10-07 20:59:48 -0500',
+                             {'branch': 'default', })
+        new_hash = repo.commitctx(ctx)
+        hg.update(repo, repo['tip'].node())
+        old_tip = repo['tip'].node()
+        try:
+          self.pushrevisions()
+        except hgutil.Abort, e:
+          assert "pull again and rebase" in str(e)
+        tip = self.repo['tip']
+        self.assertEqual(tip.node(), old_tip)
+
     def test_cant_push_with_changes(self):
         repo = self.repo
         def file_callback(repo, memctx, path):
@@ -522,14 +574,110 @@ class PushTests(test_util.TestBase):
         self.pushrevisions()
         self.assertEqual(['alpha'], list(self.repo['tip'].manifest()))
 
-def suite():
-    test_classes = [PushTests, ]
-    all_tests = []
-    # This is the quickest hack I could come up with to load all the tests from
-    # both classes. Would love a patch that simplifies this without adding
-    # dependencies.
-    for tc in test_classes:
-        for attr in dir(tc):
-            if attr.startswith('test_'):
-                all_tests.append(tc(attr))
-    return unittest.TestSuite(all_tests)
+    def test_push_without_pushing_children(self):
+        '''
+        Verify that a push of a nontip node, keeps the tip child
+        on top of the pushed commit.
+        '''
+
+        oldlen = test_util.repolen(self.repo)
+        oldtiphash = self.repo['default'].node()
+
+        changes = [('gamma', 'gamma', 'sometext')]
+        newhash1 = self.commitchanges(changes)
+
+        changes = [('delta', 'delta', 'sometext')]
+        newhash2 = self.commitchanges(changes)
+
+        # push only the first commit
+        repo = self.repo
+        hg.update(repo, newhash1)
+        commands.push(repo.ui, repo)
+        self.assertEqual(test_util.repolen(self.repo), oldlen + 2)
+
+        # verify that the first commit is pushed, and the second is not
+        commit2 = self.repo['tip']
+        self.assertEqual(commit2.files(), ['delta', ])
+        self.assertEqual(util.getsvnrev(commit2), None)
+        commit1 = commit2.parents()[0]
+        self.assertEqual(commit1.files(), ['gamma', ])
+        prefix = 'svn:' + self.repo.svnmeta().uuid
+        self.assertEqual(util.getsvnrev(commit1),
+                         prefix + '/branches/the_branch@5')
+
+    def test_push_two_that_modify_same_file(self):
+        '''
+        Push performs a rebase if two commits touch the same file.
+        This test verifies that code path works.
+        '''
+
+        oldlen = test_util.repolen(self.repo)
+        oldtiphash = self.repo['default'].node()
+
+        changes = [('gamma', 'gamma', 'sometext')]
+        newhash = self.commitchanges(changes)
+        changes = [('gamma', 'gamma', 'sometext\n moretext'),
+                   ('delta', 'delta', 'sometext\n moretext'),
+                  ]
+        newhash = self.commitchanges(changes)
+
+        repo = self.repo
+        hg.update(repo, newhash)
+        commands.push(repo.ui, repo)
+        self.assertEqual(test_util.repolen(self.repo), oldlen + 2)
+
+        # verify that both commits are pushed
+        commit1 = self.repo['tip']
+        self.assertEqual(commit1.files(), ['delta', 'gamma'])
+
+        prefix = 'svn:' + self.repo.svnmeta().uuid
+        self.assertEqual(util.getsvnrev(commit1),
+                         prefix + '/branches/the_branch@6')
+        commit2 = commit1.parents()[0]
+        self.assertEqual(commit2.files(), ['gamma'])
+        self.assertEqual(util.getsvnrev(commit2),
+                         prefix + '/branches/the_branch@5')
+
+    def test_push_in_subdir(self, commit=True):
+        repo = self.repo
+        old_tip = repo['tip'].node()
+        def file_callback(repo, memctx, path):
+            if path == 'adding_file' or path == 'newdir/new_file':
+                testData = 'fooFirstFile'
+                if path == 'newdir/new_file':
+                    testData = 'fooNewFile'
+                return context.memfilectx(path=path,
+                                          data=testData,
+                                          islink=False,
+                                          isexec=False,
+                                          copied=False)
+            raise IOError(errno.EINVAL, 'Invalid operation: ' + path)
+        ctx = context.memctx(repo,
+                             (repo['default'].node(), node.nullid),
+                             'automated test',
+                             ['adding_file'],
+                             file_callback,
+                             'an_author',
+                             '2012-12-13 20:59:48 -0500',
+                             {'branch': 'default', })
+        new_hash = repo.commitctx(ctx)
+        p = os.path.join(repo.root, "newdir")
+        os.mkdir(p)
+        ctx = context.memctx(repo,
+                             (repo['default'].node(), node.nullid),
+                             'automated test',
+                             ['newdir/new_file'],
+                             file_callback,
+                             'an_author',
+                             '2012-12-13 20:59:48 -0500',
+                             {'branch': 'default', })
+        os.chdir(p)
+        new_hash = repo.commitctx(ctx)
+        hg.update(repo, repo['tip'].node())
+        self.pushrevisions()
+        tip = self.repo['tip']
+        self.assertNotEqual(tip.node(), old_tip)
+        self.assertEqual(p, os.getcwd())
+        self.assertEqual(tip['adding_file'].data(), 'fooFirstFile')
+        self.assertEqual(tip['newdir/new_file'].data(), 'fooNewFile')
+        self.assertEqual(tip.branch(), 'default')
--- a/tests/test_push_dirs.py
+++ b/tests/test_push_dirs.py
@@ -3,6 +3,9 @@ import test_util
 import unittest
 
 class TestPushDirectories(test_util.TestBase):
+    stupid_mode_tests = True
+    obsolete_mode_tests = True
+
     def test_push_dirs(self):
         repo_path = self.load_and_fetch('emptyrepo.svndump')[1]
 
@@ -84,7 +87,6 @@ class TestPushDirectories(test_util.Test
         # Tests simple pushing from default branch to a single dir repo
         # Changes a file in a subdir (regression).
         repo, repo_path = self.load_and_fetch('branch_from_tag.svndump',
-                                              stupid=False,
                                               layout='single',
                                               subdir='tags')
         changes = [('tag_r3/alpha', 'tag_r3/alpha', 'foo'),
@@ -103,8 +105,3 @@ class TestPushDirectories(test_util.Test
                           'tag_r3/alpha',
                           'tag_r3/beta',
                           'tag_r3/new'])
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestPushDirectories),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_push_eol.py
+++ b/tests/test_push_eol.py
@@ -3,11 +3,14 @@ import test_util
 import unittest
 
 class TestPushEol(test_util.TestBase):
+    obsolete_mode_tests = True
+    stupid_mode_tests = True
+
     def setUp(self):
         test_util.TestBase.setUp(self)
         self._load_fixture_and_fetch('emptyrepo.svndump')
 
-    def _test_push_dirs(self, stupid):
+    def test_push_dirs(self):
         changes = [
             # Root files with LF, CRLF and mixed EOL
             ('lf', 'lf', 'a\nb\n\nc'),
@@ -15,7 +18,7 @@ class TestPushEol(test_util.TestBase):
             ('mixed', 'mixed', 'a\r\nb\n\r\nc\nd'),
             ]
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
 
         changes = [
@@ -25,16 +28,5 @@ class TestPushEol(test_util.TestBase):
             ('mixed', 'mixed', 'a\r\nb\n\r\nc\nd\r\na\r\nb\n\r\nc\nd'),
             ]
         self.commitchanges(changes)
-        self.pushrevisions(stupid)
+        self.pushrevisions()
         self.assertchanges(changes, self.repo['tip'])
-
-    def test_push_dirs(self):
-        self._test_push_dirs(False)
-
-    def test_push_dirs_stupid(self):
-        self._test_push_dirs(True)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestPushEol),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_push_renames.py
+++ b/tests/test_push_renames.py
@@ -4,10 +4,12 @@ import sys
 import unittest
 
 class TestPushRenames(test_util.TestBase):
+    obsolete_mode_tests = True
+    stupid_mode_tests = True
+
     def setUp(self):
         test_util.TestBase.setUp(self)
-        self.repo_path = self.load_and_fetch('pushrenames.svndump',
-                                             stupid=True)[1]
+        self.repo_path = self.load_and_fetch('pushrenames.svndump')[1]
 
     def _debug_print_copies(self, ctx):
         w = sys.stderr.write
@@ -113,9 +115,3 @@ class TestPushRenames(test_util.TestBase
             'This failure means rename of an entire tree is broken.'
             ' There is a print on the preceding line commented out '
             'that should help you.')
-
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestPushRenames),
-          ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_single_dir_clone.py
+++ b/tests/test_single_dir_clone.py
@@ -10,13 +10,17 @@ from mercurial import hg
 from mercurial import node
 from mercurial import ui
 
-class TestSingleDir(test_util.TestBase):
+from hgsubversion import compathacks
+
+class TestSingleDirClone(test_util.TestBase):
+    stupid_mode_tests = True
+
     def test_clone_single_dir_simple(self):
         repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
-                                            stupid=False,
                                             layout='single',
                                             subdir='')
-        self.assertEqual(repo.branchtags().keys(), ['default'])
+        self.assertEqual(compathacks.branchset(repo),
+                         set(['default']))
         self.assertEqual(repo['tip'].manifest().keys(),
                          ['trunk/beta',
                           'tags/copied_tag/alpha',
@@ -29,33 +33,29 @@ class TestSingleDir(test_util.TestBase):
 
     def test_auto_detect_single(self):
         repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
-                                            stupid=False,
                                             layout='auto')
-        self.assertEqual(repo.branchtags().keys(), ['default',
-                                                    'branch_from_tag'])
+        self.assertEqual(compathacks.branchset(repo),
+                         set(['default', 'branch_from_tag']))
         oldmanifest = test_util.filtermanifest(repo['default'].manifest().keys())
         # remove standard layout
         shutil.rmtree(self.wc_path)
         # try again with subdir to get single dir clone
         repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
-                                            stupid=False,
                                             layout='auto',
                                             subdir='trunk')
-        self.assertEqual(repo.branchtags().keys(), ['default', ])
+        self.assertEqual(compathacks.branchset(repo), set(['default', ]))
         self.assertEqual(repo['default'].manifest().keys(), oldmanifest)
 
-    def test_clone_subdir_is_file_prefix(self, stupid=False):
+    def test_clone_subdir_is_file_prefix(self):
         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(compathacks.branchset(repo), set(['default']))
         self.assertEqual(repo['tip'].manifest().keys(), ['flaf.txt'])
 
     def test_externals_single(self):
         repo = self._load_fixture_and_fetch('externals.svndump',
-                                            stupid=False,
                                             layout='single')
         for rev in repo:
             assert '.hgsvnexternals' not in repo[rev].manifest()
@@ -74,7 +74,6 @@ class TestSingleDir(test_util.TestBase):
         # This is the test which demonstrates the brokenness of externals
         return # TODO enable test when externals in single are fixed
         repo = self._load_fixture_and_fetch('externals.svndump',
-                                            stupid=False,
                                             layout='single',
                                             subdir='')
         for rev in repo:
@@ -86,195 +85,3 @@ class TestSingleDir(test_util.TestBase):
         expect = '' # Not honestly sure what this should be...
         test = 4
         self.assertEqual(self.repo[test]['.hgsvnexternals'].data(), expect)
-
-    def test_push_single_dir(self):
-        # Tests simple pushing from default branch to a single dir repo
-        repo, repo_path = self.load_and_fetch('branch_from_tag.svndump',
-                                              stupid=False,
-                                              layout='single',
-                                              subdir='')
-        def file_callback(repo, memctx, path):
-            if path == 'adding_file':
-                return context.memfilectx(path=path,
-                                          data='foo',
-                                          islink=False,
-                                          isexec=False,
-                                          copied=False)
-            elif path == 'adding_binary':
-                return context.memfilectx(path=path,
-                                          data='\0binary',
-                                          islink=False,
-                                          isexec=False,
-                                          copied=False)
-            raise IOError(errno.EINVAL, 'Invalid operation: ' + path)
-        ctx = context.memctx(repo,
-                             (repo['tip'].node(), node.nullid),
-                             'automated test',
-                             ['adding_file', 'adding_binary'],
-                             file_callback,
-                             'an_author',
-                             '2009-10-19 18:49:30 -0500',
-                             {'branch': 'default', })
-        repo.commitctx(ctx)
-        hg.update(repo, repo['tip'].node())
-        self.pushrevisions()
-        self.assertTrue('adding_file' in test_util.svnls(repo_path, ''))
-        self.assertEqual('application/octet-stream',
-                         test_util.svnpropget(repo_path, 'adding_binary',
-                                              'svn:mime-type'))
-        # Now add another commit and test mime-type being reset
-        changes = [('adding_binary', 'adding_binary', 'no longer binary')]
-        self.commitchanges(changes)
-        self.pushrevisions()
-        self.assertEqual('', test_util.svnpropget(repo_path, 'adding_binary',
-                                                  'svn:mime-type'))
-
-    def test_push_single_dir_at_subdir(self):
-        repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
-                                            stupid=False,
-                                            layout='single',
-                                            subdir='trunk')
-        def filectxfn(repo, memctx, path):
-            return context.memfilectx(path=path,
-                                      data='contents of %s' % path,
-                                      islink=False,
-                                      isexec=False,
-                                      copied=False)
-        ctx = context.memctx(repo,
-                             (repo['tip'].node(), node.nullid),
-                             'automated test',
-                             ['bogus'],
-                             filectxfn,
-                             'an_author',
-                             '2009-10-19 18:49:30 -0500',
-                             {'branch': 'localhacking', })
-        n = repo.commitctx(ctx)
-        self.assertEqual(self.repo['tip']['bogus'].data(),
-                         'contents of bogus')
-        before = repo['tip'].hex()
-        hg.update(repo, self.repo['tip'].hex())
-        self.pushrevisions()
-        self.assertNotEqual(before, self.repo['tip'].hex())
-        self.assertEqual(self.repo['tip']['bogus'].data(),
-                         'contents of bogus')
-
-    def test_push_single_dir_one_incoming_and_two_outgoing(self):
-        # Tests simple pushing from default branch to a single dir repo
-        # Pushes two outgoing over one incoming svn rev
-        # (used to cause an "unknown revision")
-        # This can happen if someone committed to svn since our last pull (race).
-        repo, repo_path = self.load_and_fetch('branch_from_tag.svndump',
-                                              stupid=False,
-                                              layout='single',
-                                              subdir='trunk')
-        self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
-        def file_callback(repo, memctx, path):
-            return context.memfilectx(path=path,
-                                      data='data of %s' % path,
-                                      islink=False,
-                                      isexec=False,
-                                      copied=False)
-        for fn in ['one', 'two']:
-            ctx = context.memctx(repo,
-                                 (repo['tip'].node(), node.nullid),
-                                 'automated test',
-                                 [fn],
-                                 file_callback,
-                                 'an_author',
-                                 '2009-10-19 18:49:30 -0500',
-                                 {'branch': 'default', })
-            repo.commitctx(ctx)
-        hg.update(repo, repo['tip'].node())
-        self.pushrevisions(expected_extra_back=1)
-        self.assertTrue('trunk/one' in test_util.svnls(repo_path, ''))
-        self.assertTrue('trunk/two' in test_util.svnls(repo_path, ''))
-
-    def test_push_single_dir_branch(self):
-        # Tests local branches pushing to a single dir repo. Creates a fork at
-        # tip. The default branch adds a file called default, while branch foo
-        # adds a file called foo, then tries to push the foo branch and default
-        # branch in that order.
-        repo, repo_path = self.load_and_fetch('branch_from_tag.svndump',
-                                              stupid=False,
-                                              layout='single',
-                                              subdir='')
-        def file_callback(data):
-            def cb(repo, memctx, path):
-                if path == data:
-                    return context.memfilectx(path=path,
-                                              data=data,
-                                              islink=False,
-                                              isexec=False,
-                                              copied=False)
-                raise IOError(errno.EINVAL, 'Invalid operation: ' + path)
-            return cb
-
-        def commit_to_branch(name, parent):
-            repo.commitctx(context.memctx(repo,
-                                          (parent, node.nullid),
-                                          'automated test (%s)' % name,
-                                          [name],
-                                          file_callback(name),
-                                          'an_author',
-                                          '2009-10-19 18:49:30 -0500',
-                                          {'branch': name, }))
-
-        parent = repo['tip'].node()
-        commit_to_branch('default', parent)
-        commit_to_branch('foo', parent)
-        hg.update(repo, repo['foo'].node())
-        self.pushrevisions()
-        repo = self.repo # repo is outdated after the rebase happens, refresh
-        self.assertTrue('foo' in test_util.svnls(repo_path, ''))
-        self.assertEqual(repo.branchtags().keys(), ['default'])
-        # Have to cross to another branch head, so hg.update doesn't work
-        commands.update(ui.ui(),
-                        self.repo,
-                        self.repo.branchheads('default')[1],
-                        clean=True)
-        self.pushrevisions()
-        self.assertTrue('default' in test_util.svnls(repo_path, ''))
-        self.assertEquals(len(self.repo.branchheads('default')), 1)
-
-    @test_util.requiresoption('branch')
-    def test_push_single_dir_renamed_branch(self, stupid=False):
-        # Tests pulling and pushing with a renamed branch
-        # Based on test_push_single_dir
-        repo_path = self.load_svndump('branch_from_tag.svndump')
-        cmd = ['clone', '--layout=single', '--branch=flaf']
-        if stupid:
-            cmd.append('--stupid')
-        cmd += [test_util.fileurl(repo_path), self.wc_path]
-        test_util.dispatch(cmd)
-
-        def file_callback(repo, memctx, path):
-            if path == 'adding_file':
-                return context.memfilectx(path=path,
-                                          data='foo',
-                                          islink=False,
-                                          isexec=False,
-                                          copied=False)
-            raise IOError(errno.EINVAL, 'Invalid operation: ' + path)
-        ctx = context.memctx(self.repo,
-                             (self.repo['tip'].node(), node.nullid),
-                             'automated test',
-                             ['adding_file'],
-                             file_callback,
-                             'an_author',
-                             '2009-10-19 18:49:30 -0500',
-                             {'branch': 'default', })
-        self.repo.commitctx(ctx)
-        hg.update(self.repo, self.repo['tip'].node())
-        self.pushrevisions()
-        self.assertTrue('adding_file' in test_util.svnls(repo_path, ''))
-
-        self.assertEquals(set(['flaf']),
-                          set(self.repo[i].branch() for i in self.repo))
-
-    @test_util.requiresoption('branch')
-    def test_push_single_dir_renamed_branch_stupid(self):
-        self.test_push_single_dir_renamed_branch(True)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestSingleDir)]
-    return unittest.TestSuite(all_tests)
new file mode 100644
--- /dev/null
+++ b/tests/test_single_dir_push.py
@@ -0,0 +1,197 @@
+import test_util
+
+import errno
+import shutil
+import unittest
+
+from mercurial import commands
+from mercurial import context
+from mercurial import hg
+from mercurial import node
+from mercurial import ui
+
+from hgsubversion import compathacks
+
+class TestSingleDirPush(test_util.TestBase):
+    stupid_mode_tests = True
+    obsolete_mode_tests = True
+
+    def test_push_single_dir(self):
+        # Tests simple pushing from default branch to a single dir repo
+        repo, repo_path = self.load_and_fetch('branch_from_tag.svndump',
+                                              layout='single',
+                                              subdir='')
+        def file_callback(repo, memctx, path):
+            if path == 'adding_file':
+                return context.memfilectx(path=path,
+                                          data='foo',
+                                          islink=False,
+                                          isexec=False,
+                                          copied=False)
+            elif path == 'adding_binary':
+                return context.memfilectx(path=path,
+                                          data='\0binary',
+                                          islink=False,
+                                          isexec=False,
+                                          copied=False)
+            raise IOError(errno.EINVAL, 'Invalid operation: ' + path)
+        ctx = context.memctx(repo,
+                             (repo['tip'].node(), node.nullid),
+                             'automated test',
+                             ['adding_file', 'adding_binary'],
+                             file_callback,
+                             'an_author',
+                             '2009-10-19 18:49:30 -0500',
+                             {'branch': 'default', })
+        repo.commitctx(ctx)
+        hg.update(repo, repo['tip'].node())
+        self.pushrevisions()
+        self.assertTrue('adding_file' in test_util.svnls(repo_path, ''))
+        self.assertEqual('application/octet-stream',
+                         test_util.svnpropget(repo_path, 'adding_binary',
+                                              'svn:mime-type'))
+        # Now add another commit and test mime-type being reset
+        changes = [('adding_binary', 'adding_binary', 'no longer binary')]
+        self.commitchanges(changes)
+        self.pushrevisions()
+        self.assertEqual('', test_util.svnpropget(repo_path, 'adding_binary',
+                                                  'svn:mime-type'))
+
+    def test_push_single_dir_at_subdir(self):
+        repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
+                                            layout='single',
+                                            subdir='trunk')
+        def filectxfn(repo, memctx, path):
+            return context.memfilectx(path=path,
+                                      data='contents of %s' % path,
+                                      islink=False,
+                                      isexec=False,
+                                      copied=False)
+        ctx = context.memctx(repo,
+                             (repo['tip'].node(), node.nullid),
+                             'automated test',
+                             ['bogus'],
+                             filectxfn,
+                             'an_author',
+                             '2009-10-19 18:49:30 -0500',
+                             {'branch': 'localhacking', })
+        n = repo.commitctx(ctx)
+        self.assertEqual(self.repo['tip']['bogus'].data(),
+                         'contents of bogus')
+        before = repo['tip'].hex()
+        hg.update(repo, self.repo['tip'].hex())
+        self.pushrevisions()
+        self.assertNotEqual(before, self.repo['tip'].hex())
+        self.assertEqual(self.repo['tip']['bogus'].data(),
+                         'contents of bogus')
+
+    def test_push_single_dir_one_incoming_and_two_outgoing(self):
+        # Tests simple pushing from default branch to a single dir repo
+        # Pushes two outgoing over one incoming svn rev
+        # (used to cause an "unknown revision")
+        # This can happen if someone committed to svn since our last pull (race).
+        repo, repo_path = self.load_and_fetch('branch_from_tag.svndump',
+                                              layout='single',
+                                              subdir='trunk')
+        self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
+        def file_callback(repo, memctx, path):
+            return context.memfilectx(path=path,
+                                      data='data of %s' % path,
+                                      islink=False,
+                                      isexec=False,
+                                      copied=False)
+        for fn in ['one', 'two']:
+            ctx = context.memctx(repo,
+                                 (repo['tip'].node(), node.nullid),
+                                 'automated test',
+                                 [fn],
+                                 file_callback,
+                                 'an_author',
+                                 '2009-10-19 18:49:30 -0500',
+                                 {'branch': 'default', })
+            repo.commitctx(ctx)
+        hg.update(repo, repo['tip'].node())
+        self.pushrevisions(expected_extra_back=1)
+        self.assertTrue('trunk/one' in test_util.svnls(repo_path, ''))
+        self.assertTrue('trunk/two' in test_util.svnls(repo_path, ''))
+
+    def test_push_single_dir_branch(self):
+        # Tests local branches pushing to a single dir repo. Creates a fork at
+        # tip. The default branch adds a file called default, while branch foo
+        # adds a file called foo, then tries to push the foo branch and default
+        # branch in that order.
+        repo, repo_path = self.load_and_fetch('branch_from_tag.svndump',
+                                              layout='single',
+                                              subdir='')
+        def file_callback(data):
+            def cb(repo, memctx, path):
+                if path == data:
+                    return context.memfilectx(path=path,
+                                              data=data,
+                                              islink=False,
+                                              isexec=False,
+                                              copied=False)
+                raise IOError(errno.EINVAL, 'Invalid operation: ' + path)
+            return cb
+
+        def commit_to_branch(name, parent):
+            repo.commitctx(context.memctx(repo,
+                                          (parent, node.nullid),
+                                          'automated test (%s)' % name,
+                                          [name],
+                                          file_callback(name),
+                                          'an_author',
+                                          '2009-10-19 18:49:30 -0500',
+                                          {'branch': name, }))
+
+        parent = repo['tip'].node()
+        commit_to_branch('default', parent)
+        commit_to_branch('foo', parent)
+        hg.update(repo, repo['foo'].node())
+        self.pushrevisions()
+        repo = self.repo # repo is outdated after the rebase happens, refresh
+        self.assertTrue('foo' in test_util.svnls(repo_path, ''))
+        self.assertEqual(compathacks.branchset(repo), set(['default']))
+        # Have to cross to another branch head, so hg.update doesn't work
+        commands.update(ui.ui(),
+                        self.repo,
+                        self.repo.branchheads('default')[1],
+                        clean=True)
+        self.pushrevisions()
+        self.assertTrue('default' in test_util.svnls(repo_path, ''))
+        self.assertEquals(len(self.repo.branchheads('default')), 1)
+
+    @test_util.requiresoption('branch')
+    def test_push_single_dir_renamed_branch(self):
+        # Tests pulling and pushing with a renamed branch
+        # Based on test_push_single_dir
+        repo_path = self.load_svndump('branch_from_tag.svndump')
+        cmd = ['clone', '--layout=single', '--branch=flaf']
+        if self.stupid:
+            cmd.append('--stupid')
+        cmd += [test_util.fileurl(repo_path), self.wc_path]
+        test_util.dispatch(cmd)
+
+        def file_callback(repo, memctx, path):
+            if path == 'adding_file':
+                return context.memfilectx(path=path,
+                                          data='foo',
+                                          islink=False,
+                                          isexec=False,
+                                          copied=False)
+            raise IOError(errno.EINVAL, 'Invalid operation: ' + path)
+        ctx = context.memctx(self.repo,
+                             (self.repo['tip'].node(), node.nullid),
+                             'automated test',
+                             ['adding_file'],
+                             file_callback,
+                             'an_author',
+                             '2009-10-19 18:49:30 -0500',
+                             {'branch': 'default', })
+        self.repo.commitctx(ctx)
+        hg.update(self.repo, self.repo['tip'].node())
+        self.pushrevisions()
+        self.assertTrue('adding_file' in test_util.svnls(repo_path, ''))
+
+        self.assertEquals(set(['flaf']),
+                          set(self.repo[i].branch() for i in self.repo))
new file mode 100644
--- /dev/null
+++ b/tests/test_svn_pre_commit_hooks.py
@@ -0,0 +1,31 @@
+import os
+import sys
+import test_util
+import unittest
+
+from mercurial import hg
+from mercurial import commands
+from mercurial import util
+
+
+class TestSvnPreCommitHooks(test_util.TestBase):
+    def setUp(self):
+        super(TestSvnPreCommitHooks, self).setUp()
+        self.repo_path = self.load_and_fetch('single_rev.svndump')[1]
+        # creating pre-commit hook that doesn't allow any commit
+        hook_file_name = os.path.join(
+			self.repo_path, 'hooks', 'pre-commit'
+        )
+        hook_file = open(hook_file_name, 'w')
+        hook_file.write(
+        	'#!/bin/sh\n'
+        	'echo "Commits are not allowed" >&2; exit 1;\n'
+        )
+        hook_file.close()
+        os.chmod(hook_file_name, 0755)
+
+    def test_push_with_pre_commit_hooks(self):
+        changes = [('narf/a', 'narf/a', 'ohai',),
+                   ]
+        self.commitchanges(changes)
+        self.assertRaises(util.Abort, self.pushrevisions)
--- a/tests/test_svnwrap.py
+++ b/tests/test_svnwrap.py
@@ -57,8 +57,3 @@ class TestRootAsSubdirOfRepo(TestBasicRe
         self.repo = svnwrap.SubversionRepo(test_util.fileurl(
             self.repo_path + '/dummyproj'
         ))
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestBasicRepoLayout),
-           unittest.TestLoader().loadTestsFromTestCase(TestRootAsSubdirOfRepo)]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -8,30 +8,25 @@ from mercurial import hg
 from mercurial import node
 from mercurial import ui
 
+from hgsubversion import compathacks
 from hgsubversion import svncommands
 from hgsubversion import svnrepo
 
 class TestTags(test_util.TestBase):
-    def test_tags(self, stupid=False):
-        repo = self._load_fixture_and_fetch('basic_tag_tests.svndump',
-                                            stupid=stupid)
+    stupid_mode_tests = True
+
+    def test_tags(self):
+        repo = self._load_fixture_and_fetch('basic_tag_tests.svndump')
         self.assertEqual(sorted(repo.tags()), ['copied_tag', 'tag_r3', 'tip'])
         self.assertEqual(repo['tag_r3'], repo['copied_tag'])
         self.assertEqual(repo['tag_r3'].rev(), 1)
 
-    def test_tags_stupid(self):
-        self.test_tags(stupid=True)
-
-    def test_remove_tag(self, stupid=False):
-        repo = self._load_fixture_and_fetch('remove_tag_test.svndump',
-                                            stupid=stupid)
+    def test_remove_tag(self):
+        repo = self._load_fixture_and_fetch('remove_tag_test.svndump')
         self.assertEqual(repo['tag_r3'].rev(), 1)
         self.assert_('copied_tag' not in repo.tags())
 
-    def test_remove_tag_stupid(self):
-        self.test_remove_tag(stupid=True)
-
-    def test_rename_tag(self, stupid=False):
+    def test_rename_tag(self):
         expected = """\
 node: hg=default@2:svn=trunk@4
 tagging r3
@@ -49,24 +44,16 @@ rename a tag
   copied_tag: hg=default@-1:svn=unk@unk
   other_tag_r3: hg=default@1:svn=trunk@3
 """
-        self._test_tags('rename_tag_test.svndump', expected, stupid)
+        self._test_tags('rename_tag_test.svndump', expected)
 
-    def test_rename_tag_stupid(self):
-        self.test_rename_tag(stupid=True)
-
-    def test_branch_from_tag(self, stupid=False):
-        repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
-                                            stupid=stupid)
-        self.assert_('branch_from_tag' in repo.branchtags())
+    def test_branch_from_tag(self):
+        repo = self._load_fixture_and_fetch('branch_from_tag.svndump')
+        self.assert_('branch_from_tag' in compathacks.branchset(repo))
         self.assertEqual(repo[1], repo['tag_r3'])
         self.assertEqual(repo['branch_from_tag'].parents()[0], repo['copied_tag'])
 
-    def test_branch_from_tag_stupid(self):
-        self.test_branch_from_tag(stupid=True)
-
-    def test_tag_by_renaming_branch(self, stupid=False):
-        repo = self._load_fixture_and_fetch('tag_by_rename_branch.svndump',
-                                            stupid=stupid)
+    def test_tag_by_renaming_branch(self):
+        repo = self._load_fixture_and_fetch('tag_by_rename_branch.svndump')
         branches = set(repo[h] for h in repo.heads())
         self.assert_('dummy' not in branches)
         self.assertEqual(repo['dummy'], repo['tip'].parents()[0],
@@ -76,9 +63,6 @@ rename a tag
         extra.pop('convert_revision', None)
         self.assertEqual(extra, {'branch': 'dummy', 'close': '1'})
 
-    def test_tag_by_renaming_branch_stupid(self):
-        self.test_tag_by_renaming_branch(stupid=True)
-
     def test_deletion_of_tag_on_trunk_after_branching(self):
         repo = self._load_fixture_and_fetch('tag_deletion_tag_branch.svndump')
         branches = set(repo[h].extra()['branch'] for h in repo.heads())
@@ -101,12 +85,8 @@ rename a tag
              'versions/branch_version': 'I\x89\x1c>z#\xfc._K#@:\xd6\x1f\x96\xd6\x83\x1b|',
              })
 
-    def test_most_recent_is_edited_stupid(self):
-        self.test_most_recent_is_edited(True)
-
-    def test_most_recent_is_edited(self, stupid=False):
-        repo, repo_path = self.load_and_fetch('most-recent-is-edit-tag.svndump',
-                                              stupid=stupid)
+    def test_most_recent_is_edited(self):
+        repo, repo_path = self.load_and_fetch('most-recent-is-edit-tag.svndump')
         self.repo.ui.status(
             "Note: this test failing may be because of a rebuildmeta failure.\n"
             "You should check that before assuming issues with this test.\n")
@@ -116,19 +96,15 @@ rename a tag
         svncommands.rebuildmeta(repo.ui,
                                dest,
                                args=[test_util.fileurl(repo_path), ])
-        commands.pull(self.repo.ui, self.repo, stupid=stupid)
+        commands.pull(self.repo.ui, self.repo)
         dtags, srctags = dest.tags(), self.repo.tags()
         dtags.pop('tip')
         srctags.pop('tip')
         self.assertEqual(dtags, srctags)
         self.assertEqual(dest.heads(), self.repo.heads())
 
-    def test_edited_tag_stupid(self):
-        self.test_edited_tag(True)
-
-    def test_edited_tag(self, stupid=False):
-       repo = self._load_fixture_and_fetch('commit-to-tag.svndump',
-                                           stupid=stupid)
+    def test_edited_tag(self):
+       repo = self._load_fixture_and_fetch('commit-to-tag.svndump')
        headcount = 6
        self.assertEqual(len(repo.heads()), headcount)
        heads = repo.heads()
@@ -221,8 +197,8 @@ rename a tag
             w('  %s: %s\n' % (name, formatnode(repo[node])))
         w('\n')
 
-    def _test_tags(self, testpath, expected, stupid=False):
-        repo = self._load_fixture_and_fetch(testpath, stupid=stupid)
+    def _test_tags(self, testpath, expected):
+        repo = self._load_fixture_and_fetch(testpath)
         fp = cStringIO.StringIO()
         for r in repo:
             self._debug_print_tags(repo, repo[r], fp=fp)
@@ -235,7 +211,7 @@ rename a tag
         diff = difflib.unified_diff(expected, output, 'expected', 'output')
         self.assert_(False, '\n' + '\n'.join(diff))
 
-    def test_tagging_into_tag(self, stupid=False):
+    def test_tagging_into_tag(self):
         expected = """\
 node: hg=test@2:svn=branches/test@4
 First tag.
@@ -271,11 +247,4 @@ Fix tag pt 2.
   test-0.1-real: hg=default@-1:svn=unk@unk
   test-0.1: hg=test@1:svn=branches/test@3
 """
-        self._test_tags('renametagdir.svndump', expected, stupid=stupid)
-
-    def test_tagging_into_tag_stupid(self):
-        self.test_tagging_into_tag(True)
-
-
-def suite():
-    return unittest.TestLoader().loadTestsFromTestCase(TestTags)
+        self._test_tags('renametagdir.svndump', expected)
--- a/tests/test_template_keywords.py
+++ b/tests/test_template_keywords.py
@@ -80,7 +80,3 @@ class TestLogKeywords(test_util.TestBase
         self.assertRaises(error.ParseError,
                           commands.log, self.ui(), repo,
                           template='{rev}:{svnrev} ', **defaults)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestLogKeywords), ]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_unaffected_core.py
+++ b/tests/test_unaffected_core.py
@@ -43,7 +43,7 @@ class TestMercurialCore(test_util.TestBa
         f.flush()
         commands.commit(ui, repo, message="C3")
 
-        self.assertEqual(len(repo), 3)
+        self.assertEqual(test_util.repolen(repo), 3)
 
         updaterev = 1
         _dispatch(ui, ['clone', self.wc_path, self.wc_path + '2',
@@ -77,7 +77,7 @@ class TestMercurialCore(test_util.TestBa
         commands.branch(ui, repo, label="B2")
         commands.commit(ui, repo, message="C3")
 
-        self.assertEqual(len(repo), 3)
+        self.assertEqual(test_util.repolen(repo), 3)
 
         branch = 'B1'
         _dispatch(ui, ['clone', self.wc_path, self.wc_path + '2',
@@ -86,7 +86,3 @@ class TestMercurialCore(test_util.TestBa
         repo2 = hg.repository(ui, self.wc_path + '2')
 
         self.assertEqual(repo[branch].hex(), repo2['.'].hex())
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestMercurialCore)]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_urls.py
+++ b/tests/test_urls.py
@@ -72,7 +72,3 @@ class TestSubversionUrls(test_util.TestB
         repo1 = svnrepo.svnremoterepo(ui, repo_url + subdir)
         repo2 = svnrepo.svnremoterepo(ui, repo_url + quoted_subdir)
         self.assertEqual(repo1.svnurl, repo2.svnurl)
-
-def suite():
-    all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestSubversionUrls)]
-    return unittest.TestSuite(all_tests)
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -22,10 +22,16 @@ from mercurial import dispatch as dispat
 from mercurial import hg
 from mercurial import i18n
 from mercurial import node
+from mercurial import scmutil
 from mercurial import ui
 from mercurial import util
 from mercurial import extensions
 
+try:
+    from mercurial import obsolete
+except ImportError:
+    obsolete = None
+
 try:
     SkipTest = unittest.SkipTest
 except AttributeError:
@@ -100,6 +106,87 @@ subdir = {'truncatedhistory.svndump': '/
           'non_ascii_path_2.svndump': '/b%C3%B8b',
           'subdir_is_file_prefix.svndump': '/flaf',
           }
+# map defining the layouts of the fixtures we can use with custom layout
+# these are really popular layouts, so I gave them names
+trunk_only = {
+    'default': 'trunk',
+    }
+trunk_dev_branch = {
+    'default': 'trunk',
+    'dev_branch': 'branches/dev_branch',
+    }
+custom = {
+    'addspecial.svndump': {
+        'default': 'trunk',
+        'foo': 'branches/foo',
+        },
+    'binaryfiles.svndump': trunk_only,
+    'branch_create_with_dir_delete.svndump': trunk_dev_branch,
+    'branch_delete_parent_dir.svndump': trunk_dev_branch,
+    'branchmap.svndump': {
+        'default': 'trunk',
+        'badname': 'branches/badname',
+        'feature': 'branches/feature',
+        },
+    'branch_prop_edit.svndump': trunk_dev_branch,
+    'branch_rename_to_trunk.svndump': {
+        'default': 'trunk',
+        'dev_branch': 'branches/dev_branch',
+        'old_trunk': 'branches/old_trunk',
+        },
+    'copies.svndump': trunk_only,
+    'copybeforeclose.svndump': {
+        'default': 'trunk',
+        'test': 'branches/test'
+        },
+    'delentries.svndump': trunk_only,
+    'delete_restore_trunk.svndump': trunk_only,
+    'empty_dir_in_trunk_not_repo_root.svndump': trunk_only,
+    'executebit.svndump': trunk_only,
+    'filecase.svndump': trunk_only,
+    'file_not_in_trunk_root.svndump': trunk_only,
+    'project_name_with_space.svndump': trunk_dev_branch,
+    'pushrenames.svndump': trunk_only,
+    'rename_branch_parent_dir.svndump': trunk_dev_branch,
+    'renamedproject.svndump': {
+        'default': 'trunk',
+        'branch': 'branches/branch',
+        },
+    'renames.svndump': {
+        'default': 'trunk',
+        'branch1': 'branches/branch1',
+        },
+    'replace_branch_with_branch.svndump': {
+        'default': 'trunk',
+        'branch1': 'branches/branch1',
+        'branch2': 'branches/branch2',
+        },
+    'replace_trunk_with_branch.svndump': {
+        'default': 'trunk',
+        'test': 'branches/test',
+        },
+    'revert.svndump': trunk_only,
+    'siblingbranchfix.svndump': {
+        'default': 'trunk',
+        'wrongbranch': 'branches/wrongbranch',
+        },
+    'simple_branch.svndump': {
+        'default': 'trunk',
+        'the_branch': 'branches/the_branch',
+        },
+    'spaces-in-path.svndump': trunk_dev_branch,
+    'symlinks.svndump': trunk_only,
+    'truncatedhistory.svndump': trunk_only,
+    'unorderedbranch.svndump': {
+        'default': 'trunk',
+        'branch': 'branches/branch',
+        },
+    'unrelatedbranch.svndump': {
+        'default': 'trunk',
+        'branch1': 'branches/branch1',
+        'branch2': 'branches/branch2',
+        },
+}
 
 FIXTURES = os.path.join(os.path.abspath(os.path.dirname(__file__)),
                         'fixtures')
@@ -110,6 +197,16 @@ def getlocalpeer(repo):
         localrepo = repo
     return localrepo
 
+def repolen(repo):
+    """Naively calculate the amount of available revisions in a repository.
+
+    this is usually equal to len(repo) -- except in the face of
+    obsolete revisions.
+    """
+    # kind of nasty way of calculating the length, but fortunately,
+    # our test repositories tend to be rather small
+    return len([r for r in repo])
+
 def _makeskip(name, message):
     if SkipTest:
         def skip(*args, **kwargs):
@@ -144,6 +241,18 @@ def requiresoption(option):
         raise TypeError('requiresoption takes a string argument')
     return decorator
 
+def requiresreplay(method):
+    '''Skip a test in stupid mode.'''
+    def test(self, *args, **kwargs):
+        if self.stupid:
+            if SkipTest:
+                raise SkipTest("test requires replay mode")
+        else:
+            return method(self, *args, **kwargs)
+
+    test.__name__ = method.__name__
+    return test
+
 def filtermanifest(manifest):
     return [f for f in manifest if f not in util.ignoredfiles]
 
@@ -170,7 +279,7 @@ def testui(stupid=False, layout='auto', 
 
 def dispatch(cmd):
     cmd = getattr(dispatchmod, 'request', lambda x: x)(cmd)
-    dispatchmod.dispatch(cmd)
+    return dispatchmod.dispatch(cmd)
 
 def rmtree(path):
     # Read-only files cannot be removed under Windows
@@ -240,9 +349,93 @@ def svnpropget(repo_path, path, prop, re
         raise Exception('svn ls failed on %s: %r' % (path, stderr))
     return stdout.strip()
 
+
+def _obsolete_wrap(cls, name):
+    origfunc = getattr(cls, name)
+
+    if not name.startswith('test_') or not origfunc:
+        return
+
+    if not obsolete:
+        wrapper = _makeskip(name, 'obsolete not available')
+    else:
+        def wrapper(self, *args, **opts):
+            self.assertFalse(obsolete._enabled, 'obsolete was already active')
+
+            obsolete._enabled = True
+
+            try:
+                    origfunc(self, *args, **opts)
+                    self.assertTrue(obsolete._enabled, 'obsolete remains active')
+            finally:
+                obsolete._enabled = False
+
+    if not wrapper:
+        return
+
+    wrapper.__name__ = name + ' obsolete'
+    wrapper.__module__ = origfunc.__module__
+
+    if origfunc.__doc__:
+        firstline = origfunc.__doc__.strip().splitlines()[0]
+        wrapper.__doc__ = firstline + ' (obsolete)'
+
+    assert getattr(cls, wrapper.__name__, None) is None
+
+    setattr(cls, wrapper.__name__, wrapper)
+
+
+def _stupid_wrap(cls, name):
+    origfunc = getattr(cls, name)
+
+    if not name.startswith('test_') or not origfunc:
+        return
+
+    def wrapper(self, *args, **opts):
+        self.assertFalse(self.stupid, 'stupid mode was already active')
+
+        self.stupid = True
+
+        try:
+            origfunc(self, *args, **opts)
+        finally:
+            self.stupid = False
+
+    wrapper.__name__ = name + ' stupid'
+    wrapper.__module__ = origfunc.__module__
+
+    if origfunc.__doc__:
+        firstline = origfunc.__doc__.strip().splitlines()[0]
+        wrapper.__doc__ = firstline + ' (stupid)'
+
+    assert getattr(cls, wrapper.__name__, None) is None
+
+    setattr(cls, wrapper.__name__, wrapper)
+
+class TestMeta(type):
+    def __init__(cls, *args, **opts):
+        if cls.obsolete_mode_tests:
+            for origname in dir(cls):
+                _obsolete_wrap(cls, origname)
+
+        if cls.stupid_mode_tests:
+            for origname in dir(cls):
+                _stupid_wrap(cls, origname)
+
+        return super(TestMeta, cls).__init__(*args, **opts)
+
 class TestBase(unittest.TestCase):
+    __metaclass__ = TestMeta
+
+    obsolete_mode_tests = False
+    stupid_mode_tests = False
+
+    stupid = False
+
     def setUp(self):
         _verify_our_modules()
+        if 'hgsubversion' in sys.modules:
+            sys.modules['hgext_hgsubversion'] = sys.modules['hgsubversion']
 
         # the Python 2.7 default of 640 is obnoxiously low
         self.maxDiff = 4096
@@ -256,9 +449,12 @@ class TestBase(unittest.TestCase):
         self.oldwd = os.getcwd()
         self.tmpdir = tempfile.mkdtemp(
             'svnwrap_test', dir=os.environ.get('HGSUBVERSION_TEST_TEMP', None))
+        os.chdir(self.tmpdir)
         self.hgrc = os.path.join(self.tmpdir, '.hgrc')
         os.environ['HGRCPATH'] = self.hgrc
+        scmutil._rcpath = None
         rc = open(self.hgrc, 'w')
+        rc.write('[ui]\nusername=test-user\n')
         for l in '[extensions]', 'hgsubversion=':
             print >> rc, l
 
@@ -279,8 +475,11 @@ class TestBase(unittest.TestCase):
         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 = open(self.config_dir + '/config', 'w')
+        try:
             c.write(config)
+        finally:
+            c.close()
 
     def _makerepopath(self):
         self.repocount += 1
@@ -299,8 +498,8 @@ class TestBase(unittest.TestCase):
 
         _verify_our_modules()
 
-    def ui(self, stupid=False, layout='auto'):
-        return testui(stupid, layout)
+    def ui(self, layout='auto'):
+        return testui(self.stupid, layout)
 
     def load_svndump(self, fixture_name):
         '''Loads an svnadmin dump into a fresh repo. Return the svn repo
@@ -333,7 +532,7 @@ class TestBase(unittest.TestCase):
             tarball.extract(entry, path)
         return path
 
-    def fetch(self, repo_path, subdir=None, stupid=False, layout='auto',
+    def fetch(self, repo_path, subdir=None, layout='auto',
             startrev=0, externals=None, noupdate=True, dest=None, rev=None,
             config=None):
         if layout == 'single':
@@ -352,7 +551,7 @@ class TestBase(unittest.TestCase):
             fileurl(projectpath),
             self.wc_path,
             ]
-        if stupid:
+        if self.stupid:
             cmd.append('--stupid')
         if noupdate:
             cmd.append('--noupdate')
@@ -364,7 +563,8 @@ class TestBase(unittest.TestCase):
         for k,v in reversed(sorted(config.iteritems())):
             cmd[:0] = ['--config', '%s=%s' % (k, v)]
 
-        dispatch(cmd)
+        r = dispatch(cmd)
+        assert not r, 'fetch of %s failed' % projectpath
 
         return hg.repository(testui(), self.wc_path)
 
@@ -409,11 +609,11 @@ class TestBase(unittest.TestCase):
     def repo(self):
         return hg.repository(testui(), self.wc_path)
 
-    def pushrevisions(self, stupid=False, expected_extra_back=0):
-        before = len(self.repo)
-        self.repo.ui.setconfig('hgsubversion', 'stupid', str(stupid))
+    def pushrevisions(self, expected_extra_back=0):
+        before = repolen(self.repo)
+        self.repo.ui.setconfig('hgsubversion', 'stupid', str(self.stupid))
         res = commands.push(self.repo.ui, self.repo)
-        after = len(self.repo)
+        after = repolen(self.repo)
         self.assertEqual(expected_extra_back, after - before)
         return res
 
@@ -530,7 +730,7 @@ class TestBase(unittest.TestCase):
         extensions.loadall(_ui)
         graphlog = extensions.find('graphlog')
         templ = """\
-changeset: {rev}:{node|short}
+changeset: {rev}:{node|short} (r{svnrev})
 branch:    {branches}
 tags:      {tags}
 summary:   {desc|firstline}
@@ -543,4 +743,3 @@ files:     {files}
 
     def draw(self, repo):
         sys.stdout.write(self.getgraph(repo))
-
--- a/tests/test_utility_commands.py
+++ b/tests/test_utility_commands.py
@@ -31,8 +31,17 @@ def repourl(repo_path):
     return util.normalize_url(test_util.fileurl(repo_path))
 
 class UtilityTests(test_util.TestBase):
-    def test_info_output(self):
-        repo, repo_path = self.load_and_fetch('two_heads.svndump')
+    stupid_mode_tests = True
+
+    def test_info_output(self, custom=False):
+        if custom:
+            config = {
+                'hgsubversionbranch.default': 'trunk',
+                'hgsubversionbranch.the_branch': 'branches/the_branch',
+                }
+        else:
+            config = {}
+        repo, repo_path = self.load_and_fetch('two_heads.svndump', config=config)
         hg.update(self.repo, 'the_branch')
         u = self.ui()
         u.pushbuffer()
@@ -85,8 +94,21 @@ class UtilityTests(test_util.TestBase):
                      })
         self.assertMultiLineEqual(actual, expected)
 
-    def test_info_single(self):
-        repo, repo_path = self.load_and_fetch('two_heads.svndump', subdir='trunk')
+    def test_info_output_custom(self):
+        self.test_info_output(custom=True)
+
+    def test_info_single(self, custom=False):
+        if custom:
+            subdir=None
+            config = {
+                'hgsubversionbranch.default': 'trunk/'
+                }
+        else:
+            subdir='trunk'
+            config = {}
+        repo, repo_path = self.load_and_fetch('two_heads.svndump',
+                                              subdir=subdir,
+                                              config=config)
         hg.update(self.repo, 'tip')
         u = self.ui()
         u.pushbuffer()
@@ -100,8 +122,14 @@ class UtilityTests(test_util.TestBase):
                      })
         self.assertMultiLineEqual(expected, actual)
 
+    def test_info_custom_single(self):
+        self.test_info_single(custom=True)
+
     def test_missing_metadata(self):
         self._load_fixture_and_fetch('two_heads.svndump')
+        os.remove(self.repo.join('svn/branch_info'))
+        svncommands.updatemeta(self.ui(), self.repo, [])
+
         test_util.rmtree(self.repo.join('svn'))
         self.assertRaises(hgutil.Abort,
                           self.repo.svnmeta)
@@ -227,9 +255,18 @@ class UtilityTests(test_util.TestBase):
         self.assertEqual(self.repo['tip'].parents()[0].parents()[0], self.repo[0])
         self.assertNotEqual(beforerebasehash, self.repo['tip'].node())
 
-    def test_genignore(self):
+    def test_genignore(self, layout='auto'):
         """ Test generation of .hgignore file. """
-        repo = self._load_fixture_and_fetch('ignores.svndump', noupdate=False)
+        if layout == 'custom':
+            config = {
+                'hgsubversionbranch.default': 'trunk',
+                }
+        else:
+            config = {}
+        repo = self._load_fixture_and_fetch('ignores.svndump',
+                                            layout=layout,
+                                            noupdate=False,
+                                            config=config)
         u = self.ui()
         u.pushbuffer()
         svncommands.genignore(u, repo, self.wc_path)
@@ -237,13 +274,10 @@ class UtilityTests(test_util.TestBase):
                          '.hgignore\nsyntax:glob\nblah\notherblah\nbaz/magic\n')
 
     def test_genignore_single(self):
-        self._load_fixture_and_fetch('ignores.svndump', subdir='trunk')
-        hg.update(self.repo, 'tip')
-        u = self.ui()
-        u.pushbuffer()
-        svncommands.genignore(u, self.repo, self.wc_path)
-        self.assertMultiLineEqual(open(os.path.join(self.wc_path, '.hgignore')).read(),
-                               '.hgignore\nsyntax:glob\nblah\notherblah\nbaz/magic\n')
+        self.test_genignore(layout='single')
+
+    def test_genignore_custom(self):
+        self.test_genignore(layout='custom')
 
     def test_list_authors(self):
         repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
@@ -263,16 +297,16 @@ class UtilityTests(test_util.TestBase):
                                 authors=author_path)
         self.assertMultiLineEqual(open(author_path).read(), 'Augie=\nevil=\n')
 
-    def test_svnverify(self, stupid=False):
+    def test_svnverify(self):
         repo, repo_path = self.load_and_fetch('binaryfiles.svndump',
-                                              noupdate=False, stupid=stupid)
-        ret = verify.verify(self.ui(), repo, [], rev=1, stupid=stupid)
+                                              noupdate=False)
+        ret = verify.verify(self.ui(), repo, [], rev=1)
         self.assertEqual(0, ret)
         repo_path = self.load_svndump('binaryfiles-broken.svndump')
         u = self.ui()
         u.pushbuffer()
         ret = verify.verify(u, repo, [test_util.fileurl(repo_path)],
-                            rev=1, stupid=stupid)
+                            rev=1)
         output = u.popbuffer()
         self.assertEqual(1, ret)
         output = re.sub(r'file://\S+', 'file://', output)
@@ -283,20 +317,16 @@ unexpected file: binary1
 missing file: binary3
 """, output)
 
-    def test_svnverify_stupid(self):
-        self.test_svnverify(True)
-
-    def test_corruption(self, stupid=False):
+    def test_corruption(self):
         SUCCESS = 0
         FAILURE = 1
 
         repo, repo_path = self.load_and_fetch('correct.svndump', layout='single',
-                                              subdir='', stupid=stupid)
+                                              subdir='')
 
         ui = self.ui()
 
-        self.assertEqual(SUCCESS, verify.verify(ui, self.repo, rev='tip',
-                                                stupid=stupid))
+        self.assertEqual(SUCCESS, verify.verify(ui, self.repo, rev='tip'))
 
         corrupt_source = test_util.fileurl(self.load_svndump('corrupt.svndump'))
 
@@ -322,9 +352,6 @@ missing file: binary3
 
         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)
@@ -338,8 +365,3 @@ missing file: binary3
         # 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),
-          ]
-    return unittest.TestSuite(all_tests)