[MERGE] Calculate remote path relative to the shared medium in _SmartClient

Andrew Bennetts andrew at canonical.com
Fri Dec 14 01:19:37 GMT 2007


This change fixes the paths sent by the client to bzr+http:// URLs.  Basically,
if the RemoteHTTPTransport's base is bzr+http://host/path1/path2, and the medium
it is using is “connected” to bzr+http://host/path1/, then smart requests should
use “path2” as their relpath, not “path1/path2” or anything else like that.

The behaviour for other RemoteTransports (which don't share bzr+http's problem
of dealing with paths in both the HTTP URL and the HPSS request at the same
time) is unchanged, except for tidying some of the ugly “///” mess from the
tests.

I've also removed some of the special-casing in RemoteHTTPTransport.clone, so
that it just unconditionally POSTs requests to the original URL from the user.
This might be a little controversial (I remember there was a thread about this
some time ago), but I don't think we really need to be any cleverer than that.
If people feel this is important it can be reinstated, but having it there and
working correctly is a little bit awkward with the current way it uses the
ConnectedTransport layer, so I took the easy option and just ripped it out.

I also renamed the “_shared_medium” parameter of _SmartClient to
“_shared_connection”, which is actually what it already was.

With this, and with the updated bug 124089 bundle I'm about to send, I can
successfully do e.g. “bzr log -r -1 bzr+http://localhost/code/foo” against a
local Apache configured along the lines described in the documentation.  (Here
bzr+http://localhost/code is the root of the bzr smart server, and a shared
repository.)

-Andrew.

-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: andrew.bennetts at canonical.com-20071214010545-\
#   8eowyez0zh32uu4s
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev
# testament_sha1: d79057767fbc3489830fa7d8ef503e5dccbdba38
# timestamp: 2007-12-14 12:10:11 +1100
# source_branch: http://people.ubuntu.com/~andrew/bzr/bzr-http-client
# base_revision_id: pqm at pqm.ubuntu.com-20071211175118-s94sizduj201hrs5
# 
# Begin patch
=== modified file 'bzrlib/smart/client.py'
--- bzrlib/smart/client.py	2007-08-06 05:11:55 +0000
+++ bzrlib/smart/client.py	2007-12-14 01:05:45 +0000
@@ -17,16 +17,20 @@
 from urlparse import urlparse
 
 from bzrlib.smart import protocol
-from bzrlib.urlutils import unescape
+from bzrlib import urlutils
 
 
 class _SmartClient(object):
 
-    def __init__(self, shared_medium):
-        self._shared_medium = shared_medium
+    def __init__(self, shared_connection):
+        """Constructor.
+
+        :param shared_connection: a bzrlib.transport._SharedConnection
+        """
+        self._shared_connection = shared_connection
 
     def get_smart_medium(self):
-        return self._shared_medium.connection
+        return self._shared_connection.connection
 
     def call(self, method, *args):
         """Call a method on the remote server."""
@@ -68,4 +72,9 @@
         anything but path, so it is only safe to use it in requests sent over
         the medium from the matching transport.
         """
-        return unescape(urlparse(transport.base)[2]).encode('utf8')
+        if self._shared_connection.base.startswith('bzr+http://'):
+            medium_base = self._shared_connection.base
+        else:
+            medium_base = urlutils.join(self._shared_connection.base, '/')
+            
+        return urlutils.relative_url(medium_base, transport.base).encode('utf8')

=== modified file 'bzrlib/smart/protocol.py'
--- bzrlib/smart/protocol.py	2007-11-09 20:49:54 +0000
+++ bzrlib/smart/protocol.py	2007-12-13 06:29:54 +0000
@@ -471,6 +471,8 @@
     def call(self, *args):
         if 'hpss' in debug.debug_flags:
             mutter('hpss call:   %s', repr(args)[1:-1])
+            if getattr(self._request._medium, 'base', None) is not None:
+                mutter('             (to %s)', self._request._medium.base)
             self._request_start_time = time.time()
         self._write_args(args)
         self._request.finished_writing()
@@ -482,6 +484,8 @@
         """
         if 'hpss' in debug.debug_flags:
             mutter('hpss call w/body: %s (%r...)', repr(args)[1:-1], body[:20])
+            if getattr(self._request._medium, '_path', None) is not None:
+                mutter('                  (to %s)', self._request._medium._path)
             mutter('              %d bytes', len(body))
             self._request_start_time = time.time()
         self._write_args(args)
