Rev 4702: (andrew) Implement home directory relative URLs for bzr:// and in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Fri Sep 18 09:02:44 BST 2009


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 4702 [merge]
revision-id: pqm at pqm.ubuntu.com-20090918080243-b04lrnure68z8rzc
parent: pqm at pqm.ubuntu.com-20090918033431-imjyd17yze1okeap
parent: andrew.bennetts at canonical.com-20090918071636-gxjzqyweltmxsm9b
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2009-09-18 09:02:43 +0100
message:
  (andrew) Implement home directory relative URLs for bzr:// and
  	bzr+ssh://. (#109143)
added:
  bzrlib/transport/pathfilter.py pathfilter.py-20090911074646-d3befzxpj1zglo7s-1
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/smart/server.py         server.py-20061110062051-chzu10y32vx8gvur-1
  bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
  bzrlib/tests/blackbox/test_serve.py test_serve.py-20060913064329-8t2pvmsikl4s3xhl-1
  bzrlib/tests/test_transport.py testtransport.py-20050718175618-e5cdb99f4555ddce
  bzrlib/transport/__init__.py   transport.py-20050711165921-4978aa7ce1285ad5
  bzrlib/transport/chroot.py     chroot.py-20061011104729-0us9mgm97z378vnt-1
  doc/developers/network-protocol.txt networkprotocol.txt-20070903044232-woustorrjbmg5zol-1
  doc/en/user-guide/server.txt   server.txt-20060913044801-h939fvbwzz39gf7g-1
=== modified file 'NEWS'
--- a/NEWS	2009-09-17 22:48:24 +0000
+++ b/NEWS	2009-09-18 06:06:41 +0000
@@ -2,6 +2,16 @@
 Bazaar Release Notes
 ####################
 
