# HG changeset patch # User Augie Fackler # Date 1430849383 14400 # Node ID 6fd0ec01553b91261868523173a944daf5b164f4 # Parent b13f320ff4e3af4ea4813185275a4a618d74130d# Parent fd3461f865826f682202fe199715bd343266630a Merge to stable for release. diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py --- 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', '', diff --git a/hgsubversion/layouts/__init__.py b/hgsubversion/layouts/__init__.py --- 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. """ diff --git a/hgsubversion/layouts/base.py b/hgsubversion/layouts/base.py --- 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. diff --git a/hgsubversion/layouts/custom.py b/hgsubversion/layouts/custom.py --- 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] diff --git a/hgsubversion/layouts/detect.py b/hgsubversion/layouts/detect.py 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' diff --git a/hgsubversion/layouts/single.py b/hgsubversion/layouts/single.py --- 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 diff --git a/hgsubversion/layouts/standard.py b/hgsubversion/layouts/standard.py --- 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): diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py --- 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: diff --git a/hgsubversion/svncommands.py b/hgsubversion/svncommands.py --- 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: diff --git a/hgsubversion/svnmeta.py b/hgsubversion/svnmeta.py --- 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 diff --git a/hgsubversion/util.py b/hgsubversion/util.py --- 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. diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py --- 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'), diff --git a/tests/test_fetch_branches.py b/tests/test_fetch_branches.py --- 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', diff --git a/tests/test_push_command.py b/tests/test_push_command.py --- 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 = [ diff --git a/tests/test_push_renames.py b/tests/test_push_renames.py --- 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): diff --git a/tests/test_single_dir_clone.py b/tests/test_single_dir_clone.py --- 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', diff --git a/tests/test_tags.py b/tests/test_tags.py --- 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(