Mercurial > hgsubversion
changeset 1092:cd0d14e25757
layouts: add custom layout for those of us that need weird mappings
This adds a config-driven custom layout, targeted at the case where
you need to fetch a small subset of a large number of subversion
branches, or where your subversion layout doesn't match the standard
trunk/branches/tags layout very well.
author | David Schleimer <dschleimer@fb.com> |
---|---|
date | Mon, 26 Aug 2013 16:40:31 -0700 |
parents | 384eb7e05b61 |
children | 791382a21cc4 |
files | hgsubversion/help/subversion.rst hgsubversion/layouts/__init__.py hgsubversion/layouts/custom.py hgsubversion/svncommands.py hgsubversion/wrappers.py tests/comprehensive/test_custom_layout.py tests/comprehensive/test_rebuildmeta.py tests/comprehensive/test_stupid_pull.py tests/comprehensive/test_updatemeta.py tests/comprehensive/test_verify_and_startrev.py tests/test_util.py tests/test_utility_commands.py |
diffstat | 12 files changed, 399 insertions(+), 43 deletions(-) [+] |
line wrap: on
line diff
--- a/hgsubversion/help/subversion.rst +++ b/hgsubversion/help/subversion.rst @@ -45,6 +45,10 @@ hgsubversion.layout option (see below fo $ hg clone --layout single svn+http://python-nose.googlecode.com/svn nose-hg +Finally, if you want to clone two or more directores as separate +branches, use the custom layout. See the documentation below for the +``hgsubversionbranch.*`` configuration for detailed help. + Pulling new revisions into an already-converted repository is the same as from any other Mercurial source. Within the first example above, the following three commands are all equivalent:: @@ -357,7 +361,9 @@ The following options only have an effec repository is converted into a single branch. The default, ``auto``, causes hgsubversion to assume a standard layout if any of trunk, branches, or tags exist within the specified directory - on the server. + on the server. ``custom`` causes hgsubversion to read the + ``hgsubversionbranch`` config section to determine the repository + layout. ``hgsubversion.startrev`` @@ -389,6 +395,27 @@ The following options only have an effec you use this option, be sure to carefully check the result of a pull afterwards. + ``hgsubversionbranch.*`` + + Use this config section with the custom layout to specify a cusomt + mapping of subversion path to Mercurial branch. This is useful if + your layout is substantially different from the standard + trunk/branches/tags layout and/or you are only interested in a few + branches. + + Example config that pulls in trunk as the default branch, + personal/alice as the alice branch, and releases/2.0/2.7 as + release-2.7:: + + [hgsubversionbranch] + default = trunk + alice = personal/alice + release-2.7 = releases/2.0/2.7 + + Note that it is an error to specify more than one branch for a + given path, or to sepecify nested paths (e.g. releases/2.0 and + releases/2.0/2.7) + Please note that some of these options may be specified as command line options as well, and when done so, will override the configuration. If an authormap, filemap or branchmap is specified, its contents will be read and stored for use
--- a/hgsubversion/layouts/__init__.py +++ b/hgsubversion/layouts/__init__.py @@ -11,6 +11,7 @@ NB: this has a long way to go before it from mercurial import util as hgutil +import custom import detect import persist import single @@ -26,6 +27,7 @@ import standard # The intention is for extension authors who wish to build their own # layout to add it to this dict. NAME_TO_CLASS = { + "custom": custom.CustomLayout, "single": single.SingleLayout, "standard": standard.StandardLayout, }
new file mode 100644 --- /dev/null +++ b/hgsubversion/layouts/custom.py @@ -0,0 +1,105 @@ +"""Layout that allows you to define arbitrary subversion to mercurial mappings. + +This is the simplest layout to use if your layout is just plain weird. +Also useful if your layout is pretty normal, but you personally only +want a couple of branches. + + +""" + +import base + + +class CustomLayout(base.BaseLayout): + + def __init__(self, ui): + base.BaseLayout.__init__(self, ui) + + self.svn_to_hg = {} + self.hg_to_svn = {} + + for hg_branch, svn_path in ui.configitems('hgsubversionbranch'): + + hg_branch = hg_branch.strip() + if hg_branch == 'default' or not hg_branch: + hg_branch = None + svn_path = svn_path.strip('/') + + for other_svn in self.svn_to_hg: + if other_svn == svn_path: + msg = 'specified two hg branches for svn path %s: %s and %s' + raise hgutil.Abort(msg % (svn_path, other_hg, hg_branch)) + + if (other_svn.startswith(svn_path + '/') or + svn_path.startswith(other_svn + '/')): + msg = 'specified mappings for nested svn paths: %s and %s' + raise hgutl.Abort(msg % (svn_path, other_svn)) + + self.svn_to_hg[svn_path] = hg_branch + self.hg_to_svn[hg_branch] = svn_path + + def localname(self, path): + if path in self.svn_to_hg: + return self.svn_to_hg[path] + children = [] + for svn_path in self.svn_to_hg: + if svn_path.startswith(path + '/'): + children.append(svn_path) + if len(children) == 1: + return self.svn_to_hg[children[0]] + + return '../%s' % path + + def remotename(self, branch): + if branch =='default': + branch = None + if branch and branch.startswith('../'): + return branch[3:] + if branch not in self.hg_to_svn: + raise KeyError('Unknown mercurial branch: %s' % branch) + return self.hg_to_svn[branch] + + def remotepath(self, branch, subdir='/'): + if not subdir.endswith('/'): + subdir += '/' + return subdir + self.remotename(branch) + + def taglocations(self, meta_data_dir): + return [] + + def get_path_tag(self, path, taglocations): + return None + + def split_remote_name(self, path, known_branches): + if path in self.svn_to_hg: + return path, '' + children = [] + for svn_path in self.svn_to_hg: + if path.startswith(svn_path + '/'): + return svn_path, path[len(svn_path)+1:] + if svn_path.startswith(path + '/'): + children.append(svn_path) + + # if the path represents the parent of exactly one of our svn + # branches, treat it as though it were that branch, because + # that means we are probably pulling in a subproject of an svn + # project, and someone copied the parent svn project. + if len(children) == 1: + return children[0], '' + + for branch in known_branches: + if branch and branch.startswith('../'): + if path.startswith(branch[3:] + '/'): + # -3 for the leading ../, plus one for the trailing / + return branch[3:], path[len(branch) - 2:] + if branch[3:].startswith(path + '/'): + children.append(branch[3:]) + + if len(children) == 1: + return children[0], '' + + + # this splits on the rightmost '/' but considers the entire + # string to be the branch component of the path if there is no '/' + components = path.rsplit('/', 1) + return components[0], '/'.join(components[1:])
--- a/hgsubversion/svncommands.py +++ b/hgsubversion/svncommands.py @@ -214,6 +214,9 @@ def _buildmeta(ui, repo, args, partial=F ctx.branch(), ui) existing_layout = layouts.detect.layout_from_file(svnmetadir) if layout != existing_layout: + if existing_layout == 'custom' and layout == 'standard': + import pdb + pdb.set_trace() layouts.persist.layout_to_file(svnmetadir, layout) layoutobj = layouts.layout_from_name(layout, ui) elif layout == 'single':
--- a/hgsubversion/wrappers.py +++ b/hgsubversion/wrappers.py @@ -544,7 +544,13 @@ optionmap = { 'startrev': ('hgsubversion', 'startrev'), } -dontretain = { 'hgsubversion': set(['authormap', 'filemap', 'layout', ]) } +extrasections = set(['hgsubversionbranch']) + + +dontretain = { + 'hgsubversion': set(['authormap', 'filemap', 'layout', ]), + 'hgsubversionbranch': set(), + } def clone(orig, ui, source, dest=None, **opts): """ @@ -602,7 +608,9 @@ def clone(orig, ui, source, dest=None, * fd = dstrepo.opener("hgrc", "a", text=True) else: fd = dst.opener("hgrc", "a", text=True) - for section in set(s for s, v in optionmap.itervalues()): + preservesections = set(s for s, v in optionmap.itervalues()) + preservesections |= extrasections + for section in preservesections: config = dict(ui.configitems(section)) for name in dontretain[section]: config.pop(name, None)
new file mode 100644 --- /dev/null +++ b/tests/comprehensive/test_custom_layout.py @@ -0,0 +1,65 @@ +import os +import pickle +import sys +import unittest + +from mercurial import hg +from mercurial import ui + +# wrapped in a try/except because of weirdness in how +# run.py works as compared to nose. +try: + import test_util +except ImportError: + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + import test_util + +from hgsubversion import wrappers + + +def _do_case(self, name, stupid): + subdir = test_util.subdir.get(name, '') + config = { + 'hgsubversion.stupid': stupid and '1' or '0', + } + repo, repo_path = self.load_and_fetch(name, + subdir=subdir, + layout='auto', + config=config) + assert test_util.repolen(self.repo) > 0, \ + 'Repo had no changes, maybe you need to add a subdir entry in test_util?' + wc2_path = self.wc_path + '_custom' + checkout_path = repo_path + if subdir: + checkout_path += '/' + subdir + u = ui.ui() + if stupid: + u.setconfig('hgsubversion', 'stupid', '1') + u.setconfig('hgsubversion', 'layout', 'custom') + for branch, path in test_util.custom.get(name, {}).iteritems(): + u.setconfig('hgsubversionbranch', branch, path) + test_util.hgclone(u, + test_util.fileurl(checkout_path), + wc2_path, + update=False) + self.repo2 = hg.repository(ui.ui(), wc2_path) + self.assertEqual(self.repo.heads(), self.repo2.heads()) + + +def buildmethod(case, name, stupid): + m = lambda self: self._do_case(case, stupid) + m.__name__ = name + replay = stupid and 'stupid' or 'regular' + m.__doc__ = 'Test custom produces same as standard on %s. (%s)' % (case, + replay) + return m + +attrs = {'_do_case': _do_case, + } +for case in test_util.custom: + name = 'test_' + case[:-len('.svndump')].replace('-', '_') + attrs[name] = buildmethod(case, name, stupid=False) + name += '_stupid' + attrs[name] = buildmethod(case, name, stupid=True) + +CustomPullTests = type('CustomPullTests', (test_util.TestBase,), attrs)
--- a/tests/comprehensive/test_rebuildmeta.py +++ b/tests/comprehensive/test_rebuildmeta.py @@ -30,15 +30,18 @@ expect_youngest_skew = [('file_mixed_wit -def _do_case(self, name, single): +def _do_case(self, name, layout): subdir = test_util.subdir.get(name, '') - layout = 'auto' - if single: - layout = 'single' + single = layout == 'single' + u = ui.ui() + config = {} + if layout == 'custom': + for branch, path in test_util.custom.get(name, {}).iteritems(): + config['hgsubversionbranch.%s' % branch] = path + u.setconfig('hgsubversionbranch', branch, path) repo, repo_path = self.load_and_fetch(name, subdir=subdir, layout=layout) assert test_util.repolen(self.repo) > 0 wc2_path = self.wc_path + '_clone' - u = ui.ui() src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False) src = test_util.getlocalpeer(src) dest = test_util.getlocalpeer(dest) @@ -136,11 +139,11 @@ def _run_assertions(self, name, single, self.assertEqual(srcinfo[2], destinfo[2]) -def buildmethod(case, name, single): - m = lambda self: self._do_case(case, single) +def buildmethod(case, name, layout): + m = lambda self: self._do_case(case, layout) m.__name__ = name m.__doc__ = ('Test rebuildmeta on %s (%s)' % - (case, (single and 'single') or 'standard')) + (case, layout)) return m @@ -158,8 +161,11 @@ for case in [f for f in os.listdir(test_ if case in skip: continue bname = 'test_' + case[:-len('.svndump')] - attrs[bname] = buildmethod(case, bname, False) - name = bname + '_single' - attrs[name] = buildmethod(case, name, True) + attrs[bname] = buildmethod(case, bname, 'auto') + attrs[bname + '_single'] = buildmethod(case, bname + '_single', 'single') + if case in test_util.custom: + attrs[bname + '_custom'] = buildmethod(case, + bname + '_custom', + 'single') RebuildMetaTests = type('RebuildMetaTests', (test_util.TestBase,), attrs)
--- a/tests/comprehensive/test_stupid_pull.py +++ b/tests/comprehensive/test_stupid_pull.py @@ -19,11 +19,18 @@ from hgsubversion import wrappers def _do_case(self, name, layout): subdir = test_util.subdir.get(name, '') - repo, repo_path = self.load_and_fetch(name, subdir=subdir, layout=layout) + config = {} + u = ui.ui() + for branch, path in test_util.custom.get(name, {}).iteritems(): + config['hgsubversionbranch.%s' % branch] = path + u.setconfig('hgsubversionbranch', branch, path) + repo, repo_path = self.load_and_fetch(name, + subdir=subdir, + layout=layout, + config=config) assert test_util.repolen(self.repo) > 0, \ 'Repo had no changes, maybe you need to add a subdir entry in test_util?' wc2_path = self.wc_path + '_stupid' - u = ui.ui() checkout_path = repo_path if subdir: checkout_path += '/' + subdir @@ -52,7 +59,8 @@ for case in (f for f in os.listdir(test_ # here, but since it isn't a regression we suppress the test case. if case != 'branchtagcollision.svndump': attrs[name] = buildmethod(case, name, 'auto') - name += '_single' - attrs[name] = buildmethod(case, name, 'single') + attrs[name + '_single'] = buildmethod(case, name + '_single', 'single') + if case in test_util.custom: + attrs[name + '_custom'] = buildmethod(case, name + '_custom', 'custom') StupidPullTests = type('StupidPullTests', (test_util.TestBase,), attrs)
--- a/tests/comprehensive/test_updatemeta.py +++ b/tests/comprehensive/test_updatemeta.py @@ -23,15 +23,24 @@ from hgsubversion import svnmeta -def _do_case(self, name, single): +def _do_case(self, name, layout): subdir = test_util.subdir.get(name, '') - layout = 'auto' - if single: - layout = 'single' - repo, repo_path = self.load_and_fetch(name, subdir=subdir, layout=layout) + single = layout == 'single' + u = ui.ui() + config = {} + if layout == 'custom': + config['hgsubversion.layout'] = 'custom' + u.setconfig('hgsubversion', 'layout', 'custom') + for branch, path in test_util.custom.get(name, {}).iteritems(): + config['hgsubversionbranch.%s' % branch] = path + u.setconfig('hgsubversionbranch', branch, path) + + repo, repo_path = self.load_and_fetch(name, + subdir=subdir, + layout=layout, + config=config) assert test_util.repolen(self.repo) > 0 wc2_path = self.wc_path + '_clone' - u = ui.ui() src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False) src = test_util.getlocalpeer(src) dest = test_util.getlocalpeer(dest) @@ -74,8 +83,14 @@ for case in [f for f in os.listdir(test_ if case in skip: continue bname = 'test_' + case[:-len('.svndump')] - attrs[bname] = test_rebuildmeta.buildmethod(case, bname, False) - name = bname + '_single' - attrs[name] = test_rebuildmeta.buildmethod(case, name, True) + attrs[bname] = test_rebuildmeta.buildmethod(case, bname, 'auto') + attrs[bname + '_single'] = test_rebuildmeta.buildmethod(case, + bname + '_single', + 'single') + if case in test_util.custom: + attrs[bname + '_custom'] = test_rebuildmeta.buildmethod(case, + bname + '_custom', + 'custom') + UpdateMetaTests = type('UpdateMetaTests', (test_util.TestBase,), attrs)
--- a/tests/comprehensive/test_verify_and_startrev.py +++ b/tests/comprehensive/test_verify_and_startrev.py @@ -39,7 +39,13 @@ from hgsubversion import verify def _do_case(self, name, layout): subdir = test_util.subdir.get(name, '') - repo, svnpath = self.load_and_fetch(name, subdir=subdir, layout=layout) + config = {} + for branch, path in test_util.custom.get(name, {}).iteritems(): + config['hgsubversionbranch.%s' % branch] = path + repo, svnpath = self.load_and_fetch(name, + subdir=subdir, + layout=layout, + config=config) assert test_util.repolen(self.repo) > 0 for i in repo: ctx = repo[i] @@ -98,7 +104,8 @@ for case in fixtures: bname = 'test_' + case[:-len('.svndump')] if case not in _skipstandard: attrs[bname] = buildmethod(case, bname, 'standard') - name = bname + '_single' - attrs[name] = buildmethod(case, name, 'single') + attrs[bname + '_single'] = buildmethod(case, bname + '_single', 'single') + if case in test_util.custom: + attrs[bname + '_custom'] = buildmethod(case, bname + '_custom', 'custom') VerifyTests = type('VerifyTests', (test_util.TestBase,), attrs)
--- a/tests/test_util.py +++ b/tests/test_util.py @@ -106,6 +106,87 @@ subdir = {'truncatedhistory.svndump': '/ 'non_ascii_path_2.svndump': '/b%C3%B8b', 'subdir_is_file_prefix.svndump': '/flaf', } +# map defining the layouts of the fixtures we can use with custom layout +# these are really popular layouts, so I gave them names +trunk_only = { + 'default': 'trunk', + } +trunk_dev_branch = { + 'default': 'trunk', + 'dev_branch': 'branches/dev_branch', + } +custom = { + 'addspecial.svndump': { + 'default': 'trunk', + 'foo': 'branches/foo', + }, + 'binaryfiles.svndump': trunk_only, + 'branch_create_with_dir_delete.svndump': trunk_dev_branch, + 'branch_delete_parent_dir.svndump': trunk_dev_branch, + 'branchmap.svndump': { + 'default': 'trunk', + 'badname': 'branches/badname', + 'feature': 'branches/feature', + }, + 'branch_prop_edit.svndump': trunk_dev_branch, + 'branch_rename_to_trunk.svndump': { + 'default': 'trunk', + 'dev_branch': 'branches/dev_branch', + 'old_trunk': 'branches/old_trunk', + }, + 'copies.svndump': trunk_only, + 'copybeforeclose.svndump': { + 'default': 'trunk', + 'test': 'branches/test' + }, + 'delentries.svndump': trunk_only, + 'delete_restore_trunk.svndump': trunk_only, + 'empty_dir_in_trunk_not_repo_root.svndump': trunk_only, + 'executebit.svndump': trunk_only, + 'filecase.svndump': trunk_only, + 'file_not_in_trunk_root.svndump': trunk_only, + 'project_name_with_space.svndump': trunk_dev_branch, + 'pushrenames.svndump': trunk_only, + 'rename_branch_parent_dir.svndump': trunk_dev_branch, + 'renamedproject.svndump': { + 'default': 'trunk', + 'branch': 'branches/branch', + }, + 'renames.svndump': { + 'default': 'trunk', + 'branch1': 'branches/branch1', + }, + 'replace_branch_with_branch.svndump': { + 'default': 'trunk', + 'branch1': 'branches/branch1', + 'branch2': 'branches/branch2', + }, + 'replace_trunk_with_branch.svndump': { + 'default': 'trunk', + 'test': 'branches/test', + }, + 'revert.svndump': trunk_only, + 'siblingbranchfix.svndump': { + 'default': 'trunk', + 'wrongbranch': 'branches/wrongbranch', + }, + 'simple_branch.svndump': { + 'default': 'trunk', + 'the_branch': 'branches/the_branch', + }, + 'spaces-in-path.svndump': trunk_dev_branch, + 'symlinks.svndump': trunk_only, + 'truncatedhistory.svndump': trunk_only, + 'unorderedbranch.svndump': { + 'default': 'trunk', + 'branch': 'branches/branch', + }, + 'unrelatedbranch.svndump': { + 'default': 'trunk', + 'branch1': 'branches/branch1', + 'branch2': 'branches/branch2', + }, +} FIXTURES = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'fixtures')
--- a/tests/test_utility_commands.py +++ b/tests/test_utility_commands.py @@ -33,8 +33,15 @@ def repourl(repo_path): class UtilityTests(test_util.TestBase): stupid_mode_tests = True - def test_info_output(self): - repo, repo_path = self.load_and_fetch('two_heads.svndump') + def test_info_output(self, custom=False): + if custom: + config = { + 'hgsubversionbranch.default': 'trunk', + 'hgsubversionbranch.the_branch': 'branches/the_branch', + } + else: + config = {} + repo, repo_path = self.load_and_fetch('two_heads.svndump', config=config) hg.update(self.repo, 'the_branch') u = self.ui() u.pushbuffer() @@ -87,8 +94,21 @@ class UtilityTests(test_util.TestBase): }) self.assertMultiLineEqual(actual, expected) - def test_info_single(self): - repo, repo_path = self.load_and_fetch('two_heads.svndump', subdir='trunk') + def test_info_output_custom(self): + self.test_info_output(custom=True) + + def test_info_single(self, custom=False): + if custom: + subdir=None + config = { + 'hgsubversionbranch.default': 'trunk/' + } + else: + subdir='trunk' + config = {} + repo, repo_path = self.load_and_fetch('two_heads.svndump', + subdir=subdir, + config=config) hg.update(self.repo, 'tip') u = self.ui() u.pushbuffer() @@ -102,6 +122,9 @@ class UtilityTests(test_util.TestBase): }) self.assertMultiLineEqual(expected, actual) + def test_info_custom_single(self): + self.test_info_single(custom=True) + def test_missing_metadata(self): self._load_fixture_and_fetch('two_heads.svndump') os.remove(self.repo.join('svn/branch_info')) @@ -232,9 +255,18 @@ class UtilityTests(test_util.TestBase): self.assertEqual(self.repo['tip'].parents()[0].parents()[0], self.repo[0]) self.assertNotEqual(beforerebasehash, self.repo['tip'].node()) - def test_genignore(self): + def test_genignore(self, layout='auto'): """ Test generation of .hgignore file. """ - repo = self._load_fixture_and_fetch('ignores.svndump', noupdate=False) + if layout == 'custom': + config = { + 'hgsubversionbranch.default': 'trunk', + } + else: + config = {} + repo = self._load_fixture_and_fetch('ignores.svndump', + layout=layout, + noupdate=False, + config=config) u = self.ui() u.pushbuffer() svncommands.genignore(u, repo, self.wc_path) @@ -242,13 +274,10 @@ class UtilityTests(test_util.TestBase): '.hgignore\nsyntax:glob\nblah\notherblah\nbaz/magic\n') def test_genignore_single(self): - self._load_fixture_and_fetch('ignores.svndump', subdir='trunk') - hg.update(self.repo, 'tip') - u = self.ui() - u.pushbuffer() - svncommands.genignore(u, self.repo, self.wc_path) - self.assertMultiLineEqual(open(os.path.join(self.wc_path, '.hgignore')).read(), - '.hgignore\nsyntax:glob\nblah\notherblah\nbaz/magic\n') + self.test_genignore(layout='single') + + def test_genignore_custom(self): + self.test_genignore(layout='custom') def test_list_authors(self): repo_path = self.load_svndump('replace_trunk_with_branch.svndump')