changeset 1323:6fd0ec01553b stable

Merge to stable for release.
author Augie Fackler <raf@durin42.com>
date Tue, 05 May 2015 14:09:43 -0400
parents b13f320ff4e3 (current diff) fd3461f86582 (diff)
children dde1ade36a49
files hgsubversion/layouts/detect.py
diffstat 17 files changed, 149 insertions(+), 178 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/__init__.py
+++ b/hgsubversion/__init__.py
@@ -77,6 +77,8 @@ wrapcmds = { # cmd: generic, target, fix
          'list of paths to search for tags in Subversion repositories'),
         ('', 'branchdir', '',
          'path to search for branches in subversion repositories'),
+        ('', 'trunkdir', '',
+         'path to trunk in subversion repositories'),
         ('', 'infix', '',
          'path relative to trunk, branch an tag dirs to import'),
         ('A', 'authors', '',
--- a/hgsubversion/layouts/__init__.py
+++ b/hgsubversion/layouts/__init__.py
@@ -12,12 +12,10 @@ NB: this has a long way to go before it 
 from mercurial import util as hgutil
 
 import custom
-import detect
 import single
 import standard
 
 __all__ = [
-    "detect",
     "layout_from_name",
     ]
 
@@ -34,7 +32,8 @@ NAME_TO_CLASS = {
 def layout_from_name(name, meta):
     """Returns a layout module given the layout name
 
-    You should use one of the layout.detect.* functions to get the
+    You should be able to read the layout name from meta.layout but, if
+    necessary, you can use one of the meta.layout_from_* functions to get the
     name to pass to this function.
 
     """
--- a/hgsubversion/layouts/base.py
+++ b/hgsubversion/layouts/base.py
@@ -17,6 +17,12 @@ class BaseLayout(object):
             "Incomplete layout implementation: %s.%s doesn't implement %s" %
             (self.__module__, self.__name__, method_name))
 
+    @property
+    def name(self):
+        """Return the name of the key for NAME_TO_CLASS so we can easily compute a
+        stale cache."""
+        self.__unimplemented('name')
+
     def localname(self, path):
         """Compute the local name for a branch located at path.
 
--- a/hgsubversion/layouts/custom.py
+++ b/hgsubversion/layouts/custom.py
@@ -38,6 +38,10 @@ class CustomLayout(base.BaseLayout):
             self.svn_to_hg[svn_path] = hg_branch
             self.hg_to_svn[hg_branch] = svn_path
 
+    @property
+    def name(self):
+        return 'custom'
+
     def localname(self, path):
         if path in self.svn_to_hg:
             return self.svn_to_hg[path]
deleted file mode 100644
--- a/hgsubversion/layouts/detect.py
+++ /dev/null
@@ -1,108 +0,0 @@
-""" 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, meta=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'
-    meta.ui.setconfig('hgsubversion', 'layout', layout)
-    return layout
-
-def layout_from_config(meta, 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 = meta.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):
-    """ Load the layout in use from the metadata file.
-    """
-
-    # import late to avoid trouble when running the test suite
-    try:
-        from hgext_hgsubversion import util
-    except ImportError:
-        from hgsubversion import util
-
-    layout = util.load(meta.layout_file)
-    if layout:
-        meta.ui.setconfig('hgsubversion', 'layout', layout)
-    return layout
-
-def layout_from_commit(subdir, revpath, branch, meta):
-    """ 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, meta)
-        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(meta, allow_auto=True)
-        if config_layout in candidates:
-            return config_layout
-
-    return 'standard'
--- a/hgsubversion/layouts/single.py
+++ b/hgsubversion/layouts/single.py
@@ -5,6 +5,10 @@ import base
 class SingleLayout(base.BaseLayout):
     """A layout with only the default branch"""
 
+    @property
+    def name(self):
+        return 'single'
+
     def localname(self, path):
         return None
 
--- a/hgsubversion/layouts/standard.py
+++ b/hgsubversion/layouts/standard.py
@@ -28,10 +28,15 @@ class StandardLayout(base.BaseLayout):
         # the lambda is to ensure nested paths are handled properly
         meta._gen_cachedconfig('taglocations', ['tags'], 'tag_locations',
                                'tagpaths', lambda x: list(reversed(sorted(x))))
+        meta._gen_cachedconfig('trunkdir', 'trunk', 'trunk_dir')
+
+    @property
+    def name(self):
+        return 'standard'
 
     @property
     def trunk(self):
-        return 'trunk' + self.meta.infix
+        return self.meta.trunkdir + self.meta.infix
 
     def localname(self, path):
         if path == self.trunk:
@@ -98,7 +103,7 @@ class StandardLayout(base.BaseLayout):
         if self.localname(candidate) in known_branches:
             return candidate, '/'.join(components)
 
-        if path == 'trunk' or path.startswith('trunk/'):
+        if path == self.meta.trunkdir or path.startswith(self.meta.trunkdir + '/'):
             return self.trunk, path[len(self.trunk) + 1:]
 
         if path.startswith(self.meta.branchdir):
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -194,7 +194,12 @@ def patchrepo(ui, meta, parentctx, patch
         backend = svnbackend(ui, meta.repo, parentctx, store)
 
         try:
-            ret = patch.patchbackend(ui, backend, patchfp, 0, touched)
+            try:
+                ret = patch.patchbackend(ui, backend, patchfp, 0, files=touched)
+            except TypeError:
+                # Mercurial >= 3.4 have an extra prefix parameter
+                ret = patch.patchbackend(ui, backend, patchfp, 0, '',
+                                         files=touched)
             if ret < 0:
                 raise BadPatchApply('patching failed')
             if ret > 0:
--- a/hgsubversion/svncommands.py
+++ b/hgsubversion/svncommands.py
@@ -98,9 +98,6 @@ def _buildmeta(ui, repo, args, partial=F
     if not partial and os.path.exists(meta.tagfile):
         os.unlink(meta.tagfile)
 
-    layout = None
-    layoutobj = None
-
     skipped = set()
     closed = set()
 
@@ -187,14 +184,14 @@ def _buildmeta(ui, repo, args, partial=F
         assert revpath.startswith(subdir), ('That does not look like the '
                                             'right location in the repo.')
 
-        if layout is None:
-            layout = layouts.detect.layout_from_commit(subdir, revpath,
-                                                       ctx.branch(), meta)
-            existing_layout = layouts.detect.layout_from_file(meta)
-            if layout != existing_layout:
-                util.dump(layout, meta.layout_file)
-            layoutobj = layouts.layout_from_name(layout, meta)
-        elif layout == 'single':
+        # meta.layout is a config-cached property so instead of testing for
+        # None we test to see if the layout is 'auto' and, if so, try to guess
+        # the layout based on the commits (where subdir is compared to the
+        # revpath extracted from the commit)
+        if meta.layout == 'auto':
+            meta.layout = meta.layout_from_commit(subdir, revpath,
+                                                  ctx.branch())
+        elif meta.layout == 'single':
             assert (subdir or '/') == revpath, ('Possible layout detection'
                                                 ' defect in replay')
 
@@ -219,7 +216,7 @@ def _buildmeta(ui, repo, args, partial=F
         # find commitpath, write to revmap
         commitpath = revpath[len(subdir)+1:]
 
-        tag_locations = layoutobj.taglocations
+        tag_locations = meta.layoutobj.taglocations
         found_tag = False
         for location in tag_locations:
             if commitpath.startswith(location + '/'):
@@ -228,7 +225,7 @@ def _buildmeta(ui, repo, args, partial=F
         if found_tag and ctx.extra().get('close'):
             continue
 
-        branch = layoutobj.localname(commitpath)
+        branch = meta.layoutobj.localname(commitpath)
         revmap.write('%s %s %s\n' % (revision, ctx.hex(), branch or ''))
 
         revision = int(revision)
@@ -254,7 +251,7 @@ def _buildmeta(ui, repo, args, partial=F
                 if found_tag and parentextra.get('close'):
                     continue
 
-                branch = layoutobj.localname(parentpath)
+                branch = meta.layoutobj.localname(parentpath)
                 break
 
         if rev in closed:
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -12,6 +12,7 @@ import util
 import maps
 import layouts
 import editor
+import svnwrap
 
 
 class SVNMeta(object):
@@ -40,6 +41,7 @@ class SVNMeta(object):
         self._branchmap = None
         self._tagmap = None
         self._filemap = None
+        self._layout = None
 
         # create .hg/svn folder if it doesn't exist
         if not os.path.isdir(self.metapath):
@@ -56,11 +58,12 @@ class SVNMeta(object):
         self._gen_cachedconfig('defaulthost', self.uuid)
         self._gen_cachedconfig('usebranchnames', True)
         self._gen_cachedconfig('defaultmessage', '')
+        self._gen_cachedconfig('branch', '')
+        self._gen_cachedconfig('layout', 'auto')
 
         # misc
         self.branches = util.load(self.branch_info_file) or {}
         self.prevbranches = dict(self.branches)
-        self._layout = layouts.detect.layout_from_file(self)
 
     def _get_cachedconfig(self, name, filename, configname, default, pre):
         """Return a cached value for a config option. If the cache is uninitialized
@@ -143,23 +146,73 @@ class SVNMeta(object):
                                                             filename))
         setattr(SVNMeta, name, prop)
 
