[MERGE][1.0][Bug 124089] Add root_client_path parameter to SmartWSGIApp, and use it to translate paths received from clients to local paths.

Andrew Bennetts andrew at canonical.com
Fri Dec 14 01:51:12 GMT 2007


Andrew Bennetts wrote:
[...]
> 
> I have code locally that works for bzr+http, even with a shared repository.

And here it is.

This builds on my original bug-124089 bundle:

  * allows root_client_path to be None, to indicate “no further translation
    necessary”,
  * allows passing root_client_path to the SmartServer constructor, and
  * changes the bzrlib.transport.http.wsgi module to take advantage of all this.

The basic idea in SmartWSGIApp is that it compares the path from the HTTP
request with the root_client_path, and then adjusts the backing transport and/or
root_client_path given to the smart request handler accordingly.  The end result
is that as far as the client is concerned, an HTTP POST of “mkdir c” to /a/b/ is
equivalent to POSTing “mkdir b/c” to /a/, etc.

This, combined with the small change to fix the client-side path calculations
I've posted separately, makes bzr+http actually work in non-trivial situations.

-Andrew.

-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: andrew.bennetts at canonical.com-20071214014348-\
#   xj6q4064e27mw3gl
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev
# testament_sha1: 29412be7e764fc43fa13009fbc7d8635d4eeab41
# timestamp: 2007-12-14 12:50:15 +1100
# source_branch: http://people.ubuntu.com/~andrew/bzr/bug-124089
# base_revision_id: pqm at pqm.ubuntu.com-20071204185324-guhluk76wrccurxj
# 
# Begin patch
=== modified file 'NEWS'
--- NEWS	2007-12-04 18:11:51 +0000
+++ NEWS	2007-12-06 00:29:17 +0000
@@ -27,6 +27,13 @@
 
   BUGFIXES:
 
+   * Add ``root_client_path`` parameter to SmartWSGIApp and SmartServerRequest.
+     This makes it possible to publish filesystem locations that don't exactly
+     match URL paths. SmartServerRequest subclasses should now use the new
+     ``translate_client_path`` and ``transport_from_client_path`` methods when
+     dealing with paths received from a client to take this into account.
+     (Andrew Bennetts, #124089)
+
    * Catch OSError 17 (file exists) in final phase of tree transform and show
      filename to user.
      (Alexander Belchenko, #111758)

=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py	2007-12-04 18:11:51 +0000
+++ bzrlib/errors.py	2007-12-05 01:58:00 +0000
@@ -526,11 +526,13 @@
 
 class InvalidURLJoin(PathError):
 
-    _fmt = 'Invalid URL join request: "%(args)s"%(extra)s'
+    _fmt = "Invalid URL join request: %(reason)s: %(base)r + %(join_args)r"
 
-    def __init__(self, msg, base, args):
-        PathError.__init__(self, base, msg)
-        self.args = [base] + list(args)
+    def __init__(self, reason, base, join_args):
+        self.reason = reason
+        self.base = base
+        self.join_args = join_args
+        PathError.__init__(self, base, reason)
 
 
 class UnknownHook(BzrError):

=== modified file 'bzrlib/smart/branch.py'
--- bzrlib/smart/branch.py	2007-11-26 02:06:52 +0000
+++ bzrlib/smart/branch.py	2007-12-05 07:09:42 +0000
@@ -27,14 +27,21 @@
 
 
 class SmartServerBranchRequest(SmartServerRequest):
-    """Base class for handling common branch request logic."""
+    """Base class for handling common branch request logic.
+    """
 
     def do(self, path, *args):
         """Execute a request for a branch at path.
+    
+        All Branch requests take a path to the branch as their first argument.
 
         If the branch is a branch reference, NotBranchError is raised.
+
+        :param path: The path for the repository as received from the
+            client.
+        :return: A SmartServerResponse from self.do_with_branch().
         """
-        transport = self._backing_transport.clone(path)
+        transport = self.transport_from_client_path(path)
         bzrdir = BzrDir.open_from_transport(transport)
         if bzrdir.get_branch_reference() is not None:
             raise errors.NotBranchError(transport.base)

=== modified file 'bzrlib/smart/bzrdir.py'
--- bzrlib/smart/bzrdir.py	2007-04-24 12:20:09 +0000
+++ bzrlib/smart/bzrdir.py	2007-12-05 23:30:57 +0000
@@ -30,15 +30,24 @@
 
     def do(self, path):
         from bzrlib.bzrdir import BzrDirFormat
-        t = self._backing_transport.clone(path)
-        default_format = BzrDirFormat.get_default_format()
-        real_bzrdir = default_format.open(t, _found=True)
         try:
-            real_bzrdir._format.probe_transport(t)
-        except (errors.NotBranchError, errors.UnknownFormatError):
+            t = self.transport_from_client_path(path)
+        except errors.PathNotChild:
+            # The client is trying to ask about a path that they have no access
+            # to.
+            # Ideally we'd return a FailedSmartServerResponse here rather than
+            # a "successful" negative, but we want to be compatibile with
+            # clients that don't anticipate errors from this method.
             answer = 'no'
         else:
-            answer = 'yes'
+            default_format = BzrDirFormat.get_default_format()
+            real_bzrdir = default_format.open(t, _found=True)
+            try:
+                real_bzrdir._format.probe_transport(t)
+            except (errors.NotBranchError, errors.UnknownFormatError):
+                answer = 'no'
+            else:
+                answer = 'yes'
         return SuccessfulSmartServerResponse((answer,))
 
 
@@ -54,12 +63,14 @@
 
         :return: norepository or ok, relpath.
         """
-        bzrdir = BzrDir.open_from_transport(self._backing_transport.clone(path))
+        bzrdir = BzrDir.open_from_transport(
+            self.transport_from_client_path(path))
         try:
             repository = bzrdir.find_repository()
             # the relpath of the bzrdir in the found repository gives us the 
             # path segments to pop-out.
-            relpath = repository.bzrdir.root_transport.relpath(bzrdir.root_transport.base)
+            relpath = repository.bzrdir.root_transport.relpath(
+                bzrdir.root_transport.base)
             if len(relpath):
                 segments = ['..'] * len(relpath.split('/'))
             else:
@@ -85,7 +96,7 @@
         The default format of the server is used.
         :return: SmartServerResponse(('ok', ))
         """
-        target_transport = self._backing_transport.clone(path)
+        target_transport = self.transport_from_client_path(path)
         BzrDirFormat.get_default_format().initialize_on_transport(target_transport)
         return SuccessfulSmartServerResponse(('ok', ))
 
@@ -98,7 +109,8 @@
         If a bzrdir is not present, an exception is propogated
         rather than 'no branch' because these are different conditions.
         """
-        bzrdir = BzrDir.open_from_transport(self._backing_transport.clone(path))
+        bzrdir = BzrDir.open_from_transport(
+            self.transport_from_client_path(path))
         try:
             reference_url = bzrdir.get_branch_reference()
             if reference_url is None:

=== modified file 'bzrlib/smart/medium.py'
--- bzrlib/smart/medium.py	2007-12-03 16:39:11 +0000
+++ bzrlib/smart/medium.py	2007-12-05 23:30:57 +0000
@@ -53,13 +53,14 @@
     which will typically be a LocalTransport looking at the server's filesystem.
     """
 
-    def __init__(self, backing_transport):
+    def __init__(self, backing_transport, root_client_path='/'):
         """Construct new server.
 
         :param backing_transport: Transport for the directory served.
         """
         # backing_transport could be passed to serve instead of __init__
         self.backing_transport = backing_transport
+        self.root_client_path = root_client_path
         self.finished = False
 
     def serve(self):
@@ -91,7 +92,8 @@
             bytes = bytes[len(REQUEST_VERSION_TWO):]
         else:
             protocol_class = SmartServerRequestProtocolOne
-        protocol = protocol_class(self.backing_transport, self._write_out)
+        protocol = protocol_class(
+            self.backing_transport, self._write_out, self.root_client_path)
         protocol.accept_bytes(bytes)
         return protocol
 
@@ -139,13 +141,14 @@
 
 class SmartServerSocketStreamMedium(SmartServerStreamMedium):
 
-    def __init__(self, sock, backing_transport):
+    def __init__(self, sock, backing_transport, root_client_path='/'):
         """Constructor.
 
         :param sock: the socket the server will read from.  It will be put
             into blocking mode.
         """
-        SmartServerStreamMedium.__init__(self, backing_transport)
+        SmartServerStreamMedium.__init__(
+            self, backing_transport, root_client_path=root_client_path)
         self.push_back = ''
         sock.setblocking(True)
         self.socket = sock

=== modified file 'bzrlib/smart/protocol.py'
--- bzrlib/smart/protocol.py	2007-11-09 20:49:54 +0000
+++ bzrlib/smart/protocol.py	2007-12-05 01:58:00 +0000
@@ -73,8 +73,9 @@
 class SmartServerRequestProtocolOne(SmartProtocolBase):
     """Server-side encoding and decoding logic for smart version 1."""
     
-    def __init__(self, backing_transport, write_func):
+    def __init__(self, backing_transport, write_func, root_client_path='/'):
         self._backing_transport = backing_transport
+        self._root_client_path = root_client_path
         self.excess_buffer = ''
         self._finished = False
         self.in_buffer = ''
@@ -100,7 +101,8 @@
                 first_line += '\n'
                 req_args = _decode_tuple(first_line)
                 self.request = request.SmartServerRequestHandler(
-                    self._backing_transport, commands=request.request_handlers)
+                    self._backing_transport, commands=request.request_handlers,
+                    root_client_path=self._root_client_path)
                 self.request.dispatch_command(req_args[0], req_args[1:])
                 if self.request.finished_reading:
                     # trivial request
@@ -471,6 +473,7 @@
     def call(self, *args):
         if 'hpss' in debug.debug_flags:
             mutter('hpss call:   %s', repr(args)[1:-1])
+            mutter('             (to: %r)' % (self._request._medium))
             self._request_start_time = time.time()
         self._write_args(args)
         self._request.finished_writing()

=== modified file 'bzrlib/smart/repository.py'
--- bzrlib/smart/repository.py	2007-11-26 02:08:13 +0000
+++ bzrlib/smart/repository.py	2007-12-05 07:09:42 +0000
@@ -38,14 +38,17 @@
     def do(self, path, *args):
         """Execute a repository request.
         
-        The repository must be at the exact path - no searching is done.
+        All Repository requests take a path to the repository as their first
+        argument.  The repository must be at the exact path given by the
+        client - no searching is done.
 
         The actual logic is delegated to self.do_repository_request.
 
-        :param path: The path for the repository.
-        :return: A smart server from self.do_repository_request().
+        :param client_path: The path for the repository as received from the
+            client.
+        :return: A SmartServerResponse from self.do_repository_request().
         """
-        transport = self._backing_transport.clone(path)
+        transport = self.transport_from_client_path(path)
         bzrdir = BzrDir.open_from_transport(transport)
         repository = bzrdir.open_repository()
         return self.do_repository_request(repository, *args)
@@ -257,15 +260,16 @@
             repository.unlock()
 
     def _do_repository_request(self, repository, revision_ids):
-        stream = repository.get_data_stream(revision_ids)
         filelike = StringIO()
         pack = ContainerWriter(filelike.write)
         pack.begin()
+        stream = repository.get_data_stream(revision_ids)
         try:
             for name_tuple, bytes in stream:
                 pack.add_bytes_record(bytes, [name_tuple])
         except errors.RevisionNotPresent, e:
-            return FailedSmartServerResponse(('NoSuchRevision', e.revision_id))
+            return FailedSmartServerResponse(('NoSuchRevision',
+                                              e.revision_id))
         pack.end()
         return SuccessfulSmartServerResponse(('ok',), filelike.getvalue())
 

=== modified file 'bzrlib/smart/request.py'
--- bzrlib/smart/request.py	2007-10-19 05:38:36 +0000
+++ bzrlib/smart/request.py	2007-12-14 01:43:48 +0000
@@ -24,20 +24,40 @@
     errors,
     registry,
     revision,
+    urlutils,
     )
 from bzrlib.bundle.serializer import write_bundle
