~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/__init__.py

Merge bzr.dev, update to use new hooks.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
27
27
"""
28
28
 
29
29
from cStringIO import StringIO
30
 
import re
31
30
import sys
32
31
 
33
32
from bzrlib.lazy_import import lazy_import
47
46
""")
48
47
 
49
48
from bzrlib.symbol_versioning import (
50
 
        DEPRECATED_PARAMETER,
51
 
        )
 
49
    DEPRECATED_PARAMETER,
 
50
    )
52
51
from bzrlib.trace import (
53
52
    mutter,
54
53
    )
87
86
    modules = set()
88
87
    for prefix, factory_list in transport_list_registry.items():
89
88
        for factory in factory_list:
90
 
            if hasattr(factory, "_module_name"):
91
 
                modules.add(factory._module_name)
92
 
            else:
93
 
                modules.add(factory._obj.__module__)
 
89
            modules.add(factory.get_module())
94
90
    # Add chroot and pathfilter directly, because there is no handler
95
91
    # registered for it.
96
92
    modules.add('bzrlib.transport.chroot')
125
121
    def register_transport(self, key, help=None):
126
122
        self.register(key, [], help)
127
123
 
128
 
    def set_default_transport(self, key=None):
129
 
        """Return either 'key' or the default key if key is None"""
130
 
        self._default_key = key
131
 
 
132
124
 
133
125
transport_list_registry = TransportListRegistry()
134
126
 
238
230
    def _close(self):
239
231
        """A hook point for subclasses that need to take action on close."""
240
232
 
241
 
    def close(self):
 
233
    def close(self, want_fdatasync=False):
 
234
        if want_fdatasync:
 
235
            try:
 
236
                self.fdatasync()
 
237
            except errors.TransportNotPossible:
 
238
                pass
242
239
        self._close()
243
240
        del _file_streams[self.transport.abspath(self.relpath)]
244
241
 
 
242
    def fdatasync(self):
 
243
        """Force data out to physical disk if possible.
 
244
 
 
245
        :raises TransportNotPossible: If this transport has no way to 
 
246
            flush to disk.
 
247
        """
 
248
        raise errors.TransportNotPossible(
 
249
            "%s cannot fdatasync" % (self.transport,))
 
250
 
245
251
 
246
252
class FileFileStream(FileStream):
247
253
    """A file stream object returned by open_write_stream.
256
262
    def _close(self):
257
263
        self.file_handle.close()
258
264
 
 
265
    def fdatasync(self):
 
266
        """Force data out to physical disk if possible."""
 
267
        self.file_handle.flush()
 
268
        try:
 
269
            fileno = self.file_handle.fileno()
 
270
        except AttributeError:
 
271
            raise errors.TransportNotPossible()
 
272
        osutils.fdatasync(fileno)
 
273
 
259
274
    def write(self, bytes):
260
275
        osutils.pump_string_file(bytes, self.file_handle)
261
276
 
274
289
    """Mapping of hook names to registered callbacks for transport hooks"""
275
290
    def __init__(self):
276
291
        super(TransportHooks, self).__init__()
277
 
        self.create_hook(hooks.HookPoint("post_connect",
 
292
        self.add_hook("post_connect",
278
293
            "Called after a new connection is established or a reconnect "
279
294
            "occurs. The connected transport instance is the sole argument "
280
 
            "passed.", (2, 3), None))
 
295
            "passed.", (2, 5))
281
296
 
282
297
 
283
298
class Transport(object):
310
325
    def __init__(self, base):
311
326
        super(Transport, self).__init__()
312
327
        self.base = base
 
328
        (self._raw_base, self._segment_parameters) = (
 
329
            urlutils.split_segment_parameters(base))
313
330
 
314
331
    def _translate_error(self, e, path, raise_generic=True):
315
332
        """Translate an IOError or OSError into an appropriate bzr error.
317
334
        This handles things like ENOENT, ENOTDIR, EEXIST, and EACCESS
318
335
        """
319
336
        if getattr(e, 'errno', None) is not None:
320
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
 
337
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
 
338
                raise errors.NoSuchFile(path, extra=e)
 
339
            elif e.errno == errno.EINVAL:
 
340
                mutter("EINVAL returned on path %s: %r" % (path, e))
321
341
                raise errors.NoSuchFile(path, extra=e)
322
342
            # I would rather use errno.EFOO, but there doesn't seem to be
323
343
            # any matching for 267
406
426
        """
