~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Ian Clatworthy
  • Date: 2009-09-09 11:43:10 UTC
  • mto: (4634.37.2 prepare-2.0)
  • mto: This revision was merged to the branch mainline in revision 4689.
  • Revision ID: ian.clatworthy@canonical.com-20090909114310-glw7tv76i5gnx9pt
put rules back in Makefile supporting plain-style docs

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""RemoteTransport client for the smart-server.
18
18
 
23
23
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
24
24
 
25
25
from cStringIO import StringIO
26
 
import urllib
27
 
import urlparse
28
26
 
29
27
from bzrlib import (
 
28
    config,
30
29
    debug,
31
30
    errors,
 
31
    remote,
32
32
    trace,
33
33
    transport,
34
34
    urlutils,
35
35
    )
36
 
from bzrlib.smart import client, medium, protocol
37
 
 
38
 
# must do this otherwise urllib can't parse the urls properly :(
39
 
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
40
 
    transport.register_urlparse_netloc_protocol(scheme)
41
 
del scheme
42
 
 
43
 
 
44
 
# Port 4155 is the default port for bzr://, registered with IANA.
45
 
BZR_DEFAULT_INTERFACE = '0.0.0.0'
46
 
BZR_DEFAULT_PORT = 4155
 
36
from bzrlib.smart import client, medium
 
37
from bzrlib.symbol_versioning import (
 
38
    deprecated_method,
 
39
    )
47
40
 
48
41
 
49
42
class _SmartStat(object):
61
54
 
62
55
    The connection has a notion of the current directory to which it's
63
56
    connected; this is incorporated in filenames passed to the server.
64
 
    
65
 
    This supports some higher-level RPC operations and can also be treated 
 
57
 
 
58
    This supports some higher-level RPC operations and can also be treated
66
59
    like a Transport to do file-like operations.
67
60
 
68
61
    The connection can be made over a tcp socket, an ssh pipe or a series of
70
63
    RemoteTCPTransport, etc.
71
64
    """
72
65
 
 
66
    # When making a readv request, cap it at requesting 5MB of data
 
67
    _max_readv_bytes = 5*1024*1024
 
68
 
73
69
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
74
70
    # responsibilities: Put those on SmartClient or similar. This is vital for
75
71
    # the ability to support multiple versions of the smart protocol over time:
76
 
    # RemoteTransport is an adapter from the Transport object model to the 
 
72
    # RemoteTransport is an adapter from the Transport object model to the
77
73
    # SmartClient model, not an encoder.
78
74
 
79
75
    # FIXME: the medium parameter should be private, only the tests requires
87
83
            one is being cloned from.  Attributes such as the medium will
88
84
            be reused.
89
85
 
90
 
        :param medium: The medium to use for this RemoteTransport. This must be
91
 
            supplied if _from_transport is None.
 
86
        :param medium: The medium to use for this RemoteTransport.  If None,
 
87
            the medium from the _from_transport is shared.  If both this
 
88
            and _from_transport are None, a new medium will be built.
 
89
            _from_transport and medium cannot both be specified.
92
90
 
93
91
        :param _client: Override the _SmartClient used by this transport.  This
94
92
            should only be used for testing purposes; normally this is
95
93
            determined from the medium.
96
94
        """
97
 
        super(RemoteTransport, self).__init__(url,
98
 
                                              _from_transport=_from_transport)
 
95
        super(RemoteTransport, self).__init__(
 
96
            url, _from_transport=_from_transport)
99
97
 
100
98
        # The medium is the connection, except when we need to share it with
101
99
        # other objects (RemoteBzrDir, RemoteRepository etc). In these cases
102
100
        # what we want to share is really the shared connection.
103
101
 
104
 
        if _from_transport is None:
 
102
        if (_from_transport is not None
 
103
            and isinstance(_from_transport, RemoteTransport)):
 
104
            _client = _from_transport._client
 