+from bzrlib.transport import get_transport
+from bzrlib.transport.chroot import ChrootServer
 
 
 class SmartServerRequest(object):
-    """Base class for request handlers."""
+    """Base class for request handlers.
+    
+    To define a new request, subclass this class and override the `do` method
+    (and if appropriate, `do_body` as well).  Request implementors should take
+    care to call `translate_client_path` and `transport_from_client_path` as
+    appropriate when dealing with paths received from the client.
+    """
 
-    def __init__(self, backing_transport):
+    def __init__(self, backing_transport, root_client_path='/'):
         """Constructor.
 
         :param backing_transport: the base transport to be used when performing
             this request.
+        :param root_client_path: the client path that maps to the root of
+            backing_transport.  This is used to interpret relpaths received
+            from the client.  Clients will not be able to refer to paths above
+            this root.  If root_client_path is None, then no translation will
+            be performed on client paths.  Default is '/'.
         """
         self._backing_transport = backing_transport
+        if root_client_path is not None:
+            if not root_client_path.startswith('/'):
+                root_client_path = '/' + root_client_path
+            if not root_client_path.endswith('/'):
+                root_client_path += '/'
+        self._root_client_path = root_client_path
 
     def _check_enabled(self):
         """Raises DisabledMethod if this method is disabled."""
@@ -74,6 +94,40 @@
         # this NotImplementedError and translate it into a 'bad request' error
         # to send to the client.
         raise NotImplementedError(self.do_body)
+    
+    def translate_client_path(self, client_path):
+        """Translate a path received from a network client into a local
+        relpath.
+
+        All paths received from the client *must* be translated.
+
+        :param client_path: the path from the client.
+        :returns: a relpath that may be used with self._backing_transport
+            (unlike the untranslated client_path, which must not be used with
+            the backing transport).
+        """
+        if self._root_client_path is None:
+            # no translation necessary!
+            return client_path
+        if not client_path.startswith('/'):
+            client_path = '/' + client_path
+        if client_path.startswith(self._root_client_path):
+            path = client_path[len(self._root_client_path):]
+            relpath = urlutils.joinpath('/', path)
+            assert relpath.startswith('/')
+            return '.' + relpath
+        else:
+            raise errors.PathNotChild(client_path, self._root_client_path)
+
+    def transport_from_client_path(self, client_path):
+        """Get a backing transport corresponding to the location referred to by
+        a network client.
+
+        :seealso: translate_client_path
+        :returns: a transport cloned from self._backing_transport
+        """
+        relpath = self.translate_client_path(client_path)
+        return self._backing_transport.clone(relpath)
 
 
 class SmartServerResponse(object):
@@ -106,8 +160,9 @@
                 other.body_stream is self.body_stream)
 
     def __repr__(self):
-        return "<SmartServerResponse %r args=%r body=%r>" % (
-            self.is_successful(), self.args, self.body)
+        status = {True: 'OK', False: 'ERR'}[self.is_successful()]
+        return "<SmartServerResponse status=%s args=%r body=%r>" % (status,
+            self.args, self.body)
 
 
 class FailedSmartServerResponse(SmartServerResponse):
@@ -143,7 +198,7 @@
     # TODO: Better way of representing the body for commands that take it,
     # and allow it to be streamed into the server.
 
-    def __init__(self, backing_transport, commands):
+    def __init__(self, backing_transport, commands, root_client_path):
         """Constructor.
 
         :param backing_transport: a Transport to handle requests for.
@@ -151,6 +206,7 @@
             subclasses. e.g. bzrlib.transport.smart.vfs.vfs_commands.
         """
         self._backing_transport = backing_transport
+        self._root_client_path = root_client_path
         self._commands = commands
         self._body_bytes = ''
         self.response = None
@@ -180,7 +236,7 @@
             command = self._commands.get(cmd)
         except LookupError:
             raise errors.SmartProtocolError("bad request %r" % (cmd,))
-        self._command = command(self._backing_transport)
+        self._command = command(self._backing_transport, self._root_client_path)
         self._run_handler_code(self._command.execute, args, {})
 
     def _run_handler_code(self, callable, args, kwargs):
@@ -247,7 +303,7 @@
 
     def do(self, path, revision_id):
         # open transport relative to our base
-        t = self._backing_transport.clone(path)
+        t = self.transport_from_client_path(path)
         control, extra_path = bzrdir.BzrDir.open_containing_from_transport(t)
         repo = control.open_repository()
         tmpf = tempfile.TemporaryFile()