+bzr 2.0.1
+##########
+
+Bug Fixes
+*********
+
+* Make sure that we unlock the tree if we fail to create a TreeTransform
+  object when doing a merge, and there is limbo, or pending-deletions
+  directory.  (Gary van der Merwe, #427773)
+
 
 .. contents:: List of Releases
    :depth: 1
@@ -15,6 +25,25 @@
 New Features
 ************
 
+* ``bzr+ssh`` and ``bzr`` paths can now be relative to home directories
+  specified in the URL.  Paths starting with a path segment of ``~`` are
+  relative to the home directory of the user running the server, and paths
+  starting with ``~user`` are relative to the home directory of the named
+  user.  For example, for a user "bob" with a home directory of
+  ``/home/bob``, these URLs are all equivalent:
+
+  * ``bzr+ssh://bob@host/~/repo``
+  * ``bzr+ssh://bob@host/~bob/repo``
+  * ``bzr+ssh://bob@host/home/bob/repo``
+
+  If ``bzr serve`` was invoked with a ``--directory`` argument, then no
+  home directories outside that directory will be accessible via this
+  method.
+
+  This is a feature of ``bzr serve``, so pre-2.1 clients will
+  automatically benefit from this feature when ``bzr`` on the server is
+  upgraded.  (Andrew Bennetts, #109143)
+
 * Give more control on BZR_PLUGIN_PATH by providing a way to refer to or
   disable the user, site and core plugin directories.
   (Vincent Ladeuil, #412930, #316192, #145612)

=== modified file 'bzrlib/smart/server.py'
--- a/bzrlib/smart/server.py	2009-07-20 11:27:05 +0000
+++ b/bzrlib/smart/server.py	2009-09-18 05:45:06 +0000
@@ -17,6 +17,7 @@
 """Server for smart-server protocol."""
 
 import errno
+import os.path
 import socket
 import sys
 import threading
@@ -30,6 +31,14 @@
 from bzrlib.lazy_import import lazy_import
 lazy_import(globals(), """
 from bzrlib.smart import medium
+from bzrlib.transport import (
+    chroot,
+    get_transport,
+    pathfilter,
+    )
+from bzrlib import (
+    urlutils,
+    )
 """)
 
 
@@ -313,34 +322,132 @@
         return transport.get_transport(url)
 
 
-def serve_bzr(transport, host=None, port=None, inet=False):
-    from bzrlib import lockdir, ui
-    from bzrlib.transport import get_transport
-    from bzrlib.transport.chroot import ChrootServer
-    chroot_server = ChrootServer(transport)
-    chroot_server.setUp()
-    transport = get_transport(chroot_server.get_url())
-    if inet:
-        smart_server = medium.SmartServerPipeStreamMedium(
-            sys.stdin, sys.stdout, transport)
+def _local_path_for_transport(transport):
+    """Return a local path for transport, if reasonably possible.
+    
+    This function works even if transport's url has a "readonly+" prefix,
+    unlike local_path_from_url.
+    
+    This essentially recovers the --directory argument the user passed to "bzr
+    serve" from the transport passed to serve_bzr.
+    """
+    try:
+        base_url = transport.external_url()
+    except (errors.InProcessTransport, NotImplementedError):
+        return None
     else:
-        if host is None:
-            host = medium.BZR_DEFAULT_INTERFACE
-        if port is None:
-            port = medium.BZR_DEFAULT_PORT
-        smart_server = SmartTCPServer(transport, host=host, port=port)
-        trace.note('listening on port: %s' % smart_server.port)
-    # For the duration of this server, no UI output is permitted. note
-    # that this may cause problems with blackbox tests. This should be
-    # changed with care though, as we dont want to use bandwidth sending
-    # progress over stderr to smart server clients!
-    old_factory = ui.ui_factory
-    old_lockdir_timeout = lockdir._DEFAULT_TIMEOUT_SECONDS
-    try:
+        # Strip readonly prefix
+        if base_url.startswith('readonly+'):
+            base_url = base_url[len('readonly+'):]
+        try:
+            return urlutils.local_path_from_url(base_url)
+        except errors.InvalidURL:
+            return None
+
+
+class BzrServerFactory(object):
+    """Helper class for serve_bzr."""
+
+    def __init__(self, userdir_expander=None, get_base_path=None):
+        self.cleanups = []
+        self.base_path = None
+        self.backing_transport = None
+        if userdir_expander is None:
+            userdir_expander = os.path.expanduser
+        self.userdir_expander = userdir_expander
+        if get_base_path is None:
+            get_base_path = _local_path_for_transport
+        self.get_base_path = get_base_path
+
+    def _expand_userdirs(self, path):
+        """Translate /~/ or /~user/ to e.g. /home/foo, using
+        self.userdir_expander (os.path.expanduser by default).
+
+        If the translated path would fall outside base_path, or the path does
+        not start with ~, then no translation is applied.
+
+        If the path is inside, it is adjusted to be relative to the base path.
+
+        e.g. if base_path is /home, and the expanded path is /home/joe, then
+        the translated path is joe.
+        """
+        result = path
+        if path.startswith('~'):
+            expanded = self.userdir_expander(path)
+            if not expanded.endswith('/'):
+                expanded += '/'
+            if expanded.startswith(self.base_path):
+                result = expanded[len(self.base_path):]
+        return result
+
+    def _make_expand_userdirs_filter(self, transport):
+        return pathfilter.PathFilteringServer(transport, self._expand_userdirs)
+
+    def _make_backing_transport(self, transport):
+        """Chroot transport, and decorate with userdir expander."""
+        self.base_path = self.get_base_path(transport)
+        chroot_server = chroot.ChrootServer(transport)
+        chroot_server.setUp()
+        self.cleanups.append(chroot_server.tearDown)
+        transport = get_transport(chroot_server.get_url())
+        if self.base_path is not None:
+            # Decorate the server's backing transport with a filter that can
+            # expand homedirs.
+            expand_userdirs = self._make_expand_userdirs_filter(transport)
+            expand_userdirs.setUp()
+            self.cleanups.append(expand_userdirs.tearDown)
+            transport = get_transport(expand_userdirs.get_url())
+        self.transport = transport
+
+    def _make_smart_server(self, host, port, inet):
+        if inet:
+            smart_server = medium.SmartServerPipeStreamMedium(
+                sys.stdin, sys.stdout, self.transport)
+        else:
+            if host is None:
+                host = medium.BZR_DEFAULT_INTERFACE
+            if port is None:
+                port = medium.BZR_DEFAULT_PORT
+            smart_server = SmartTCPServer(self.transport, host=host, port=port)
+            trace.note('listening on port: %s' % smart_server.port)
+        self.smart_server = smart_server
+
+    def _change_globals(self):
+        from bzrlib import lockdir, ui
+        # For the duration of this server, no UI output is permitted. note
+        # that this may cause problems with blackbox tests. This should be
+        # changed with care though, as we dont want to use bandwidth sending
+        # progress over stderr to smart server clients!
+        old_factory = ui.ui_factory
+        old_lockdir_timeout = lockdir._DEFAULT_TIMEOUT_SECONDS
+        def restore_default_ui_factory_and_lockdir_timeout():
+            ui.ui_factory = old_factory
+            lockdir._DEFAULT_TIMEOUT_SECONDS = old_lockdir_timeout
+        self.cleanups.append(restore_default_ui_factory_and_lockdir_timeout)
         ui.ui_factory = ui.SilentUIFactory()
         lockdir._DEFAULT_TIMEOUT_SECONDS = 0
-        smart_server.serve()
+
+    def set_up(self, transport, host, port, inet):
+        self._make_backing_transport(transport)
+        self._make_smart_server(host, port, inet)
+        self._change_globals()
+
+    def tear_down(self):
+        for cleanup in reversed(self.cleanups):
+            cleanup()
+
+
+def serve_bzr(transport, host=None, port=None, inet=False):
+    """This is the default implementation of 'bzr serve'.
+    
+    It creates a TCP or pipe smart server on 'transport, and runs it.  The
+    transport will be decorated with a chroot and pathfilter (using
+    os.path.expanduser).
+    """
+    bzr_server = BzrServerFactory()
+    try:
+        bzr_server.set_up(transport, host, port, inet)
+        bzr_server.smart_server.serve()
     finally:
-        ui.ui_factory = old_factory
-        lockdir._DEFAULT_TIMEOUT_SECONDS = old_lockdir_timeout
+        bzr_server.tear_down()
 

=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py	2009-09-18 02:03:31 +0000
+++ b/bzrlib/tests/__init__.py	2009-09-18 06:06:41 +0000
@@ -91,7 +91,7 @@
     deprecated_passed,
     )
 import bzrlib.trace
-from bzrlib.transport import chroot, get_transport
+from bzrlib.transport import get_transport, pathfilter
 import bzrlib.transport
 from bzrlib.transport.local import LocalURLServer
 from bzrlib.transport.memory import MemoryServer
@@ -983,12 +983,11 @@
 
     def _preopen_isolate_transport(self, transport):
         """Check that all transport openings are done in the test work area."""
-        if isinstance(transport, chroot.ChrootTransport):
-            # Unwrap chrooted transports
-            url = transport.server.backing_transport.clone(
-                transport._safe_relpath('.')).base
-        else:
-            url = transport.base
+        while isinstance(transport, pathfilter.PathFilteringTransport):
+            # Unwrap pathfiltered transports
+            transport = transport.server.backing_transport.clone(
+                transport._filter('.'))
+        url = transport.base
         # ReadonlySmartTCPServer_for_testing decorates the backing transport
         # urls it is given by prepending readonly+. This is appropriate as the
         # client shouldn't know that the server is readonly (or not readonly).

=== modified file 'bzrlib/tests/blackbox/test_serve.py'
--- a/bzrlib/tests/blackbox/test_serve.py	2009-09-17 22:26:36 +0000
+++ b/bzrlib/tests/blackbox/test_serve.py	2009-09-18 06:06:41 +0000
@@ -18,6 +18,7 @@
 """Tests of the bzr serve command."""
 
 import os
+import os.path
 import signal
 import subprocess
 import sys
@@ -25,18 +26,21 @@
 import threading
 
 from bzrlib import (
-    config,
+    builtins,
     errors,
     osutils,
     revision as _mod_revision,
-    transport,
     )
 from bzrlib.branch import Branch
 from bzrlib.bzrdir import BzrDir
 from bzrlib.errors import ParamikoNotPresent
 from bzrlib.smart import client, medium
-from bzrlib.smart.server import SmartTCPServer
-from bzrlib.tests import TestCaseWithTransport, TestSkipped
+from bzrlib.smart.server import BzrServerFactory, SmartTCPServer
+from bzrlib.tests import (
+    TestCaseWithTransport,
+    TestCaseWithMemoryTransport,
+    TestSkipped,
+    )
 from bzrlib.trace import mutter
 from bzrlib.transport import get_transport, remote
 
@@ -327,4 +331,61 @@
         client_medium.disconnect()
 
 
+class TestUserdirExpansion(TestCaseWithMemoryTransport):
+
+    def fake_expanduser(self, path):
+        """A simple, environment-independent, function for the duration of this
+        test.
+
+        Paths starting with a path segment of '~user' will expand to start with
+        '/home/user/'.  Every other path will be unchanged.
+        """
+        if path.split('/', 1)[0] == '~user':
+            return '/home/user' + path[len('~user'):]
+        return path
+
+    def make_test_server(self, base_path='/'):
+        """Make and setUp a BzrServerFactory, backed by a memory transport, and
+        creat '/home/user' in that transport.
+        """
+        bzr_server = BzrServerFactory(
+            self.fake_expanduser, lambda t: base_path)
+        mem_transport = self.get_transport()
+        mem_transport.mkdir_multi(['home', 'home/user'])
+        bzr_server.set_up(mem_transport, None, None, inet=True)
+        self.addCleanup(bzr_server.tear_down)
+        return bzr_server
+
+    def test_bzr_serve_expands_userdir(self):
+        bzr_server = self.make_test_server()
+        self.assertTrue(bzr_server.smart_server.backing_transport.has('~user'))
+
+    def test_bzr_serve_does_not_expand_userdir_outside_base(self):
+        bzr_server = self.make_test_server('/foo')
+        self.assertFalse(bzr_server.smart_server.backing_transport.has('~user'))
+
+    def test_get_base_path(self):
+        """cmd_serve will turn the --directory option into a LocalTransport
+        (optionally decorated with 'readonly+').  BzrServerFactory can
+        determine the original --directory from that transport.
+        """
+        # Define a fake 'protocol' to capture the transport that cmd_serve
+        # passes to serve_bzr.
+        def capture_transport(transport, host, port, inet):
+            self.bzr_serve_transport = transport
+        cmd = builtins.cmd_serve()
+        # Read-only
+        cmd.run(directory='/a/b/c', protocol=capture_transport)
+        server_maker = BzrServerFactory()
+        self.assertEqual(
+            'readonly+file:///a/b/c/', self.bzr_serve_transport.base)
+        self.assertEqual(
+            u'/a/b/c/', server_maker.get_base_path(self.bzr_serve_transport))
+        # Read-write
+        cmd.run(directory='/a/b/c', protocol=capture_transport,
+            allow_writes=True)
+        server_maker = BzrServerFactory()
+        self.assertEqual('file:///a/b/c/', self.bzr_serve_transport.base)
+        self.assertEqual(
+            u'/a/b/c/', server_maker.get_base_path(self.bzr_serve_transport))
 

=== modified file 'bzrlib/tests/test_transport.py'
--- a/bzrlib/tests/test_transport.py	2009-08-27 22:17:35 +0000
+++ b/bzrlib/tests/test_transport.py	2009-09-18 07:16:36 +0000
@@ -48,6 +48,7 @@
 from bzrlib.transport.memory import MemoryTransport
 from bzrlib.transport.local import (LocalTransport,
                                     EmulatedWin32LocalTransport)
+from bzrlib.transport.pathfilter import PathFilteringServer
 
 
 # TODO: Should possibly split transport-specific tests into their own files.
@@ -80,7 +81,8 @@
             register_lazy_transport('bar', 'bzrlib.tests.test_transport',
                                     'TestTransport.SampleHandler')
             self.assertEqual([SampleHandler.__module__,
-                              'bzrlib.transport.chroot'],
+                              'bzrlib.transport.chroot',
+                              'bzrlib.transport.pathfilter'],
                              _get_transport_modules())
         finally:
             _set_protocol_handlers(handlers)
@@ -446,6 +448,90 @@
             server.tearDown()
 
 
+class PathFilteringDecoratorTransportTest(TestCase):
+    """Pathfilter decoration specific tests."""
+
+    def test_abspath(self):
+        # The abspath is always relative to the base of the backing transport.
+        server = PathFilteringServer(get_transport('memory:///foo/bar/'),
+            lambda x: x)
+        server.setUp()
+        transport = get_transport(server.get_url())
+        self.assertEqual(server.get_url(), transport.abspath('/'))
+
+        subdir_transport = transport.clone('subdir')
+        self.assertEqual(server.get_url(), subdir_transport.abspath('/'))
+        server.tearDown()
+
+    def make_pf_transport(self, filter_func=None):
+        """Make a PathFilteringTransport backed by a MemoryTransport.
+        
+        :param filter_func: by default this will be a no-op function.  Use this
+            parameter to override it."""
+        if filter_func is None:
+            filter_func = lambda x: x
+        server = PathFilteringServer(
+            get_transport('memory:///foo/bar/'), filter_func)
+        server.setUp()
+        self.addCleanup(server.tearDown)
+        return get_transport(server.get_url())
+
+    def test__filter(self):
+        # _filter (with an identity func as filter_func) always returns
+        # paths relative to the base of the backing transport.
+        transport = self.make_pf_transport()
+        self.assertEqual('foo', transport._filter('foo'))
+        self.assertEqual('foo/bar', transport._filter('foo/bar'))
+        self.assertEqual('', transport._filter('..'))
+        self.assertEqual('', transport._filter('/'))
+        # The base of the pathfiltering transport is taken into account too.
+        transport = transport.clone('subdir1/subdir2')
+        self.assertEqual('subdir1/subdir2/foo', transport._filter('foo'))
+        self.assertEqual(
+            'subdir1/subdir2/foo/bar', transport._filter('foo/bar'))
+        self.assertEqual('subdir1', transport._filter('..'))
+        self.assertEqual('', transport._filter('/'))
+
+    def test_filter_invocation(self):
+        filter_log = []
+        def filter(path):
+            filter_log.append(path)
+            return path
+        transport = self.make_pf_transport(filter)
+        transport.has('abc')
+        self.assertEqual(['abc'], filter_log)
+        del filter_log[:]
+        transport.clone('abc').has('xyz')
+        self.assertEqual(['abc/xyz'], filter_log)
+        del filter_log[:]
+        transport.has('/abc')
+        self.assertEqual(['abc'], filter_log)
+
+    def test_clone(self):
+        transport = self.make_pf_transport()
+        # relpath from root and root path are the same
+        relpath_cloned = transport.clone('foo')
+        abspath_cloned = transport.clone('/foo')
+        self.assertEqual(transport.server, relpath_cloned.server)
+        self.assertEqual(transport.server, abspath_cloned.server)
+
+    def test_url_preserves_pathfiltering(self):
+        """Calling get_transport on a pathfiltered transport's base should
+        produce a transport with exactly the same behaviour as the original
+        pathfiltered transport.
+
+        This is so that it is not possible to escape (accidentally or
+        otherwise) the filtering by doing::
+            url = filtered_transport.base
+            parent_url = urlutils.join(url, '..')
+            new_transport = get_transport(parent_url)
+        """
+        transport = self.make_pf_transport()
+        new_transport = get_transport(transport.base)
+        self.assertEqual(transport.server, new_transport.server)
+        self.assertEqual(transport.base, new_transport.base)
+
+
 class ReadonlyDecoratorTransportTest(TestCase):
     """Readonly decoration specific tests."""
 

=== modified file 'bzrlib/transport/__init__.py'
--- a/bzrlib/transport/__init__.py	2009-08-04 11:40:59 +0000
+++ b/bzrlib/transport/__init__.py	2009-09-14 05:39:09 +0000
@@ -90,8 +90,10 @@
                 modules.add(factory._module_name)
             else:
                 modules.add(factory._obj.__module__)
-    # Add chroot directly, because there is no handler registered for it.
+    # Add chroot and pathfilter directly, because there is no handler
+    # registered for it.
     modules.add('bzrlib.transport.chroot')
+    modules.add('bzrlib.transport.pathfilter')
     result = list(modules)
     result.sort()
     return result

=== modified file 'bzrlib/transport/chroot.py'
--- a/bzrlib/transport/chroot.py	2009-03-23 14:59:43 +0000
+++ b/bzrlib/transport/chroot.py	2009-09-16 04:37:33 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006 Canonical Ltd
+# Copyright (C) 2006-2009 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -17,21 +17,18 @@
 """Implementation of Transport that prevents access to locations above a set
 root.
 """
-from urlparse import urlparse
 
-from bzrlib import errors, urlutils
 from bzrlib.transport import (
     get_transport,
+    pathfilter,
     register_transport,
     Server,
     Transport,
     unregister_transport,
     )
-from bzrlib.transport.decorator import TransportDecorator, DecoratorServer
-from bzrlib.transport.memory import MemoryTransport
-
-
-class ChrootServer(Server):
+
+
+class ChrootServer(pathfilter.PathFilteringServer):
     """User space 'chroot' facility.
 
     The server's get_url returns the url for a chroot transport mapped to the
@@ -39,126 +36,40 @@
     directories of the backing transport are not visible. The chroot url will
     not allow '..' sequences to result in requests to the chroot affecting
     directories outside the backing transport.
+
+    PathFilteringServer does all the path sanitation needed to enforce a
+    chroot, so this is a simple subclass of PathFilteringServer that ignores
+    filter_func.
     """
 
     def __init__(self, backing_transport):
-        self.backing_transport = backing_transport
+        pathfilter.PathFilteringServer.__init__(self, backing_transport, None)
 
     def _factory(self, url):
         return ChrootTransport(self, url)
 
-    def get_url(self):
-        return self.scheme
-
     def setUp(self):
         self.scheme = 'chroot-%d:///' % id(self)
         register_transport(self.scheme, self._factory)
 
-    def tearDown(self):
-        unregister_transport(self.scheme, self._factory)
-
-
-class ChrootTransport(Transport):
+
+class ChrootTransport(pathfilter.PathFilteringTransport):
     """A ChrootTransport.
 
     Please see ChrootServer for details.
     """
 
-    def __init__(self, server, base):
-        self.server = server
-        if not base.endswith('/'):
-            base += '/'
-        Transport.__init__(self, base)
-        self.base_path = self.base[len(self.server.scheme)-1:]
-        self.scheme = self.server.scheme
-
-    def _call(self, methodname, relpath, *args):
-        method = getattr(self.server.backing_transport, methodname)
-        return method(self._safe_relpath(relpath), *args)
-
-    def _safe_relpath(self, relpath):
-        safe_relpath = self._combine_paths(self.base_path, relpath)
-        if not safe_relpath.startswith('/'):
-            raise ValueError(safe_relpath)
-        return safe_relpath[1:]
-
-    # Transport methods
-    def abspath(self, relpath):
-        return self.scheme + self._safe_relpath(relpath)
-
-    def append_file(self, relpath, f, mode=None):
-        return self._call('append_file', relpath, f, mode)
-
-    def _can_roundtrip_unix_modebits(self):
-        return self.server.backing_transport._can_roundtrip_unix_modebits()
-
-    def clone(self, relpath):
-        return ChrootTransport(self.server, self.abspath(relpath))
-
-    def delete(self, relpath):
-        return self._call('delete', relpath)
-
-    def delete_tree(self, relpath):
-        return self._call('delete_tree', relpath)
-
-    def external_url(self):
-        """See bzrlib.transport.Transport.external_url."""
-        # Chroots, like MemoryTransport depend on in-process
-        # state and thus the base cannot simply be handed out.
-        # See the base class docstring for more details and
-        # possible directions. For now we return the chrooted
-        # url.
-        return self.server.backing_transport.external_url()
-
-    def get(self, relpath):
-        return self._call('get', relpath)
-
-    def has(self, relpath):
-        return self._call('has', relpath)
-
-    def is_readonly(self):
-        return self.server.backing_transport.is_readonly()
-
-    def iter_files_recursive(self):
-        backing_transport = self.server.backing_transport.clone(
-            self._safe_relpath('.'))
-        return backing_transport.iter_files_recursive()
-
-    def listable(self):
-        return self.server.backing_transport.listable()
-
-    def list_dir(self, relpath):
-        return self._call('list_dir', relpath)
-
-    def lock_read(self, relpath):
-        return self._call('lock_read', relpath)
-
-    def lock_write(self, relpath):
-        return self._call('lock_write', relpath)
-
-    def mkdir(self, relpath, mode=None):
-        return self._call('mkdir', relpath, mode)
-
-    def open_write_stream(self, relpath, mode=None):
-        return self._call('open_write_stream', relpath, mode)
-
-    def put_file(self, relpath, f, mode=None):
-        return self._call('put_file', relpath, f, mode)
-
-    def rename(self, rel_from, rel_to):
-        return self._call('rename', rel_from, self._safe_relpath(rel_to))
-
-    def rmdir(self, relpath):
-        return self._call('rmdir', relpath)
-
-    def stat(self, relpath):
-        return self._call('stat', relpath)
+    def _filter(self, relpath):
+        # A simplified version of PathFilteringTransport's _filter that omits
+        # the call to self.server.filter_func.
+        return self._relpath_from_server_root(relpath)
 
 
 class TestingChrootServer(ChrootServer):
 
     def __init__(self):
         """TestingChrootServer is not usable until setUp is called."""
+        ChrootServer.__init__(self, None)
 
     def setUp(self, backing_server=None):
         """Setup the Chroot on backing_server."""
@@ -171,5 +82,4 @@
 
 def get_test_permutations():
     """Return the permutations to be used in testing."""
-    return [(ChrootTransport, TestingChrootServer),
-            ]
+    return [(ChrootTransport, TestingChrootServer)]

=== added file 'bzrlib/transport/pathfilter.py'
--- a/bzrlib/transport/pathfilter.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/transport/pathfilter.py	2009-09-16 05:00:54 +0000
@@ -0,0 +1,195 @@
+# Copyright (C) 2009 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""A transport decorator that filters all paths that are passed to it."""
+
+
+from bzrlib.transport import (
+    get_transport,
+    register_transport,
+    Server,
+    Transport,
+    unregister_transport,
+    )
+
+
+class PathFilteringServer(Server):
+    """Transport server for PathFilteringTransport.
+
+    It holds the backing_transport and filter_func for PathFilteringTransports.
+    All paths will be passed through filter_func before calling into the
+    backing_transport.
+
+    Note that paths returned from the backing transport are *not* altered in
+    anyway.  So, depending on the filter_func, PathFilteringTransports might
+    not conform to the usual expectations of Transport behaviour; e.g. 'name'
+    in t.list_dir('dir') might not imply t.has('dir/name') is True!  A filter
+    that merely prefixes a constant path segment will be essentially
+    transparent, whereas a filter that does rot13 to paths will break
+    expectations and probably cause confusing errors.  So choose your
+    filter_func with care.
+    """
+
+    def __init__(self, backing_transport, filter_func):
+        """Constructor.
+
+        :param backing_transport: a transport
+        :param filter_func: a callable that takes paths, and translates them
+            into paths for use with the backing transport.
+        """
+        self.backing_transport = backing_transport
+        self.filter_func = filter_func
+
+    def _factory(self, url):
+        return PathFilteringTransport(self, url)
+
+    def get_url(self):
+        return self.scheme
+
+    def setUp(self):
+        self.scheme = 'filtered-%d:///' % id(self)
+        register_transport(self.scheme, self._factory)
+
+    def tearDown(self):
+        unregister_transport(self.scheme, self._factory)
+
+
+class PathFilteringTransport(Transport):
+    """A PathFilteringTransport.
+
+    Please see PathFilteringServer for details.
+    """
+
+    def __init__(self, server, base):
+        self.server = server
+        if not base.endswith('/'):
+            base += '/'
+        Transport.__init__(self, base)
+        self.base_path = self.base[len(self.server.scheme)-1:]
+        self.scheme = self.server.scheme
+
+    def _relpath_from_server_root(self, relpath):
+        unfiltered_path = self._combine_paths(self.base_path, relpath)
+        if not unfiltered_path.startswith('/'):
+            raise ValueError(unfiltered_path)
+        return unfiltered_path[1:]
+
+    def _filter(self, relpath):
+        return self.server.filter_func(self._relpath_from_server_root(relpath))
+
+    def _call(self, methodname, relpath, *args):
+        """Helper for Transport methods of the form:
+            operation(path, [other args ...])
+        """
+        backing_method = getattr(self.server.backing_transport, methodname)
+        return backing_method(self._filter(relpath), *args)
+
+    # Transport methods
+    def abspath(self, relpath):
+        # We do *not* want to filter at this point; e.g if the filter is
+        # homedir expansion, self.base == 'this:///' and relpath == '~/foo',
+        # then the abspath should be this:///~/foo (not this:///home/user/foo).
+        # Instead filtering should happen when self's base is passed to the
+        # backing.
+        return self.scheme + self._relpath_from_server_root(relpath)
+
+    def append_file(self, relpath, f, mode=None):
+        return self._call('append_file', relpath, f, mode)
+
+    def _can_roundtrip_unix_modebits(self):
+        return self.server.backing_transport._can_roundtrip_unix_modebits()
+
+    def clone(self, relpath):
+        return self.__class__(self.server, self.abspath(relpath))
+
+    def delete(self, relpath):
+        return self._call('delete', relpath)
+
+    def delete_tree(self, relpath):
+        return self._call('delete_tree', relpath)
+
+    def external_url(self):
+        """See bzrlib.transport.Transport.external_url."""
+        # PathFilteringTransports, like MemoryTransport, depend on in-process
+        # state and thus the base cannot simply be handed out.  See the base
+        # class docstring for more details and possible directions. For now we
+        # return the path-filtered URL.
+        return self.server.backing_transport.external_url()
+
+    def get(self, relpath):
+        return self._call('get', relpath)
+
+    def has(self, relpath):
+        return self._call('has', relpath)
+
+    def is_readonly(self):
+        return self.server.backing_transport.is_readonly()
+
+    def iter_files_recursive(self):
+        backing_transport = self.server.backing_transport.clone(
+            self._filter('.'))
+        return backing_transport.iter_files_recursive()
+
+    def listable(self):
+        return self.server.backing_transport.listable()
+
+    def list_dir(self, relpath):
+        return self._call('list_dir', relpath)
+
+    def lock_read(self, relpath):
+        return self._call('lock_read', relpath)
+
+    def lock_write(self, relpath):
+        return self._call('lock_write', relpath)
+
+    def mkdir(self, relpath, mode=None):
+        return self._call('mkdir', relpath, mode)
+
+    def open_write_stream(self, relpath, mode=None):
+        return self._call('open_write_stream', relpath, mode)
+
+    def put_file(self, relpath, f, mode=None):
+        return self._call('put_file', relpath, f, mode)
+
+    def rename(self, rel_from, rel_to):
+        return self._call('rename', rel_from, self._filter(rel_to))
+
+    def rmdir(self, relpath):
+        return self._call('rmdir', relpath)
+
+    def stat(self, relpath):
+        return self._call('stat', relpath)
+
+
+class TestingPathFilteringServer(PathFilteringServer):
+
+    def __init__(self):
+        """TestingChrootServer is not usable until setUp is called."""
+
+    def setUp(self, backing_server=None):
+        """Setup the Chroot on backing_server."""
+        if backing_server is not None:
+            self.backing_transport = get_transport(backing_server.get_url())
+        else:
+            self.backing_transport = get_transport('.')
+        self.backing_transport.clone('added-by-filter').ensure_base()
+        self.filter_func = lambda x: 'added-by-filter/' + x
+        PathFilteringServer.setUp(self)
+
+
+def get_test_permutations():
+    """Return the permutations to be used in testing."""
+    return [(PathFilteringTransport, TestingPathFilteringServer)]

=== modified file 'doc/developers/network-protocol.txt'
--- a/doc/developers/network-protocol.txt	2009-04-04 02:50:01 +0000
+++ b/doc/developers/network-protocol.txt	2009-09-17 03:21:14 +0000
@@ -431,11 +431,10 @@
 tricks).  The default implementation in bzrlib does this using the
 `bzrlib.transport.chroot` module.
 