+    def layout_from_subversion(self, svn, revision=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'
+        self.ui.setconfig('hgsubversion', 'layout', layout)
+        return layout
+
+    def layout_from_commit(self, subdir, revpath, branch):
+        """ 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, self)
+            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 = self.layout
+            if config_layout in candidates:
+                return config_layout
+
+        return 'standard'
+
     @property
     def layout_file(self):
         return os.path.join(self.metapath, 'layout')
 
-    @property
-    def layout(self):
-        # this method can't determine the layout, but it needs to be
-        # resolved into something other than auto before this ever
-        # gets called
-        if not self._layout or self._layout == 'auto':
-            self._layout = layouts.detect.layout_from_config(self)
-            util.dump(self._layout, self.layout_file)
-        return self._layout
-
     @property
     def layoutobj(self):
-        if not self._layoutobj:
+        # if self.layout has changed, we need to create a new layoutobj
+        if not self._layoutobj or self._layoutobj.name != self.layout:
             self._layoutobj = layouts.layout_from_name(self.layout, self)
         return self._layoutobj
 
--- a/hgsubversion/util.py
+++ b/hgsubversion/util.py
@@ -215,6 +215,18 @@ class PrefixMatch(object):
     def __call__(self, fn):
         return fn.startswith(self.p)
 
+    def bad(self, f, msg):
+        pass
+
+    def always(self):
+        return False
+
+    def isexact(self):
+        return False
+
+    def anypats(self):
+        return True
+
 def outgoing_revisions(repo, reverse_map, sourcerev):
     """Given a repo and an hg_editor, determines outgoing revisions for the
     current working copy state.
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -399,21 +399,17 @@ def pull(repo, source, heads=[], force=F
 
         stopat_rev = util.parse_revnum(svn, checkout)
 
-        layout = layouts.detect.layout_from_config(meta, allow_auto=True)
-        if layout == 'auto':
-            layout = layouts.detect.layout_from_subversion(svn,
-                                                           (stopat_rev or None),
-                                                           meta)
-            repo.ui.note('using %s layout\n' % layout)
-
-        branch = repo.ui.config('hgsubversion', 'branch')
-        if branch:
-            if layout != 'single':
+        if meta.layout == 'auto':
+            meta.layout = meta.layout_from_subversion(svn, (stopat_rev or None))
+            repo.ui.note('using %s layout\n' % meta.layout)
+
+        if meta.branch:
+            if meta.layout != 'single':
                 msg = ('branch cannot be specified for Subversion clones using '
                        'standard directory layout')
                 raise hgutil.Abort(msg)
 
-            meta.branchmap['default'] = branch
+            meta.branchmap['default'] = meta.branch
 
         ui = repo.ui
         start = meta.lastpulled
@@ -425,7 +421,7 @@ def pull(repo, source, heads=[], force=F
                                                           'startrev', 0))
 
             if start > 0:
-                if layout == 'standard':
+                if meta.layout == 'standard':
                     raise hgutil.Abort('non-zero start revisions are only '
                                        'supported for single-directory clones.')
                 ui.note('starting at revision %d; any prior will be ignored\n'
@@ -593,6 +589,7 @@ optionmap = {
     'tagpaths': ('hgsubversion', 'tagpaths'),
     'authors': ('hgsubversion', 'authormap'),
     'branchdir': ('hgsubversion', 'branchdir'),
+    'trunkdir': ('hgsubversion', 'trunkdir'),
     'infix': ('hgsubversion', 'infix'),
     'filemap': ('hgsubversion', 'filemap'),
     'branchmap': ('hgsubversion', 'branchmap'),
--- a/tests/test_fetch_branches.py
+++ b/tests/test_fetch_branches.py
@@ -82,8 +82,8 @@ class TestFetchBranches(test_util.TestBa
 
     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', ])
+        self.assertEqual(sorted(repo['tip'].manifest().keys()),
+                         ['alpha', 'beta', 'gamma', 'iota', ])
 
     def test_branch_tip_update_to_default(self):
         repo = self._load_fixture_and_fetch('unorderedbranch.svndump',
--- a/tests/test_push_command.py
+++ b/tests/test_push_command.py
@@ -267,8 +267,8 @@ class PushTests(test_util.TestBase):
         commands.push(repo.ui, repo)
         self.assertEqual(self.repo['tip'].parents()[0].parents()[0].node(), oldtiphash)
         self.assertEqual(self.repo['tip'].files(), ['delta', ])
-        self.assertEqual(self.repo['tip'].manifest().keys(),
-                         ['alpha', 'beta', 'gamma', 'delta'])
+        self.assertEqual(sorted(self.repo['tip'].manifest().keys()),
+                         ['alpha', 'beta', 'delta', 'gamma'])
 
     def test_push_two_revs(self):
         # set up some work for us
@@ -419,8 +419,9 @@ class PushTests(test_util.TestBase):
         self.assert_('@' in self.repo['tip'].user())
         self.assertEqual(tip['gamma'].flags(), 'x')
         self.assertEqual(tip['gamma'].data(), 'foo')
-        self.assertEqual([x for x in tip.manifest().keys() if 'x' not in
-                          tip[x].flags()], ['alpha', 'beta', 'adding_file', ])
+        self.assertEqual(sorted([x for x in tip.manifest().keys() if 'x' not in
+                                tip[x].flags()]),
+                         ['adding_file', 'alpha', 'beta', ])
 
     def test_push_symlink_file(self):
         self.test_push_to_default(commit=True)
@@ -451,8 +452,9 @@ class PushTests(test_util.TestBase):
         self.assertNotEqual(tip.node(), new_hash)
         self.assertEqual(tip['gamma'].flags(), 'l')
         self.assertEqual(tip['gamma'].data(), 'foo')
-        self.assertEqual([x for x in tip.manifest().keys() if 'l' not in
-                          tip[x].flags()], ['alpha', 'beta', 'adding_file', ])
+        self.assertEqual(sorted([x for x in tip.manifest().keys() if 'l' not in
+                                 tip[x].flags()]),
+                         ['adding_file', 'alpha', 'beta', ])
 
         def file_callback2(repo, memctx, path):
             if path == 'gamma':
@@ -636,7 +638,7 @@ class PushTests(test_util.TestBase):
         parent = self.repo['tip'].rev()
         self.commitchanges(changes, parent=parent)
         self.pushrevisions()
-        self.assertEqual({}, self.repo['tip'].manifest())
+        self.assertEqual(len(self.repo['tip'].manifest()), 0)
 
         # Try to re-add a file after emptying the branch
         changes = [
--- a/tests/test_push_renames.py
+++ b/tests/test_push_renames.py
@@ -66,8 +66,8 @@ class TestPushRenames(test_util.TestBase
             ]
         self.commitchanges(changes)
         self.pushrevisions()
-        self.assertEqual(self.repo['tip'].manifest().keys(),
-                         ['a', 'c', 'b', 'e', 'd',
+        self.assertEqual(sorted(self.repo['tip'].manifest().keys()),
+                         ['a', 'b', 'c', 'd', 'e',
                           'random2/dir with space/file with space'])
 
     def test_push_rename_tree(self):
--- a/tests/test_single_dir_clone.py
+++ b/tests/test_single_dir_clone.py
@@ -21,15 +21,15 @@ class TestSingleDirClone(test_util.TestB
                                             subdir='')
         self.assertEqual(compathacks.branchset(repo),
                          set(['default']))
-        self.assertEqual(repo['tip'].manifest().keys(),
-                         ['trunk/beta',
+        self.assertEqual(sorted(repo['tip'].manifest().keys()),
+                         ['branches/branch_from_tag/alpha',
+                          'branches/branch_from_tag/beta',
                           'tags/copied_tag/alpha',
-                          'trunk/alpha',
                           'tags/copied_tag/beta',
-                          'branches/branch_from_tag/alpha',
                           'tags/tag_r3/alpha',
                           'tags/tag_r3/beta',
-                          'branches/branch_from_tag/beta'])
+                          'trunk/alpha',
+                          'trunk/beta'])
 
     def test_auto_detect_single(self):
         repo = self._load_fixture_and_fetch('branch_from_tag.svndump',
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -138,16 +138,9 @@ rename a tag
             'branch': 'magic',
             'convert_revision': 'svn:af82cc90-c2d2-43cd-b1aa-c8a78449440a/tags/also-edit@14'})
        self.assertEqual(repo[alsoedit].parents()[0].node(), repo.tags()['also-edit'])
-       self.assertEqual(repo['also-edit'].manifest().keys(),
-                        ['beta',
-                         '.hgtags',
-                         'delta',
-                         'alpha',
-                         'omega',
-                         'iota',
-                         'gamma',
-                         'lambda',
-                         ])
+       self.assertEqual(sorted(repo['also-edit'].manifest().keys()),
+                        ['.hgtags', 'alpha', 'beta', 'delta', 'gamma', 'iota',
+                         'lambda', 'omega'])
 
        self.assertEqual(editlater, repo['edit-later'].node())
        self.assertEqual(