105
        elif _from_transport is None:
105
106
            # If no _from_transport is specified, we need to intialize the
106
107
            # shared medium.
107
108
            credentials = None
111
112
                    trace.mutter('hpss: Built a new medium: %s',
112
113
                                 medium.__class__.__name__)
113
114
            self._shared_connection = transport._SharedConnection(medium,
114
 
                                                                  credentials)
 
115
                                                                  credentials,
 
116
                                                                  self.base)
 
117
        elif medium is None:
 
118
            # No medium was specified, so share the medium from the
 
119
            # _from_transport.
 
120
            medium = self._shared_connection.connection
 
121
        else:
 
122
            raise AssertionError(
 
123
                "Both _from_transport (%r) and medium (%r) passed to "
 
124
                "RemoteTransport.__init__, but these parameters are mutally "
 
125
                "exclusive." % (_from_transport, medium))
115
126
 
116
127
        if _client is None:
117
 
            self._client = client._SmartClient(self.get_shared_medium())
 
128
            self._client = client._SmartClient(medium)
118
129
        else:
119
130
            self._client = _client
120
131
 
127
138
        # No credentials
128
139
        return None, None
129
140
 
 
141
    def _report_activity(self, bytes, direction):
 
142
        """See Transport._report_activity.
 
143
 
 
144
        Does nothing; the smart medium will report activity triggered by a
 
145
        RemoteTransport.
 
146
        """
 
147
        pass
 
148
 
130
149
    def is_readonly(self):
131
150
        """Smart server transport can do read/write file operations."""
132
 
        resp = self._call2('Transport.is_readonly')
133
 
        if resp == ('yes', ):
134
 
            return True
135
 
        elif resp == ('no', ):
136
 
            return False
137
 
        elif (resp == ('error', "Generic bzr smart protocol error: "
138
 
                                "bad request 'Transport.is_readonly'") or
139
 
              resp == ('error', "Generic bzr smart protocol error: "
140
 
                                "bad request u'Transport.is_readonly'")):
 
151
        try:
 
152
            resp = self._call2('Transport.is_readonly')
 
153
        except errors.UnknownSmartMethod:
141
154
            # XXX: nasty hack: servers before 0.16 don't have a
142
155
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
143
156
            # did: assume False.
144
157
            return False
 
158
        if resp == ('yes', ):
 
159
            return True
 
160
        elif resp == ('no', ):
 
161
            return False
145
162
        else:
146
 
            self._translate_error(resp)
147
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
163
            raise errors.UnexpectedSmartServerResponse(resp)
148
164
 
149
165
    def get_smart_client(self):
150
166
        return self._get_connection()
152
168
    def get_smart_medium(self):
153
169
        return self._get_connection()
154
170
 
155
 
    def get_shared_medium(self):
156
 
        return self._get_shared_connection()
157
 
 
158
171
    def _remote_path(self, relpath):
159
172
        """Returns the Unicode version of the absolute path for relpath."""
160
173
        return self._combine_paths(self._path, relpath)
161
174
 
162
175
    def _call(self, method, *args):
163
176
        resp = self._call2(method, *args)
164
 
        self._translate_error(resp)
 
177
        self._ensure_ok(resp)
165
178
 
166
179
    def _call2(self, method, *args):
167
180
        """Call a method on the remote server."""
168
 
        return self._client.call(method, *args)
 
181
        try:
 
182
            return self._client.call(method, *args)
 
183
        except errors.ErrorFromSmartServer, err:
 
184
            # The first argument, if present, is always a path.
 
185
            if args:
 
186
                context = {'relpath': args[0]}
 
187
            else:
 
188
                context = {}
 
189
            self._translate_error(err, **context)
169
190
 
170
191
    def _call_with_body_bytes(self, method, args, body):
171
192
        """Call a method on the remote server with body bytes."""
172
 
        return self._client.call_with_body_bytes(method, args, body)
 
