url: move URL parsing functions into util to improve startup time
authorBrodie Rao <brodie@bitheap.org>
Sat, 30 Apr 2011 09:43:20 -0700
changeset 14076 924c82157d46
parent 14075 bc101902a68d
child 14077 c285bdb0572a
url: move URL parsing functions into util to improve startup time The introduction of the new URL parsing code has created a startup time regression. This is mainly due to the use of url.hasscheme() in the ui class. It ends up importing many libraries that the url module requires. This fix helps marginally, but if we can get rid of the urllib import in the URL parser all together, startup time will go back to normal. perfstartup time before the URL refactoring (8796fb6af67e): ! wall 0.050692 comb 0.000000 user 0.000000 sys 0.000000 (best of 100) current startup time (139fb11210bb): ! wall 0.070685 comb 0.000000 user 0.000000 sys 0.000000 (best of 100) after this change: ! wall 0.064667 comb 0.000000 user 0.000000 sys 0.000000 (best of 100)
hgext/fetch.py
hgext/patchbomb.py
hgext/schemes.py
mercurial/bundlerepo.py
mercurial/commands.py
mercurial/hg.py
mercurial/hgweb/hgwebdir_mod.py
mercurial/httprepo.py
mercurial/localrepo.py
mercurial/sshrepo.py
mercurial/statichttprepo.py
mercurial/subrepo.py
mercurial/ui.py
mercurial/url.py
mercurial/util.py
tests/test-url.py
--- a/hgext/fetch.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/hgext/fetch.py	Sat Apr 30 09:43:20 2011 -0700
@@ -9,7 +9,7 @@
 
 from mercurial.i18n import _
 from mercurial.node import nullid, short
-from mercurial import commands, cmdutil, hg, util, url, error
+from mercurial import commands, cmdutil, hg, util, error
 from mercurial.lock import release
 
 def fetch(ui, repo, source='default', **opts):
@@ -66,7 +66,7 @@
         other = hg.repository(hg.remoteui(repo, opts),
                               ui.expandpath(source))
         ui.status(_('pulling from %s\n') %
-                  url.hidepassword(ui.expandpath(source)))
+                  util.hidepassword(ui.expandpath(source)))
         revs = None
         if opts['rev']:
             try:
@@ -125,7 +125,7 @@
             # we don't translate commit messages
             message = (cmdutil.logmessage(opts) or
                        ('Automated merge with %s' %
-                        url.removeauth(other.url())))
+                        util.removeauth(other.url())))
             editor = cmdutil.commiteditor
             if opts.get('force_editor') or opts.get('edit'):
                 editor = cmdutil.commitforceeditor
--- a/hgext/patchbomb.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/hgext/patchbomb.py	Sat Apr 30 09:43:20 2011 -0700
@@ -48,7 +48,7 @@
 import os, errno, socket, tempfile, cStringIO, time
 import email.MIMEMultipart, email.MIMEBase
 import email.Utils, email.Encoders, email.Generator
-from mercurial import cmdutil, commands, hg, mail, patch, util, discovery, url
+from mercurial import cmdutil, commands, hg, mail, patch, util, discovery
 from mercurial.i18n import _
 from mercurial.node import bin
 
@@ -239,7 +239,7 @@
         dest, branches = hg.parseurl(dest)
         revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
         other = hg.repository(hg.remoteui(repo, opts), dest)
-        ui.status(_('comparing with %s\n') % url.hidepassword(dest))
+        ui.status(_('comparing with %s\n') % util.hidepassword(dest))
         common, _anyinc, _heads = discovery.findcommonincoming(repo, other)
         nodes = revs and map(repo.lookup, revs) or revs
         o = repo.changelog.findmissing(common, heads=nodes)
