changeset 1021:f67f9d28b0ac

Merge with stable.
author Augie Fackler <raf@durin42.com>
date Sun, 23 Jun 2013 18:18:32 -0500
parents c6e9889dba27 (diff) b5b1fce26f1f (current diff)
children 1c9b1d0e0ba3
files hgsubversion/svncommands.py
diffstat 24 files changed, 627 insertions(+), 203 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/__init__.py
+++ b/hgsubversion/__init__.py
@@ -180,6 +180,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):
--- a/hgsubversion/editor.py
+++ b/hgsubversion/editor.py
@@ -570,13 +570,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
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/__init__.py
@@ -0,0 +1,44 @@
+"""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 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 = {
+    "single": single.SingleLayout,
+    "standard": standard.StandardLayout,
+}
+
+
+def layout_from_name(name):
+    """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]()
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/base.py
@@ -0,0 +1,40 @@
+"""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 __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
+
+        """
+        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 /.
+
+        """
+        self.__unimplemented('remotepath')
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/detect.py
@@ -0,0 +1,87 @@
+""" 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 hgsubversion.svnwrap
+
+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.
+
+    """
+
+    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 ('auto', 'single', 'standard'):
+        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):
+    """ 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 (subdir or '/') == revpath:
+        layout = 'single'
+    else:
+        layout = 'standard'
+
+    return layout
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,15 @@
+
+
+import base
+
+class SingleLayout(base.BaseLayout):
+    """A layout with only the default branch"""
+
+    def localname(self, path):
+        return 'default'
+
+    def remotename(self, branch):
+        return ''
+
+    def remotepath(self, branch, subdir='/'):
+        return subdir or '/'
new file mode 100644
--- /dev/null
+++ b/hgsubversion/layouts/standard.py
@@ -0,0 +1,31 @@
+
+
+import base
+
+
+class StandardLayout(base.BaseLayout):
+    """The standard trunk, branches, tags layout"""
+
+    def localname(self, path):
+        if path == 'trunk':
+            return None
+        elif path.startswith('branches/'):
+            return path[len('branches/'):]
+        return  '../%s' % path
+
+    def remotename(self, branch):
+        if branch == 'default' or branch is None:
+            return 'trunk'
+        elif branch.startswith('../'):
+            return branch[3:]
+        return 'branches/%s' % branch
+
+    def remotepath(self, branch, subdir='/'):
+        branchpath = 'trunk'
+        if branch:
+            if branch.startswith('../'):
+                branchpath = branch[3:]
+            else:
+                branchpath = 'branches/%s' % branch
+
+        return '%s/%s' % (subdir or '', branchpath)
--- a/hgsubversion/maps.py
+++ b/hgsubversion/maps.py
@@ -45,7 +45,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):
 
@@ -88,7 +88,7 @@ class AuthorMap(dict):
         elif self.ui.configbool('hgsubversion', 'defaultauthors', True):
             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 +333,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 +355,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 +394,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 +456,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)
 
@@ -210,6 +205,9 @@ def commit(ui, repo, rev_ctx, meta, base
             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
 
--- 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')
@@ -102,10 +129,6 @@ def _buildmeta(ui, repo, args, partial=F
 
     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
@@ -186,27 +209,25 @@ 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)
+            existing_layout = layouts.detect.layout_from_file(svnmetadir)
+            if layout != existing_layout:
+                layouts.persist.layout_to_file(svnmetadir, layout)
         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
@@ -363,10 +384,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 +423,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/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -10,6 +10,7 @@ from mercurial import node
 
 import util
 import maps
+import layouts
 import editor
 
 
@@ -69,13 +70,9 @@ class SVNMeta(object):
             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
+        self._layout = layouts.detect.layout_from_file(self.meta_data_dir,
+                                                       ui=self.repo.ui)
+        self._layoutobj = None
         pickle_atomic(self.tag_locations, self.tag_locations_file)
         # ensure nested paths are handled properly
         self.tag_locations.sort()
@@ -107,15 +104,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)
+        return self._layoutobj
+
     @property
     def editor(self):
         if not hasattr(self, '_editor'):
@@ -206,10 +204,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]
@@ -228,22 +222,10 @@ class SVNMeta(object):
     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 +235,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,
--- 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)
@@ -218,7 +218,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 +227,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 +252,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 +293,3 @@ class SubversionPrompt(object):
         else:
             creds = None
         return creds
-
--- a/hgsubversion/svnwrap/subvertpy_wrapper.py
+++ b/hgsubversion/svnwrap/subvertpy_wrapper.py
@@ -58,6 +58,7 @@ ERR_FS_TXN_OUT_OF_DATE = subvertpy.ERR_F
 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
+ERR_REPOS_HOOK_FAILURE = subvertpy.ERR_REPOS_HOOK_FAILURE
 SSL_UNKNOWNCA = subvertpy.SSL_UNKNOWNCA
 SSL_CNMISMATCH = subvertpy.SSL_CNMISMATCH
 SSL_NOTYETVALID = subvertpy.SSL_NOTYETVALID