407
427
        raise NotImplementedError(self.external_url)
408
428
 
 
429
    def get_segment_parameters(self):
 
430
        """Return the segment parameters for the top segment of the URL.
 
431
        """
 
432
        return self._segment_parameters
 
433
 
 
434
    def set_segment_parameter(self, name, value):
 
435
        """Set a segment parameter.
 
436
 
 
437
        :param name: Segment parameter name (urlencoded string)
 
438
        :param value: Segment parameter value (urlencoded string)
 
439
        """
 
440
        if value is None:
 
441
            try:
 
442
                del self._segment_parameters[name]
 
443
            except KeyError:
 
444
                pass
 
445
        else:
 
446
            self._segment_parameters[name] = value
 
447
        self.base = urlutils.join_segment_parameters(
 
448
            self._raw_base, self._segment_parameters)
 
449
 
409
450
    def _pump(self, from_file, to_file):
410
451
        """Most children will need to copy from one file-like
411
452
        object or string to another one.
476
517
        # interface ?
477
518
        raise NotImplementedError(self.abspath)
478
519
 
479
 
    def _combine_paths(self, base_path, relpath):
480
 
        """Transform a Transport-relative path to a remote absolute path.
481
 
 
482
 
        This does not handle substitution of ~ but does handle '..' and '.'
483
 
        components.
484
 
 
485
 
        Examples::
486
 
 
487
 
            t._combine_paths('/home/sarah', 'project/foo')
488
 
                => '/home/sarah/project/foo'
489
 
            t._combine_paths('/home/sarah', '../../etc')
490
 
                => '/etc'
491
 
            t._combine_paths('/home/sarah', '/etc')
492
 
                => '/etc'
493
 
 
494
 
        :param base_path: urlencoded path for the transport root; typically a
495
 
             URL but need not contain scheme/host/etc.
496
 
        :param relpath: relative url string for relative part of remote path.
497
 
        :return: urlencoded string for final path.
498
 
        """
499
 
        if not isinstance(relpath, str):
500
 
            raise errors.InvalidURL(relpath)
501
 
        if relpath.startswith('/'):
502
 
            base_parts = []
503
 
        else:
504
 
            base_parts = base_path.split('/')
505
 
        if len(base_parts) > 0 and base_parts[-1] == '':
506
 
            base_parts = base_parts[:-1]
507
 
        for p in relpath.split('/'):
508
 
            if p == '..':
509
 
                if len(base_parts) == 0:
510
 
                    # In most filesystems, a request for the parent
511
 
                    # of root, just returns root.
512
 
                    continue
513
 
                base_parts.pop()
514
 
            elif p == '.':
515
 
                continue # No-op
516
 
            elif p != '':
517
 
                base_parts.append(p)
518
 
        path = '/'.join(base_parts)
519
 
        if not path.startswith('/'):
520
 
            path = '/' + path
521
 
        return path
522
 
 
523
520
    def recommended_page_size(self):
524
521
        """Return the recommended page size for this transport.
525
522
 
691
688
 
692
689
        This uses _coalesce_offsets to issue larger reads and fewer seeks.
693
690
 
694
 
        :param fp: A file-like object that supports seek() and read(size)
 
691
        :param fp: A file-like object that supports seek() and read(size).
 
692
            Note that implementations are allowed to call .close() on this file
 
693
            handle, so don't trust that you can use it for other work.
695
694
        :param offsets: A list of offsets to be read from the given file.
696
695
        :return: yield (pos, data) tuples for each request