--- a/hgext/schemes.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/hgext/schemes.py	Sat Apr 30 09:43:20 2011 -0700
@@ -41,7 +41,7 @@
 """
 
 import os, re
-from mercurial import extensions, hg, templater, url as urlmod, util
+from mercurial import extensions, hg, templater, util
 from mercurial.i18n import _
 
 
@@ -95,4 +95,4 @@
                                'letter %s:\\\n') % (scheme, scheme.upper()))
         hg.schemes[scheme] = ShortRepository(url, scheme, t)
 
-    extensions.wrapfunction(urlmod, 'hasdriveletter', hasdriveletter)
+    extensions.wrapfunction(util, 'hasdriveletter', hasdriveletter)
--- a/mercurial/bundlerepo.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/bundlerepo.py	Sat Apr 30 09:43:20 2011 -0700
@@ -15,7 +15,7 @@
 from i18n import _
 import os, struct, tempfile, shutil
 import changegroup, util, mdiff, discovery
-import localrepo, changelog, manifest, filelog, revlog, error, url
+import localrepo, changelog, manifest, filelog, revlog, error
 
 class bundlerevlog(revlog.revlog):
     def __init__(self, opener, indexfile, bundle,
@@ -274,7 +274,7 @@
             cwd = os.path.join(cwd,'')
             if parentpath.startswith(cwd):
                 parentpath = parentpath[len(cwd):]
-    u = url.url(path)
+    u = util.url(path)
     path = u.localpath()
     if u.scheme == 'bundle':
         s = path.split("+", 1)
--- a/mercurial/commands.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/commands.py	Sat Apr 30 09:43:20 2011 -0700
@@ -2607,7 +2607,7 @@
         if 'bookmarks' not in other.listkeys('namespaces'):
             ui.warn(_("remote doesn't support bookmarks\n"))
             return 0
-        ui.status(_('comparing with %s\n') % url.hidepassword(source))
+        ui.status(_('comparing with %s\n') % util.hidepassword(source))
         return bookmarks.diff(ui, repo, other)
 
     ret = hg.incoming(ui, repo, source, opts)
@@ -2894,7 +2894,7 @@
         if 'bookmarks' not in other.listkeys('namespaces'):
             ui.warn(_("remote doesn't support bookmarks\n"))
             return 0
-        ui.status(_('comparing with %s\n') % url.hidepassword(dest))
+        ui.status(_('comparing with %s\n') % util.hidepassword(dest))
         return bookmarks.diff(ui, other, repo)
 
     ret = hg.outgoing(ui, repo, dest, opts)
@@ -2968,13 +2968,13 @@
     if search:
         for name, path in ui.configitems("paths"):
             if name == search:
-                ui.write("%s\n" % url.hidepassword(path))
+                ui.write("%s\n" % util.hidepassword(path))
                 return
         ui.warn(_("not found!\n"))
         return 1
     else:
         for name, path in ui.configitems("paths"):
-            ui.write("%s = %s\n" % (name, url.hidepassword(path)))
+            ui.write("%s = %s\n" % (name, util.hidepassword(path)))
 
 def postincoming(ui, repo, modheads, optupdate, checkout):
     if modheads == 0:
@@ -3017,7 +3017,7 @@
     """
     source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
     other = hg.repository(hg.remoteui(repo, opts), source)
-    ui.status(_('pulling from %s\n') % url.hidepassword(source))
+    ui.status(_('pulling from %s\n') % util.hidepassword(source))
     revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev'))
 
     if opts.get('bookmark'):
@@ -3100,7 +3100,7 @@
 
     dest = ui.expandpath(dest or 'default-push', dest or 'default')
     dest, branches = hg.parseurl(dest, opts.get('branch'))
-    ui.status(_('pushing to %s\n') % url.hidepassword(dest))
+    ui.status(_('pushing to %s\n') % util.hidepassword(dest))
     revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev'))
     other = hg.repository(hg.remoteui(repo, opts), dest)
     if revs:
@@ -3919,7 +3919,7 @@
         source, branches = hg.parseurl(ui.expandpath('default'))
         other = hg.repository(hg.remoteui(repo, {}), source)
         revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev'))
-        ui.debug('comparing with %s\n' % url.hidepassword(source))
+        ui.debug('comparing with %s\n' % util.hidepassword(source))
         repo.ui.pushbuffer()
         common, incoming, rheads = discovery.findcommonincoming(repo, other)
         repo.ui.popbuffer()
@@ -3929,7 +3929,7 @@
         dest, branches = hg.parseurl(ui.expandpath('default-push', 'default'))
         revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
         other = hg.repository(hg.remoteui(repo, {}), dest)
-        ui.debug('comparing with %s\n' % url.hidepassword(dest))
+        ui.debug('comparing with %s\n' % util.hidepassword(dest))
         repo.ui.pushbuffer()
         common, _anyinc, _heads = discovery.findcommonincoming(repo, other)
         repo.ui.popbuffer()
--- a/mercurial/hg.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/hg.py	Sat Apr 30 09:43:20 2011 -0700
@@ -11,13 +11,13 @@
 from node import hex, nullid
 import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo, bookmarks
 import lock, util, extensions, error, node
-import cmdutil, discovery, url
+import cmdutil, discovery
 import merge as mergemod
 import verify as verifymod
 import errno, os, shutil
 
 def _local(path):
