~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Martin Pool
  • Date: 2005-07-13 00:30:30 UTC
  • Revision ID: mbp@sourcefrog.net-20050713003030-2e89871a9ce24c7b
- typo in testsweet

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
"""RemoteTransport client for the smart-server.
18
 
 
19
 
This module shouldn't be accessed directly.  The classes defined here should be
20
 
imported from bzrlib.smart.
21
 
"""
22
 
 
23
 
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
24
 
 
25
 
from cStringIO import StringIO
26
 
 
27
 
from bzrlib import (
28
 
    config,
29
 
    debug,
30
 
    errors,
31
 
    remote,
32
 
    trace,
33
 
    transport,
34
 
    urlutils,
35
 
    )
36
 
from bzrlib.smart import client, medium
37
 
from bzrlib.symbol_versioning import (
38
 
    deprecated_method,
39
 
    )
40
 
 
41
 
 
42
 
class _SmartStat(object):
43
 
 
44
 
    def __init__(self, size, mode):
45
 
        self.st_size = size
46
 
        self.st_mode = mode
47
 
 
48
 
 
49
 
class RemoteTransport(transport.ConnectedTransport):
50
 
    """Connection to a smart server.
51
 
 
52
 
    The connection holds references to the medium that can be used to send
53
 
    requests to the server.
54
 
 
55
 
    The connection has a notion of the current directory to which it's
56
 
    connected; this is incorporated in filenames passed to the server.
57
 
 
58
 
    This supports some higher-level RPC operations and can also be treated
59
 
    like a Transport to do file-like operations.
60
 
 
61
 
    The connection can be made over a tcp socket, an ssh pipe or a series of
62
 
    http requests.  There are concrete subclasses for each type:
63
 
    RemoteTCPTransport, etc.
64
 
    """
65
 
 
66
 
    # When making a readv request, cap it at requesting 5MB of data
67
 
    _max_readv_bytes = 5*1024*1024
68
 
 
69
 
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
70
 
    # responsibilities: Put those on SmartClient or similar. This is vital for
71
 
    # the ability to support multiple versions of the smart protocol over time:
72
 
    # RemoteTransport is an adapter from the Transport object model to the
73
 
    # SmartClient model, not an encoder.
74
 
 
75
 
    # FIXME: the medium parameter should be private, only the tests requires
76
 
    # it. It may be even clearer to define a TestRemoteTransport that handles
77
 
    # the specific cases of providing a _client and/or a _medium, and leave
78
 
    # RemoteTransport as an abstract class.
79
 
    def __init__(self, url, _from_transport=None, medium=None, _client=None):
80
 
        """Constructor.
81
 
 
82
 
        :param _from_transport: Another RemoteTransport instance that this
83
 
            one is being cloned from.  Attributes such as the medium will
84
 
            be reused.
85
 
 
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.
90
 
 
91
 
        :param _client: Override the _SmartClient used by this transport.  This
92
 
            should only be used for testing purposes; normally this is
93
 
            determined from the medium.
94
 
        """
95
 
        super(RemoteTransport, self).__init__(
96
 
            url, _from_transport=_from_transport)
97
 
 
98
 
        # The medium is the connection, except when we need to share it with
99
 
        # other objects (RemoteBzrDir, RemoteRepository etc). In these cases
100
 
        # what we want to share is really the shared connection.
101
 
 
102
 
        if (_from_transport is not None
103
 
            and isinstance(_from_transport, RemoteTransport)):
104
 
            _client = _from_transport._client
105
 
        elif _from_transport is None:
106
 
            # If no _from_transport is specified, we need to intialize the
107
 
            # shared medium.
108
 
            credentials = None
109
 
            if medium is None:
110
 
                medium, credentials = self._build_medium()
111
 
                if 'hpss' in debug.debug_flags:
112
 
                    trace.mutter('hpss: Built a new medium: %s',
113
 
                                 medium.__class__.__name__)
114
 
            self._shared_connection = transport._SharedConnection(medium,
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))
126
 
 
127
 
        if _client is None:
128
 
            self._client = client._SmartClient(medium)