697
696
        """
708
707
 
709
708
        # Cache the results, but only until they have been fulfilled
710
709
        data_map = {}
711
 
        for c_offset in coalesced:
712
 
            # TODO: jam 20060724 it might be faster to not issue seek if
713
 
            #       we are already at the right location. This should be
714
 
            #       benchmarked.
715
 
            fp.seek(c_offset.start)
716
 
            data = fp.read(c_offset.length)
717
 
            if len(data) < c_offset.length:
718
 
                raise errors.ShortReadvError(relpath, c_offset.start,
719
 
                            c_offset.length, actual=len(data))
720
 
            for suboffset, subsize in c_offset.ranges:
721
 
                key = (c_offset.start+suboffset, subsize)
722
 
                data_map[key] = data[suboffset:suboffset+subsize]
 
710
        try:
 
711
            for c_offset in coalesced:
 
712
                # TODO: jam 20060724 it might be faster to not issue seek if
 
713
                #       we are already at the right location. This should be
 
714
                #       benchmarked.
 
715
                fp.seek(c_offset.start)
 
716
                data = fp.read(c_offset.length)
 
717
                if len(data) < c_offset.length:
 
718
                    raise errors.ShortReadvError(relpath, c_offset.start,
 
719
                                c_offset.length, actual=len(data))
 
720
                for suboffset, subsize in c_offset.ranges:
 
721
                    key = (c_offset.start+suboffset, subsize)
 
722
                    data_map[key] = data[suboffset:suboffset+subsize]
723
723
 
724
 
            # Now that we've read some data, see if we can yield anything back
725
 
            while cur_offset_and_size in data_map:
726
 
                this_data = data_map.pop(cur_offset_and_size)
727
 
                this_offset = cur_offset_and_size[0]
728
 
                try:
729
 
                    cur_offset_and_size = offset_stack.next()
730
 
                except StopIteration:
731
 
                    # Close the file handle as there will be no more data
732
 
                    # The handle would normally be cleaned up as this code goes
733
 
                    # out of scope, but as we are a generator, not all code
734
 
                    # will re-enter once we have consumed all the expected
735
 
                    # data. For example:
736
 
                    #   zip(range(len(requests)), readv(foo, requests))
737
 
                    # Will stop because the range is done, and not run the
738
 
                    # cleanup code for the readv().
739
 
                    fp.close()
740
 
                    cur_offset_and_size = None
741
 
                yield this_offset, this_data
 
724
                # Now that we've read some data, see if we can yield anything back
 
725
                while cur_offset_and_size in data_map:
 
726
                    this_data = data_map.pop(cur_offset_and_size)
 
727
                    this_offset = cur_offset_and_size[0]
 
728
                    try:
 
729
                        cur_offset_and_size = offset_stack.next()
 
730
                    except StopIteration:
 
731
                        fp.close()
 
732
                        cur_offset_and_size = None
 
733
                    yield this_offset, this_data
 
734
        finally:
 
735
            fp.close()
742
736
 
743
737
    def _sort_expand_and_combine(self, offsets, upper_limit):
744
738
        """Helper for readv.
1363
1357
        """
1364
1358
        if not base.endswith('/'):
1365
1359
            base += '/'
1366
 
        (self._scheme,
1367
 
         self._user, self._password,
1368
 
         self._host, self._port,
1369
 
         self._path) = self._split_url(base)
 
1360
        self._parsed_url = self._split_url(base)
1370
1361
        if _from_transport is not None:
1371
1362
            # Copy the password as it does not appear in base and will be lost
1372
1363
            # otherwise. It can appear in the _split_url above if the user
1373
1364
            # provided it on the command line. Otherwise, daughter classes will
1374
1365
            # prompt the user for one when appropriate.
1375
 
            self._password = _from_transport._password
 
1366
            self._parsed_url.password = _from_transport._parsed_url.password
 
1367
            self._parsed_url.quoted_password = (
 
1368
                _from_transport._parsed_url.quoted_password)
1376
1369
 
1377
 
        base = self._unsplit_url(self._scheme,
1378
 
                                 self._user, self._password,
1379
 
                                 self._host, self._port,
1380
 
                                 self._path)
 
1370
        base = str(self._parsed_url)
1381
1371
 
1382
1372
        super(ConnectedTransport, self).__init__(base)
1383
1373
        if _from_transport is None:
1385
1375
        else:
1386
1376
            self._shared_connection = _from_transport._shared_connection
1387
1377
 
 
1378
    @property
 
1379
    def _user(self):
 
1380
        return self._parsed_url.user
 
1381
 
 
1382
    @property
 
1383
    def _password(self):
 
1384
        return self._parsed_url.password
 
1385
 
 
1386
    @property
 
1387
    def _host(self):
 
1388
        return self._parsed_url.host
 
1389
 
 
1390
    @property
 
1391
    def _port(self):
 
1392
        return self._parsed_url.port
 
1393
 
 
1394
    @property
 
1395
    def _path(self):
 
1396
        return self._parsed_url.path
 
1397
 
 
1398
    @property
 
1399
    def _scheme(self):
 
1400
        return self._parsed_url.scheme
 
1401
 
1388
1402
    def clone(self, offset=None):
1389
1403
        """Return a new transport with root at self.base + offset