-    path = util.expandpath(url.localpath(path))
+    path = util.expandpath(util.localpath(path))
     return (os.path.isfile(path) and bundlerepo or localrepo)
 
 def addbranchrevs(lrepo, repo, branches, revs):
@@ -54,7 +54,7 @@
 def parseurl(path, branches=None):
     '''parse url#branch, returning (url, (branch, branches))'''
 
-    u = url.url(path)
+    u = util.url(path)
     branch = None
     if u.fragment:
         branch = u.fragment
@@ -71,7 +71,7 @@
 }
 
 def _lookup(path):
-    u = url.url(path)
+    u = util.url(path)
     scheme = u.scheme or 'file'
     thing = schemes.get(scheme) or schemes['file']
     try:
@@ -221,8 +221,8 @@
     else:
         dest = ui.expandpath(dest)
 
-    dest = url.localpath(dest)
-    source = url.localpath(source)
+    dest = util.localpath(dest)
+    source = util.localpath(source)
 
     if os.path.exists(dest):
         if not os.path.isdir(dest):
@@ -248,7 +248,7 @@
         abspath = origsource
         copy = False
         if src_repo.cancopy() and islocal(dest):
-            abspath = os.path.abspath(url.localpath(origsource))
+            abspath = os.path.abspath(util.localpath(origsource))
             copy = not pull and not rev
 
         if copy:
@@ -421,7 +421,7 @@
     """
     source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
     other = repository(remoteui(repo, opts), source)
-    ui.status(_('comparing with %s\n') % url.hidepassword(source))
+    ui.status(_('comparing with %s\n') % util.hidepassword(source))
     revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
 
     if revs:
@@ -477,7 +477,7 @@
 def _outgoing(ui, repo, dest, opts):
     dest = ui.expandpath(dest or 'default-push', dest or 'default')
     dest, branches = parseurl(dest, opts.get('branch'))
-    ui.status(_('comparing with %s\n') % url.hidepassword(dest))
+    ui.status(_('comparing with %s\n') % util.hidepassword(dest))
     revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
     if revs:
         revs = [repo.lookup(rev) for rev in revs]
--- a/mercurial/hgweb/hgwebdir_mod.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/hgweb/hgwebdir_mod.py	Sat Apr 30 09:43:20 2011 -0700
@@ -9,7 +9,7 @@
 import os, re, time
 from mercurial.i18n import _
 from mercurial import ui, hg, scmutil, util, templater
-from mercurial import error, encoding, url
+from mercurial import error, encoding
 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
                    get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
 from hgweb_mod import hgweb
@@ -364,7 +364,7 @@
 
     def updatereqenv(self, env):
         if self._baseurl is not None:
-            u = url.url(self._baseurl)
+            u = util.url(self._baseurl)
             env['SERVER_NAME'] = u.host
             if u.port:
                 env['SERVER_PORT'] = u.port
--- a/mercurial/httprepo.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/httprepo.py	Sat Apr 30 09:43:20 2011 -0700
@@ -28,7 +28,7 @@
         self.path = path
         self.caps = None
         self.handler = None
-        u = url.url(path)
+        u = util.url(path)
         if u.query or u.fragment:
             raise util.Abort(_('unsupported URL component: "%s"') %
                              (u.query or u.fragment))
@@ -111,12 +111,12 @@
         except AttributeError:
             proto = resp.headers['content-type']
 
-        safeurl = url.hidepassword(self._url)
+        safeurl = util.hidepassword(self._url)
         # accept old "text/plain" and "application/hg-changegroup" for now
         if not (proto.startswith('application/mercurial-') or
                 proto.startswith('text/plain') or
                 proto.startswith('application/hg-changegroup')):
-            self.ui.debug("requested URL: '%s'\n" % url.hidepassword(cu))
+            self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
             raise error.RepoError(
                 _("'%s' does not appear to be an hg repository:\n"
                   "---%%<--- (%s)\n%s\n---%%<---\n")
--- a/mercurial/localrepo.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/localrepo.py	Sat Apr 30 09:43:20 2011 -0700
@@ -14,7 +14,6 @@
 import match as matchmod
 import merge as mergemod
 import tags as tagsmod
-import url as urlmod
 from lock import release
 import weakref, errno, os, time, inspect
 propertycache = util.propertycache
@@ -1695,7 +1694,7 @@
         cl.delayupdate()
         oldheads = cl.heads()
 
-        tr = self.transaction("\n".join([srctype, urlmod.hidepassword(url)]))
+        tr = self.transaction("\n".join([srctype, util.hidepassword(url)]))
         try:
             trp = weakref.proxy(tr)
             # pull off the changeset group
@@ -1937,7 +1936,7 @@
     return a
 
 def instance(ui, path, create):
-    return localrepository(ui, urlmod.localpath(path), create)
+    return localrepository(ui, util.localpath(path), create)
 
 def islocal(path):
     return True
--- a/mercurial/sshrepo.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/sshrepo.py	Sat Apr 30 09:43:20 2011 -0700
@@ -6,7 +6,7 @@
 # GNU General Public License version 2 or any later version.
 
 from i18n import _
-import util, error, wireproto, url
+import util, error, wireproto
 
 class remotelock(object):
     def __init__(self, repo):
@@ -23,7 +23,7 @@
         self._url = path
         self.ui = ui
 
-        u = url.url(path, parsequery=False, parsefragment=False)
+        u = util.url(path, parsequery=False, parsefragment=False)
         if u.scheme != 'ssh' or not u.host or u.path is None:
             self._abort(error.RepoError(_("couldn't parse location %s") % path))
 
--- a/mercurial/statichttprepo.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/statichttprepo.py	Sat Apr 30 09:43:20 2011 -0700
@@ -85,7 +85,7 @@
         self.ui = ui
 
         self.root = path
-        u = url.url(path.rstrip('/') + "/.hg")
+        u = util.url(path.rstrip('/') + "/.hg")
         self.path, authinfo = u.authinfo()
 
         opener = build_opener(ui, authinfo)
--- a/mercurial/subrepo.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/subrepo.py	Sat Apr 30 09:43:20 2011 -0700
@@ -8,7 +8,7 @@
 import errno, os, re, xml.dom.minidom, shutil, posixpath
 import stat, subprocess, tarfile
 from i18n import _
-import config, scmutil, util, node, error, cmdutil, url, bookmarks
+import config, scmutil, util, node, error, cmdutil, bookmarks
 hg = None
 propertycache = util.propertycache
 
@@ -194,13 +194,13 @@
     """return pull/push path of repo - either based on parent repo .hgsub info
     or on the top repo config. Abort or return None if no source found."""
     if hasattr(repo, '_subparent'):
-        source = url.url(repo._subsource)
+        source = util.url(repo._subsource)
         source.path = posixpath.normpath(source.path)
         if posixpath.isabs(source.path) or source.scheme:
             return str(source)
         parent = _abssource(repo._subparent, push, abort=False)
         if parent:
-            parent = url.url(parent)
+            parent = util.url(parent)
             parent.path = posixpath.join(parent.path, source.path)
             parent.path = posixpath.normpath(parent.path)
             return str(parent)
--- a/mercurial/ui.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/ui.py	Sat Apr 30 09:43:20 2011 -0700
@@ -7,7 +7,7 @@
 
 from i18n import _
 import errno, getpass, os, socket, sys, tempfile, traceback
-import config, scmutil, util, error, url
+import config, scmutil, util, error
 
 class ui(object):
     def __init__(self, src=None):
@@ -111,7 +111,7 @@
                                   % (n, p, self.configsource('paths', n)))
                         p = p.replace('%%', '%')
                     p = util.expandpath(p)
-                    if not url.hasscheme(p) and not os.path.isabs(p):
+                    if not util.hasscheme(p) and not os.path.isabs(p):
                         p = os.path.normpath(os.path.join(root, p))
                     c.set("paths", n, p)
 
@@ -332,7 +332,7 @@
 
     def expandpath(self, loc, default=None):
         """Return repository location relative to cwd or from [paths]"""
-        if url.hasscheme(loc) or os.path.isdir(os.path.join(loc, '.hg')):
+        if util.hasscheme(loc) or os.path.isdir(os.path.join(loc, '.hg')):
             return loc
 
         path = self.config('paths', loc)
--- a/mercurial/url.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/url.py	Sat Apr 30 09:43:20 2011 -0700
@@ -7,273 +7,11 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-import urllib, urllib2, httplib, os, socket, cStringIO, re
+import urllib, urllib2, httplib, os, socket, cStringIO
 import __builtin__
 from i18n import _
 import keepalive, util
 
-class url(object):
-    """Reliable URL parser.
-
-    This parses URLs and provides attributes for the following
-    components:
-
-    <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
-
-    Missing components are set to None. The only exception is
-    fragment, which is set to '' if present but empty.
-
-    If parsefragment is False, fragment is included in query. If
-    parsequery is False, query is included in path. If both are
-    False, both fragment and query are included in path.
-
-    See http://www.ietf.org/rfc/rfc2396.txt for more information.
-
-    Note that for backward compatibility reasons, bundle URLs do not
-    take host names. That means 'bundle://../' has a path of '../'.
-
-    Examples:
-
-    >>> url('http://www.ietf.org/rfc/rfc2396.txt')
-    <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
-    >>> url('ssh://[::1]:2200//home/joe/repo')
-    <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
-    >>> url('file:///home/joe/repo')
-    <url scheme: 'file', path: '/home/joe/repo'>
-    >>> url('bundle:foo')
-    <url scheme: 'bundle', path: 'foo'>
-    >>> url('bundle://../foo')
-    <url scheme: 'bundle', path: '../foo'>
-    >>> url('c:\\\\foo\\\\bar')
-    <url path: 'c:\\\\foo\\\\bar'>
-
-    Authentication credentials:
-
-    >>> url('ssh://joe:xyz@x/repo')
-    <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
-    >>> url('ssh://joe@x/repo')
-    <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
-
-    Query strings and fragments:
-
-    >>> url('http://host/a?b#c')
-    <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
-    >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
-    <url scheme: 'http', host: 'host', path: 'a?b#c'>
-    """
-
-    _safechars = "!~*'()+"
-    _safepchars = "/!~*'()+"
-    _matchscheme = re.compile(r'^[a-zA-Z0-9+.\-]+:').match
-
-    def __init__(self, path, parsequery=True, parsefragment=True):
-        # We slowly chomp away at path until we have only the path left
-        self.scheme = self.user = self.passwd = self.host = None
-        self.port = self.path = self.query = self.fragment = None
-        self._localpath = True
-        self._hostport = ''
-        self._origpath = path
-
-        # special case for Windows drive letters
-        if hasdriveletter(path):
-            self.path = path
-            return
-
-        # For compatibility reasons, we can't handle bundle paths as
-        # normal URLS
-        if path.startswith('bundle:'):
-            self.scheme = 'bundle'
-            path = path[7:]
-            if path.startswith('//'):
-                path = path[2:]
-            self.path = path
-            return
-
-        if self._matchscheme(path):
-            parts = path.split(':', 1)
-            if parts[0]:
-                self.scheme, path = parts
-                self._localpath = False
-
-        if not path:
-            path = None
-            if self._localpath:
-                self.path = ''
-                return
-        else:
-            if parsefragment and '#' in path:
-                path, self.fragment = path.split('#', 1)
-                if not path:
-                    path = None
-            if self._localpath:
-                self.path = path
-                return
-
-            if parsequery and '?' in path:
-                path, self.query = path.split('?', 1)
-                if not path:
-                    path = None
-                if not self.query:
-                    self.query = None
-
-            # // is required to specify a host/authority
-            if path and path.startswith('//'):
-                parts = path[2:].split('/', 1)
-                if len(parts) > 1:
-                    self.host, path = parts
-                    path = path
-                else:
-                    self.host = parts[0]
-                    path = None
-                if not self.host:
-                    self.host = None
-                    if path:
-                        path = '/' + path
-
-            if self.host and '@' in self.host:
-                self.user, self.host = self.host.rsplit('@', 1)
-                if ':' in self.user:
-                    self.user, self.passwd = self.user.split(':', 1)
-                if not self.host:
-                    self.host = None
-
-            # Don't split on colons in IPv6 addresses without ports
-            if (self.host and ':' in self.host and
-                not (self.host.startswith('[') and self.host.endswith(']'))):
-                self._hostport = self.host
-                self.host, self.port = self.host.rsplit(':', 1)
-                if not self.host:
-                    self.host = None
-
-            if (self.host and self.scheme == 'file' and
-                self.host not in ('localhost', '127.0.0.1', '[::1]')):
-                raise util.Abort(_('file:// URLs can only refer to localhost'))
-
-        self.path = path
-
-        for a in ('user', 'passwd', 'host', 'port',
-                  'path', 'query', 'fragment'):
-            v = getattr(self, a)
-            if v is not None:
-                setattr(self, a, urllib.unquote(v))
-
-    def __repr__(self):
-        attrs = []
-        for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
-                  'query', 'fragment'):
-            v = getattr(self, a)
-            if v is not None:
-                attrs.append('%s: %r' % (a, v))
-        return '<url %s>' % ', '.join(attrs)
-
-    def __str__(self):
-        """Join the URL's components back into a URL string.
-
-        Examples:
-
-        >>> str(url('http://user:pw@host:80/?foo#bar'))
-        'http://user:pw@host:80/?foo#bar'
-        >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
-        'ssh://user:pw@[::1]:2200//home/joe#'
-        >>> str(url('http://localhost:80//'))
-        'http://localhost:80//'
-        >>> str(url('http://localhost:80/'))
-        'http://localhost:80/'
-        >>> str(url('http://localhost:80'))
-        'http://localhost:80/'
-        >>> str(url('bundle:foo'))
-        'bundle:foo'
-        >>> str(url('bundle://../foo'))
-        'bundle:../foo'
-        >>> str(url('path'))
-        'path'
-        """
-        if self._localpath:
-            s = self.path
-            if self.scheme == 'bundle':
-                s = 'bundle:' + s
-            if self.fragment:
-                s += '#' + self.fragment
-            return s
-
-        s = self.scheme + ':'
-        if (self.user or self.passwd or self.host or
-            self.scheme and not self.path):
-            s += '//'
-        if self.user:
-            s += urllib.quote(self.user, safe=self._safechars)
-        if self.passwd:
-            s += ':' + urllib.quote(self.passwd, safe=self._safechars)
-        if self.user or self.passwd:
-            s += '@'
-        if self.host:
-            if not (self.host.startswith('[') and self.host.endswith(']')):
-                s += urllib.quote(self.host)
-            else:
-                s += self.host
-        if self.port:
-            s += ':' + urllib.quote(self.port)
-        if self.host:
-            s += '/'
-        if self.path:
-            s += urllib.quote(self.path, safe=self._safepchars)
-        if self.query:
-            s += '?' + urllib.quote(self.query, safe=self._safepchars)
-        if self.fragment is not None:
-            s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
-        return s
-
-    def authinfo(self):
-        user, passwd = self.user, self.passwd
-        try:
-            self.user, self.passwd = None, None
-            s = str(self)
-        finally:
-            self.user, self.passwd = user, passwd
-        if not self.user:
-            return (s, None)
-        return (s, (None, (str(self), self.host),
-                    self.user, self.passwd or ''))
-
-    def localpath(self):
-        if self.scheme == 'file' or self.scheme == 'bundle':
-            path = self.path or '/'
-            # For Windows, we need to promote hosts containing drive
-            # letters to paths with drive letters.
-            if hasdriveletter(self._hostport):
-                path = self._hostport + '/' + self.path
-            elif self.host is not None and self.path:
-                path = '/' + path
-            # We also need to handle the case of file:///C:/, which
-            # should return C:/, not /C:/.
-            elif hasdriveletter(path):
-                # Strip leading slash from paths with drive names
-                return path[1:]
-            return path
-        return self._origpath
-
-def hasscheme(path):
-    return bool(url(path).scheme)
-
-def hasdriveletter(path):
-    return path[1:2] == ':' and path[0:1].isalpha()
-
-def localpath(path):
-    return url(path, parsequery=False, parsefragment=False).localpath()
-
-def hidepassword(u):
-    '''hide user credential in a url string'''
-    u = url(u)
-    if u.passwd:
-        u.passwd = '***'
-    return str(u)
-
-def removeauth(u):
-    '''remove all authentication information from a url string'''
-    u = url(u)
-    u.user = u.passwd = None
-    return str(u)
-
 def readauthforuri(ui, uri):
     # Read configuration
     config = dict()
@@ -357,7 +95,7 @@
             if not (proxyurl.startswith('http:') or
                     proxyurl.startswith('https:')):
                 proxyurl = 'http://' + proxyurl + '/'
-            proxy = url(proxyurl)
+            proxy = util.url(proxyurl)
             if not proxy.user:
                 proxy.user = ui.config("http_proxy", "user")
                 proxy.passwd = ui.config("http_proxy", "passwd")
@@ -545,7 +283,7 @@
         new_tunnel = False
 
     if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
-        u = url(tunnel_host)
+        u = util.url(tunnel_host)
         if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
             h.realhostport = ':'.join([u.host, (u.port or '443')])
             h.headers = req.headers.copy()
@@ -876,7 +614,7 @@
     return opener
 
 def open(ui, url_, data=None):
-    u = url(url_)
+    u = util.url(url_)
     if u.scheme:
         u.scheme = u.scheme.lower()
         url_, authinfo = u.authinfo()
--- a/mercurial/util.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/mercurial/util.py	Sat Apr 30 09:43:20 2011 -0700
@@ -17,7 +17,7 @@
 import error, osutil, encoding
 import errno, re, shutil, sys, tempfile, traceback
 import os, time, calendar, textwrap, unicodedata, signal
-import imp, socket
+import imp, socket, urllib
 
 # Python compatibility
 
@@ -1283,3 +1283,265 @@
     If s is not a valid boolean, returns None.
     """
     return _booleans.get(s.lower(), None)