129
 
        else:
130
 
            self._client = _client
131
 
 
132
 
    def _build_medium(self):
133
 
        """Create the medium if _from_transport does not provide one.
134
 
 
135
 
        The medium is analogous to the connection for ConnectedTransport: it
136
 
        allows connection sharing.
137
 
        """
138
 
        # No credentials
139
 
        return None, None
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
 
 
149
 
    def is_readonly(self):
150
 
        """Smart server transport can do read/write file operations."""
151
 
        try:
152
 
            resp = self._call2('Transport.is_readonly')
153
 
        except errors.UnknownSmartMethod:
154
 
            # XXX: nasty hack: servers before 0.16 don't have a
155
 
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
156
 
            # did: assume False.
157
 
            return False
158
 
        if resp == ('yes', ):
159
 
            return True
160
 
        elif resp == ('no', ):
161
 
            return False
162
 
        else:
163
 
            raise errors.UnexpectedSmartServerResponse(resp)
164
 
 
165
 
    def get_smart_client(self):
166
 
        return self._get_connection()
167
 
 
168
 
    def get_smart_medium(self):
169
 
        return self._get_connection()
170
 
 
171
 
    def _remote_path(self, relpath):
172
 
        """Returns the Unicode version of the absolute path for relpath."""
173
 
        return self._combine_paths(self._path, relpath)
174
 
 
175
 
    def _call(self, method, *args):
176
 
        resp = self._call2(method, *args)
177
 
        self._ensure_ok(resp)
178
 
 
179
 
    def _call2(self, method, *args):
180
 
        """Call a method on the remote server."""
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)
190
 
 
191
 
    def _call_with_body_bytes(self, method, args, body):
192
 
        """Call a method on the remote server with body bytes."""
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)
202
 
 
203
 
    def has(self, relpath):
204
 
        """Indicate whether a remote file of the given name exists or not.
205
 
 
206
 
        :see: Transport.has()
207
 
        """
208
 
        resp = self._call2('has', self._remote_path(relpath))
209
 
        if resp == ('yes', ):
210
 
            return True
211
 
        elif resp == ('no', ):
212
 
            return False
213
 
        else:
214
 
            raise errors.UnexpectedSmartServerResponse(resp)
215
 
 
216
 
    def get(self, relpath):
217
 
        """Return file-like object reading the contents of a remote file.
218
 
 
219
 
        :see: Transport.get_bytes()/get_file()
220
 
        """
221
 
        return StringIO(self.get_bytes(relpath))
222
 
 
223
 
    def get_bytes(self, relpath):
224
 
        remote = self._remote_path(relpath)
225
 
        try:
226
 
            resp, response_handler = self._client.call_expecting_body('get', remote)
227
 
        except errors.ErrorFromSmartServer, err:
228
 
            self._translate_error(err, relpath)
229
 
        if resp != ('ok', ):
230
 
            response_handler.cancel_read_body()
231
 
            raise errors.UnexpectedSmartServerResponse(resp)
232
 
        return response_handler.read_body_bytes()
233
 
 
234
 
    def _serialise_optional_mode(self, mode):
235
 
        if mode is None:
236
 
            return ''
237
 
        else:
238
 
            return '%d' % mode
239
 
 
240
 
    def mkdir(self, relpath, mode=None):
241
 
        resp = self._call2('mkdir', self._remote_path(relpath),
242
 
            self._serialise_optional_mode(mode))
243
 
 
244
 
    def open_write_stream(self, relpath, mode=None):
245
 
        """See Transport.open_write_stream."""
246
 
        self.put_bytes(relpath, "", mode)
247
 
        result = transport.AppendBasedFileStream(self, relpath)
248
 
        transport._file_streams[self.abspath(relpath)] = result
249
 
        return result
250
 
 
251
 
    def put_bytes(self, relpath, upload_contents, mode=None):
252
 
        # FIXME: upload_file is probably not safe for non-ascii characters -
253
 
        # should probably just pass all parameters as length-delimited
254
 
        # strings?
255
 
        if type(upload_contents) is unicode:
256
 
            # Although not strictly correct, we raise UnicodeEncodeError to be
257
 
            # compatible with other transports.
258
 
            raise UnicodeEncodeError(
259
 
                'undefined', upload_contents, 0, 1,
260
 
                'put_bytes must be given bytes, not unicode.')
261
 
        resp = self._call_with_body_bytes('put',
262
 
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
263
 
            upload_contents)
264
 
        self._ensure_ok(resp)
265
 
        return len(upload_contents)
266
 
 
267
 
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
268
 
                             create_parent_dir=False,
269
 
                             dir_mode=None):
270
 
        """See Transport.put_bytes_non_atomic."""
271
 
        # FIXME: no encoding in the transport!
272
 
        create_parent_str = 'F'
273
 
        if create_parent_dir:
274
 
            create_parent_str = 'T'
275
 
 
276
 
        resp = self._call_with_body_bytes(
277
 
            'put_non_atomic',
278
 
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
279
 
             create_parent_str, self._serialise_optional_mode(dir_mode)),
280
 
            bytes)
281
 
        self._ensure_ok(resp)
282
 
 
283
 
    def put_file(self, relpath, upload_file, mode=None):
284
 
        # its not ideal to seek back, but currently put_non_atomic_file depends
285
 
        # on transports not reading before failing - which is a faulty
286
 
        # assumption I think - RBC 20060915
287
 
        pos = upload_file.tell()
288
 
        try:
289
 
            return self.put_bytes(relpath, upload_file.read(), mode)
290
 
        except:
291
 
            upload_file.seek(pos)
292
 
            raise
293
 
 
294
 
    def put_file_non_atomic(self, relpath, f, mode=None,
295
 
                            create_parent_dir=False,
296
 
                            dir_mode=None):
297
 
        return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
298
 
                                         create_parent_dir=create_parent_dir,
299
 
                                         dir_mode=dir_mode)
300
 
 
301
 
    def append_file(self, relpath, from_file, mode=None):
302
 
        return self.append_bytes(relpath, from_file.read(), mode)
303
 
 
304
 
    def append_bytes(self, relpath, bytes, mode=None):
305
 
        resp = self._call_with_body_bytes(
306
 
            'append',
307
 
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
308
 
            bytes)
309
 
        if resp[0] == 'appended':
310
 
            return int(resp[1])
311
 
        raise errors.UnexpectedSmartServerResponse(resp)
312
 
 
313
 
    def delete(self, relpath):
314
 
        resp = self._call2('delete', self._remote_path(relpath))
315
 
        self._ensure_ok(resp)
316
 
 
317
 
    def external_url(self):
318
 
        """See bzrlib.transport.Transport.external_url."""
319
 
        # the external path for RemoteTransports is the base
320
 
        return self.base
321
 
 
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):
327
 
        if not offsets:
328
 
            return
329
 
 
330
 
        offsets = list(offsets)
331
 
 
332
 
        sorted_offsets = sorted(offsets)
333
 
        coalesced = list(self._coalesce_offsets(sorted_offsets,
334
 
                               limit=self._max_readv_combine,
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)))
357
 
        # Cache the results, but only until they have been fulfilled
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
389
 
        for c_offset in coalesced:
390
 
            if len(data) < c_offset.length:
391
 
                raise errors.ShortReadvError(relpath, c_offset.start,
392
 
                            c_offset.length, actual=len(data))
393
 
            for suboffset, subsize in c_offset.ranges:
394
 
                key = (c_offset.start+suboffset, subsize)
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
410
 
 
411
 
            # Now that we've read some data, see if we can yield anything back
412
 
            while cur_offset_and_size in data_map:
413
 
                this_data = data_map.pop(cur_offset_and_size)
414
 
                yield cur_offset_and_size[0], this_data
415
 
                cur_offset_and_size = next_offset[0] = offset_stack.next()
416
 
 
417
 
    def rename(self, rel_from, rel_to):
418
 
        self._call('rename',
419
 
                   self._remote_path(rel_from),
420
 
                   self._remote_path(rel_to))
421
 
 
422
 
    def move(self, rel_from, rel_to):