1390
1404
 
1398
1412
 
1399
1413
    @staticmethod
1400
1414
    def _split_url(url):
1401
 
        return urlutils.parse_url(url)
 
1415
        return urlutils.URL.from_string(url)
1402
1416
 
1403
1417
    @staticmethod
1404
1418
    def _unsplit_url(scheme, user, password, host, port, path):
1405
 
        """
1406
 
        Build the full URL for the given already URL encoded path.
 
1419
        """Build the full URL for the given already URL encoded path.
1407
1420
 
1408
1421
        user, password, host and path will be quoted if they contain reserved
1409
1422
        chars.
1410
1423
 
1411
1424
        :param scheme: protocol
1412
 
 
1413
1425
        :param user: login
1414
 
 
1415
1426
        :param password: associated password
1416
 
 
1417
1427
        :param host: the server address
1418
 
 
1419
1428
        :param port: the associated port
1420
 
 
1421
1429
        :param path: the absolute path on the server
1422
1430
 
1423
1431
        :return: The corresponding URL.
1435
1443
 
1436
1444
    def relpath(self, abspath):
1437
1445
        """Return the local path portion from a given absolute path"""
1438
 
        scheme, user, password, host, port, path = self._split_url(abspath)
 
1446
        parsed_url = self._split_url(abspath)
1439
1447
        error = []
1440
 
        if (scheme != self._scheme):
 
1448
        if parsed_url.scheme != self._parsed_url.scheme:
1441
1449
            error.append('scheme mismatch')
1442
 
        if (user != self._user):
 
1450
        if parsed_url.user != self._parsed_url.user:
1443
1451
            error.append('user name mismatch')
1444
 
        if (host != self._host):
 
1452
        if parsed_url.host != self._parsed_url.host:
1445
1453
            error.append('host mismatch')
1446
 
        if (port != self._port):
 
1454
        if parsed_url.port != self._parsed_url.port:
1447
1455
            error.append('port mismatch')
1448
 
        if not (path == self._path[:-1] or path.startswith(self._path)):
 
1456
        if (not (parsed_url.path == self._parsed_url.path[:-1] or
 
1457
            parsed_url.path.startswith(self._parsed_url.path))):
1449
1458
            error.append('path mismatch')
1450
1459
        if error:
1451
1460
            extra = ', '.join(error)
1452
1461
            raise errors.PathNotChild(abspath, self.base, extra=extra)
1453
 
        pl = len(self._path)
1454
 
        return path[pl:].strip('/')
 
1462
        pl = len(self._parsed_url.path)
 
1463
        return parsed_url.path[pl:].strip('/')
1455
1464
 
1456
1465
    def abspath(self, relpath):
1457
1466
        """Return the full url to the given relative path.
1460
1469
 
1461
1470
        :returns: the Unicode version of the absolute path for relpath.
1462
1471
        """
1463
 
        relative = urlutils.unescape(relpath).encode('utf-8')
1464
 
        path = self._combine_paths(self._path, relative)
1465
 
        return self._unsplit_url(self._scheme, self._user, self._password,
1466
 
                                 self._host, self._port,
1467
 
                                 path)
 
1472
        return str(self._parsed_url.clone(relpath))
1468
1473
 
1469
1474
    def _remote_path(self, relpath):
1470
1475
        """Return the absolute path part of the url to the given relative path.
1477
1482
 
1478
1483
        :return: the absolute Unicode path on the server,
1479
1484
        """
1480
 
        relative = urlutils.unescape(relpath).encode('utf-8')
1481
 
        remote_path = self._combine_paths(self._path, relative)
1482
 
        return remote_path
 
1485
        return self._parsed_url.clone(relpath).path
1483
1486
 
1484
1487
    def _get_shared_connection(self):
1485
1488
        """Get the object shared amongst cloned transports.
1546
1549
        :return: A new transport or None if the connection cannot be shared.
1547
1550
        """
1548
1551
        try:
1549
 
            (scheme, user, password,
1550
 
             host, port, path) = self._split_url(other_base)
 
1552
            parsed_url = self._split_url(other_base)
1551
1553
        except errors.InvalidURL:
1552
1554
            # No hope in trying to reuse an existing transport for an invalid
1553
1555
            # URL
1556
1558
        transport = None