193
        try:
 
194
            return self._client.call_with_body_bytes(method, args, body)
 
195
        except errors.ErrorFromSmartServer, err:
 
196
            # The first argument, if present, is always a path.
 
197
            if args:
 
198
                context = {'relpath': args[0]}
 
199
            else:
 
200
                context = {}
 
201
            self._translate_error(err, **context)
173
202
 
174
203
    def has(self, relpath):
175
204
        """Indicate whether a remote file of the given name exists or not.
182
211
        elif resp == ('no', ):
183
212
            return False
184
213
        else:
185
 
            self._translate_error(resp)
 
214
            raise errors.UnexpectedSmartServerResponse(resp)
186
215
 
187
216
    def get(self, relpath):
188
217
        """Return file-like object reading the contents of a remote file.
189
 
        
 
218
 
190
219
        :see: Transport.get_bytes()/get_file()
191
220
        """
192
221
        return StringIO(self.get_bytes(relpath))
193
222
 
194
223
    def get_bytes(self, relpath):
195
224
        remote = self._remote_path(relpath)
196
 
        request = self.get_smart_medium().get_request()
197
 
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
198
 
        smart_protocol.call('get', remote)
199
 
        resp = smart_protocol.read_response_tuple(True)
 
225
        try:
 
226
            resp, response_handler = self._client.call_expecting_body('get', remote)
 
227
        except errors.ErrorFromSmartServer, err:
 
228
            self._translate_error(err, relpath)
200
229
        if resp != ('ok', ):
201
 
            smart_protocol.cancel_read_body()
202
 
            self._translate_error(resp, relpath)
203
 
        return smart_protocol.read_body_bytes()
 
230
            response_handler.cancel_read_body()
 
231
            raise errors.UnexpectedSmartServerResponse(resp)
 
232
        return response_handler.read_body_bytes()
204
233
 
205
234
    def _serialise_optional_mode(self, mode):
206
235
        if mode is None:
211
240
    def mkdir(self, relpath, mode=None):
212
241
        resp = self._call2('mkdir', self._remote_path(relpath),
213
242
            self._serialise_optional_mode(mode))
214
 
        self._translate_error(resp)
215
243
 
216
244
    def open_write_stream(self, relpath, mode=None):
217
245
        """See Transport.open_write_stream."""
233
261
        resp = self._call_with_body_bytes('put',
234
262
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
235
263
            upload_contents)
236
 
        self._translate_error(resp)
 
264
        self._ensure_ok(resp)
 
265
        return len(upload_contents)
237
266
 
238
267
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
239
268
                             create_parent_dir=False,
249
278
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
250
279
             create_parent_str, self._serialise_optional_mode(dir_mode)),
251
280
            bytes)
252
 
        self._translate_error(resp)
 
281
        self._ensure_ok(resp)
253
282
 
254
283
    def put_file(self, relpath, upload_file, mode=None):
255
284
        # its not ideal to seek back, but currently put_non_atomic_file depends
271
300
 
272
301
    def append_file(self, relpath, from_file, mode=None):
273
302
        return self.append_bytes(relpath, from_file.read(), mode)
274
 
        
 
303
 
275
304
    def append_bytes(self, relpath, bytes, mode=None):
276
305
        resp = self._call_with_body_bytes(
277
306
            'append',
279
308
            bytes)
280
309
        if resp[0] == 'appended':
281
310
            return int(resp[1])
282
 
        self._translate_error(resp)
 
311
        raise errors.UnexpectedSmartServerResponse(resp)
283
312
 
284
313
    def delete(self, relpath):
285
314
        resp = self._call2('delete', self._remote_path(relpath))
286
 
        self._translate_error(resp)
 
315
        self._ensure_ok(resp)
287
316
 
288
317
    def external_url(self):
289
318
        """See bzrlib.transport.Transport.external_url."""
290
319
        # the external path for RemoteTransports is the base
291
320
        return self.base
292
321
 
293
 
    def readv(self, relpath, offsets):
 
322
    def recommended_page_size(self):
 
323
        """Return the recommended page size for this transport."""
 
324
        return 64 * 1024
 
325
 
 
326
    def _readv(self, relpath, offsets):
294
327
        if not offsets:
295
328
            return
296
329
 
297
330
        offsets = list(offsets)
298
331
 
299
332
        sorted_offsets = sorted(offsets)
300
 
        # turn the list of offsets into a stack
301
 
        offset_stack = iter(offsets)
302
 
        cur_offset_and_size = offset_stack.next()
303
333
        coalesced = list(self._coalesce_offsets(sorted_offsets,
304
334
                               limit=self._max_readv_combine,
305
 
                               fudge_factor=self._bytes_to_read_before_seek))
306
 
 
307
 
        request = self.get_smart_medium().get_request()
308
 
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
309
 
        smart_protocol.call_with_body_readv_array(
310
 
            ('readv', self._remote_path(relpath)),
311
 
            [(c.start, c.length) for c in coalesced])
312
 
        resp = smart_protocol.read_response_tuple(True)
313
 
 
314
 
        if resp[0] != 'readv':
315
 
            # This should raise an exception
316
 
            smart_protocol.cancel_read_body()
317
 
            self._translate_error(resp)
318
 
            return
319
 
 
320
 
        # FIXME: this should know how many bytes are needed, for clarity.
321
 
        data = smart_protocol.read_body_bytes()
 
335
                               fudge_factor=self._bytes_to_read_before_seek,
 
336
                               max_size=self._max_readv_bytes))
 
337
 
 
338
        # now that we've coallesced things, avoid making enormous requests
 
339
        requests = []
 
340
        cur_request = []
 
341
        cur_len = 0
 
342
        for c in coalesced:
 
343
            if c.length + cur_len > self._max_readv_bytes:
 
344
                requests.append(cur_request)
 
345
                cur_request = [c]
 
346
                cur_len = c.length
 
347
                continue
 
348
            cur_request.append(c)
 
349
            cur_len += c.length
 
350
        if cur_request:
 
351
            requests.append(cur_request)
 
352
        if 'hpss' in debug.debug_flags:
 
353
            trace.mutter('%s.readv %s offsets => %s coalesced'
 
354
                         ' => %s requests (%s)',
 
355
                         self.__class__.__name__, len(offsets), len(coalesced),
 
356
                         len(requests), sum(map(len, requests)))
322
357
        # Cache the results, but only until they have been fulfilled
323
358
        data_map = {}
 
359
        # turn the list of offsets into a single stack to iterate
 
360
        offset_stack = iter(offsets)
 
361
        # using a list so it can be modified when passing down and coming back
 
362
        next_offset = [offset_stack.next()]
 
363
        for cur_request in requests:
 
364
            try:
 
365
                result = self._client.call_with_body_readv_array(
 
366
                    ('readv', self._remote_path(relpath),),
 
367
                    [(c.start, c.length) for c in cur_request])
 
368
                resp, response_handler = result
 
369
            except errors.ErrorFromSmartServer, err:
 
370
                self._translate_error(err, relpath)
 
371
 
 
372
            if resp[0] != 'readv':
 
373
                # This should raise an exception
 
374
                response_handler.cancel_read_body()
 
375
                raise errors.UnexpectedSmartServerResponse(resp)
 
376
 
 
377
            for res in self._handle_response(offset_stack, cur_request,
 
378
                                             response_handler,
 
379
                                             data_map,
 
380
                                             next_offset):
 
381
                yield res
 
382
 
 
383
    def _handle_response(self, offset_stack, coalesced, response_handler,
 
384
                         data_map, next_offset):
 
385
        cur_offset_and_size = next_offset[0]
 
386
        # FIXME: this should know how many bytes are needed, for clarity.
 
387
        data = response_handler.read_body_bytes()
 
388
        data_offset = 0
324
389
        for c_offset in coalesced:
325
390
            if len(data) < c_offset.length:
326
391
                raise errors.ShortReadvError(relpath, c_offset.start,
327
392
                            c_offset.length, actual=len(data))
328
393
            for suboffset, subsize in c_offset.ranges:
329
394
                key = (c_offset.start+suboffset, subsize)
330
 
                data_map[key] = data[suboffset:suboffset+subsize]
331
 
            data = data[c_offset.length:]
 
395
                this_data = data[data_offset+suboffset:
 
396
                                 data_offset+suboffset+subsize]
 
397
                # Special case when the data is in-order, rather than packing
 
398
                # into a map and then back out again. Benchmarking shows that
 
399
                # this has 100% hit rate, but leave in the data_map work just
 
400
                # in case.
 
401
                # TODO: Could we get away with using buffer() to avoid the
 
402
                #       memory copy?  Callers would need to realize they may
 
403
                #       not have a real string.
 
404
                if key == cur_offset_and_size:
 
405
                    yield cur_offset_and_size[0], this_data
 
406
                    cur_offset_and_size = next_offset[0] = offset_stack.next()
 
407
                else:
 
408
                    data_map[key] = this_data
 
409
            data_offset += c_offset.length
332
410
 
333
411
            # Now that we've read some data, see if we can yield anything back
334
412
            while cur_offset_and_size in data_map:
335
413
                this_data = data_map.pop(cur_offset_and_size)
336
414
                yield cur_offset_and_size[0], this_data
337
 
                cur_offset_and_size = offset_stack.next()
 
415
                cur_offset_and_size = next_offset[0] = offset_stack.next()
338
416
 
339
417
    def rename(self, rel_from, rel_to):
340
418
        self._call('rename',
349
427
    def rmdir(self, relpath):
350
428
        resp = self._call('rmdir', self._remote_path(relpath))
351
429
 
352
 
    def _translate_error(self, resp, orig_path=None):
353
 
        """Raise an exception from a response"""
354
 
        if resp is None:
355
 
            what = None
356
 
        else:
357
 
            what = resp[0]
358
 
        if what == 'ok':
359
 
            return
360
 
        elif what == 'NoSuchFile':
361
 
            if orig_path is not None:
362
 
                error_path = orig_path
363
 
            else:
364
 
                error_path = resp[1]
365
 
            raise errors.NoSuchFile(error_path)
366
 
        elif what == 'error':
367
 
            raise errors.SmartProtocolError(unicode(resp[1]))
368
 
        elif what == 'FileExists':
369
 
            raise errors.FileExists(resp[1])
370
 
        elif what == 'DirectoryNotEmpty':
371
 
            raise errors.DirectoryNotEmpty(resp[1])
372
 
        elif what == 'ShortReadvError':
373
 
            raise errors.ShortReadvError(resp[1], int(resp[2]),
374
 
                                         int(resp[3]), int(resp[4]))
375
 
        elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
376
 
            encoding = str(resp[1]) # encoding must always be a string
377
 
            val = resp[2]
378
 
            start = int(resp[3])
379
 
            end = int(resp[4])
380
 
            reason = str(resp[5]) # reason must always be a string
381
 
            if val.startswith('u:'):
382
 
                val = val[2:].decode('utf-8')
383
 
            elif val.startswith('s:'):
384
 
                val = val[2:].decode('base64')
385
 
            if what == 'UnicodeDecodeError':
386
 
                raise UnicodeDecodeError(encoding, val, start, end, reason)
387
 
            elif what == 'UnicodeEncodeError':
388
 
                raise UnicodeEncodeError(encoding, val, start, end, reason)
389
 
        elif what == "ReadOnlyError":
390
 
            raise errors.TransportNotPossible('readonly transport')
391
 
        elif what == "ReadError":
392
 
            if orig_path is not None:
393
 
                error_path = orig_path
394
 
            else:
395
 
                error_path = resp[1]
396
 
            raise errors.ReadError(error_path)
397
 
        else:
398
 
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
 
430
    def _ensure_ok(self, resp):
 
431
        if resp[0] != 'ok':
 
432
            raise errors.UnexpectedSmartServerResponse(resp)
 
433
 
 
434
    def _translate_error(self, err, relpath=None):
 
435
        remote._translate_error(err, path=relpath)
399
436
 
400
437
    def disconnect(self):
401
438
        self.get_smart_medium().disconnect()
402
439
 
403
 
    def delete_tree(self, relpath):
404
 
        raise errors.TransportNotPossible('readonly transport')
405
 
 
406
440
    def stat(self, relpath):
407
441
        resp = self._call2('stat', self._remote_path(relpath))
408
442
        if resp[0] == 'stat':
409
443
            return _SmartStat(int(resp[1]), int(resp[2], 8))
410
 
        else:
411
 
            self._translate_error(resp)
 
444
        raise errors.UnexpectedSmartServerResponse(resp)
412
445
 
413
446
    ## def lock_read(self, relpath):
414
447
    ##     """Lock the given file for shared (read) access.