=== modified file 'bzrlib/smart/server.py'
--- bzrlib/smart/server.py	2007-07-20 03:20:20 +0000
+++ bzrlib/smart/server.py	2007-12-13 22:22:58 +0000
@@ -38,14 +38,18 @@
     hooks: An instance of SmartServerHooks.
     """
 
-    def __init__(self, backing_transport, host='127.0.0.1', port=0):
+    def __init__(self, backing_transport, host='127.0.0.1', port=0,
+                 root_client_path='/'):
         """Construct a new server.
 
         To actually start it running, call either start_background_thread or
         serve.
 
+        :param backing_transport: The transport to serve.
         :param host: Name of the interface to listen on.
         :param port: TCP port to listen on, or 0 to allocate a transient port.
+        :param root_client_path: The client path that will correspond to root
+            of backing_transport.
         """
         # let connections timeout so that we get a chance to terminate
         # Keep a reference to the exceptions we want to catch because the socket
@@ -63,6 +67,7 @@
         self.backing_transport = backing_transport
         self._started = threading.Event()
         self._stopped = threading.Event()
+        self.root_client_path = root_client_path
 
     def serve(self):
         self._should_terminate = False
@@ -134,7 +139,8 @@
         # propogates to the newly accepted socket.
         conn.setblocking(True)
         conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
-        handler = SmartServerSocketStreamMedium(conn, self.backing_transport)
+        handler = SmartServerSocketStreamMedium(
+            conn, self.backing_transport, self.root_client_path)
         connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
         connection_thread.setDaemon(True)
         connection_thread.start()
@@ -204,13 +210,27 @@
 
     def __init__(self):
         SmartTCPServer.__init__(self, None)
+        self.client_path_extra = None
         
     def get_backing_transport(self, backing_transport_server):
         """Get a backing transport from a server we are decorating."""
         return transport.get_transport(backing_transport_server.get_url())
 
-    def setUp(self, backing_transport_server=None):
-        """Set up server for testing"""
+    def setUp(self, backing_transport_server=None,
+              client_path_extra='/extra/'):
+        """Set up server for testing.
+        
+        :param backing_transport_server: backing server to use.  If not
+            specified, a LocalURLServer at the current working directory will
+            be used.
+        :param client_path_extra: a path segment starting with '/' to append to
+            the root URL for this server.  For instance, a value of '/foo/bar/'
+            will mean the root of the backing transport will be published at a
+            URL like `bzr://127.0.0.1:nnnn/foo/bar/`, rather than
+            `bzr://127.0.0.1:nnnn/`.  Default value is `extra`, so that tests
+            by default will fail unless they do the necessary path translation.
+        """
+        assert client_path_extra.startswith('/')
         from bzrlib.transport.chroot import ChrootServer
         if backing_transport_server is None:
             from bzrlib.transport.local import LocalURLServer
@@ -220,12 +240,18 @@
         self.chroot_server.setUp()
         self.backing_transport = transport.get_transport(
             self.chroot_server.get_url())
+        self.root_client_path = self.client_path_extra = client_path_extra
         self.start_background_thread()
 
     def tearDown(self):
         self.stop_background_thread()
         self.chroot_server.tearDown()
 
+    def get_url(self):
+        url = super(SmartTCPServer_for_testing, self).get_url()
+        assert url.endswith('/')
+        return url[:-1] + self.client_path_extra
+
     def get_bogus_url(self):
         """Return a URL which will fail to connect"""
         return 'bzr://127.0.0.1:1/'

=== modified file 'bzrlib/smart/vfs.py'
--- bzrlib/smart/vfs.py	2007-07-04 08:08:13 +0000
+++ bzrlib/smart/vfs.py	2007-12-05 23:30:57 +0000
@@ -63,6 +63,7 @@
 class HasRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         r = self._backing_transport.has(relpath) and 'yes' or 'no'
         return request.SuccessfulSmartServerResponse((r,))
 
@@ -70,6 +71,7 @@
 class GetRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         try:
             backing_bytes = self._backing_transport.get_bytes(relpath)
         except errors.ReadError:
@@ -81,6 +83,7 @@
 class AppendRequest(VfsRequest):
 
     def do(self, relpath, mode):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
         self._mode = _deserialise_optional_mode(mode)
     
@@ -93,6 +96,7 @@
 class DeleteRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         self._backing_transport.delete(relpath)
         return request.SuccessfulSmartServerResponse(('ok', ))
 
@@ -100,6 +104,9 @@
 class IterFilesRecursiveRequest(VfsRequest):
 
     def do(self, relpath):
+        if not relpath.endswith('/'):
+            relpath += '/'
+        relpath = self.translate_client_path(relpath)
         transport = self._backing_transport.clone(relpath)
         filenames = transport.iter_files_recursive()
         return request.SuccessfulSmartServerResponse(('names',) + tuple(filenames))
@@ -108,6 +115,9 @@
 class ListDirRequest(VfsRequest):
 
     def do(self, relpath):
+        if not relpath.endswith('/'):
+            relpath += '/'
+        relpath = self.translate_client_path(relpath)
         filenames = self._backing_transport.list_dir(relpath)
         return request.SuccessfulSmartServerResponse(('names',) + tuple(filenames))
 
@@ -115,6 +125,7 @@
 class MkdirRequest(VfsRequest):
 
     def do(self, relpath, mode):
+        relpath = self.translate_client_path(relpath)
         self._backing_transport.mkdir(relpath,
                                       _deserialise_optional_mode(mode))
         return request.SuccessfulSmartServerResponse(('ok',))
@@ -123,6 +134,8 @@
 class MoveRequest(VfsRequest):
 
     def do(self, rel_from, rel_to):
+        rel_from = self.translate_client_path(rel_from)
+        rel_to = self.translate_client_path(rel_to)
         self._backing_transport.move(rel_from, rel_to)
         return request.SuccessfulSmartServerResponse(('ok',))
 
@@ -130,6 +143,7 @@
 class PutRequest(VfsRequest):
 
     def do(self, relpath, mode):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
         self._mode = _deserialise_optional_mode(mode)
 
@@ -141,6 +155,7 @@
 class PutNonAtomicRequest(VfsRequest):
 
     def do(self, relpath, mode, create_parent, dir_mode):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
         self._dir_mode = _deserialise_optional_mode(dir_mode)
         self._mode = _deserialise_optional_mode(mode)
@@ -159,6 +174,7 @@
 class ReadvRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
 
     def do_body(self, body_bytes):
@@ -182,6 +198,8 @@
 class RenameRequest(VfsRequest):
 
     def do(self, rel_from, rel_to):
+        rel_from = self.translate_client_path(rel_from)
+        rel_to = self.translate_client_path(rel_to)
         self._backing_transport.rename(rel_from, rel_to)
         return request.SuccessfulSmartServerResponse(('ok', ))
 
@@ -189,6 +207,7 @@
 class RmdirRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         self._backing_transport.rmdir(relpath)
         return request.SuccessfulSmartServerResponse(('ok', ))
 
@@ -196,6 +215,9 @@
 class StatRequest(VfsRequest):
 
     def do(self, relpath):
+        if not relpath.endswith('/'):
+            relpath += '/'
+        relpath = self.translate_client_path(relpath)
         stat = self._backing_transport.stat(relpath)
         return request.SuccessfulSmartServerResponse(
             ('stat', str(stat.st_size), oct(stat.st_mode)))

=== modified file 'bzrlib/tests/test_errors.py'
--- bzrlib/tests/test_errors.py	2007-12-04 18:11:51 +0000
+++ bzrlib/tests/test_errors.py	2007-12-05 01:58:00 +0000
@@ -409,6 +409,13 @@
             "Unable to create symlink u'\\xb5' on this platform",
             str(err))
 
+    def test_invalid_url_join(self):
+        """Test the formatting of InvalidURLJoin."""
+        e = errors.InvalidURLJoin('Reason', 'base path', ('args',))
+        self.assertEqual(
+            "Invalid URL join request: Reason: 'base path' + ('args',)",
+            str(e))
+
     def test_incorrect_url(self):
         err = errors.InvalidBugTrackerURL('foo', 'http://bug.com/')
         self.assertEquals(

=== modified file 'bzrlib/tests/test_smart.py'
--- bzrlib/tests/test_smart.py	2007-11-26 03:36:30 +0000
+++ bzrlib/tests/test_smart.py	2007-12-05 02:13:22 +0000
@@ -25,17 +25,49 @@
 """
 
 from StringIO import StringIO
-import tempfile
 import tarfile
 