1557
1559
        # Don't compare passwords, they may be absent from other_base or from
1558
1560
        # self and they don't carry more information than user anyway.
1559
 
        if (scheme == self._scheme
1560
 
            and user == self._user
1561
 
            and host == self._host
1562
 
            and port == self._port):
 
1561
        if (parsed_url.scheme == self._parsed_url.scheme
 
1562
            and parsed_url.user == self._parsed_url.user
 
1563
            and parsed_url.host == self._parsed_url.host
 
1564
            and parsed_url.port == self._parsed_url.port):
 
1565
            path = parsed_url.path
1563
1566
            if not path.endswith('/'):
1564
1567
                # This normally occurs at __init__ time, but it's easier to do
1565
1568
                # it now to avoid creating two transports for the same base.
1566
1569
                path += '/'
1567
 
            if self._path  == path:
 
1570
            if self._parsed_url.path  == path:
1568
1571
                # shortcut, it's really the same transport
1569
1572
                return self
1570
1573
            # We don't call clone here because the intent is different: we
1581
1584
        raise NotImplementedError(self.disconnect)
1582
1585
 
1583
1586
 
1584
 
def get_transport(base, possible_transports=None):
1585
 
    """Open a transport to access a URL or directory.
1586
 
 
1587
 
    :param base: either a URL or a directory name.
1588
 
 
1589
 
    :param transports: optional reusable transports list. If not None, created
1590
 
        transports will be added to the list.
1591
 
 
1592
 
    :return: A new transport optionally sharing its connection with one of
1593
 
        possible_transports.
 
1587
def location_to_url(location):
 
1588
    """Determine a fully qualified URL from a location string.
 
1589
 
 
1590
    This will try to interpret location as both a URL and a directory path. It
 
1591
    will also lookup the location in directories.
 
1592
 
 
1593
    :param location: Unicode or byte string object with a location
 
1594
    :raise InvalidURL: If the location is already a URL, but not valid.
 
1595
    :return: Byte string with resulting URL
1594
1596
    """
1595
 
    if base is None:
1596
 
        base = '.'
1597
 
    last_err = None
 
1597
    if not isinstance(location, basestring):
 
1598
        raise AssertionError("location not a byte or unicode string")
1598
1599
    from bzrlib.directory_service import directories
1599
 
    base = directories.dereference(base)
1600
 
 
1601
 
    def convert_path_to_url(base, error_str):
1602
 
        if urlutils.is_url(base):
1603
 
            # This looks like a URL, but we weren't able to
1604
 
            # instantiate it as such raise an appropriate error
1605
 
            # FIXME: we have a 'error_str' unused and we use last_err below
1606
 
            raise errors.UnsupportedProtocol(base, last_err)
1607
 
        # This doesn't look like a protocol, consider it a local path
1608
 
        new_base = urlutils.local_path_to_url(base)
1609
 
        # mutter('converting os path %r => url %s', base, new_base)
1610
 
        return new_base
 
1600
    location = directories.dereference(location)
1611
1601
 
1612
1602
    # Catch any URLs which are passing Unicode rather than ASCII
1613
1603
    try:
1614
 
        base = base.encode('ascii')
 
1604
        location = location.encode('ascii')
1615
1605
    except UnicodeError:
1616
 
        # Only local paths can be Unicode
1617
 
        base = convert_path_to_url(base,
1618
 
            'URLs must be properly escaped (protocol: %s)')
1619
 
 
 
1606
        if urlutils.is_url(location):
 
1607
            raise errors.InvalidURL(path=location,
 
1608
                extra='URLs must be properly escaped')
 
1609
        location = urlutils.local_path_to_url(location)
 
1610
 
 
1611
    if location.startswith("file:") and not location.startswith("file://"):
 
1612
        return urlutils.join(urlutils.local_path_to_url("."), location[5:])
 
1613
 
 
1614
    if not urlutils.is_url(location):
 
1615
        return urlutils.local_path_to_url(location)
 
1616
 
 
1617
    return location
 
1618
 
 
1619
 
 
1620
def get_transport_from_path(path, possible_transports=None):
 
1621
    """Open a transport for a local path.
 
1622
 
 
1623
    :param path: Local path as byte or unicode string
 
1624
    :return: Transport object for path
 