@@ -497,6 +501,8 @@
         """
         if 'hpss' in debug.debug_flags:
             mutter('hpss call w/readv: %s', repr(args)[1:-1])
+            if getattr(self._request._medium, '_path', None) is not None:
+                mutter('                  (to %s)', self._request._medium._path)
             self._request_start_time = time.time()
         self._write_args(args)
         readv_bytes = self._serialise_offsets(body)

=== modified file 'bzrlib/tests/test_remote.py'
--- bzrlib/tests/test_remote.py	2007-10-12 06:24:42 +0000
+++ bzrlib/tests/test_remote.py	2007-12-13 06:24:49 +0000
@@ -124,16 +124,16 @@
 class FakeClient(_SmartClient):
     """Lookalike for _SmartClient allowing testing."""
     
-    def __init__(self, responses):
-        # We don't call the super init because there is no medium.
+    def __init__(self, responses, fake_medium_base='fake base'):
         """Create a FakeClient.
 
-        :param respones: A list of response-tuple, body-data pairs to be sent
+        :param responses: A list of response-tuple, body-data pairs to be sent
             back to callers.
         """
         self.responses = responses
         self._calls = []
         self.expecting_body = False
+        _SmartClient.__init__(self, FakeMedium(fake_medium_base))
 
     def call(self, method, *args):
         self._calls.append(('call', method, args))
@@ -146,34 +146,44 @@
         return result[0], FakeProtocol(result[1], self)
 
 
+class FakeMedium(object):
+
+    def __init__(self, base):
+        self.base = base
+
+
 class TestBzrDirOpenBranch(tests.TestCase):
 
     def test_branch_present(self):
-        client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no'), )])
         transport = MemoryTransport()
         transport.mkdir('quack')
         transport = transport.clone('quack')
+        client = FakeClient([(('ok', ''), ), (('ok', '', 'no', 'no'), )],
+                            transport.base)
         bzrdir = RemoteBzrDir(transport, _client=client)
         result = bzrdir.open_branch()
         self.assertEqual(
-            [('call', 'BzrDir.open_branch', ('///quack/',)),
-             ('call', 'BzrDir.find_repository', ('///quack/',))],
+            [('call', 'BzrDir.open_branch', ('quack/',)),
+             ('call', 'BzrDir.find_repository', ('quack/',))],
             client._calls)
         self.assertIsInstance(result, RemoteBranch)
         self.assertEqual(bzrdir, result.bzrdir)
 
     def test_branch_missing(self):
-        client = FakeClient([(('nobranch',), )])
         transport = MemoryTransport()
         transport.mkdir('quack')
         transport = transport.clone('quack')
+        client = FakeClient([(('nobranch',), )], transport.base)
         bzrdir = RemoteBzrDir(transport, _client=client)
         self.assertRaises(errors.NotBranchError, bzrdir.open_branch)
         self.assertEqual(
-            [('call', 'BzrDir.open_branch', ('///quack/',))],
+            [('call', 'BzrDir.open_branch', ('quack/',))],
             client._calls)
 
     def check_open_repository(self, rich_root, subtrees):
+        transport = MemoryTransport()
+        transport.mkdir('quack')
+        transport = transport.clone('quack')
         if rich_root:
             rich_response = 'yes'
         else:
@@ -182,14 +192,12 @@
             subtree_response = 'yes'
         else:
             subtree_response = 'no'
-        client = FakeClient([(('ok', '', rich_response, subtree_response), ),])
-        transport = MemoryTransport()
-        transport.mkdir('quack')
-        transport = transport.clone('quack')
+        client = FakeClient([(('ok', '', rich_response, subtree_response), ),],
+                            transport.base)
         bzrdir = RemoteBzrDir(transport, _client=client)
         result = bzrdir.open_repository()
         self.assertEqual(
-            [('call', 'BzrDir.find_repository', ('///quack/',))],
+            [('call', 'BzrDir.find_repository', ('quack/',))],
             client._calls)
         self.assertIsInstance(result, RemoteRepository)
         self.assertEqual(bzrdir, result.bzrdir)
@@ -239,8 +247,8 @@
 
     def test_empty_branch(self):
         # in an empty branch we decode the response properly
-        client = FakeClient([(('ok', '0', 'null:'), )])
         transport = MemoryTransport()
+        client = FakeClient([(('ok', '0', 'null:'), )], transport.base)
         transport.mkdir('quack')
         transport = transport.clone('quack')
         # we do not want bzrdir to make any remote calls
@@ -249,15 +257,15 @@
         result = branch.last_revision_info()
 
         self.assertEqual(
-            [('call', 'Branch.last_revision_info', ('///quack/',))],
+            [('call', 'Branch.last_revision_info', ('quack/',))],
             client._calls)
         self.assertEqual((0, NULL_REVISION), result)
 
     def test_non_empty_branch(self):
         # in a non-empty branch we also decode the response properly
         revid = u'\xc8'.encode('utf8')
-        client = FakeClient([(('ok', '2', revid), )])
         transport = MemoryTransport()
+        client = FakeClient([(('ok', '2', revid), )], transport.base)
         transport.mkdir('kwaak')
         transport = transport.clone('kwaak')
         # we do not want bzrdir to make any remote calls
@@ -266,7 +274,7 @@
         result = branch.last_revision_info()
 
         self.assertEqual(
-            [('call', 'Branch.last_revision_info', ('///kwaak/',))],
+            [('call', 'Branch.last_revision_info', ('kwaak/',))],
             client._calls)
         self.assertEqual((2, revid), result)
 
@@ -276,17 +284,18 @@
     def test_set_empty(self):
         # set_revision_history([]) is translated to calling
         # Branch.set_last_revision(path, '') on the wire.
+        transport = MemoryTransport()
+        transport.mkdir('branch')
+        transport = transport.clone('branch')
+
         client = FakeClient([
             # lock_write
             (('ok', 'branch token', 'repo token'), ),
             # set_last_revision
             (('ok',), ),
             # unlock
-            (('ok',), )])
-        transport = MemoryTransport()
-        transport.mkdir('branch')
-        transport = transport.clone('branch')
-
+            (('ok',), )],
+            transport.base)
         bzrdir = RemoteBzrDir(transport, _client=False)
         branch = RemoteBranch(bzrdir, None, _client=client)
         # This is a hack to work around the problem that RemoteBranch currently
@@ -297,7 +306,7 @@
         result = branch.set_revision_history([])
         self.assertEqual(
             [('call', 'Branch.set_last_revision',
-                ('///branch/', 'branch token', 'repo token', 'null:'))],
+                ('branch/', 'branch token', 'repo token', 'null:'))],
             client._calls)
         branch.unlock()
         self.assertEqual(None, result)
@@ -305,17 +314,18 @@
     def test_set_nonempty(self):
         # set_revision_history([rev-id1, ..., rev-idN]) is translated to calling
         # Branch.set_last_revision(path, rev-idN) on the wire.
+        transport = MemoryTransport()
+        transport.mkdir('branch')
+        transport = transport.clone('branch')
+
         client = FakeClient([
             # lock_write
             (('ok', 'branch token', 'repo token'), ),
             # set_last_revision
             (('ok',), ),
             # unlock
-            (('ok',), )])
-        transport = MemoryTransport()
-        transport.mkdir('branch')
-        transport = transport.clone('branch')
-
+            (('ok',), )],
+            transport.base)
         bzrdir = RemoteBzrDir(transport, _client=False)
         branch = RemoteBranch(bzrdir, None, _client=client)
         # This is a hack to work around the problem that RemoteBranch currently
@@ -328,7 +338,7 @@
         result = branch.set_revision_history(['rev-id1', 'rev-id2'])
         self.assertEqual(
             [('call', 'Branch.set_last_revision',
-                ('///branch/', 'branch token', 'repo token', 'rev-id2'))],
+                ('branch/', 'branch token', 'repo token', 'rev-id2'))],
             client._calls)
         branch.unlock()
         self.assertEqual(None, result)
@@ -368,7 +378,7 @@
 
     def test_get_branch_conf(self):
         # in an empty branch we decode the response properly
-        client = FakeClient([(('ok', ), 'config file body')])
+        client = FakeClient([(('ok', ), 'config file body')], self.get_url())
         # we need to make a real branch because the remote_branch.control_files
         # will trigger _ensure_real.
         branch = self.make_branch('quack')
@@ -378,7 +388,7 @@
         branch = RemoteBranch(bzrdir, None, _client=client)
         result = branch.control_files.get('branch.conf')
         self.assertEqual(
-            [('call_expecting_body', 'Branch.get_config_file', ('///quack/',))],
+            [('call_expecting_body', 'Branch.get_config_file', ('quack/',))],
             client._calls)
         self.assertEqual('config file body', result.read())
 
@@ -386,8 +396,8 @@
 class TestBranchLockWrite(tests.TestCase):
 
     def test_lock_write_unlockable(self):
-        client = FakeClient([(('UnlockableTransport', ), '')])
         transport = MemoryTransport()
+        client = FakeClient([(('UnlockableTransport', ), '')], transport.base)
         transport.mkdir('quack')
         transport = transport.clone('quack')
         # we do not want bzrdir to make any remote calls
@@ -395,7 +405,7 @@
         branch = RemoteBranch(bzrdir, None, _client=client)
         self.assertRaises(errors.UnlockableTransport, branch.lock_write)
         self.assertEqual(
-            [('call', 'Branch.lock_write', ('///quack/', '', ''))],
+            [('call', 'Branch.lock_write', ('quack/', '', ''))],
             client._calls)
 
 
@@ -468,9 +478,9 @@
         :param transport_path: Path below the root of the MemoryTransport
             where the repository will be created.
         """