-from bzrlib import bzrdir, errors, pack, smart, tests
-from bzrlib.smart.request import SmartServerResponse
+from bzrlib import (
+    bzrdir,
+    errors,
+    pack,
+    smart,
+    tests,
+    urlutils,
+    )
+from bzrlib.branch import BranchReferenceFormat
+# Some imports so that "smart.branch.foo", etc, work correctly.
+import bzrlib.smart.branch
 import bzrlib.smart.bzrdir
-import bzrlib.smart.branch
 import bzrlib.smart.repository
+from bzrlib.smart.request import (
+    FailedSmartServerResponse,
+    SmartServerRequest,
+    SmartServerResponse,
+    SuccessfulSmartServerResponse,
+    )
+from bzrlib.transport import chroot, get_transport
 from bzrlib.util import bencode
 
 
+class TestCaseWithChrootedTransport(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        tests.TestCaseWithTransport.setUp(self)
+        self._chroot_server = None
+
+    def get_transport(self, relpath=None):
+        if self._chroot_server is None:
+            backing_transport = tests.TestCaseWithTransport.get_transport(self)
+            self._chroot_server = chroot.ChrootServer(backing_transport)
+            self._chroot_server.setUp()
+            self.addCleanup(self._chroot_server.tearDown)
+        t = get_transport(self._chroot_server.get_url())
+        if relpath is not None:
+            t = t.clone(relpath)
+        return t
+
+
 class TestCaseWithSmartMedium(tests.TestCaseWithTransport):
 
     def setUp(self):
@@ -65,8 +97,39 @@
         self.assertNotEqual(None,
             SmartServerResponse(('ok', )))
 
-
-class TestSmartServerRequestFindRepository(tests.TestCaseWithTransport):
+    def test__str__(self):
+        """SmartServerResponses can be stringified."""
+        self.assertEqual(
+            "<SmartServerResponse status=OK args=('args',) body='body'>",
+            str(SuccessfulSmartServerResponse(('args',), 'body')))
+        self.assertEqual(
+            "<SmartServerResponse status=ERR args=('args',) body='body'>",
+            str(FailedSmartServerResponse(('args',), 'body')))
+
+
+class TestSmartServerRequest(tests.TestCaseWithMemoryTransport):
+
+    def test_translate_client_path(self):
+        transport = self.get_transport()
+        request = SmartServerRequest(transport, 'foo/')
+        self.assertEqual('./', request.translate_client_path('foo/'))
+        self.assertRaises(
+            errors.InvalidURLJoin, request.translate_client_path, 'foo/..')
+        self.assertRaises(
+            errors.PathNotChild, request.translate_client_path, '/')
+        self.assertRaises(
+            errors.PathNotChild, request.translate_client_path, 'bar/')
+        self.assertEqual('./baz', request.translate_client_path('foo/baz'))
+
+    def test_transport_from_client_path(self):
+        transport = self.get_transport()
+        request = SmartServerRequest(transport, 'foo/')
+        self.assertEqual(
+            transport.base,
+            request.transport_from_client_path('foo/').base)
+
+
+class TestSmartServerRequestFindRepository(tests.TestCaseWithMemoryTransport):
     """Tests for BzrDir.find_repository."""
 
     def test_no_repository(self):
@@ -75,7 +138,7 @@
         request = smart.bzrdir.SmartServerRequestFindRepository(backing)
         self.make_bzrdir('.')
         self.assertEqual(SmartServerResponse(('norepository', )),
-            request.execute(backing.local_abspath('')))
+            request.execute('/'))
 
     def test_nonshared_repository(self):
         # nonshared repositorys only allow 'find' to return a handle when the 
@@ -84,10 +147,10 @@
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestFindRepository(backing)
         result = self._make_repository_and_result()
