url: refactor util.drop_scheme() and hg.localpath() into url.localpath()

This replaces util.drop_scheme() with url.localpath(), using url.url for
parsing instead of doing it on its own. The function is moved from
util to url to avoid an import cycle.

hg.localpath() is removed in favor of using url.localpath(). This
provides more consistent behavior between "hg clone" and other
commands.

To preserve backwards compatibility, URLs like bundle://../foo still
refer to ../foo, not /foo.

If a URL contains a scheme, percent-encoded entities are decoded. When
there's no scheme, all characters are left untouched.

Comparison of old and new behaviors:

URL                      drop_scheme()   hg.localpath()    url.localpath()
===                      =============   ==============    ===============
file://foo/foo           /foo            foo/foo           /foo
file://localhost:80/foo  /foo            localhost:80/foo  /foo
file://localhost:/foo    /foo            localhost:/foo    /foo
file://localhost/foo     /foo            /foo              /foo
file:///foo              /foo            /foo              /foo
file://foo               (empty string)  foo               /
file:/foo                /foo            /foo              /foo
file:foo                 foo             foo               foo
file:foo%23bar           foo%23bar       foo%23bar         foo#bar
foo%23bar                foo%23bar       foo%23bar         foo%23bar
/foo                     /foo            /foo              /foo

Windows-related paths on Windows:

URL                      drop_scheme()   hg.localpath()    url.localpath()
===                      =============   ==============    ===============
file:///C:/foo           C:/C:/foo       /C:/foo           C:/foo
file:///D:/foo           C:/D:/foo       /D:/foo           D:/foo
file://C:/foo            C:/foo          C:/foo            C:/foo
file://D:/foo            C:/foo          D:/foo            D:/foo
file:////foo/bar         //foo/bar       //foo/bar         //foo/bar
//foo/bar                //foo/bar       //foo/bar         //foo/bar
\\foo\bar                //foo/bar       //foo/bar         \\foo\bar

Windows-related paths on other platforms:

file:///C:/foo           C:/C:/foo       /C:/foo           C:/foo
file:///D:/foo           C:/D:/foo       /D:/foo           D:/foo
file://C:/foo            C:/foo          C:/foo            C:/foo
file://D:/foo            C:/foo          D:/foo            D:/foo
file:////foo/bar         //foo/bar       //foo/bar         //foo/bar
//foo/bar                //foo/bar       //foo/bar         //foo/bar
\\foo\bar                //foo/bar       //foo/bar         \\foo\bar

For more information about file:// URL handling, see:
http://www-archive.mozilla.org/quality/networking/testing/filetests.html

Related issues:

- issue1153: File URIs aren't handled correctly in windows

  This patch should preserve the fix implemented in
  5c92d05b064e. However, it goes a step further and "promotes"
  Windows-style drive letters from being interpreted as host names to
  being part of the path.

