[merge] Repository.tarball hpss command

Martin Pool mbp at canonical.com
Mon Apr 16 17:15:33 BST 2007


This adds a new Repository.tarball rpc, which returns in the response
body a tarball containing the contents of the repository.  The goal is
to reduce the number of roundtrips needed for an initial repository
download.

This diff is against the hpss branch.

On both the sender and the receiver this is not taken from or written
directly into the real repository but rather a temporary directory is
used.

This is used by copy_content_into, which is used when making a new
branch.

At the moment there is no scope for detecting data that's already
present on the client.  You can only get the whole repository.  In the
next release we should look at faster downloads of just the missing
data.

For a server 32ms away, this cuts the time to make a new branch of bzr
from about 6m to about 4m.  For higher latency I expect it would be
larger but I have not done a proper measurement yet.

There is still one test failure in this branch,

bzrlib.tests.bzrdir_implementations.test_bzrdir.TestBzrDir.test_sprout_bzrdir_repository_branch_only_source_under_shared(BzrDirMetaFormat1)

which is caused by this change:

-                result_repo.fetch(source_repository, revision_id=revision_id)
+                # would rather do 
+                # source_repository.copy_content_into(result_repo, revision_id=revision_id)
+                # so we can override the copy method
+                if new_result_repo:
+                    # can only do this when the destination is empty
+                    source_repository.copy_content_into(result_repo, revision_id=revision_id)
+                else:
+                    result_repo.fetch(source_repository, revision_id=revision_id)

It looks like the easiest way to fix it is to make fetch use that
tarball operation as well.

I have a couple of questions someone might be able to help with:

Is there still a useful distinction between Repository.copy_content_into
and fetch?

It seems strange that RemoteBranch overrides Branch.sprout, but
RemoteBzrDir doesn't need to override sprout.

-- 
Martin
-------------- next part --------------
=== modified file 'Makefile'
--- Makefile	2007-04-15 10:03:17 +0000
+++ Makefile	2007-04-16 15:33:06 +0000
@@ -19,7 +19,7 @@
 	pyflakes bzrlib | grep -v ' imported but unused'
 
 clean:
-	./setup.py clean
+	python setup.py clean
 	-find . -name "*.pyc" -o -name "*.pyo" | xargs rm -f
 	rm -rf test????.tmp
 