-        self.assertEqual(result, request.execute(backing.local_abspath('')))
+        self.assertEqual(result, request.execute(''))
         self.make_bzrdir('subdir')
         self.assertEqual(SmartServerResponse(('norepository', )),
-            request.execute(backing.local_abspath('subdir')))
+            request.execute('subdir'))
 
     def _make_repository_and_result(self, shared=False, format=None):
         """Convenience function to setup a repository.
@@ -110,15 +173,15 @@
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestFindRepository(backing)
         result = self._make_repository_and_result(shared=True)
-        self.assertEqual(result, request.execute(backing.local_abspath('')))
+        self.assertEqual(result, request.execute(''))
         self.make_bzrdir('subdir')
         result2 = SmartServerResponse(result.args[0:1] + ('..', ) + result.args[2:])
         self.assertEqual(result2,
-            request.execute(backing.local_abspath('subdir')))
+            request.execute('subdir'))
         self.make_bzrdir('subdir/deeper')
         result3 = SmartServerResponse(result.args[0:1] + ('../..', ) + result.args[2:])
         self.assertEqual(result3,
-            request.execute(backing.local_abspath('subdir/deeper')))
+            request.execute('subdir/deeper'))
 
     def test_rich_root_and_subtree_encoding(self):
         """Test for the format attributes for rich root and subtree support."""
@@ -128,17 +191,17 @@
         # check the test will be valid
         self.assertEqual('yes', result.args[2])
         self.assertEqual('yes', result.args[3])
-        self.assertEqual(result, request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerRequestInitializeBzrDir(tests.TestCaseWithTransport):
+        self.assertEqual(result, request.execute(''))
+
+
+class TestSmartServerRequestInitializeBzrDir(tests.TestCaseWithMemoryTransport):
 
     def test_empty_dir(self):
         """Initializing an empty dir should succeed and do it."""
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestInitializeBzrDir(backing)
         self.assertEqual(SmartServerResponse(('ok', )),
-            request.execute(backing.local_abspath('.')))
+            request.execute('.'))
         made_dir = bzrdir.BzrDir.open_from_transport(backing)
         # no branch, tree or repository is expected with the current 
         # default formart.
@@ -151,7 +214,7 @@
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestInitializeBzrDir(backing)
         self.assertRaises(errors.NoSuchFile,
-            request.execute, backing.local_abspath('subdir'))
+            request.execute, 'subdir')
 
     def test_initialized_dir(self):
         """Initializing an extant bzrdir should fail like the bzrdir api."""
@@ -159,10 +222,10 @@
         request = smart.bzrdir.SmartServerRequestInitializeBzrDir(backing)
         self.make_bzrdir('subdir')
         self.assertRaises(errors.FileExists,
-            request.execute, backing.local_abspath('subdir'))
-
-
-class TestSmartServerRequestOpenBranch(tests.TestCaseWithTransport):
+            request.execute, 'subdir')
+
+
+class TestSmartServerRequestOpenBranch(TestCaseWithChrootedTransport):
 
     def test_no_branch(self):
         """When there is no branch, ('nobranch', ) is returned."""
@@ -170,7 +233,7 @@
         request = smart.bzrdir.SmartServerRequestOpenBranch(backing)
         self.make_bzrdir('.')
         self.assertEqual(SmartServerResponse(('nobranch', )),
-            request.execute(backing.local_abspath('')))
+            request.execute('/'))
 
     def test_branch(self):
         """When there is a branch, 'ok' is returned."""
@@ -178,7 +241,7 @@
         request = smart.bzrdir.SmartServerRequestOpenBranch(backing)
         self.make_branch('.')
         self.assertEqual(SmartServerResponse(('ok', '')),
-            request.execute(backing.local_abspath('')))
+            request.execute('/'))
 
     def test_branch_reference(self):
         """When there is a branch reference, the reference URL is returned."""
@@ -186,15 +249,13 @@
         request = smart.bzrdir.SmartServerRequestOpenBranch(backing)
         branch = self.make_branch('branch')
         checkout = branch.create_checkout('reference',lightweight=True)
-        # TODO: once we have an API to probe for references of any sort, we
-        # can use it here.
-        reference_url = backing.abspath('branch') + '/'
+        reference_url = BranchReferenceFormat().get_reference(checkout.bzrdir)
         self.assertFileEqual(reference_url, 'reference/.bzr/branch/location')
         self.assertEqual(SmartServerResponse(('ok', reference_url)),
-            request.execute(backing.local_abspath('reference')))
-
-
-class TestSmartServerRequestRevisionHistory(tests.TestCaseWithTransport):
+            request.execute('/reference'))
+
+
+class TestSmartServerRequestRevisionHistory(tests.TestCaseWithMemoryTransport):
 
     def test_empty(self):
         """For an empty branch, the body is empty."""
@@ -202,7 +263,7 @@
         request = smart.branch.SmartServerRequestRevisionHistory(backing)
         self.make_branch('.')
         self.assertEqual(SmartServerResponse(('ok', ), ''),
-            request.execute(backing.local_abspath('')))
+            request.execute(''))
 
     def test_not_empty(self):
         """For a non-empty branch, the body is empty."""
@@ -216,10 +277,10 @@
         tree.unlock()
         self.assertEqual(
             SmartServerResponse(('ok', ), ('\x00'.join([r1, r2]))),
-            request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerBranchRequest(tests.TestCaseWithTransport):
+            request.execute(''))
+
+
+class TestSmartServerBranchRequest(tests.TestCaseWithMemoryTransport):
 
     def test_no_branch(self):
         """When there is a bzrdir and no branch, NotBranchError is raised."""
@@ -227,7 +288,7 @@
         request = smart.branch.SmartServerBranchRequest(backing)
         self.make_bzrdir('.')
         self.assertRaises(errors.NotBranchError,
-            request.execute, backing.local_abspath(''))
+            request.execute, '')
 
     def test_branch_reference(self):
         """When there is a branch reference, NotBranchError is raised."""
@@ -236,10 +297,10 @@
         branch = self.make_branch('branch')
         checkout = branch.create_checkout('reference',lightweight=True)
         self.assertRaises(errors.NotBranchError,
-            request.execute, backing.local_abspath('checkout'))
-
-
-class TestSmartServerBranchRequestLastRevisionInfo(tests.TestCaseWithTransport):
+            request.execute, 'checkout')
+
+
+class TestSmartServerBranchRequestLastRevisionInfo(tests.TestCaseWithMemoryTransport):
 
     def test_empty(self):
         """For an empty branch, the result is ('ok', '0', 'null:')."""
@@ -247,7 +308,7 @@
         request = smart.branch.SmartServerBranchRequestLastRevisionInfo(backing)
         self.make_branch('.')
         self.assertEqual(SmartServerResponse(('ok', '0', 'null:')),
-            request.execute(backing.local_abspath('')))
+            request.execute(''))
 
     def test_not_empty(self):
         """For a non-empty branch, the result is ('ok', 'revno', 'revid')."""
@@ -262,10 +323,10 @@
         tree.unlock()
         self.assertEqual(
             SmartServerResponse(('ok', '2', rev_id_utf8)),
-            request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerBranchRequestGetConfigFile(tests.TestCaseWithTransport):
+            request.execute(''))
+
+
+class TestSmartServerBranchRequestGetConfigFile(tests.TestCaseWithMemoryTransport):
 
     def test_default(self):
         """With no file, we get empty content."""
@@ -275,7 +336,7 @@
         # there should be no file by default
         content = ''
         self.assertEqual(SmartServerResponse(('ok', ), content),
-            request.execute(backing.local_abspath('')))
+            request.execute(''))
 
     def test_with_content(self):
         # SmartServerBranchGetConfigFile should return the content from
@@ -286,10 +347,10 @@
         branch = self.make_branch('.')
         branch.control_files.put_utf8('branch.conf', 'foo bar baz')
         self.assertEqual(SmartServerResponse(('ok', ), 'foo bar baz'),
-            request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerBranchRequestSetLastRevision(tests.TestCaseWithTransport):
+            request.execute(''))
+
+
+class TestSmartServerBranchRequestSetLastRevision(tests.TestCaseWithMemoryTransport):
 
     def test_empty(self):
         backing = self.get_transport()
@@ -301,7 +362,7 @@
         try:
             self.assertEqual(SmartServerResponse(('ok',)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     'null:'))
         finally:
             b.unlock()
@@ -318,7 +379,7 @@
             self.assertEqual(
                 SmartServerResponse(('NoSuchRevision', revision_id)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     revision_id))
         finally:
             b.unlock()
@@ -340,7 +401,7 @@
             self.assertEqual(
                 SmartServerResponse(('ok',)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     rev_id_utf8))
             self.assertEqual([rev_id_utf8], tree.branch.revision_history())
         finally:
@@ -364,17 +425,17 @@
             self.assertEqual(
                 SmartServerResponse(('ok',)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     rev_id_utf8))
             self.assertEqual([rev_id_utf8], tree.branch.revision_history())
         finally:
             tree.branch.unlock()
 
 
-class TestSmartServerBranchRequestLockWrite(tests.TestCaseWithTransport):
+class TestSmartServerBranchRequestLockWrite(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_lock_write_on_unlocked_branch(self):
@@ -382,7 +443,7 @@
         request = smart.branch.SmartServerBranchRequestLockWrite(backing)
         branch = self.make_branch('.', format='knit')
         repository = branch.repository
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         branch_nonce = branch.control_files._lock.peek().get('nonce')
         repository_nonce = repository.control_files._lock.peek().get('nonce')
         self.assertEqual(
@@ -400,7 +461,7 @@
         branch.lock_write()
         branch.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         self.assertEqual(
             SmartServerResponse(('LockContention',)), response)
 
@@ -414,7 +475,7 @@
         branch.leave_lock_in_place()
         branch.repository.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''),
+        response = request.execute('',
                                    branch_token, repo_token)
         self.assertEqual(
             SmartServerResponse(('ok', branch_token, repo_token)), response)
@@ -429,7 +490,7 @@
         branch.leave_lock_in_place()
         branch.repository.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''),
+        response = request.execute('',
                                    branch_token+'xxx', repo_token)
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
@@ -441,7 +502,7 @@
         branch.repository.lock_write()
         branch.repository.leave_lock_in_place()
         branch.repository.unlock()
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         self.assertEqual(
             SmartServerResponse(('LockContention',)), response)
 
@@ -449,16 +510,18 @@
         backing = self.get_readonly_transport()
         request = smart.branch.SmartServerBranchRequestLockWrite(backing)
         branch = self.make_branch('.')
-        response = request.execute('')
+        root = self.get_transport().clone('/')
+        path = urlutils.relative_url(root.base, self.get_transport().base)
+        response = request.execute(path)
         error_name, lock_str, why_str = response.args
         self.assertFalse(response.is_successful())
         self.assertEqual('LockFailed', error_name)
 
 
-class TestSmartServerBranchRequestUnlock(tests.TestCaseWithTransport):
+class TestSmartServerBranchRequestUnlock(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_unlock_on_locked_branch_and_repo(self):
@@ -474,7 +537,7 @@
         branch.leave_lock_in_place()
         branch.repository.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''),
+        response = request.execute('',
                                    branch_token, repo_token)
         self.assertEqual(
             SmartServerResponse(('ok',)), response)
@@ -489,7 +552,7 @@
         request = smart.branch.SmartServerBranchRequestUnlock(backing)
         branch = self.make_branch('.', format='knit')
         response = request.execute(
-            backing.local_abspath(''), 'branch token', 'repo token')
+            '', 'branch token', 'repo token')
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
 
@@ -504,12 +567,12 @@
         # Issue branch lock_write request on the unlocked branch (with locked
         # repo).
         response = request.execute(
-            backing.local_abspath(''), 'branch token', repo_token)
+            '', 'branch token', repo_token)
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
 
 
-class TestSmartServerRepositoryRequest(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryRequest(tests.TestCaseWithMemoryTransport):
 
     def test_no_repository(self):
         """Raise NoRepositoryPresent when there is a bzrdir and no repo."""
@@ -522,10 +585,10 @@
         self.make_repository('.', shared=True)
         self.make_bzrdir('subdir')
         self.assertRaises(errors.NoRepositoryPresent,
-            request.execute, backing.local_abspath('subdir'))
-
-
-class TestSmartServerRepositoryGetRevisionGraph(tests.TestCaseWithTransport):
+            request.execute, 'subdir')
+
+
+class TestSmartServerRepositoryGetRevisionGraph(tests.TestCaseWithMemoryTransport):
 
     def test_none_argument(self):
         backing = self.get_transport()
@@ -540,7 +603,7 @@
         # the lines of revision_id->revision_parent_list has no guaranteed
         # order coming out of a dict, so sort both our test and response
         lines = sorted([' '.join([r2, r1]), r1])
-        response = request.execute(backing.local_abspath(''), '')
+        response = request.execute('', '')
         response.body = '\n'.join(sorted(response.body.split('\n')))
 
         self.assertEqual(
@@ -558,7 +621,7 @@
         tree.unlock()
 
         self.assertEqual(SmartServerResponse(('ok', ), rev_id_utf8),
-            request.execute(backing.local_abspath(''), rev_id_utf8))
+            request.execute('', rev_id_utf8))
     
     def test_no_such_revision(self):
         backing = self.get_transport()
@@ -572,10 +635,10 @@
         # Note that it still returns body (of zero bytes).
         self.assertEqual(
             SmartServerResponse(('nosuchrevision', 'missingrevision', ), ''),
-            request.execute(backing.local_abspath(''), 'missingrevision'))
-
-
-class TestSmartServerRequestHasRevision(tests.TestCaseWithTransport):
+            request.execute('', 'missingrevision'))
+
+
+class TestSmartServerRequestHasRevision(tests.TestCaseWithMemoryTransport):
 
     def test_missing_revision(self):
         """For a missing revision, ('no', ) is returned."""
@@ -583,7 +646,7 @@
         request = smart.repository.SmartServerRequestHasRevision(backing)
         self.make_repository('.')
         self.assertEqual(SmartServerResponse(('no', )),
-            request.execute(backing.local_abspath(''), 'revid'))
+            request.execute('', 'revid'))
 
     def test_present_revision(self):
         """For a present revision, ('yes', ) is returned."""
@@ -597,10 +660,10 @@
         tree.unlock()
         self.assertTrue(tree.branch.repository.has_revision(rev_id_utf8))
         self.assertEqual(SmartServerResponse(('yes', )),
-            request.execute(backing.local_abspath(''), rev_id_utf8))
-
-
-class TestSmartServerRepositoryGatherStats(tests.TestCaseWithTransport):
+            request.execute('', rev_id_utf8))
+
+
+class TestSmartServerRepositoryGatherStats(tests.TestCaseWithMemoryTransport):
 
     def test_empty_revid(self):
         """With an empty revid, we get only size an number and revisions"""
@@ -611,7 +674,7 @@
         size = stats['size']
         expected_body = 'revisions: 0\nsize: %d\n' % size
         self.assertEqual(SmartServerResponse(('ok', ), expected_body),
-                         request.execute(backing.local_abspath(''), '', 'no'))
+                         request.execute('', '', 'no'))
 
     def test_revid_with_committers(self):
         """For a revid we get more infos."""
@@ -634,7 +697,7 @@
                          'revisions: 2\n'
                          'size: %d\n' % size)
         self.assertEqual(SmartServerResponse(('ok', ), expected_body),
-                         request.execute(backing.local_abspath(''),
+                         request.execute('',
                                          rev_id_utf8, 'no'))
 
     def test_not_empty_repository_with_committers(self):
@@ -660,11 +723,11 @@
                          'revisions: 2\n'
                          'size: %d\n' % size)
         self.assertEqual(SmartServerResponse(('ok', ), expected_body),
-                         request.execute(backing.local_abspath(''),
+                         request.execute('',
                                          rev_id_utf8, 'yes'))
 
 
-class TestSmartServerRepositoryIsShared(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryIsShared(tests.TestCaseWithMemoryTransport):
 
     def test_is_shared(self):
         """For a shared repository, ('yes', ) is returned."""
@@ -672,7 +735,7 @@
         request = smart.repository.SmartServerRepositoryIsShared(backing)
         self.make_repository('.', shared=True)
         self.assertEqual(SmartServerResponse(('yes', )),
-            request.execute(backing.local_abspath(''), ))
+            request.execute('', ))
 
     def test_is_not_shared(self):
         """For a shared repository, ('no', ) is returned."""
@@ -680,20 +743,20 @@
         request = smart.repository.SmartServerRepositoryIsShared(backing)
         self.make_repository('.', shared=False)
         self.assertEqual(SmartServerResponse(('no', )),
-            request.execute(backing.local_abspath(''), ))
-
-
-class TestSmartServerRepositoryLockWrite(tests.TestCaseWithTransport):
+            request.execute('', ))
+
+
+class TestSmartServerRepositoryLockWrite(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_lock_write_on_unlocked_repo(self):
         backing = self.get_transport()
         request = smart.repository.SmartServerRepositoryLockWrite(backing)
         repository = self.make_repository('.', format='knit')
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         nonce = repository.control_files._lock.peek().get('nonce')
         self.assertEqual(SmartServerResponse(('ok', nonce)), response)
         # The repository is now locked.  Verify that with a new repository
@@ -708,7 +771,7 @@
         repository.lock_write()
         repository.leave_lock_in_place()
         repository.unlock()
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         self.assertEqual(
             SmartServerResponse(('LockContention',)), response)
 
@@ -721,10 +784,10 @@
         self.assertEqual('LockFailed', response.args[0])
 
 
-class TestSmartServerRepositoryUnlock(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryUnlock(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_unlock_on_locked_repo(self):
@@ -734,7 +797,7 @@
         token = repository.lock_write()
         repository.leave_lock_in_place()
         repository.unlock()
-        response = request.execute(backing.local_abspath(''), token)
+        response = request.execute('', token)
         self.assertEqual(
             SmartServerResponse(('ok',)), response)
         # The repository is now unlocked.  Verify that with a new repository
@@ -747,7 +810,7 @@
         backing = self.get_transport()
         request = smart.repository.SmartServerRepositoryUnlock(backing)
         repository = self.make_repository('.', format='knit')
-        response = request.execute(backing.local_abspath(''), 'some token')
+        response = request.execute('', 'some token')
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
 
@@ -761,7 +824,7 @@
         # make some extraneous junk in the repository directory which should
         # not be copied
         self.build_tree(['.bzr/repository/extra-junk'])
-        response = request.execute(backing.local_abspath(''), 'bz2')
+        response = request.execute('', 'bz2')
         self.assertEqual(('ok',), response.args)
         # body should be a tbz2
         body_file = StringIO(response.body)
@@ -776,7 +839,7 @@
             "extraneous file present in tar file")
 
 
-class TestSmartServerRepositoryStreamKnitData(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryStreamKnitData(tests.TestCaseWithMemoryTransport):
 
     def test_fetch_revisions(self):
         backing = self.get_transport()
@@ -790,7 +853,7 @@
         r1 = tree.commit('2nd commit', rev_id=rev_id2_utf8)
         tree.unlock()
 
-        response = request.execute(backing.local_abspath(''), rev_id2_utf8)
+        response = request.execute('', rev_id2_utf8)
         self.assertEqual(('ok',), response.args)
         from cStringIO import StringIO
         unpacker = pack.ContainerReader(StringIO(response.body))
@@ -808,13 +871,13 @@
         request = smart.repository.SmartServerRepositoryStreamKnitDataForRevisions(backing)
         repo = self.make_repository('.')
         rev_id1_utf8 = u'\xc8'.encode('utf-8')
-        response = request.execute(backing.local_abspath(''), rev_id1_utf8)
+        response = request.execute('', rev_id1_utf8)
         self.assertEqual(
             SmartServerResponse(('NoSuchRevision', rev_id1_utf8)),
             response)
 
 
-class TestSmartServerIsReadonly(tests.TestCaseWithTransport):
+class TestSmartServerIsReadonly(tests.TestCaseWithMemoryTransport):
 
     def test_is_readonly_no(self):
         backing = self.get_transport()

=== modified file 'bzrlib/tests/test_smart_transport.py'
--- bzrlib/tests/test_smart_transport.py	2007-11-09 20:49:54 +0000
+++ bzrlib/tests/test_smart_transport.py	2007-12-05 06:49:15 +0000
@@ -34,7 +34,6 @@
         client,
         medium,
         protocol,
-        request,
         request as _mod_request,
         server,
         vfs,
@@ -1113,7 +1112,7 @@
     """
         
     def test_hello(self):