+
+class url(object):
+    """Reliable URL parser.
+
+    This parses URLs and provides attributes for the following
+    components:
+
+    <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
+
+    Missing components are set to None. The only exception is
+    fragment, which is set to '' if present but empty.
+
+    If parsefragment is False, fragment is included in query. If
+    parsequery is False, query is included in path. If both are
+    False, both fragment and query are included in path.
+
+    See http://www.ietf.org/rfc/rfc2396.txt for more information.
+
+    Note that for backward compatibility reasons, bundle URLs do not
+    take host names. That means 'bundle://../' has a path of '../'.
+
+    Examples:
+
+    >>> url('http://www.ietf.org/rfc/rfc2396.txt')
+    <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
+    >>> url('ssh://[::1]:2200//home/joe/repo')
+    <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
+    >>> url('file:///home/joe/repo')
+    <url scheme: 'file', path: '/home/joe/repo'>
+    >>> url('bundle:foo')
+    <url scheme: 'bundle', path: 'foo'>
+    >>> url('bundle://../foo')
+    <url scheme: 'bundle', path: '../foo'>
+    >>> url('c:\\\\foo\\\\bar')
+    <url path: 'c:\\\\foo\\\\bar'>
+
+    Authentication credentials:
+
+    >>> url('ssh://joe:xyz@x/repo')
+    <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
+    >>> url('ssh://joe@x/repo')
+    <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
+
+    Query strings and fragments:
+
+    >>> url('http://host/a?b#c')
+    <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
+    >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
+    <url scheme: 'http', host: 'host', path: 'a?b#c'>
+    """
+
+    _safechars = "!~*'()+"
+    _safepchars = "/!~*'()+"
+    _matchscheme = re.compile(r'^[a-zA-Z0-9+.\-]+:').match
+
+    def __init__(self, path, parsequery=True, parsefragment=True):
+        # We slowly chomp away at path until we have only the path left
+        self.scheme = self.user = self.passwd = self.host = None
+        self.port = self.path = self.query = self.fragment = None
+        self._localpath = True
+        self._hostport = ''
+        self._origpath = path
+
+        # special case for Windows drive letters
+        if hasdriveletter(path):
+            self.path = path
+            return
+
+        # For compatibility reasons, we can't handle bundle paths as
+        # normal URLS
+        if path.startswith('bundle:'):
+            self.scheme = 'bundle'
+            path = path[7:]
+            if path.startswith('//'):
+                path = path[2:]
+            self.path = path
+            return
+
+        if self._matchscheme(path):
+            parts = path.split(':', 1)
+            if parts[0]:
+                self.scheme, path = parts
+                self._localpath = False
+
+        if not path:
+            path = None
+            if self._localpath:
+                self.path = ''
+                return
+        else:
+            if parsefragment and '#' in path:
+                path, self.fragment = path.split('#', 1)
+                if not path:
+                    path = None
+            if self._localpath:
+                self.path = path
+                return
+
+            if parsequery and '?' in path:
+                path, self.query = path.split('?', 1)
+                if not path:
+                    path = None
+                if not self.query:
+                    self.query = None
+
+            # // is required to specify a host/authority
+            if path and path.startswith('//'):
+                parts = path[2:].split('/', 1)
+                if len(parts) > 1:
+                    self.host, path = parts
+                    path = path
+                else:
+                    self.host = parts[0]
+                    path = None
+                if not self.host:
+                    self.host = None
+                    if path:
+                        path = '/' + path
+
+            if self.host and '@' in self.host:
+                self.user, self.host = self.host.rsplit('@', 1)
+                if ':' in self.user:
+                    self.user, self.passwd = self.user.split(':', 1)
+                if not self.host:
+                    self.host = None
+
+            # Don't split on colons in IPv6 addresses without ports
+            if (self.host and ':' in self.host and
+                not (self.host.startswith('[') and self.host.endswith(']'))):
+                self._hostport = self.host
+                self.host, self.port = self.host.rsplit(':', 1)
+                if not self.host:
+                    self.host = None
+
+            if (self.host and self.scheme == 'file' and
+                self.host not in ('localhost', '127.0.0.1', '[::1]')):
+                raise Abort(_('file:// URLs can only refer to localhost'))
+
+        self.path = path
+
+        for a in ('user', 'passwd', 'host', 'port',
+                  'path', 'query', 'fragment'):
+            v = getattr(self, a)
+            if v is not None:
+                setattr(self, a, urllib.unquote(v))
+
+    def __repr__(self):
+        attrs = []
+        for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
+                  'query', 'fragment'):
+            v = getattr(self, a)
+            if v is not None:
+                attrs.append('%s: %r' % (a, v))
+        return '<url %s>' % ', '.join(attrs)
+
+    def __str__(self):
+        """Join the URL's components back into a URL string.
+
+        Examples:
+
+        >>> str(url('http://user:pw@host:80/?foo#bar'))
+        'http://user:pw@host:80/?foo#bar'
+        >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
+        'ssh://user:pw@[::1]:2200//home/joe#'
+        >>> str(url('http://localhost:80//'))
+        'http://localhost:80//'
+        >>> str(url('http://localhost:80/'))
+        'http://localhost:80/'
+        >>> str(url('http://localhost:80'))
+        'http://localhost:80/'
+        >>> str(url('bundle:foo'))
+        'bundle:foo'
+        >>> str(url('bundle://../foo'))
+        'bundle:../foo'
+        >>> str(url('path'))
+        'path'
+        """
+        if self._localpath:
+            s = self.path
+            if self.scheme == 'bundle':
+                s = 'bundle:' + s
+            if self.fragment:
+                s += '#' + self.fragment
+            return s
+
+        s = self.scheme + ':'
+        if (self.user or self.passwd or self.host or
+            self.scheme and not self.path):
+            s += '//'
+        if self.user:
+            s += urllib.quote(self.user, safe=self._safechars)
+        if self.passwd:
+            s += ':' + urllib.quote(self.passwd, safe=self._safechars)
+        if self.user or self.passwd:
+            s += '@'
+        if self.host:
+            if not (self.host.startswith('[') and self.host.endswith(']')):
+                s += urllib.quote(self.host)
+            else:
+                s += self.host
+        if self.port:
+            s += ':' + urllib.quote(self.port)
+        if self.host:
+            s += '/'
+        if self.path:
+            s += urllib.quote(self.path, safe=self._safepchars)
+        if self.query:
+            s += '?' + urllib.quote(self.query, safe=self._safepchars)
+        if self.fragment is not None:
+            s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
+        return s
+
+    def authinfo(self):
+        user, passwd = self.user, self.passwd
+        try:
+            self.user, self.passwd = None, None
+            s = str(self)
+        finally:
+            self.user, self.passwd = user, passwd
+        if not self.user:
+            return (s, None)
+        return (s, (None, (str(self), self.host),
+                    self.user, self.passwd or ''))
+
+    def localpath(self):
+        if self.scheme == 'file' or self.scheme == 'bundle':
+            path = self.path or '/'
+            # For Windows, we need to promote hosts containing drive
+            # letters to paths with drive letters.
+            if hasdriveletter(self._hostport):
+                path = self._hostport + '/' + self.path
+            elif self.host is not None and self.path:
+                path = '/' + path
+            # We also need to handle the case of file:///C:/, which
+            # should return C:/, not /C:/.
+            elif hasdriveletter(path):
+                # Strip leading slash from paths with drive names
+                return path[1:]
+            return path
+        return self._origpath
+
+def hasscheme(path):
+    return bool(url(path).scheme)
+
+def hasdriveletter(path):
+    return path[1:2] == ':' and path[0:1].isalpha()
+
+def localpath(path):
+    return url(path, parsequery=False, parsefragment=False).localpath()
+
+def hidepassword(u):
+    '''hide user credential in a url string'''
+    u = url(u)
+    if u.passwd:
+        u.passwd = '***'
+    return str(u)
+
+def removeauth(u):
+    '''remove all authentication information from a url string'''
+    u = url(u)
+    u.user = u.passwd = None
+    return str(u)
--- a/tests/test-url.py	Sat Apr 30 16:33:47 2011 +0200
+++ b/tests/test-url.py	Sat Apr 30 09:43:20 2011 -0700
@@ -53,7 +53,7 @@
 
 def test_url():
     """
-    >>> from mercurial.url import url
+    >>> from mercurial.util import url
 
     This tests for edge cases in url.URL's parsing algorithm. Most of
     these aren't useful for documentation purposes, so they aren't