=== modified file 'bzrlib/bzrdir.py'
--- bzrlib/bzrdir.py	2007-04-10 07:06:54 +0000
+++ bzrlib/bzrdir.py	2007-04-16 15:42:51 +0000
@@ -750,6 +750,7 @@
         if revision_id is not None, then the clone operation may tune
             itself to download less data.
         """
+        new_result_repo = False
         self._make_tail(url)
         cloning_format = self.cloning_metadir()
         result = cloning_format.initialize(url)
@@ -780,10 +781,18 @@
             # like is_shared(), and we have not yet implemented a 
             # repository sprout().
             result_repo = result.create_repository()
+            new_result_repo = True
         if result_repo is not None:
             # fetch needed content into target.
             if source_repository is not None:
-                result_repo.fetch(source_repository, revision_id=revision_id)
+                # would rather do 
+                # source_repository.copy_content_into(result_repo, revision_id=revision_id)
+                # so we can override the copy method
+                if new_result_repo:
+                    # can only do this when the destination is empty
+                    source_repository.copy_content_into(result_repo, revision_id=revision_id)
+                else:
+                    result_repo.fetch(source_repository, revision_id=revision_id)
         if source_branch is not None:
             source_branch.sprout(result, revision_id=revision_id)
         else:

=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py	2007-04-15 10:03:17 +0000
+++ bzrlib/errors.py	2007-04-15 10:04:39 +0000
@@ -2067,3 +2067,13 @@
 
     def __init__(self, tag_name):
         self.tag_name = tag_name
+
+
+class SmartServerError(BzrError):
+
+    _fmt = "Smart Server encountered error %(error_cde)s: %(error_msg)s."
+
+    def __init__(self, error_code, error_msg=''):
+        self.error_code = error_code
+        self.error_msg = error_msg
+

=== modified file 'bzrlib/remote.py'
--- bzrlib/remote.py	2007-04-16 04:14:36 +0000
+++ bzrlib/remote.py	2007-04-16 15:16:20 +0000
@@ -29,6 +29,7 @@
 from bzrlib.lockable_files import LockableFiles
 from bzrlib.revision import NULL_REVISION
 from bzrlib.smart import client, vfs
+from bzrlib.trace import note
 from bzrlib.urlutils import unescape
 
 # Note: RemoteBzrDirFormat is in bzrdir.py
@@ -425,6 +426,20 @@
         self._ensure_real()
         return self._real_repository.break_lock()
 
+    def _get_tarball(self, compression):
+        """See Repository.tarball()."""
+        path = self.bzrdir._path_for_remote_call(self._client)
+        response, protocol = self._client.call_expecting_body(
+            'Repository.tarball', path, compression)
+        assert response[0] in ('ok', 'failure'), \
+            'unexpected response code %s' % (response,)
+        if response[0] == 'ok':
+            # Extract the tarball and return it
+            body = protocol.read_body_bytes()
+            return body
+        else:
+            raise errors.SmartServerError(error_code=response)
+
     ### These methods are just thin shims to the VFS object for now.
 
     def revision_tree(self, revision_id):
@@ -561,9 +576,27 @@
         return self._real_repository.check(revision_ids)
 
     def copy_content_into(self, destination, revision_id=None):
-        self._ensure_real()
-        return self._real_repository.copy_content_into(
-            destination, revision_id=revision_id)
+        # get a tarball of the remote repository, and copy from that into the
+        # destination
+        from bzrlib import osutils
+        import tarfile
+        import tempfile
+        from StringIO import StringIO
+        # TODO: Maybe a progress bar while streaming the tarball?
+        note("Copying repository content as tarball...")
+        tar_file = StringIO(self._get_tarball('bz2'))
+        tar = tarfile.open('repository', fileobj=tar_file,
+            mode='r:bz2')
+        tmpdir = tempfile.mkdtemp()
+        try:
+            tar.extractall(tmpdir)
+            tmp_bzrdir = BzrDir.open(tmpdir)
+            tmp_repo = tmp_bzrdir.open_repository()
+            tmp_repo.copy_content_into(destination, revision_id)
+        finally:
+            osutils.rmtree(tmpdir)
+        # TODO: if the server doesn't support this operation, maybe do it the
+        # slow way using the _real_repository?
 
     def set_make_working_trees(self, new_value):
         raise NotImplementedError(self.set_make_working_trees)
@@ -907,9 +940,8 @@
         # format, because RemoteBranches can't be created at arbitrary URLs.
         # XXX: if to_bzrdir is a RemoteBranch, this should perhaps do
         # to_bzrdir.create_branch...
-        self._ensure_real()
         result = branch.BranchFormat.get_default_format().initialize(to_bzrdir)
-        self._real_branch.copy_content_into(result, revision_id=revision_id)
+        self.copy_content_into(result, revision_id=revision_id)
         result.set_parent(self.bzrdir.root_transport.base)
         return result
 

=== modified file 'bzrlib/smart/repository.py'
--- bzrlib/smart/repository.py	2007-03-29 04:59:53 +0000
+++ bzrlib/smart/repository.py	2007-04-16 03:21:04 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006 Canonical Ltd
+# Copyright (C) 2006, 2007 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -16,10 +16,14 @@
 
 """Server-side repository related request implmentations."""
 
+import sys
+import tempfile
+import tarfile
 
 from bzrlib import errors
 from bzrlib.bzrdir import BzrDir
 from bzrlib.smart.request import SmartServerRequest, SmartServerResponse
+from bzrlib.transport.local import LocalTransport
 
 
 class SmartServerRepositoryRequest(SmartServerRequest):
@@ -173,3 +177,59 @@
         repository.unlock()
         return SmartServerResponse(('ok',))
 
+
+class SmartServerRepositoryTarball(SmartServerRepositoryRequest):
+    """Get the raw repository files as a tarball.
+
+    The returned tarball contains a .bzr control directory which in turn
+    contains a repository.
+    
+    This takes one parameter, compression, which currently must be 
+    "", "gz", or "bz2".
+
+    This is used to implement the Repository.copy_content_into operation.
+    """
+
+    def do_repository_request(self, repository, compression):
+        from bzrlib import osutils
+        repo_transport = repository.control_files._transport
+        tmp_dirname, tmp_repo = self._copy_to_tempdir(repository)
+        try:
+            controldir_name = tmp_dirname + '/.bzr'
+            return self._tarfile_response(controldir_name, compression)
+        finally:
+            osutils.rmtree(tmp_dirname)
+
+    def _copy_to_tempdir(self, from_repo):
+        tmp_dirname = tempfile.mkdtemp(prefix='tmpbzrclone')
+        tmp_bzrdir = from_repo.bzrdir._format.initialize(tmp_dirname)
+        tmp_repo = from_repo._format.initialize(tmp_bzrdir)
+        from_repo.copy_content_into(tmp_repo)
+        return tmp_dirname, tmp_repo
+
+    def _tarfile_response(self, tmp_dirname, compression):
+        temp = tempfile.NamedTemporaryFile()
+        try:
+            self._tarball_of_dir(tmp_dirname, compression, temp.name)
+            # all finished; write the tempfile out to the network
+            temp.seek(0)
+            return SmartServerResponse(('ok',), temp.read())
+            # FIXME: Don't read the whole thing into memory here; rather stream it
+            # out from the file onto the network. mbp 20070411
+        finally:
+            temp.close()
+
+    def _tarball_of_dir(self, dirname, compression, tarfile_name):
+        tarball = tarfile.open(tarfile_name, mode='w:' + compression)
+        try:
+            # The tarball module only accepts ascii names, and (i guess)
+            # packs them with their 8bit names.  We know all the files
+            # within the repository have ASCII names so the should be safe
+            # to pack in.
+            dirname = dirname.encode(sys.getfilesystemencoding())
+            # python's tarball module includes the whole path by default so
+            # override it
+            assert dirname.endswith('.bzr')
+            tarball.add(dirname, '.bzr') # recursive by default
+        finally:
+            tarball.close()

=== modified file 'bzrlib/smart/request.py'
--- bzrlib/smart/request.py	2007-04-16 09:45:25 +0000
+++ bzrlib/smart/request.py	2007-04-16 10:01:15 +0000
@@ -312,6 +312,9 @@
 request_handlers.register_lazy(
     'Repository.unlock', 'bzrlib.smart.repository', 'SmartServerRepositoryUnlock')
 request_handlers.register_lazy(
+    'Repository.tarball', 'bzrlib.smart.repository',
+    'SmartServerRepositoryTarball')
+request_handlers.register_lazy(
     'rmdir', 'bzrlib.smart.vfs', 'RmdirRequest')
 request_handlers.register_lazy(
     'stat', 'bzrlib.smart.vfs', 'StatRequest')

=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py	2007-04-16 09:45:25 +0000
+++ bzrlib/tests/__init__.py	2007-04-16 10:02:59 +0000
@@ -753,13 +753,16 @@
         self._benchtime = None
         # prevent hooks affecting tests
         self._preserved_hooks = {
-            bzrlib.branch.Branch:bzrlib.branch.Branch.hooks,
-            bzrlib.smart.server.SmartTCPServer:bzrlib.smart.server.SmartTCPServer.hooks,
+            bzrlib.branch.Branch: bzrlib.branch.Branch.hooks,
+            bzrlib.smart.server.SmartTCPServer: bzrlib.smart.server.SmartTCPServer.hooks,
             }
         self.addCleanup(self._restoreHooks)
-        # this list of hooks must be kept in sync with the defaults
-        # in branch.py
+        # reset all hooks to an empty instance of the appropriate type
         bzrlib.branch.Branch.hooks = bzrlib.branch.BranchHooks()
+        bzrlib.smart.server.SmartTCPServer.hooks = bzrlib.smart.server.SmartServerHooks()
+        # FIXME: Rather than constructing new objects like this, how about
+        # having save() and clear() methods on the base Hook class? mbp
+        # 20070416
 
     def _silenceUI(self):
         """Turn off UI for duration of test"""
@@ -1714,7 +1717,13 @@
     def get_vfs_only_url(self, relpath=None):
         """Get a URL (or maybe a path for the plain old vfs transport.
 
-        This will never be a smart protocol.
+        This will never be a smart protocol.  It always has all the
+        capabilities of the local filesystem, but it might actually be a
+        MemoryTransport or some other similar virtual filesystem.
+
+        This is the backing transport (if any) of the server returned by 
+        get_url and get_readonly_url.
+
         :param relpath: provides for clients to get a path relative to the base
             url.  These should only be downwards relative, not upwards.
         """
@@ -1780,7 +1789,14 @@
             raise TestSkipped("Format %s is not initializable." % format)
 
     def make_repository(self, relpath, shared=False, format=None):
-        """Create a repository on our default transport at relpath."""
+        """Create a repository on our default transport at relpath.
+        
+        Note that relpath must be a relative path, not a full url.
+        """
+        # FIXME: If you create a remoterepository this returns the underlying
+        # real format, which is incorrect.  Actually we should make sure that 
+        # RemoteBzrDir returns a RemoteRepository.
+        # maybe  mbp 20070410
         made_control = self.make_bzrdir(relpath, format=format)
         return made_control.create_repository(shared=shared)
 

=== modified file 'bzrlib/tests/test_remote.py'
--- bzrlib/tests/test_remote.py	2007-04-16 04:14:36 +0000
+++ bzrlib/tests/test_remote.py	2007-04-16 15:24:34 +0000
@@ -19,6 +19,8 @@
 These are proxy objects which act on remote objects by sending messages
 through a smart client.  The proxies are to be created when attempting to open
 the object given a transport that supports smartserver rpc operations. 
+
+These tests correspond to tests.test_smart, which exercises the server side.
 """
 
 from cStringIO import StringIO
@@ -27,6 +29,7 @@
     bzrdir,
     errors,
     remote,
+    repository,
     tests,
     )
 from bzrlib.branch import Branch