@@ -95,7 +96,7 @@ class PathAdapter(object):
         if self.copyfrom_path:
             self.copyfrom_path = intern(self.copyfrom_path)
 
-class AbstractEditor(object):
+class BaseEditor(object):
     __slots__ = ('editor', 'baton')
 
     def __init__(self, editor, baton=None):
@@ -116,7 +117,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 +133,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)
 
@@ -417,7 +422,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 +458,15 @@ class SubversionRepo(object):
             rooteditor = commiteditor.open_root()
             visitdir(rooteditor, '', paths, 0)
             rooteditor.close()
-            commiteditor.close()
         except:
             commiteditor.abort()
             raise
+        commiteditor.close()
 
     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 +481,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
@@ -42,6 +42,7 @@ ERR_FS_NOT_FOUND = core.SVN_ERR_FS_NOT_F
 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
+ERR_REPOS_HOOK_FAILURE = core.SVN_ERR_REPOS_HOOK_FAILURE
 SSL_UNKNOWNCA = core.SVN_AUTH_SSL_UNKNOWNCA
 SSL_CNMISMATCH = core.SVN_AUTH_SSL_CNMISMATCH
 SSL_NOTYETVALID = core.SVN_AUTH_SSL_NOTYETVALID
@@ -436,13 +437,14 @@ 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)
+
     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()
--- a/hgsubversion/util.py
+++ b/hgsubversion/util.py
@@ -340,6 +340,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)
--- a/hgsubversion/verify.py
+++ b/hgsubversion/verify.py
@@ -1,3 +1,4 @@
+import difflib
 import posixpath
 
 from mercurial import util as hgutil
@@ -38,6 +39,18 @@ 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
@@ -62,6 +75,7 @@ def verify(ui, repo, args=None, **opts):
                 continue
             if not fctx.data() == data:
                 ui.write('difference in: %s\n' % fn)
+                diff_file(fn, data)
                 result = 1
             if not fctx.flags() == mode:
                 ui.write('wrong flags for: %s\n' % fn)
@@ -154,6 +168,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:
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -14,6 +14,8 @@ from mercurial import i18n
 from mercurial import extensions
 from mercurial import repair
 
+import layouts
+import os
 import replay
 import pushmod
 import stupid as stupidmod
@@ -98,7 +100,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 +116,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
@@ -179,6 +191,8 @@ def push(repo, dest, force, revs):
         checkpush(force, revs)
     ui = repo.ui
     old_encoding = util.swap_out_encoding()
+
+    temporary_commits = []
     try:
         # TODO: implement --rev/#rev support
         # TODO: do credentials specified in the URL still work?
@@ -194,106 +208,129 @@ 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.status('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)
+                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:
+                    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=True)
+                    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")
+
+        # strip the original changesets since the push was successful
+        repair.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())
+                repair.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 +353,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 +405,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!
@@ -429,6 +457,9 @@ def pull(repo, source, heads=[], force=F
                         else:
                             ui.traceback()
                             raise hgutil.Abort(*e.args)
+
+                lastpulled = r.revnum
+
         except KeyboardInterrupt:
             ui.traceback()
     finally:
--- a/setup.py
+++ b/setup.py
@@ -118,7 +118,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,
--- a/tests/run.py
+++ b/tests/run.py
@@ -19,6 +19,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
--- a/tests/test_push_command.py
+++ b/tests/test_push_command.py
@@ -522,6 +522,109 @@ class PushTests(test_util.TestBase):
         self.pushrevisions()
         self.assertEqual(['alpha'], list(self.repo['tip'].manifest()))
 
+    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 = len(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(len(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.assertTrue(commit2.mutable())
+        commit1 = commit2.parents()[0]
+        self.assertEqual(commit1.files(), ['gamma', ])
+        self.assertFalse(commit1.mutable())
+
+    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 = len(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(len(self.repo), oldlen + 2)
+
+        # verify that both commits are pushed
+        commit1 = self.repo['tip']
+        self.assertEqual(commit1.files(), ['delta', 'gamma'])
+        self.assertFalse(commit1.mutable())
+        commit2 = commit1.parents()[0]
+        self.assertEqual(commit2.files(), ['gamma'])
+        self.assertFalse(commit2.mutable())
+
+    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')
+
+
 def suite():
     test_classes = [PushTests, ]
     all_tests = []
new file mode 100644
--- /dev/null
+++ b/tests/test_svn_pre_commit_hooks.py
@@ -0,0 +1,34 @@
+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)
+
+def suite():
+    return unittest.findTestCases(sys.modules[__name__])
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -22,6 +22,7 @@ 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
@@ -258,7 +259,9 @@ class TestBase(unittest.TestCase):
             'svnwrap_test', dir=os.environ.get('HGSUBVERSION_TEST_TEMP', None))
         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
 
@@ -543,4 +546,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
@@ -102,6 +102,9 @@ class UtilityTests(test_util.TestBase):
 
     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)
@@ -338,7 +341,7 @@ 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),
           ]