423
 
        self._call('move',
424
 
                   self._remote_path(rel_from),
425
 
                   self._remote_path(rel_to))
426
 
 
427
 
    def rmdir(self, relpath):
428
 
        resp = self._call('rmdir', self._remote_path(relpath))
429
 
 
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)
436
 
 
437
 
    def disconnect(self):
438
 
        m = self.get_smart_medium()
439
 
        if m is not None:
440
 
            m.disconnect()
441
 
 
442
 
    def stat(self, relpath):
443
 
        resp = self._call2('stat', self._remote_path(relpath))
444
 
        if resp[0] == 'stat':
445
 
            return _SmartStat(int(resp[1]), int(resp[2], 8))
446
 
        raise errors.UnexpectedSmartServerResponse(resp)
447
 
 
448
 
    ## def lock_read(self, relpath):
449
 
    ##     """Lock the given file for shared (read) access.
450
 
    ##     :return: A lock object, which should be passed to Transport.unlock()
451
 
    ##     """
452
 
    ##     # The old RemoteBranch ignore lock for reading, so we will
453
 
    ##     # continue that tradition and return a bogus lock object.
454
 
    ##     class BogusLock(object):
455
 
    ##         def __init__(self, path):
456
 
    ##             self.path = path
457
 
    ##         def unlock(self):
458
 
    ##             pass
459
 
    ##     return BogusLock(relpath)
460
 
 
461
 
    def listable(self):
462
 
        return True
463
 
 
464
 
    def list_dir(self, relpath):
465
 
        resp = self._call2('list_dir', self._remote_path(relpath))
466
 
        if resp[0] == 'names':
467
 
            return [name.encode('ascii') for name in resp[1:]]
468
 
        raise errors.UnexpectedSmartServerResponse(resp)
469
 
 
470
 
    def iter_files_recursive(self):
471
 
        resp = self._call2('iter_files_recursive', self._remote_path(''))
472
 
        if resp[0] == 'names':
473
 
            return resp[1:]
474
 
        raise errors.UnexpectedSmartServerResponse(resp)
475
 
 
476
 
 
477
 
class RemoteTCPTransport(RemoteTransport):
478
 
    """Connection to smart server over plain tcp.
479
 
 
480
 
    This is essentially just a factory to get 'RemoteTransport(url,
481
 
        SmartTCPClientMedium).
482
 
    """
483
 
 
484
 
    def _build_medium(self):
485
 
        client_medium = medium.SmartTCPClientMedium(
486
 
            self._host, self._port, self.base)
487
 
        return client_medium, None
488
 
 
489
 
 
490
 
class RemoteTCPTransportV2Only(RemoteTransport):
491
 
    """Connection to smart server over plain tcp with the client hard-coded to
492
 
    assume protocol v2 and remote server version <= 1.6.
493
 
 
494
 
    This should only be used for testing.
495
 
    """
496
 
 
497
 
    def _build_medium(self):
498
 
        client_medium = medium.SmartTCPClientMedium(
499
 
            self._host, self._port, self.base)
500
 
        client_medium._protocol_version = 2
501
 
        client_medium._remember_remote_is_before((1, 6))
502
 
        return client_medium, None
503
 
 
504
 
 
505
 
class RemoteSSHTransport(RemoteTransport):
506
 
    """Connection to smart server over SSH.
507
 
 
508
 
    This is essentially just a factory to get 'RemoteTransport(url,
509
 
        SmartSSHClientMedium).
510
 
    """
511
 
 
512
 
    def _build_medium(self):
513
 
        location_config = config.LocationConfig(self.base)
514
 
        bzr_remote_path = location_config.get_bzr_remote_path()
515
 
        user = self._user
516
 
        if user is None:
517
 
            auth = config.AuthenticationConfig()
518
 
            user = auth.get_user('ssh', self._host, self._port)
519
 
        ssh_params = medium.SSHParams(self._host, self._port, user,
520
 
            self._password, bzr_remote_path)
521
 
        client_medium = medium.SmartSSHClientMedium(self.base, ssh_params)