@@ -364,9 +367,22 @@
 
 
 class TestRemoteRepository(tests.TestCase):
+    """Base for testing RemoteRepository protocol usage.
+    
+    These tests contain frozen requests and responses.  We want any changes to 
+    what is sent or expected to be require a thoughtful update to these tests
+    because they might break compatibility with different-versioned servers.
+    """
 
     def setup_fake_client_and_repository(self, responses, transport_path):
-        """Create the fake client and repository for testing with."""
+        """Create the fake client and repository for testing with.
+        
+        There's no real server here; we just have canned responses sent
+        back one by one.
+        
+        :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)
@@ -604,3 +620,86 @@
 
         # The remote repo shouldn't be accessed.
         self.assertEqual([], client._calls)
+
+
+class TestRepositoryTarball(TestRemoteRepository):
+
+    # This is a canned tarball reponse we can validate against
+    tarball_content = (
+        "BZh91AY&SY\xd1\xa4\x8f|\x00\x01a\x7f\x93\xc6\x90\x00 H\x07\xff\xc0"
+        "\xf7\xef\x1c!\x7f\xef\xdfp\x00\x08@\x18\x00\x10\x04\x08@\x0272j\xb2"
+        "\x02I\x14\xf4\xd2\x9eM@\xde\xa8z\x994\x00\x00\x00\r=A\xe4j\t\x12LI"
+        "\x1a\x03OPh\xd0\x00\x00h\x00\x00\x070\t\x80\x99\x18\x01\x18\x98\x98"
+        "L&\x08i\x89\xa6\x01RB\x9a\x9ad\x87\xa8\xd3\x19CC@\x06C at z\x8d\x00\x1aw"
+        "\x80z\xf9\xe5\xc1\n\x06\xccL\xcdW6\x98\xc6\x9f\xf5RLW\xcc\rBA}\x01$"
+        "\xc6Y-\xc3\xc1\xae[\x89\x8a\xbeRC(\xa3Xv\x19\xe9\x95-V\xaa\xa5`7A"
+        "\xd9\n\x02\xc1\xafF\xc9l\x06`3\x009\x06!\x92\x92\xed\xfc\xe2\x85\x9e"
+        "yS\xf0M\x0f\x98\xef>d\xd4\xa8\xd9e\xaax,\xc6\x07\x97\xc1]\x9d\xc8"
+        "\xbd^\xee4\x95D\xec6'Z>\xaa\xc0\x199\x0c\x90\xc8\xad\x0eN at q\xc8\x8e"
+        "D\xbd*\xac\x06\xdeg7\xb7\xe1u\xfa+\xa3o\xcfn\x7f M \xa0\xa0y\x90V"
+        "\xae^\\_\x16\xcc\x1a\x83,s\xd2nRH9b\xc8\x95q\xa6I\xba\xb7:(\xb8"
+        "\xba\xc8\xa3\xa4\x80%\x0e\x94\x89N\xa4\xea\x17\xca\xb4\xc9\x8c"
+        "\xaf\x19\x81\xb9%E\xe0\xdd\xc0i\x85\x82\xd7dV\xc0\x08\xb2T\x95"
+        "\t\xde\x00\xb4\x00\x92-\xbc\xa1,z\xf2}\xbdx<\xa7\xa0\xadP\xdb"
+        "k\xe5<8\xbe\x8aB\xdc\xdd3\x91\xcb\x8c\x07<\xfe\x9e\xa7\xd2X\x1aYO,"
+        "\x92\x03\xbb\xc6\xeaONL\xae\xc2\x17\x92rG\xa3\x14\xb2\xb9O\x86N\xa3"
+        "A\x9dk\xdb\xe2h\x88i\x0fT'\xec\x9a\xaa\x94\xa0\t\xa8$)\x94\xf2\xa9"
+        "\xdaz\x81X\x07\xf0\x04f\x8e\xc1Z\xf3\xdf-S\xf9FY6+&\\{\x1b\xd8\x9a"
+        "\xb5\xf1?\x95h?d\xe0\xff\x03\xa8M\xe0\xefJ\x95$\xbb\xab\xa6p\xc8"
+        "\xe9T\xb9\xa2\xa0\xc3=V\xedV313\x13 \xe2\xbc\xd0V*g\xd8\x1b\xf5}S"
+        "J\xd2*\xb7\x1a]!\x86y\xc7*o\xde\x83)\xd6C\xb4\x07\xfc]\xc9\x14\xe1"
+        "BCF\x92=\xf0"
+        )
+
+    def test_repository_tarball(self):
+        # Test that Repository.tarball generates the right operations
+        transport_path = 'repo'
+        expected_responses = [(('ok',), self.tarball_content),
+            ]
+        expected_calls = [('call_expecting_body', 'Repository.tarball',
+                           ('///repo/', 'bz2',),),
+            ]
+        remote_repo, client = self.setup_fake_client_and_repository(
+            expected_responses, transport_path)
+        # Now actually ask for the tarball
+        tarball_data = remote_repo._get_tarball('bz2')
+        self.assertEqual(expected_calls, client._calls)
+        self.assertEqual(self.tarball_content, tarball_data)
+
+    def test_copy_content_into_uses_tarball(self):
+        # copy_content_into on a RemoteRepository should try to use the
+        # tarball command rather than accessing all the files
+        transport_path = 'srcrepo'
+        expected_responses = [(('ok',), self.tarball_content),
+            ]
+        expected_calls = [('call2', 'Repository.tarball', ('///srcrepo/', 'bz2',),),
+            ]
+        remote_repo, client = self.setup_fake_client_and_repository(
+            expected_responses, transport_path)
+        # make a regular local repository to receive the results
+        dest_transport = MemoryTransport()
+        dest_transport.mkdir('destrepo')
+        bzrdir_format = bzrdir.format_registry.make_bzrdir('default')
+        dest_bzrdir = bzrdir_format.initialize_on_transport(dest_transport)
+        dest_repo = dest_bzrdir.create_repository()
+        # try to copy...
+        remote_repo.copy_content_into(dest_repo)
+
+
+class TestRemoteRepositoryCopyContent(tests.TestCaseWithTransport):
+    """RemoteRepository.copy_content_into optimizations"""
+
+    def test_copy_content_remote_to_local(self):
+        self.transport_server = server.SmartTCPServer_for_testing
+        src_repo = self.make_repository('repo1')
+        src_repo = repository.Repository.open(self.get_url('repo1'))
+        # At the moment the tarball-based copy_content_into can't write back
+        # into a smart server.  It would be good if it could upload the
+        # tarball; once that works we'd have to create repositories of
+        # different formats. -- mbp 20070410
+        dest_url = self.get_vfs_only_url('repo2')
+        dest_bzrdir = BzrDir.create(dest_url)
+        dest_repo = dest_bzrdir.create_repository()
+        self.assertFalse(isinstance(dest_repo, RemoteRepository))
+        self.assertTrue(isinstance(src_repo, RemoteRepository))
+        src_repo.copy_content_into(dest_repo)

=== modified file 'bzrlib/tests/test_smart.py'
--- bzrlib/tests/test_smart.py	2007-04-15 10:03:17 +0000
+++ bzrlib/tests/test_smart.py	2007-04-16 03:25:57 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006 Canonical Ltd
+# Copyright (C) 2006, 2007 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -16,6 +16,10 @@
 
 """Tests for the smart wire/domain protococl."""
 
+from StringIO import StringIO
+import tempfile
+import tarfile
+
 from bzrlib import bzrdir, errors, smart, tests
 from bzrlib.smart.request import SmartServerResponse
 import bzrlib.smart.bzrdir
@@ -737,6 +741,30 @@
             SmartServerResponse(('TokenMismatch',)), response)
 
 
+class TestSmartServerRepositoryTarball(tests.TestCaseWithTransport):
+
+    def test_repository_tarball(self):
+        backing = self.get_transport()
+        request = smart.repository.SmartServerRepositoryTarball(backing)
+        repository = self.make_repository('.')
+        # make some extraneous junk in the repository directory which should
+        # not be copied
+        self.build_tree(['.bzr/repository/extra-junk'])
+        response = request.execute(backing.local_abspath(''), 'bz2')
+        self.assertEqual(('ok',), response.args)
+        # body should be a tbz2
+        body_file = StringIO(response.body)
+        body_tar = tarfile.open('body_tar.tbz2', fileobj=body_file,
+            mode='r:bz2')
+        # let's make sure there are some key repository components inside it.
+        # the tarfile returns directories with trailing slashes...
+        names = set([n.rstrip('/') for n in body_tar.getnames()])
+        self.assertTrue('.bzr/repository/lock' in names)
+        self.assertTrue('.bzr/repository/format' in names)
+        self.assertTrue('.bzr/repository/extra-junk' not in names,
+            "extraneous file present in tar file")
+
+
 class TestSmartServerIsReadonly(tests.TestCaseWithTransport):
 
     def test_is_readonly_no(self):
@@ -805,5 +833,8 @@
             smart.request.request_handlers.get('Repository.unlock'),
             smart.repository.SmartServerRepositoryUnlock)
         self.assertEqual(
+            smart.request.request_handlers.get('Repository.tarball'),
+            smart.repository.SmartServerRepositoryTarball)
+        self.assertEqual(
             smart.request.request_handlers.get('Transport.is_readonly'),
             smart.request.SmartServerIsReadonly)

=== modified file 'bzrlib/tests/test_transport.py'
--- bzrlib/tests/test_transport.py	2007-03-29 04:59:58 +0000
+++ bzrlib/tests/test_transport.py	2007-04-16 03:31:43 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2004, 2005, 2006 Canonical Ltd
+# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -21,7 +21,10 @@
 from cStringIO import StringIO
 
 import bzrlib
-from bzrlib import urlutils
+from bzrlib import (
+    errors,
+    urlutils,
+    )
 from bzrlib.errors import (ConnectionError,
                            DependencyNotPresent,
                            FileExists,
@@ -121,6 +124,12 @@
         self.assertEqual('/etc',
                          t._combine_paths('/home/sarah', '/etc'))
 
+    def test_local_abspath_non_local_transport(self):
+        # the base implementation should throw
+        t = MemoryTransport()
+        e = self.assertRaises(errors.NotLocalUrl, t.local_abspath, 't')
+        self.assertEqual('memory:///t is not a local path.', str(e))
+
 
 class TestCoalesceOffsets(TestCase):
     
@@ -461,7 +470,7 @@
         transport = self.get_nfs_transport('.')
         self.build_tree(['from/', 'from/foo', 'to/', 'to/bar'],
                         transport=transport)
-        self.assertRaises(bzrlib.errors.ResourceBusy,
+        self.assertRaises(errors.ResourceBusy,
                           transport.rename, 'from', 'to')
 
 
@@ -560,6 +569,11 @@
         self.assertIsInstance(t, LocalTransport)
         self.assertEquals(t.base, here_url)
 
+    def test_local_abspath(self):
+        here = os.path.abspath('.')
+        t = get_transport(here)
+        self.assertEquals(t.local_abspath(''), here)
+
 
 class TestWin32LocalTransport(TestCase):
 

=== modified file 'bzrlib/tests/test_transport_implementations.py'
--- bzrlib/tests/test_transport_implementations.py	2007-04-16 04:14:36 +0000
+++ bzrlib/tests/test_transport_implementations.py	2007-04-16 15:33:07 +0000
@@ -1143,8 +1143,8 @@
         transport = self.get_transport()
         try:
             p = transport.local_abspath('.')
-        except TransportNotPossible:
-            pass # This is not a local transport
+        except (errors.NotLocalUrl, TransportNotPossible), e:
+            s = str(e)
         else:
             self.assertEqual(getcwd(), p)
 

=== modified file 'bzrlib/transport/__init__.py'
--- bzrlib/transport/__init__.py	2007-04-16 09:45:25 +0000
+++ bzrlib/transport/__init__.py	2007-04-16 15:33:06 +0000
@@ -404,8 +404,7 @@
         physical local filesystem representation.
         """
         # TODO: jam 20060426 Should this raise NotLocalUrl instead?
-        raise errors.TransportNotPossible('This is not a LocalTransport,'
-            ' so there is no local representation for a path')
+        raise errors.NotLocalUrl(self.abspath(relpath))
 
     def has(self, relpath):
         """Does the file relpath exist?



More information about the bazaar mailing list