-URLs that include ~ should probably be passed across to the server
-verbatim and the server can expand them.  This will proably not be
-meaningful when limited to a directory?  See `bug 109143`_.
-
-.. _bug 109143: https://bugs.launchpad.net/bzr/+bug/109143
+URLs that include ~ are passed across to the server verbatim and the
+server can expand them.  The default implementation in bzrlib does this
+using `bzrlib.transport.pathfilter` and `os.path.expanduser`, taking care
+to respect the virtual root.
 
 
 Requests

=== modified file 'doc/en/user-guide/server.txt'
--- a/doc/en/user-guide/server.txt	2009-02-22 16:54:02 +0000
+++ b/doc/en/user-guide/server.txt	2009-09-17 03:16:05 +0000
@@ -36,18 +36,24 @@
 SSH
 ~~~
 
-Using Bazaar over SSH requires no special configuration on the server::
+Using Bazaar over SSH requires no special configuration on the server; so long
+as Bazaar is installed on the server you can use ``bzr+ssh`` URLs, e.g.::
+
+    bzr log bzr+ssh://host/path/to/branch
+
+If `bzr` is not installed system-wide on the server you may need to explicitly
+tell the local `bzr` where to find the remote `bzr`::
 
     BZR_REMOTE_PATH=~/bin/bzr bzr log bzr+ssh://host/path/to/branch
 
 The ``BZR_REMOTE_PATH`` environment variable adjusts how `bzr` will be
 invoked on the remote system.  By default, just `bzr` will be invoked,
-which requires the `bzr` executable to be on the default search path.
+which requires the `bzr` executable to be on the default search path.  You can
+also set this permanently per-location in ``locations.conf``.
 
-The ``bzr+ssh://`` URL scheme only supports absolute paths from the
-root of the filesystem.  Future versions are expected to support ``~``
-in the same way as ``sftp://`` URLs
-(https://bugs.launchpad.net/bzr/+bug/109143).
+Like SFTP, paths starting with ``~`` are relative to your home directory, e.g.
+``bzr+ssh://example.com/~/code/proj``.  Additionally, paths starting with
+``~user`` will be relative to that user's home directory.
 
 inetd
 ~~~~~
@@ -65,6 +71,10 @@
 
     bzr log bzr://host/branchname
 
+If possible, paths starting with ``~`` and ``~user`` will be expanded as for
+``bzr+ssh``.  Home directories outside the ``--directory`` specified to ``bzr
+serve`` will not be accessible.
+
 Dedicated
 ~~~~~~~~~
 




More information about the bazaar-commits mailing list