1625
    """
 
1626
    return get_transport_from_url(urlutils.local_path_to_url(path),
 
1627
        possible_transports)
 
1628
 
 
1629
 
 
1630
def get_transport_from_url(url, possible_transports=None):
 
1631
    """Open a transport to access a URL.
 
1632
    
 
1633
    :param base: a URL
 
1634
    :param transports: optional reusable transports list. If not None, created
 
1635
        transports will be added to the list.
 
1636
 
 
1637
    :return: A new transport optionally sharing its connection with one of
 
1638
        possible_transports.
 
1639
    """
1620
1640
    transport = None
1621
1641
    if possible_transports is not None:
1622
1642
        for t in possible_transports:
1623
 
            t_same_connection = t._reuse_for(base)
 
1643
            t_same_connection = t._reuse_for(url)
1624
1644
            if t_same_connection is not None:
1625
1645
                # Add only new transports
1626
1646
                if t_same_connection not in possible_transports:
1627
1647
                    possible_transports.append(t_same_connection)
1628
1648
                return t_same_connection
1629
1649
 
 
1650
    last_err = None
1630
1651
    for proto, factory_list in transport_list_registry.items():
1631
 
        if proto is not None and base.startswith(proto):
1632
 
            transport, last_err = _try_transport_factories(base, factory_list)
 
1652
        if proto is not None and url.startswith(proto):
 
1653
            transport, last_err = _try_transport_factories(url, factory_list)
1633
1654
            if transport:
1634
1655
                if possible_transports is not None:
1635
1656
                    if transport in possible_transports:
1636
1657
                        raise AssertionError()
1637
1658
                    possible_transports.append(transport)
1638
1659
                return transport
1639
 
 
1640
 
    # We tried all the different protocols, now try one last time
1641
 
    # as a local protocol
1642
 
    base = convert_path_to_url(base, 'Unsupported protocol: %s')
1643
 
 
1644
 
    # The default handler is the filesystem handler, stored as protocol None
1645
 
    factory_list = transport_list_registry.get(None)
1646
 
    transport, last_err = _try_transport_factories(base, factory_list)
1647
 
 
1648
 
    return transport
 
1660
    if not urlutils.is_url(url):
 
1661
        raise errors.InvalidURL(path=url)
 
1662
    raise errors.UnsupportedProtocol(url, last_err)
 
1663
 
 
1664
 
 
1665
def get_transport(base, possible_transports=None):
 
1666
    """Open a transport to access a URL or directory.
 
1667
 
 
1668
    :param base: either a URL or a directory name.
 
1669
 
 
1670
    :param transports: optional reusable transports list. If not None, created
 
1671
        transports will be added to the list.
 
1672
 
 
1673
    :return: A new transport optionally sharing its connection with one of
 
1674
        possible_transports.
 
1675
    """
 
1676
    if base is None:
 
1677
        base = '.'
 
1678
    return get_transport_from_url(location_to_url(base), possible_transports)
1649
1679
 
1650
1680
 
1651
1681
def _try_transport_factories(base, factory_list):
1720
1750
register_transport_proto('file://',
1721
1751
            help="Access using the standard filesystem (default)")
1722
1752
register_lazy_transport('file://', 'bzrlib.transport.local', 'LocalTransport')
1723
 
transport_list_registry.set_default_transport("file://")
1724
1753
 
1725
1754
register_transport_proto('sftp://',
1726
1755
            help="Access using SFTP (most SSH servers provide SFTP).",
1793
1822
register_lazy_transport('memory://', 'bzrlib.transport.memory',
1794
1823
                        'MemoryTransport')
1795
1824
 
1796
 
# chroots cannot be implicitly accessed, they must be explicitly created:
1797
 
register_transport_proto('chroot+')
1798
 
 
1799
1825
register_transport_proto('readonly+',
1800
1826
#              help="This modifier converts any transport to be readonly."
1801
1827
            )
1830
1856
register_lazy_transport('nosmart+', 'bzrlib.transport.nosmart',
1831
1857
                        'NoSmartTransportDecorator')
1832
1858
 
1833
 
# These two schemes were registered, but don't seem to have an actual transport
1834
 
# protocol registered
1835
 
for scheme in ['ssh', 'bzr+loopback']:
1836
 
    register_urlparse_netloc_protocol(scheme)
1837
 
del scheme
1838
 
 
1839
1859
register_transport_proto('bzr://',
1840
1860
            help="Fast access using the Bazaar smart server.",
1841
1861
                         register_netloc=True)