430
463
        resp = self._call2('list_dir', self._remote_path(relpath))
431
464
        if resp[0] == 'names':
432
465
            return [name.encode('ascii') for name in resp[1:]]
433
 
        else:
434
 
            self._translate_error(resp)
 
466
        raise errors.UnexpectedSmartServerResponse(resp)
435
467
 
436
468
    def iter_files_recursive(self):
437
469
        resp = self._call2('iter_files_recursive', self._remote_path(''))
438
470
        if resp[0] == 'names':
439
471
            return resp[1:]
440
 
        else:
441
 
            self._translate_error(resp)
 
472
        raise errors.UnexpectedSmartServerResponse(resp)
442
473
 
443
474
 
444
475
class RemoteTCPTransport(RemoteTransport):
445
476
    """Connection to smart server over plain tcp.
446
 
    
 
477
 
447
478
    This is essentially just a factory to get 'RemoteTransport(url,
448
479
        SmartTCPClientMedium).
449
480
    """
450
481
 
451
482
    def _build_medium(self):
452
 
        assert self.base.startswith('bzr://')
453
 
        if self._port is None:
454
 
            self._port = BZR_DEFAULT_PORT
455
 
        return medium.SmartTCPClientMedium(self._host, self._port), None
 
483
        client_medium = medium.SmartTCPClientMedium(
 
484
            self._host, self._port, self.base)
 
