[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