# HG changeset patch # User Dan Villiom Podlaski Christiansen # Date 1242837481 -7200 # Node ID 76c833526fbce97f55a547155dedd82646f1242a # Parent cfbd0e563af9f2650864a901783c8b091896029e Use fallbacks in the wrappers for Subversion support, instead of prefixes. The change only applies to the ambiguous URL schemes: file, http and https. The others - svn+ssh and svn - behave the same as previously. For http and https, the wrapping is implemented in 'svnrepo.py': Only when the attempt to create a httprepo instance fails, will the URL be considered for Subversion URL. For file, the ambiguity is treated much like the Mercurial core distinguishes bundle and directories. In this case, a directory that looks like a Subversion repository will *not* be considered for a Mercurial clone. Tthe command lines are more similar to before this refactor. The only option added to push & pull is --stupid; others are only added to clone. Any of these options specified to clone will be added to the generated '.hgrc'. Also, the -r/--rev option now works for clone & push. diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py --- a/hgsubversion/__init__.py +++ b/hgsubversion/__init__.py @@ -28,17 +28,17 @@ from mercurial import commands from mercurial import extensions from mercurial import hg from mercurial import util as hgutil +from mercurial import cmdutil as hgcmdutil from svn import core import svncommands +import cmdutil import svnrepo import util import wrappers import svnexternals -schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+file') - optionmap = { 'tagpaths': ('hgsubversion', 'tagpaths'), 'authors': ('hgsubversion', 'authormap'), @@ -49,14 +49,25 @@ optionmap = { 'usebranchnames': ('hgsubversion', 'usebranchnames'), } +svnopts = (('', 'stupid', '', 'use slower, but more compatible, protocol for ' + 'Subversion'),) + +svncloneopts = (('T', 'tagpaths', [], 'list of path s to search for tags ' + 'in Subversion repositories'), + ('A', 'authors', '', 'path to file mapping Subversion ' + 'usernames to Mercurial authors'), + ('', 'filemap', '', 'path to file containing rules for ' + 'remapping Subversion repository paths'),) + + + def wrapper(orig, ui, repo, *args, **opts): """ - Some of the options listed below only apply to Subversion - %(action)s. See 'hg help %(extension)s' for more information on - them as well as other ways of customising the conversion process. + Subversion %(target)s can be used for %(command)s. See 'hg help + %(extension)s' for more on the conversion process. """ for opt, (section, name) in optionmap.iteritems(): - if opt in opts: + if opt in opts and opts[opt]: if isinstance(repo, str): ui.setconfig(section, name, opts.pop(opt)) else: @@ -64,6 +75,35 @@ def wrapper(orig, ui, repo, *args, **opt return orig(ui, repo, *args, **opts) +def clonewrapper(orig, ui, source, dest=None, **opts): + """ + Some of the options listed below only apply to Subversion + %(target)s. See 'hg help %(extension)s' for more information on + them as well as other ways of customising the conversion process. + """ + + for opt, (section, name) in optionmap.iteritems(): + if opt in opts and opts[opt]: + ui.setconfig(section, name, opts.pop(opt)) + + # this must be kept in sync with mercurial/commands.py + srcrepo, dstrepo = hg.clone(hgcmdutil.remoteui(ui, opts), source, dest, + pull=opts.get('pull'), + stream=opts.get('uncompressed'), + rev=opts.get('rev'), + update=not opts.get('noupdate')) + + if dstrepo.local() and srcrepo.capable('subversion'): + fd = dstrepo.opener("hgrc", "a", text=True) + for section in set(s for s, v in optionmap.itervalues()): + if ui.has_section(section): + fd.write('\n[%s]\n' % section) + + for key, value in ui.configitems(section): + if not isinstance(value, str): + value = ', '.join(value) + fd.write('%s = %s\n' % (key, value)) + def uisetup(ui): """Do our UI setup. @@ -82,21 +122,19 @@ def uisetup(ui): entry[1].append(('', 'svn', None, "show svn-style diffs, default against svn parent")) - newflags = (('A', 'authors', '', 'path to file mapping Subversion ' - 'usernames to Mercurial authors.'), - ('', 'filemap', '', 'path to file containing rules for ' - 'mapping Subversion repository paths.'), - ('T', 'tagpaths', ['tags'], 'list of paths to search for tags ' - 'in Subversion repositories.')) - - for command, action in [('clone', 'sources'), ('pull', 'sources'), - ('push', 'destinations')]: - doc = wrapper.__doc__.strip() % { 'extension': 'hgsubversion', - 'action': action } + for command, target, isclone in [('clone', 'sources', True), + ('pull', 'sources', False), + ('push', 'destinations', False)]: + doc = wrapper.__doc__.strip() % { 'command': command, + 'Command': command.capitalize(), + 'extension': 'hgsubversion', + 'target': target } fn = getattr(commands, command) fn.__doc__ = fn.__doc__.rstrip() + '\n\n ' + doc - entry = extensions.wrapcommand(commands.table, command, wrapper) - entry[1].extend(newflags) + entry = extensions.wrapcommand(commands.table, command, + (wrapper, clonewrapper)[isclone]) + entry[1].extend(svnopts) + if isclone: entry[1].extend(svncloneopts) try: rebase = extensions.find('rebase') @@ -147,8 +185,16 @@ def reposetup(ui, repo): if repo.local(): svnrepo.generate_repo_class(ui, repo) -for scheme in schemes: - hg.schemes[scheme] = svnrepo +_origschemes = hg.schemes.copy() +def _lookup(url): + if cmdutil.islocalrepo(url): + return svnrepo + else: + return _origschemes['file'](url) + +# install scheme handlers +hg.schemes.update({ 'file': _lookup, 'http': svnrepo, 'https': svnrepo, + 'svn': svnrepo, 'svn+ssh': svnrepo }) cmdtable = { "svn": @@ -165,3 +211,6 @@ cmdtable = { svncommands._helpgen(), ), } + +# only these methods are public +__all__ = ('cmdtable', 'reposetup', 'uisetup') diff --git a/hgsubversion/cmdutil.py b/hgsubversion/cmdutil.py --- a/hgsubversion/cmdutil.py +++ b/hgsubversion/cmdutil.py @@ -297,4 +297,7 @@ def islocalrepo(url): return False def issvnurl(url): - return url.startswith('svn') or islocalrepo(url) + for scheme in ('svn', 'http', 'https', 'svn+ssh'): + if url.startswith(scheme + '://'): + return True + return islocalrepo(url) diff --git a/hgsubversion/hg_delta_editor.py b/hgsubversion/hg_delta_editor.py --- a/hgsubversion/hg_delta_editor.py +++ b/hgsubversion/hg_delta_editor.py @@ -96,7 +96,7 @@ class HgChangeReceiver(delta.Editor): if not filemap: filemap = self.ui.config('hgsubversion', 'filemap') if not tag_locations: - tag_locations = self.ui.config('hgsubversion', 'tagpaths', ['tags']) + tag_locations = self.ui.configlist('hgsubversion', 'tagpaths', ['tags']) self.usebranchnames = self.ui.configbool('hgsubversion', 'usebranchnames', True) diff --git a/hgsubversion/svncommands.py b/hgsubversion/svncommands.py --- a/hgsubversion/svncommands.py +++ b/hgsubversion/svncommands.py @@ -62,8 +62,6 @@ def rebuildmeta(ui, repo, hg_repo_path, url = repo.ui.expandpath(dest or 'default-push', dest or 'default') else: url = args[0] - if not (url.startswith('svn+') or url.startswith('svn:')): - raise hgutil.Abort('No valid Subversion URI found; please specify one.') uuid = None url = util.normalize_url(url.rstrip('/')) user, passwd = util.getuserpass(opts) diff --git a/hgsubversion/svnrepo.py b/hgsubversion/svnrepo.py --- a/hgsubversion/svnrepo.py +++ b/hgsubversion/svnrepo.py @@ -14,8 +14,10 @@ subclass: pull() is called on the instan are used to distinguish and filter these operations from others. """ +from mercurial import error from mercurial import node from mercurial import util as hgutil +from mercurial import httprepo import mercurial.repo import hg_delta_editor @@ -42,7 +44,8 @@ def generate_repo_class(ui, repo): """ original = getattr(repo.__class__, fn.__name__) def wrapper(self, *args, **opts): - if 'subversion' in getattr(args[0], 'capabilities', []): + capable = getattr(args[0], 'capable', lambda x: False) + if capable('subversion'): return fn(self, *args, **opts) else: return original(self, *args, **opts) @@ -53,16 +56,14 @@ def generate_repo_class(ui, repo): class svnlocalrepo(repo.__class__): @remotesvn def push(self, remote, force=False, revs=None): - # TODO: pass on revs wrappers.push(self, dest=remote.svnurl, force=force, revs=None) @remotesvn def pull(self, remote, heads=None, force=False): + lock = self.wlock() try: - lock = self.wlock() - wrappers.pull(self, source=remote.svnurl, rev=heads, force=force) - except KeyboardInterrupt: - pass + wrappers.pull(self, source=remote.svnurl, + rev=heads, force=force) finally: lock.release() @@ -90,7 +91,7 @@ class svnremoterepo(mercurial.repo.repos return util.normalize_url(self.path) def url(self): - return self.path + return self.path.rstrip('/') def lookup(self, key): return key @@ -106,6 +107,13 @@ class svnremoterepo(mercurial.repo.repos raise hgutil.Abort('command unavailable for Subversion repositories') def instance(ui, url, create): + if url.startswith('http://') or url.startswith('https://'): + try: + # may yield a bogus 'real URL...' message + return httprepo.instance(ui, url, create) + except error.RepoError: + pass + if create: raise hgutil.Abort('cannot create new remote Subversion repository') diff --git a/hgsubversion/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py --- a/hgsubversion/svnwrap/svn_swig_wrapper.py +++ b/hgsubversion/svnwrap/svn_swig_wrapper.py @@ -358,7 +358,7 @@ class SubversionRepo(object): The reason this is lazy is so that you can use the same repo object to perform RA calls to get deltas. """ - return self.fetch_history_at_paths([''], start=start, + return self.fetch_history_at_paths([''], start=start, stop=stop, chunk_size=chunk_size) def fetch_history_at_paths(self, paths, start=None, stop=None, diff --git a/hgsubversion/util.py b/hgsubversion/util.py --- a/hgsubversion/util.py +++ b/hgsubversion/util.py @@ -24,8 +24,6 @@ def version(ui): def normalize_url(svnurl): - if svnurl.startswith('svn+') and not svnurl.startswith('svn+ssh'): - svnurl = svnurl[4:] url, revs, checkout = hg.parseurl(svnurl) url = url.rstrip('/') if checkout: diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py --- a/hgsubversion/wrappers.py +++ b/hgsubversion/wrappers.py @@ -202,12 +202,18 @@ def pull(repo, source="default", rev=Non url = repo.ui.expandpath(source) svn_url = util.normalize_url(url) - # Split off #rev; TODO: implement --rev/#rev support limiting the pulled/cloned revisions + # Split off #rev svn_url, revs, checkout = hg.parseurl(svn_url, rev) old_encoding = util.swap_out_encoding() # TODO implement skipto support skipto_rev = 0 + try: + stopat_rev = int(checkout or 0) + except ValueError: + raise hgutil.Abort('unrecognised Subversion revision; ' + 'only numbers work.') + have_replay = not repo.ui.configbool('hgsubversion', 'stupid') if have_replay and not callable( delta.svn_txdelta_apply(None, None, None)[0]): #pragma: no cover @@ -235,8 +241,9 @@ def pull(repo, source="default", rev=Non revisions = 0 - # start converting revisions - for r in svn.revisions(start=start): + try: + # start converting revisions + for r in svn.revisions(start=start, stop=stopat_rev): valid = True hg_editor.update_branch_tag_map_for_rev(r) for p in r.paths: @@ -270,6 +277,9 @@ def pull(repo, source="default", rev=Non else: raise hgutil.Abort(*e.args) revisions += 1 + except KeyboardInterrupt: + pass + util.swap_out_encoding(old_encoding) if revisions == 0: diff --git a/tests/test_pull.py b/tests/test_pull.py --- a/tests/test_pull.py +++ b/tests/test_pull.py @@ -21,7 +21,7 @@ class TestPull(test_util.TestBase): if self.svn_wc is None: self.svn_wc = os.path.join(self.tmpdir, 'testsvn_wc') subprocess.call([ - 'svn', 'co', '-q', test_util.fileurl(self.repo_path)[4:], + 'svn', 'co', '-q', test_util.fileurl(self.repo_path), self.svn_wc ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) diff --git a/tests/test_util.py b/tests/test_util.py --- a/tests/test_util.py +++ b/tests/test_util.py @@ -31,7 +31,7 @@ def fileurl(path): path = urllib.pathname2url(path) if drive: drive = '/' + drive - url = 'svn+file://%s%s' % (drive, path) + url = 'file://%s%s' % (drive, path) return url def load_svndump_fixture(path, fixture_name):