485
        return client_medium, None
 
486
 
 
487
 
 
488
class RemoteTCPTransportV2Only(RemoteTransport):
 
489
    """Connection to smart server over plain tcp with the client hard-coded to
 
490
    assume protocol v2 and remote server version <= 1.6.
 
491
 
 
492
    This should only be used for testing.
 
493
    """
 
494
 
 
495
    def _build_medium(self):
 
496
        client_medium = medium.SmartTCPClientMedium(
 
497
            self._host, self._port, self.base)
 
498
        client_medium._protocol_version = 2
 
499
        client_medium._remember_remote_is_before((1, 6))
 
500
        return client_medium, None
456
501
 
457
502
 
458
503
class RemoteSSHTransport(RemoteTransport):
463
508
    """
464
509
 
465
510
    def _build_medium(self):
466
 
        assert self.base.startswith('bzr+ssh://')
467
 
        # ssh will prompt the user for a password if needed and if none is
468
 
        # provided but it will not give it back, so no credentials can be
469
 
        # stored.
470
 
        return medium.SmartSSHClientMedium(self._host, self._port,
471
 
                                           self._user, self._password), None
 
511
        location_config = config.LocationConfig(self.base)
 
512
        bzr_remote_path = location_config.get_bzr_remote_path()
 
513
        user = self._user
 
514
        if user is None:
 
515
            auth = config.AuthenticationConfig()
 
516
            user = auth.get_user('ssh', self._host, self._port)
 
517
        client_medium = medium.SmartSSHClientMedium(self._host, self._port,
 
518
            user, self._password, self.base,
 
519
            bzr_remote_path=bzr_remote_path)
 
520
        return client_medium, (user, self._password)
472
521
 
473
522
 
474
523
class RemoteHTTPTransport(RemoteTransport):
475
524
    """Just a way to connect between a bzr+http:// url and http://.