-        client = FakeClient(responses)
         transport = MemoryTransport()
         transport.mkdir(transport_path)
+        client = FakeClient(responses, transport.base)
         transport = transport.clone(transport_path)
         # we do not want bzrdir to make any remote calls
         bzrdir = RemoteBzrDir(transport, _client=False)
@@ -489,7 +499,7 @@
         result = repo.gather_stats(None)
         self.assertEqual(
             [('call_expecting_body', 'Repository.gather_stats',
-             ('///quack/','','no'))],
+             ('quack/','','no'))],
             client._calls)
         self.assertEqual({'revisions': 2, 'size': 18}, result)
 
@@ -507,7 +517,7 @@
         result = repo.gather_stats(revid)
         self.assertEqual(
             [('call_expecting_body', 'Repository.gather_stats',
-              ('///quick/', revid, 'no'))],
+              ('quick/', revid, 'no'))],
             client._calls)
         self.assertEqual({'revisions': 2, 'size': 18,
                           'firstrev': (123456.300, 3600),
@@ -529,7 +539,7 @@
         result = repo.gather_stats(revid, True)
         self.assertEqual(
             [('call_expecting_body', 'Repository.gather_stats',
-              ('///buick/', revid, 'yes'))],
+              ('buick/', revid, 'yes'))],
             client._calls)
         self.assertEqual({'revisions': 2, 'size': 18,
                           'committers': 128,
@@ -565,7 +575,7 @@
         result = repo.get_revision_graph()
         self.assertEqual(
             [('call_expecting_body', 'Repository.get_revision_graph',
-             ('///sinhala/', ''))],
+             ('sinhala/', ''))],
             client._calls)
         self.assertEqual({r1: (), r2: (r1, )}, result)
 