-        cmd = request.HelloRequest(None)
+        cmd = _mod_request.HelloRequest(None, '/')
         response = cmd.execute()
         self.assertEqual(('ok', '2'), response.args)
         self.assertEqual(None, response.body)
@@ -1125,7 +1124,7 @@
         wt.add('hello')
         rev_id = wt.commit('add hello')
         
-        cmd = request.GetBundleRequest(self.get_transport())
+        cmd = _mod_request.GetBundleRequest(self.get_transport(), '/')
         response = cmd.execute('.', rev_id)
         bundle = serializer.read_bundle(StringIO(response.body))
         self.assertEqual((), response.args)
@@ -1140,12 +1139,13 @@
 
     def build_handler(self, transport):
         """Returns a handler for the commands in protocol version one."""
-        return request.SmartServerRequestHandler(transport,
-                                                 request.request_handlers)
+        return _mod_request.SmartServerRequestHandler(
+            transport, _mod_request.request_handlers, '/')
 
     def test_construct_request_handler(self):
         """Constructing a request handler should be easy and set defaults."""
-        handler = request.SmartServerRequestHandler(None, None)
+        handler = _mod_request.SmartServerRequestHandler(None, commands=None,
+                root_client_path='/')
         self.assertFalse(handler.finished_reading)
 
     def test_hello(self):