- issue2154: Cannot escape '#' in Mercurial URLs (#1172 in THG)

  The fragment is still interpreted as a revision or a branch, even in
  paths to bundles. However, when file: is used, percent-encoded
  entities are decoded, so file:test%23bundle.hg can refer to
  test#bundle.hg ond isk.
This commit is contained in:
Brodie Rao 2011-03-30 20:03:05 -07:00
parent 8f5f4f569c
commit 3a43fa887e
7 changed files with 52 additions and 38 deletions

View File

@ -15,7 +15,7 @@ from node import nullid
from i18n import _
import os, struct, tempfile, shutil
import changegroup, util, mdiff, discovery
import localrepo, changelog, manifest, filelog, revlog, error
import localrepo, changelog, manifest, filelog, revlog, error, url
class bundlerevlog(revlog.revlog):
def __init__(self, opener, indexfile, bundle,
@ -274,9 +274,9 @@ def instance(ui, path, create):
cwd = os.path.join(cwd,'')
if parentpath.startswith(cwd):
parentpath = parentpath[len(cwd):]
path = util.drop_scheme('file', path)
if path.startswith('bundle:'):
path = util.drop_scheme('bundle', path)
u = url.url(path)
path = u.localpath()
if u.scheme == 'bundle':
s = path.split("+", 1)
if len(s) == 1:
repopath, bundlename = parentpath, s[0]

View File

@ -17,7 +17,7 @@ import verify as verifymod
import errno, os, shutil
def _local(path):
path = util.expandpath(util.drop_scheme('file', path))
path = util.expandpath(url.localpath(path))
return (os.path.isfile(path) and bundlerepo or localrepo)
def addbranchrevs(lrepo, repo, branches, revs):
@ -102,15 +102,6 @@ def defaultdest(source):
'''return default destination of clone if none is given'''
return os.path.basename(os.path.normpath(source))
def localpath(path):
if path.startswith('file://localhost/'):
return path[16:]
if path.startswith('file://'):
return path[7:]
if path.startswith('file:'):
return path[5:]
return path
def share(ui, source, dest=None, update=True):
'''create a shared repository'''
@ -230,8 +221,8 @@ def clone(ui, source, dest=None, pull=False, rev=None, update=True,
else:
dest = ui.expandpath(dest)
dest = localpath(dest)
source = localpath(source)
dest = url.localpath(dest)
source = url.localpath(source)
if os.path.exists(dest):
if not os.path.isdir(dest):
@ -257,7 +248,7 @@ def clone(ui, source, dest=None, pull=False, rev=None, update=True,
abspath = origsource
copy = False
if src_repo.cancopy() and islocal(dest):
abspath = os.path.abspath(util.drop_scheme('file', origsource))
abspath = os.path.abspath(url.localpath(origsource))
copy = not pull and not rev
if copy:

View File

@ -1928,7 +1928,7 @@ def aftertrans(files):
return a
def instance(ui, path, create):
return localrepository(ui, util.drop_scheme('file', path), create)
return localrepository(ui, urlmod.localpath(path), create)
def islocal(path):
return True

View File

@ -70,6 +70,8 @@ class url(object):
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 has_drive_letter(path):
@ -137,6 +139,7 @@ class url(object):
# 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
@ -231,12 +234,32 @@ class url(object):
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 has_drive_letter(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 has_drive_letter(path):
# Strip leading slash from paths with drive names
return path[1:]
return path
return self._origpath
def has_scheme(path):
return bool(url(path).scheme)
def has_drive_letter(path):
return path[1:2] == ':' and path[0:1].isalpha()
def localpath(path):
return url(path, parse_query=False, parse_fragment=False).localpath()
def hidepassword(u):
'''hide user credential in a url string'''
u = url(u)

View File

@ -1384,26 +1384,6 @@ def bytecount(nbytes):
return format % (nbytes / float(divisor))
return units[-1][2] % nbytes
def drop_scheme(scheme, path):
sc = scheme + ':'
if path.startswith(sc):
path = path[len(sc):]
if path.startswith('//'):
if scheme == 'file':
i = path.find('/', 2)
if i == -1:
return ''
# On Windows, absolute paths are rooted at the current drive
# root. On POSIX they are rooted at the file system root.
if os.name == 'nt':
droot = os.path.splitdrive(os.getcwd())[0] + '/'
path = os.path.join(droot, path[i + 1:])
else:
path = path[i:]
else:
path = path[2:]
return path
def uirepr(s):
# Avoid double backslash in Windows path repr()
return repr(s).replace('\\\\', '\\')

View File

@ -470,6 +470,22 @@ test for 540d1059c802
$ cd ..
test bundle with # in the filename (issue2154):
$ cp bundle.hg 'test#bundle.hg'
$ cd orig
$ hg incoming '../test#bundle.hg'
comparing with ../test
abort: unknown revision 'bundle.hg'!
[255]
note that percent encoding is not handled:
$ hg incoming ../test%23bundle.hg
abort: repository ../test%23bundle.hg not found!
[255]
$ cd ..
test for http://mercurial.selenic.com/bts/issue1144
test that verify bundle does not traceback

View File

@ -71,6 +71,10 @@ Test 'file:' uri handling:
abort: file:// URLs can only refer to localhost
[255]
$ hg pull -q file://../test
abort: file:// URLs can only refer to localhost
[255]
$ hg pull -q file:../test
It's tricky to make file:// URLs working on every platform with