@@ -585,7 +595,7 @@
         result = repo.get_revision_graph(r2)
         self.assertEqual(
             [('call_expecting_body', 'Repository.get_revision_graph',
-             ('///sinhala/', r2))],
+             ('sinhala/', r2))],
             client._calls)
         self.assertEqual({r11: (), r12: (), r2: (r11, r12), }, result)
 
@@ -600,7 +610,7 @@
             repo.get_revision_graph, revid)
         self.assertEqual(
             [('call_expecting_body', 'Repository.get_revision_graph',
-             ('///sinhala/', revid))],
+             ('sinhala/', revid))],
             client._calls)
 
         
@@ -614,7 +624,7 @@
             responses, transport_path)
         result = repo.is_shared()
         self.assertEqual(
-            [('call', 'Repository.is_shared', ('///quack/',))],
+            [('call', 'Repository.is_shared', ('quack/',))],
             client._calls)
         self.assertEqual(True, result)
 
@@ -626,7 +636,7 @@
             responses, transport_path)
         result = repo.is_shared()
         self.assertEqual(
-            [('call', 'Repository.is_shared', ('///qwack/',))],
+            [('call', 'Repository.is_shared', ('qwack/',))],
             client._calls)
         self.assertEqual(False, result)
 
@@ -640,7 +650,7 @@
             responses, transport_path)
         result = repo.lock_write()
         self.assertEqual(
-            [('call', 'Repository.lock_write', ('///quack/', ''))],
+            [('call', 'Repository.lock_write', ('quack/', ''))],
             client._calls)
         self.assertEqual('a token', result)
 
@@ -651,7 +661,7 @@
             responses, transport_path)
         self.assertRaises(errors.LockContention, repo.lock_write)
         self.assertEqual(
-            [('call', 'Repository.lock_write', ('///quack/', ''))],
+            [('call', 'Repository.lock_write', ('quack/', ''))],
             client._calls)
 
     def test_lock_write_unlockable(self):
@@ -661,7 +671,7 @@
             responses, transport_path)
         self.assertRaises(errors.UnlockableTransport, repo.lock_write)
         self.assertEqual(
-            [('call', 'Repository.lock_write', ('///quack/', ''))],
+            [('call', 'Repository.lock_write', ('quack/', ''))],
             client._calls)
 
 