@@ -1157,7 +1157,7 @@
     def test_disable_vfs_handler_classes_via_environment(self):
         # VFS handler classes will raise an error from "execute" if
         # BZR_NO_SMART_VFS is set.
-        handler = vfs.HasRequest(None)
+        handler = vfs.HasRequest(None, '/')
         # set environment variable after construction to make sure it's
         # examined.
         # Note that we can safely clobber BZR_NO_SMART_VFS here, because setUp
@@ -1222,7 +1222,7 @@
         handler.accept_body('100,1')
         handler.end_of_body()
         self.assertTrue(handler.finished_reading)
-        self.assertEqual(('ShortReadvError', 'a-file', '100', '1', '0'),
+        self.assertEqual(('ShortReadvError', './a-file', '100', '1', '0'),
             handler.response.args)
         self.assertEqual(None, handler.response.body)
 
@@ -1313,8 +1313,8 @@
             self.to_server)
         self.client_protocol = self.client_protocol_class(self.client_medium)
         self.smart_server = InstrumentedServerProtocol(self.server_to_client)
-        self.smart_server_request = request.SmartServerRequestHandler(
-            None, request.request_handlers)
+        self.smart_server_request = _mod_request.SmartServerRequestHandler(
+            None, _mod_request.request_handlers, root_client_path='/')
 
     def assertOffsetSerialisation(self, expected_offsets, expected_serialised,
         client):
@@ -1329,7 +1329,7 @@
         """
         # XXX: '_deserialise_offsets' should be a method of the
         # SmartServerRequestProtocol in future.
-        readv_cmd = vfs.ReadvRequest(None)
+        readv_cmd = vfs.ReadvRequest(None, '/')
         offsets = readv_cmd._deserialise_offsets(expected_serialised)
         self.assertEqual(expected_offsets, offsets)
         serialised = client._serialise_offsets(offsets)
@@ -1344,7 +1344,7 @@
             def do_body(cmd, body_bytes):
                 self.end_received = True
                 self.assertEqual('abcdefg', body_bytes)
-                return request.SuccessfulSmartServerResponse(('ok', ))
+                return _mod_request.SuccessfulSmartServerResponse(('ok', ))
         smart_protocol.request._command = FakeCommand()
         # Call accept_bytes to make sure that internal state like _body_decoder
         # is initialised.  This test should probably be given a clearer
@@ -1509,7 +1509,7 @@
             None, lambda x: None)
         self.assertEqual(1, smart_protocol.next_read_size())
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',)))
+            _mod_request.SuccessfulSmartServerResponse(('x',)))
         self.assertEqual(0, smart_protocol.next_read_size())
 
     def test__send_response_errors_with_base_response(self):
@@ -1517,7 +1517,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolOne(
             None, lambda x: None)
         self.assertRaises(AttributeError, smart_protocol._send_response,
-            request.SmartServerResponse(('x',)))
+            _mod_request.SmartServerResponse(('x',)))
 
     def test_query_version(self):
         """query_version on a SmartClientProtocolOne should return a number.
@@ -1704,7 +1704,7 @@
 
     def test_body_stream_error_serialistion(self):
         stream = ['first chunk',
-                  request.FailedSmartServerResponse(
+                  _mod_request.FailedSmartServerResponse(
                       ('FailureName', 'failure arg'))]
         expected_bytes = (
             'chunked\n' + 'b\nfirst chunk' +
@@ -1796,7 +1796,7 @@
             None, lambda x: None)
         self.assertEqual(1, smart_protocol.next_read_size())
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',)))
+            _mod_request.SuccessfulSmartServerResponse(('x',)))
         self.assertEqual(0, smart_protocol.next_read_size())
 
     def test__send_response_with_body_stream_sets_finished_reading(self):
@@ -1804,7 +1804,7 @@
             None, lambda x: None)
         self.assertEqual(1, smart_protocol.next_read_size())
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',), body_stream=[]))
+            _mod_request.SuccessfulSmartServerResponse(('x',), body_stream=[]))
         self.assertEqual(0, smart_protocol.next_read_size())
 
     def test__send_response_errors_with_base_response(self):
@@ -1812,7 +1812,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolTwo(
             None, lambda x: None)
         self.assertRaises(AttributeError, smart_protocol._send_response,
-            request.SmartServerResponse(('x',)))
+            _mod_request.SmartServerResponse(('x',)))
 
     def test__send_response_includes_failure_marker(self):
         """FailedSmartServerResponse have 'failed\n' after the version."""
@@ -1820,7 +1820,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolTwo(
             None, out_stream.write)
         smart_protocol._send_response(
-            request.FailedSmartServerResponse(('x',)))
+            _mod_request.FailedSmartServerResponse(('x',)))
         self.assertEqual(protocol.RESPONSE_VERSION_TWO + 'failed\nx\n',
                          out_stream.getvalue())
 
@@ -1830,7 +1830,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolTwo(
             None, out_stream.write)
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',)))
+            _mod_request.SuccessfulSmartServerResponse(('x',)))
         self.assertEqual(protocol.RESPONSE_VERSION_TWO + 'success\nx\n',
                          out_stream.getvalue())
 
@@ -1991,7 +1991,7 @@
         smart_protocol.read_response_tuple(True)
         expected_chunks = [
             'aaaa',
-            request.FailedSmartServerResponse(('error arg1', 'arg2'))]
+            _mod_request.FailedSmartServerResponse(('error arg1', 'arg2'))]
         stream = smart_protocol.read_streamed_body()
         self.assertEqual(expected_chunks, list(stream))
 
@@ -2252,7 +2252,7 @@
         decoder.accept_bytes(chunk_one + error_signal + error_chunks + finish)
         self.assertTrue(decoder.finished_reading)
         self.assertEqual('first chunk', decoder.read_next_chunk())
-        expected_failure = request.FailedSmartServerResponse(
+        expected_failure = _mod_request.FailedSmartServerResponse(
             ('part1', 'part2'))
         self.assertEqual(expected_failure, decoder.read_next_chunk())
 
@@ -2268,19 +2268,19 @@
 class TestSuccessfulSmartServerResponse(tests.TestCase):
 
     def test_construct_no_body(self):
-        response = request.SuccessfulSmartServerResponse(('foo', 'bar'))
+        response = _mod_request.SuccessfulSmartServerResponse(('foo', 'bar'))
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual(None, response.body)
 
     def test_construct_with_body(self):
-        response = request.SuccessfulSmartServerResponse(
-            ('foo', 'bar'), 'bytes')
+        response = _mod_request.SuccessfulSmartServerResponse(('foo', 'bar'),
+                                                              'bytes')
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual('bytes', response.body)
 
     def test_construct_with_body_stream(self):
         bytes_iterable = ['abc']
-        response = request.SuccessfulSmartServerResponse(
+        response = _mod_request.SuccessfulSmartServerResponse(
             ('foo', 'bar'), body_stream=bytes_iterable)
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual(bytes_iterable, response.body_stream)
@@ -2289,27 +2289,27 @@
         """'body' and 'body_stream' are mutually exclusive."""
         self.assertRaises(
             errors.BzrError,
-            request.SuccessfulSmartServerResponse, (), 'body', ['stream'])
+            _mod_request.SuccessfulSmartServerResponse, (), 'body', ['stream'])
 
     def test_is_successful(self):
         """is_successful should return True for SuccessfulSmartServerResponse."""
-        response = request.SuccessfulSmartServerResponse(('error',))
+        response = _mod_request.SuccessfulSmartServerResponse(('error',))
         self.assertEqual(True, response.is_successful())
 
 
 class TestFailedSmartServerResponse(tests.TestCase):
 
     def test_construct(self):
-        response = request.FailedSmartServerResponse(('foo', 'bar'))
+        response = _mod_request.FailedSmartServerResponse(('foo', 'bar'))
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual(None, response.body)
-        response = request.FailedSmartServerResponse(('foo', 'bar'), 'bytes')
+        response = _mod_request.FailedSmartServerResponse(('foo', 'bar'), 'bytes')
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual('bytes', response.body)
 
     def test_is_successful(self):
         """is_successful should return False for FailedSmartServerResponse."""
-        response = request.FailedSmartServerResponse(('error',))
+        response = _mod_request.FailedSmartServerResponse(('error',))
         self.assertEqual(False, response.is_successful())
 
 

=== modified file 'bzrlib/tests/test_transport_implementations.py'
--- bzrlib/tests/test_transport_implementations.py	2007-11-25 14:42:29 +0000
+++ bzrlib/tests/test_transport_implementations.py	2007-12-05 23:30:57 +0000
@@ -169,6 +169,11 @@
         self.assertEqual(True, t.has_any(['b', 'b', 'b']))
 
     def test_has_root_works(self):
+        from bzrlib.smart import server
+        if self.transport_server is server.SmartTCPServer_for_testing:
+            raise TestNotApplicable(
+                "SmartTCPServer_for_testing intentionally does not allow "
+                "access to /.")
         current_transport = self.get_transport()
         self.assertTrue(current_transport.has('/'))
         root = current_transport.clone('/')

=== modified file 'bzrlib/tests/test_wsgi.py'
--- bzrlib/tests/test_wsgi.py	2007-04-26 06:01:31 +0000
+++ bzrlib/tests/test_wsgi.py	2007-12-13 22:22:58 +0000
@@ -83,7 +83,7 @@
         self.assertEqual('405 Method not allowed', self.status)
         self.assertTrue(('Allow', 'POST') in self.headers)
         
-    def _fake_make_request(self, transport, write_func, bytes):
+    def _fake_make_request(self, transport, write_func, bytes, rcp):
         request = FakeRequest(transport, write_func)
         request.accept_bytes(bytes)
         self.request = request
@@ -106,7 +106,7 @@
         })
         iterable = wsgi_app(environ, self.start_response)
         response = self.read_response(iterable)