476
 
    
 
525
 
477
526
    This connection operates slightly differently than the RemoteSSHTransport.
478
527
    It uses a plain http:// transport underneath, which defines what remote
479
528
    .bzr/smart URL we are connected to. From there, all paths that are sent are
483
532
    """
484
533
 
485
534
    def __init__(self, base, _from_transport=None, http_transport=None):
486
 
        assert base.startswith('bzr+http://')
487
 
 
488
535
        if http_transport is None:
489
536
            # FIXME: the password may be lost here because it appears in the
490
537
            # url only for an intial construction (when the url came from the
504
551
        """After connecting, HTTP Transport only deals in relative URLs."""
505
552
        # Adjust the relpath based on which URL this smart transport is
506
553
        # connected to.
507
 
        http_base = urlutils.normalize_url(self._http_transport.base)
 
554
        http_base = urlutils.normalize_url(self.get_smart_medium().base)
508
555
        url = urlutils.join(self.base[len('bzr+'):], relpath)
509
556
        url = urlutils.normalize_url(url)
510
557
        return urlutils.relative_url(http_base, url)
521
568
        smart requests may be different).  This is so that the server doesn't
522
569
        have to handle .bzr/smart requests at arbitrary places inside .bzr
523
570
        directories, just at the initial URL the user uses.
524
 
 
525
 
        The exception is parent paths (i.e. relative_url of "..").
526
571
        """
