diff --git a/mercurial/bundlerepo.py b/mercurial/bundlerepo.py
--- a/mercurial/bundlerepo.py
+++ b/mercurial/bundlerepo.py
@@ -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]
diff --git a/mercurial/hg.py b/mercurial/hg.py
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -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=Fa
     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=Fa
         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:
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -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
diff --git a/mercurial/url.py b/mercurial/url.py
--- a/mercurial/url.py
+++ b/mercurial/url.py
@@ -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)
diff --git a/mercurial/util.py b/mercurial/util.py
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -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('\\\\', '\\')
diff --git a/tests/test-bundle.t b/tests/test-bundle.t
--- a/tests/test-bundle.t
+++ b/tests/test-bundle.t
@@ -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
diff --git a/tests/test-pull.t b/tests/test-pull.t
--- a/tests/test-pull.t
+++ b/tests/test-pull.t
@@ -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