-        self.assertEqual([('clone', 'foo/bar')] , transport.calls)
+        self.assertEqual([('clone', 'foo/bar/')] , transport.calls)
 
     def test_smart_wsgi_app_request_and_response(self):
         # SmartWSGIApp reads the smart request from the 'wsgi.input' file-like
@@ -179,13 +179,13 @@
         backing_transport = app.app.backing_transport
         chroot_backing_transport = backing_transport.server.backing_transport
         self.assertEndsWith(chroot_backing_transport.base, 'a%20root/')
-        self.assertEqual(app.prefix, 'a prefix')
+        self.assertEqual(app.app.root_client_path, 'a prefix')
         self.assertEqual(app.path_var, 'a path_var')
 
     def test_incomplete_request(self):
         transport = FakeTransport()
         wsgi_app = wsgi.SmartWSGIApp(transport)
-        def make_request(transport, write_func, bytes):
+        def make_request(transport, write_func, bytes, root_client_path):
             request = IncompleteRequest(transport, write_func)
             request.accept_bytes(bytes)
             self.request = request

=== modified file 'bzrlib/transport/chroot.py'
--- bzrlib/transport/chroot.py	2007-08-22 08:09:05 +0000
+++ bzrlib/transport/chroot.py	2007-12-05 01:58:00 +0000
@@ -116,6 +116,9 @@
     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('.'))

=== modified file 'bzrlib/transport/http/wsgi.py'
--- bzrlib/transport/http/wsgi.py	2007-08-15 19:16:00 +0000
+++ bzrlib/transport/http/wsgi.py	2007-12-14 01:43:48 +0000
@@ -39,8 +39,8 @@
         base_transport = get_transport('readonly+' + local_url)
     else:
         base_transport = get_transport(local_url)
-    app = SmartWSGIApp(base_transport)
-    app = RelpathSetter(app, prefix, path_var)
+    app = SmartWSGIApp(base_transport, prefix)
+    app = RelpathSetter(app, '', path_var)
     return app
 
 
@@ -85,11 +85,14 @@
 class SmartWSGIApp(object):
     """A WSGI application for the bzr smart server."""
 
-    def __init__(self, backing_transport):
+    def __init__(self, backing_transport, root_client_path='/'):
         """Constructor.
 
         :param backing_transport: a transport.  Requests will be processed
             relative to this transport.
+        :param root_client_path: the client path that maps to the root of
+            backing_transport.  This is used to interpret relpaths received from
+            the client.
         """
         # Use a ChrootTransportDecorator so that this web application won't
         # accidentally let people access locations they shouldn't.
@@ -98,6 +101,7 @@
         self.chroot_server = chroot.ChrootServer(backing_transport)
         self.chroot_server.setUp()
         self.backing_transport = get_transport(self.chroot_server.get_url())
+        self.root_client_path = root_client_path
         # While the chroot server can technically be torn down at this point,
         # as all it does is remove the scheme registration from transport's 
         # protocol dictionary, we don't *just in case* there are parts of 
@@ -112,12 +116,42 @@
             return []
 
         relpath = environ['bzrlib.relpath']
-        transport = self.backing_transport.clone(relpath)
+
+        if not relpath.startswith('/'):
+            relpath = '/' + relpath
+        if not relpath.endswith('/'):
+            relpath += '/'
+
+        # Compare the HTTP path (relpath) and root_client_path, and calculate
+        # new relpath and root_client_path accordingly, to be used to build the
+        # request.
+        if relpath.startswith(self.root_client_path):
+            # The relpath traverses all of the mandatory root client path.
+            # Remove the root_client_path from the relpath, and set
+            # adjusted_tcp to None to tell the request handler that no further
+            # path translation is required.
+            adjusted_rcp = None
+            adjusted_relpath = relpath[len(self.root_client_path):]
+        elif self.root_client_path.startswith(relpath):
+            # The relpath traverses some of the mandatory root client path.
+            # Subtract the relpath from the root_client_path, and set the
+            # relpath to '.'.
+            adjusted_rcp = '/' + self.root_client_path[len(relpath):]
+            adjusted_relpath = '.'
+        else:
+            adjusted_rcp = self.root_client_path
+            adjusted_relpath = relpath
+
+        if adjusted_relpath.startswith('/'):
+            adjusted_relpath = adjusted_relpath[1:]
+        assert not adjusted_relpath.startswith('/')
+
+        transport = self.backing_transport.clone(adjusted_relpath)
         out_buffer = StringIO()
         request_data_length = int(environ['CONTENT_LENGTH'])
         request_data_bytes = environ['wsgi.input'].read(request_data_length)
         smart_protocol_request = self.make_request(
-            transport, out_buffer.write, request_data_bytes)
+            transport, out_buffer.write, request_data_bytes, adjusted_rcp)
         if smart_protocol_request.next_read_size() != 0:
             # The request appears to be incomplete, or perhaps it's just a
             # newer version we don't understand.  Regardless, all we can do
@@ -131,7 +165,7 @@
         start_response('200 OK', headers)
         return [response_data]
 
-    def make_request(self, transport, write_func, request_bytes):
+    def make_request(self, transport, write_func, request_bytes, rcp):
         # XXX: This duplicates the logic in
         # SmartServerStreamMedium._build_protocol.
         if request_bytes.startswith(protocol.REQUEST_VERSION_TWO):
@@ -139,6 +173,7 @@
             request_bytes = request_bytes[len(protocol.REQUEST_VERSION_TWO):]
         else:
             protocol_class = protocol.SmartServerRequestProtocolOne
-        server_protocol = protocol_class(transport, write_func)
+        server_protocol = protocol_class(
+            transport, write_func, rcp)
         server_protocol.accept_bytes(request_bytes)
         return server_protocol

=== modified file 'doc/en/user-guide/http_smart_server.txt'
--- doc/en/user-guide/http_smart_server.txt	2007-11-14 03:50:56 +0000
+++ doc/en/user-guide/http_smart_server.txt	2007-12-13 22:22:58 +0000
@@ -1,9 +1,6 @@
 Serving Bazaar with FastCGI
 ===========================
 
-**This feature is EXPERIMENTAL and is NOT SECURE.  It will allow access to
-arbitrary files on your server.**
-
 This document describes one way to set up a Bazaar HTTP smart server,
 using Apache 2.0 and FastCGI or mod_python.
 

# Begin bundle
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
9gmRtn4JUQ+tCsQCPcQtej/Iu5IpwoSFce1z8A==


More information about the bazaar mailing list