522
 
        return client_medium, (user, self._password)
523
 
 
524
 
 
525
 
class RemoteHTTPTransport(RemoteTransport):
526
 
    """Just a way to connect between a bzr+http:// url and http://.
527
 
 
528
 
    This connection operates slightly differently than the RemoteSSHTransport.
529
 
    It uses a plain http:// transport underneath, which defines what remote
530
 
    .bzr/smart URL we are connected to. From there, all paths that are sent are
531
 
    sent as relative paths, this way, the remote side can properly
532
 
    de-reference them, since it is likely doing rewrite rules to translate an
533
 
    HTTP path into a local path.
534
 
    """
535
 
 
536
 
    def __init__(self, base, _from_transport=None, http_transport=None):
537
 
        if http_transport is None:
538
 
            # FIXME: the password may be lost here because it appears in the
539
 
            # url only for an intial construction (when the url came from the
540
 
            # command-line).
541
 
            http_url = base[len('bzr+'):]
542
 
            self._http_transport = transport.get_transport(http_url)
543
 
        else:
544
 
            self._http_transport = http_transport
545
 
        super(RemoteHTTPTransport, self).__init__(
546
 
            base, _from_transport=_from_transport)
547
 
 
548
 
    def _build_medium(self):
549
 
        # We let http_transport take care of the credentials
550
 
        return self._http_transport.get_smart_medium(), None
551
 
 
552
 
    def _remote_path(self, relpath):
553
 
        """After connecting, HTTP Transport only deals in relative URLs."""
554
 
        # Adjust the relpath based on which URL this smart transport is
555
 
        # connected to.
556
 
        http_base = urlutils.normalize_url(self.get_smart_medium().base)
557
 
        url = urlutils.join(self.base[len('bzr+'):], relpath)
558
 
        url = urlutils.normalize_url(url)
559
 
        return urlutils.relative_url(http_base, url)
560
 
 
561
 
    def clone(self, relative_url):
562
 
        """Make a new RemoteHTTPTransport related to me.
563
 
 
564
 
        This is re-implemented rather than using the default
565
 
        RemoteTransport.clone() because we must be careful about the underlying
566
 
        http transport.
567
 
 
568
 
        Also, the cloned smart transport will POST to the same .bzr/smart
569
 
        location as this transport (although obviously the relative paths in the
570
 
        smart requests may be different).  This is so that the server doesn't
571
 
        have to handle .bzr/smart requests at arbitrary places inside .bzr
572
 
        directories, just at the initial URL the user uses.
573
 
        """
574
 
        if relative_url:
575
 
            abs_url = self.abspath(relative_url)
576
 
        else:
577
 
            abs_url = self.base
578
 
        return RemoteHTTPTransport(abs_url,
579
 
                                   _from_transport=self,
580
 
                                   http_transport=self._http_transport)
581
 
 
582
 
    def _redirected_to(self, source, target):
583
 
        """See transport._redirected_to"""
584
 
        redirected = self._http_transport._redirected_to(source, target)
585
 
        if (redirected is not None
586
 
            and isinstance(redirected, type(self._http_transport))):
587
 
            return RemoteHTTPTransport('bzr+' + redirected.external_url(),
588
 
                                       http_transport=redirected)
589
 
        else:
590
 
            # Either None or a transport for a different protocol
591
 
            return redirected
592
 
 
593
 
 
594
 
class HintingSSHTransport(transport.Transport):
595
 
    """Simple transport that handles ssh:// and points out bzr+ssh://."""
596
 
 
597
 
    def __init__(self, url):
598
 
        raise errors.UnsupportedProtocol(url,
599
 
            'bzr supports bzr+ssh to operate over ssh, use "bzr+%s".' % url)
600
 
 
601
 
 
602
 
def get_test_permutations():
603
 
    """Return (transport, server) permutations for testing."""
604
 
    ### We may need a little more test framework support to construct an
605
 
    ### appropriate RemoteTransport in the future.
606
 
    from bzrlib.tests import test_server
607
 
    return [(RemoteTCPTransport, test_server.SmartTCPServer_for_testing)]