@@ -676,8 +686,8 @@
         repo.lock_write()
         repo.unlock()
         self.assertEqual(
-            [('call', 'Repository.lock_write', ('///quack/', '')),
-             ('call', 'Repository.unlock', ('///quack/', 'a token'))],
+            [('call', 'Repository.lock_write', ('quack/', '')),
+             ('call', 'Repository.unlock', ('quack/', 'a token'))],
             client._calls)
 
     def test_unlock_wrong_token(self):
@@ -731,7 +741,7 @@
         expected_responses = [(('ok',), self.tarball_content),
             ]
         expected_calls = [('call_expecting_body', 'Repository.tarball',
-                           ('///repo/', 'bz2',),),
+                           ('repo/', 'bz2',),),
             ]
         remote_repo, client = self.setup_fake_client_and_repository(
             expected_responses, transport_path)

=== 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-13 06:24:49 +0000
@@ -2458,20 +2458,6 @@
                          new_transport._http_transport)
         self.assertEqual('child_dir/foo', new_transport._remote_path('foo'))
 
-    def test_remote_path_after_clone_parent(self):
-        # However, accessing a parent directory should go direct to the parent's
-        # URL.  We don't send relpaths like "../foo" in smart requests.
-        base_transport = remote.RemoteHTTPTransport('bzr+http://host/path1/path2')
-        new_transport = base_transport.clone('..')
-        self.assertEqual('foo', new_transport._remote_path('foo'))
-        new_transport = base_transport.clone('../')
-        self.assertEqual('foo', new_transport._remote_path('foo'))
-        new_transport = base_transport.clone('../abc')
-        self.assertEqual('foo', new_transport._remote_path('foo'))
-        # "abc/../.." should be equivalent to ".."
-        new_transport = base_transport.clone('abc/../..')
-        self.assertEqual('foo', new_transport._remote_path('foo'))
-
     def test_remote_path_unnormal_base(self):
         # If the transport's base isn't normalised, the _remote_path should
         # still be calculated correctly.

=== modified file 'bzrlib/transport/__init__.py'
--- bzrlib/transport/__init__.py	2007-12-08 23:15:18 +0000
+++ bzrlib/transport/__init__.py	2007-12-12 01:57:40 +0000
@@ -1262,7 +1262,7 @@
 class _SharedConnection(object):
     """A connection shared between several transports."""
 
-    def __init__(self, connection=None, credentials=None):
+    def __init__(self, connection=None, credentials=None, base=None):
         """Constructor.
 
         :param connection: An opaque object specific to each transport.
@@ -1272,6 +1272,7 @@
         """
         self.connection = connection
         self.credentials = credentials
+        self.base = base
 
 
 class ConnectedTransport(Transport):

=== modified file 'bzrlib/transport/remote.py'
--- bzrlib/transport/remote.py	2007-11-19 13:44:25 +0000
+++ bzrlib/transport/remote.py	2007-12-13 06:29:54 +0000
@@ -102,7 +102,8 @@
                     trace.mutter('hpss: Built a new medium: %s',
                                  medium.__class__.__name__)
             self._shared_connection = transport._SharedConnection(medium,
-                                                                  credentials)
+                                                                  credentials,
+                                                                  self.base)
 
         if _client is None:
             self._client = client._SmartClient(self.get_shared_medium())
@@ -496,7 +497,7 @@
         """After connecting, HTTP Transport only deals in relative URLs."""
         # Adjust the relpath based on which URL this smart transport is
         # connected to.
-        http_base = urlutils.normalize_url(self._http_transport.base)
+        http_base = urlutils.normalize_url(self.get_smart_medium().base)
         url = urlutils.join(self.base[len('bzr+'):], relpath)
         url = urlutils.normalize_url(url)
         return urlutils.relative_url(http_base, url)
@@ -513,26 +514,14 @@
         smart requests may be different).  This is so that the server doesn't
         have to handle .bzr/smart requests at arbitrary places inside .bzr
         directories, just at the initial URL the user uses.
-
-        The exception is parent paths (i.e. relative_url of "..").
         """
         if relative_url:
             abs_url = self.abspath(relative_url)
         else:
             abs_url = self.base
-        # We either use the exact same http_transport (for child locations), or
-        # a clone of the underlying http_transport (for parent locations).  This
-        # means we share the connection.
-        norm_base = urlutils.normalize_url(self.base)
-        norm_abs_url = urlutils.normalize_url(abs_url)
-        normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
-        if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
-            http_transport = self._http_transport.clone(normalized_rel_url)
-        else:
-            http_transport = self._http_transport
         return RemoteHTTPTransport(abs_url,
                                    _from_transport=self,
-                                   http_transport=http_transport)
+                                   http_transport=self._http_transport)
 
 
 def get_test_permutations():

# Begin bundle
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


More information about the bazaar mailing list