527
572
        if relative_url:
528
573
            abs_url = self.abspath(relative_url)
529
574
        else:
530
575
            abs_url = self.base
531
 
        # We either use the exact same http_transport (for child locations), or
532
 
        # a clone of the underlying http_transport (for parent locations).  This
533
 
        # means we share the connection.
534
 
        norm_base = urlutils.normalize_url(self.base)
535
 
        norm_abs_url = urlutils.normalize_url(abs_url)
536
 
        normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
537
 
        if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
538
 
            http_transport = self._http_transport.clone(normalized_rel_url)
539
 
        else:
540
 
            http_transport = self._http_transport
541
576
        return RemoteHTTPTransport(abs_url,
542
577
                                   _from_transport=self,
543
 
                                   http_transport=http_transport)
 
578
                                   http_transport=self._http_transport)
 
579
 
 
580
    def _redirected_to(self, source, target):
 
581
        """See transport._redirected_to"""
 
582
        redirected = self._http_transport._redirected_to(source, target)
 
583
        if (redirected is not None
 
584
            and isinstance(redirected, type(self._http_transport))):
 
585
            return RemoteHTTPTransport('bzr+' + redirected.external_url(),
 
586
                                       http_transport=redirected)
 
587
        else:
 
588
            # Either None or a transport for a different protocol
 
589
            return redirected
 
590
 
 
591
 
 
592
class HintingSSHTransport(transport.Transport):
 
593
    """Simple transport that handles ssh:// and points out bzr+ssh://."""
 
594
 
 
595
    def __init__(self, url):
 
596
        raise errors.UnsupportedProtocol(url,
 
597
            'bzr supports bzr+ssh to operate over ssh, use "bzr+%s".' % url)
544
598
 
545
599
 
546
600
def get_test_permutations():