Rev 267: Switch to using CommitBuilder in lp:~jameinel/bzr-fastimport/trunk

John Arbash Meinel john at arbash-meinel.com
Fri Dec 11 14:55:05 GMT 2009


At lp:~jameinel/bzr-fastimport/trunk

------------------------------------------------------------
revno: 267 [merge]
revision-id: john at arbash-meinel.com-20091211145504-ez9cj300ty9g4ly4
parent: ian.clatworthy at canonical.com-20091208063153-k6e30yvea4vqhczq
parent: john at arbash-meinel.com-20091209201446-cqvb4hhugk8lf9m0
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: trunk
timestamp: Fri 2009-12-11 08:55:04 -0600
message:
  Switch to using CommitBuilder
  Rather than holding most of the logic ourselves.
added:
  tests/test_revision_store.py   test_revision_store.-20091209195309-1n6nymzyp11333z4-1
modified:
  bzr_commit_handler.py          bzr_commit_handler.p-20090219023154-k6kmec3yofultgot-1
  cache_manager.py               cache_manager.py-20090218011515-0mzjwhy87ila5v51-1
  revision_store.py              revisionloader.py-20080215075452-zn0u2pky2jnf7j07-1
  tests/__init__.py              __init__.py-20080214062642-3skl9e4t111bnuu3-15
  tests/test_generic_processor.py test_processor.py-20080308155038-70jd11gikr0aguq9-1
-------------- next part --------------
=== modified file 'bzr_commit_handler.py'
--- a/bzr_commit_handler.py	2009-11-30 21:31:31 +0000
+++ b/bzr_commit_handler.py	2009-12-11 14:55:04 +0000
@@ -71,9 +71,9 @@
         """Prepare for committing."""
         self.revision_id = self.gen_revision_id()
         # cache of texts for this commit, indexed by file-id
-        self.lines_for_commit = {}
+        self.data_for_commit = {}
         #if self.rev_store.expects_rich_root():
-        self.lines_for_commit[inventory.ROOT_ID] = []
+        self.data_for_commit[inventory.ROOT_ID] = []
 
         # Track the heads and get the real parent list
         parents = self.cache_mgr.track_heads(self.command)
@@ -126,9 +126,13 @@
             self.cache_mgr.inventories[revision_id] = inv
         return inv
 
+    def _get_data(self, file_id):
+        """Get the data bytes for a file-id."""
+        return self.data_for_commit[file_id]
+
     def _get_lines(self, file_id):
         """Get the lines for a file-id."""
-        return self.lines_for_commit[file_id]
+        return osutils.split_lines(self._get_data(file_id))
 
     def _get_per_file_parents(self, file_id):
         """Get the lines for a file-id."""
@@ -288,20 +292,20 @@
         ie.revision = self.revision_id
         if kind == 'file':
             ie.executable = is_executable
-            lines = osutils.split_lines(data)
-            ie.text_sha1 = osutils.sha_strings(lines)
-            ie.text_size = sum(map(len, lines))
-            self.lines_for_commit[file_id] = lines
+            # lines = osutils.split_lines(data)
+            ie.text_sha1 = osutils.sha_string(data)
+            ie.text_size = len(data)
+            self.data_for_commit[file_id] = data
         elif kind == 'directory':
             self.directory_entries[path] = ie
             # There are no lines stored for a directory so
             # make sure the cache used by get_lines knows that
-            self.lines_for_commit[file_id] = []
+            self.data_for_commit[file_id] = ''
         elif kind == 'symlink':
             ie.symlink_target = data.encode('utf8')
             # There are no lines stored for a symlink so
             # make sure the cache used by get_lines knows that
-            self.lines_for_commit[file_id] = []
+            self.data_for_commit[file_id] = ''
         else:
             self.warning("Cannot import items of kind '%s' yet - ignoring '%s'"
                 % (kind, path))
@@ -345,7 +349,7 @@
         self.directory_entries[dirname] = ie
         # There are no lines stored for a directory so
         # make sure the cache used by get_lines knows that
-        self.lines_for_commit[dir_file_id] = []
+        self.data_for_commit[dir_file_id] = ''
 
         # It's possible that a file or symlink with that file-id
         # already exists. If it does, we need to delete it.
@@ -415,7 +419,7 @@
         kind = ie.kind
         if kind == 'file':
             if newly_changed:
-                content = ''.join(self.lines_for_commit[file_id])
+                content = self.data_for_commit[file_id]
             else:
                 content = self.rev_store.get_file_text(self.parents[0], file_id)
             self._modify_item(dest_path, kind, ie.executable, content, inv)
@@ -451,7 +455,7 @@
         # that means the loader then needs to know what the "new" text is.
         # We therefore must go back to the revision store to get it.
         lines = self.rev_store.get_file_lines(rev_id, file_id)
-        self.lines_for_commit[file_id] = lines
+        self.data_for_commit[file_id] = ''.join(lines)
 
     def _delete_all_items(self, inv):
         for name, root_item in inv.root.children.iteritems():
@@ -499,7 +503,7 @@
         """Save the revision."""
         self.cache_mgr.inventories[self.revision_id] = self.inventory
         self.rev_store.load(self.revision, self.inventory, None,
-            lambda file_id: self._get_lines(file_id),
+            lambda file_id: self._get_data(file_id),
             lambda file_id: self._get_per_file_parents(file_id),
             lambda revision_ids: self._get_inventories(revision_ids))
 
@@ -598,9 +602,9 @@
         delta = self._get_final_delta()
         inv = self.rev_store.load_using_delta(self.revision,
             self.basis_inventory, delta, None,
-            lambda file_id: self._get_lines(file_id),
-            lambda file_id: self._get_per_file_parents(file_id),
-            lambda revision_ids: self._get_inventories(revision_ids))
+            self._get_data,
+            self._get_per_file_parents,
+            self._get_inventories)
         self.cache_mgr.inventories[self.revision_id] = inv
         #print "committed %s" % self.revision_id
 

=== modified file 'cache_manager.py'
--- a/cache_manager.py	2009-12-08 06:26:34 +0000
+++ b/cache_manager.py	2009-12-11 14:55:04 +0000
@@ -54,6 +54,34 @@
             shutils.rmtree(self.tempdir)
         
 
+
+class _Cleanup(object):
+    """This class makes sure we clean up when CacheManager goes away.
+
+    We use a helper class to ensure that we are never in a refcycle.
+    """
+
+    def __init__(self, disk_blobs):
+        self.disk_blobs = disk_blobs
+        self.tempdir = None
+        self.small_blobs = None
+
+    def __del__(self):
+        self.finalize()
+
+    def finalize(self):
+        if self.disk_blobs is not None:
+            for info in self.disk_blobs.itervalues():
+                if info[-1] is not None:
+                    os.unlink(info[-1])
+            self.disk_blobs = None
+        if self.small_blobs is not None:
+            self.small_blobs.close()
+            self.small_blobs = None
+        if self.tempdir is not None:
+            shutils.rmtree(self.tempdir)
+        
+
 class CacheManager(object):
     
     _small_blob_threshold = 25*1024

=== modified file 'revision_store.py'
--- a/revision_store.py	2009-10-25 22:05:48 +0000
+++ b/revision_store.py	2009-12-09 20:14:46 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2008 Canonical Ltd
+# Copyright (C) 2008, 2009 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,11 +16,137 @@
 
 """An abstraction of a repository providing just the bits importing needs."""
 
+import cStringIO
 
 from bzrlib import errors, inventory, knit, lru_cache, osutils, trace
 from bzrlib import revision as _mod_revision
 
 
+class _TreeShim(object):
+    """Fake a Tree implementation.
+
+    This implements just enough of the tree api to make commit builder happy.
+    """
+
+    def __init__(self, repo, basis_inv, inv_delta, content_provider):
+        self._repo = repo
+        self._content_provider = content_provider
+        self._basis_inv = basis_inv
+        self._inv_delta = inv_delta
+        self._new_info_by_id = dict([(file_id, (new_path, ie))
+                                    for _, new_path, file_id, ie in inv_delta])
+
+    def id2path(self, file_id):
+        if file_id in self._new_info_by_id:
+            new_path = self._new_info_by_id[file_id][0]
+            if new_path is None:
+                raise errors.NoSuchId(self, file_id)
+            return new_path
+        return self._basis_inv.id2path(file_id)
+
+    def path2id(self, path):
+        # CommitBuilder currently only requires access to the root id. We don't
+        # build a map of renamed files, etc. One possibility if we ever *do*
+        # need more than just root, is to defer to basis_inv.path2id() and then
+        # check if the file_id is in our _new_info_by_id dict. And in that
+        # case, return _new_info_by_id[file_id][0]
+        if path != '':
+            raise NotImplementedError(_TreeShim.path2id)
+        # TODO: Handle root renames?
+        return self._basis_inv.root.file_id
+
+    def get_file_with_stat(self, file_id, path=None):
+        try:
+            content = self._content_provider(file_id)
+        except KeyError:
+            # The content wasn't shown as 'new'. Just validate this fact
+            assert file_id not in self._new_info_by_id
+            old_ie = self._basis_inv[file_id]
+            old_text_key = (file_id, old_ie.revision)
+            stream = self._repo.texts.get_record_stream([old_text_key],
+                                                        'unordered', True)
+            content = stream.next().get_bytes_as('fulltext')
+        sio = cStringIO.StringIO(content)
+        return sio, None
+
+    def get_symlink_target(self, file_id):
+        if file_id in self._new_info_by_id:
+            ie = self._new_info_by_id[file_id][1]
+            return ie.symlink_target
+        return self._basis_inv[file_id].symlink_target
+
+    def get_reference_revision(self, file_id, path=None):
+        raise NotImplementedError(_TreeShim.get_reference_revision)
+
+    def _delta_to_iter_changes(self):
+        """Convert the inv_delta into an iter_changes repr."""
+        # iter_changes is:
+        #   (file_id,
+        #    (old_path, new_path),
+        #    content_changed,
+        #    (old_versioned, new_versioned),
+        #    (old_parent_id, new_parent_id),
+        #    (old_name, new_name),
+        #    (old_kind, new_kind),
+        #    (old_exec, new_exec),
+        #   )
+        basis_inv = self._basis_inv
+        for old_path, new_path, file_id, ie in self._inv_delta:
+            # Perf: Would this be faster if we did 'if file_id in basis_inv'?
+            # Since the *very* common case is that the file already exists, it
+            # probably is better to optimize for that
+            try:
+                old_ie = basis_inv[file_id]
+            except errors.NoSuchId:
+                old_ie = None
+                if ie is None:
+                    raise AssertionError('How is both old and new None?')
+                    change = (file_id,
+                        (old_path, new_path),
+                        False,
+                        (False, False),
+                        (None, None),
+                        (None, None),
+                        (None, None),
+                        (None, None),
+                        )
+                change = (file_id,
+                    (old_path, new_path),
+                    True,
+                    (False, True),
+                    (None, ie.parent_id),
+                    (None, ie.name),
+                    (None, ie.kind),
+                    (None, ie.executable),
+                    )
+            else:
+                if ie is None:
+                    change = (file_id,
+                        (old_path, new_path),
+                        True,
+                        (True, False),
+                        (old_ie.parent_id, None),
+                        (old_ie.name, None),
+                        (old_ie.kind, None),
+                        (old_ie.executable, None),
+                        )
+                else:
+                    content_modified = (ie.text_sha1 != old_ie.text_sha1
+                                        or ie.text_size != old_ie.text_size)
+                    # TODO: ie.kind != old_ie.kind
+                    # TODO: symlinks changing targets, content_modified?
+                    change = (file_id,
+                        (old_path, new_path),
+                        content_modified,
+                        (True, True),
+                        (old_ie.parent_id, ie.parent_id),
+                        (old_ie.name, ie.name),
+                        (old_ie.kind, ie.kind),
+                        (old_ie.executable, ie.executable),
+                        )
+            yield change
+
+
 class AbstractRevisionStore(object):
 
     def __init__(self, repo):
@@ -224,29 +350,41 @@
                 including an empty inventory for the missing revisions
             If None, a default implementation is provided.
         """
-        # Get the non-ghost parents and their inventories
-        if inventories_provider is None:
-            inventories_provider = self._default_inventories_provider
-        present_parents, parent_invs = inventories_provider(rev.parent_ids)
+        # TODO: set revision_id = rev.revision_id
+        builder = self.repo._commit_builder_class(self.repo,
+            parents=rev.parent_ids, config=None, timestamp=rev.timestamp,
+            timezone=rev.timezone, committer=rev.committer,
+            revprops=rev.properties, revision_id=rev.revision_id)
 
-        # Load the inventory
-        try:
-            rev_id = rev.revision_id
-            rev.inventory_sha1, inv = self._add_inventory_by_delta(
-                rev_id, basis_inv, inv_delta, present_parents, parent_invs)
-        except errors.RevisionAlreadyPresent:
+        if rev.parent_ids:
+            basis_rev_id = rev.parent_ids[0]
+        else:
+            basis_rev_id = _mod_revision.NULL_REVISION
+        tree = _TreeShim(self.repo, basis_inv, inv_delta, text_provider)
+        changes = tree._delta_to_iter_changes()
+        for (file_id, path, fs_hash) in builder.record_iter_changes(
+                tree, basis_rev_id, changes):
+            # So far, we don't *do* anything with the result
             pass
+        builder.finish_inventory()
+        # TODO: This is working around a bug in the bzrlib code base.
+        # 'builder.finish_inventory()' ends up doing:
+        # self.inv_sha1 = self.repository.add_inventory_by_delta(...)
+        # However, add_inventory_by_delta returns (sha1, inv)
+        # And we *want* to keep a handle on both of those objects
+        if isinstance(builder.inv_sha1, tuple):
+            builder.inv_sha1, builder.new_inventory = builder.inv_sha1
+        # This is a duplicate of Builder.commit() since we already have the
+        # Revision object, and we *don't* want to call commit_write_group()
+        rev.inv_sha1 = builder.inv_sha1
+        builder.repository.add_revision(builder._new_revision_id, rev,
+            builder.new_inventory, builder._config)
 
-        # Load the texts, signature and revision
-        file_rev_ids_needing_texts = [(id, ie.revision)
-            for _, n, id, ie in inv_delta
-            if n is not None and ie.revision == rev_id]
-        self._load_texts_for_file_rev_ids(file_rev_ids_needing_texts,
-            text_provider, parents_provider)
         if signature is not None:
+            raise AssertionError('signatures not guaranteed yet')
             self.repo.add_signature_text(rev_id, signature)
-        self._add_revision(rev, inv)
-        return inv
+        # self._add_revision(rev, inv)
+        return builder.revision_tree().inventory
 
     def _non_root_entries_iter(self, inv, revision_id):
         if hasattr(inv, 'iter_non_root_entries'):

=== modified file 'tests/__init__.py'
--- a/tests/__init__.py	2009-02-25 11:33:28 +0000
+++ b/tests/__init__.py	2009-12-09 20:14:46 +0000
@@ -21,15 +21,16 @@
 
 
 def test_suite():
-    module_names = [
-        'bzrlib.plugins.fastimport.tests.test_branch_mapper',
-        'bzrlib.plugins.fastimport.tests.test_commands',
-        'bzrlib.plugins.fastimport.tests.test_errors',
-        'bzrlib.plugins.fastimport.tests.test_filter_processor',
-        'bzrlib.plugins.fastimport.tests.test_generic_processor',
-        'bzrlib.plugins.fastimport.tests.test_head_tracking',
-        'bzrlib.plugins.fastimport.tests.test_helpers',
-        'bzrlib.plugins.fastimport.tests.test_parser',
-        ]
+    module_names = [__name__ + '.' + x for x in [
+        'test_branch_mapper',
+        'test_commands',
+        'test_errors',
+        'test_filter_processor',
+        'test_generic_processor',
+        'test_head_tracking',
+        'test_helpers',
+        'test_parser',
+        'test_revision_store',
+        ]]
     loader = TestLoader()
     return loader.loadTestsFromModuleNames(module_names)

=== modified file 'tests/test_generic_processor.py'
--- a/tests/test_generic_processor.py	2009-11-12 07:51:21 +0000
+++ b/tests/test_generic_processor.py	2009-11-12 14:07:24 +0000
@@ -1832,3 +1832,54 @@
     def test_import_symlink(self):
         handler, branch = self.get_handler()
         handler.process(self.get_command_iter('foo', 'symlink', 'bar'))
+
+
+class TestModifyRevertInBranch(TestCaseForGenericProcessor):
+
+    def file_command_iter(self):
+        # A     add 'foo'
+        # |\
+        # | B   modify 'foo'
+        # | |
+        # | C   revert 'foo' back to A
+        # |/
+        # D     merge 'foo'
+        def command_list():
+            committer_a = ['', 'a at elmer.com', time.time(), time.timezone]
+            committer_b = ['', 'b at elmer.com', time.time(), time.timezone]
+            committer_c = ['', 'c at elmer.com', time.time(), time.timezone]
+            committer_d = ['', 'd at elmer.com', time.time(), time.timezone]
+            def files_one():
+                yield commands.FileModifyCommand('foo', 'file', False,
+                        None, "content A\n")
+            yield commands.CommitCommand('head', '1', None,
+                committer_a, "commit 1", None, [], files_one)
+            def files_two():
+                yield commands.FileModifyCommand('foo', 'file', False,
+                        None, "content B\n")
+            yield commands.CommitCommand('head', '2', None,
+                committer_b, "commit 2", ":1", [], files_two)
+            def files_three():
+                yield commands.FileModifyCommand('foo', 'file', False,
+                        None, "content A\n")
+            yield commands.CommitCommand('head', '3', None,
+                committer_c, "commit 3", ":2", [], files_three)
+            yield commands.CommitCommand('head', '4', None,
+                committer_d, "commit 4", ":1", [':3'], lambda: [])
+        return command_list
+
+    def test_modify_revert(self):
+        handler, branch = self.get_handler()
+        handler.process(self.file_command_iter())
+        branch.lock_read()
+        self.addCleanup(branch.unlock)
+        rev_d = branch.last_revision()
+        rev_a, rev_c = branch.repository.get_parent_map([rev_d])[rev_d]
+        rev_b = branch.repository.get_parent_map([rev_c])[rev_c][0]
+        rtree_a, rtree_b, rtree_c, rtree_d = branch.repository.revision_trees([
+            rev_a, rev_b, rev_c, rev_d])
+        foo_id = rtree_a.path2id('foo')
+        self.assertEqual(rev_a, rtree_a.inventory[foo_id].revision)
+        self.assertEqual(rev_b, rtree_b.inventory[foo_id].revision)
+        self.assertEqual(rev_c, rtree_c.inventory[foo_id].revision)
+        self.assertEqual(rev_c, rtree_d.inventory[foo_id].revision)

=== added file 'tests/test_revision_store.py'
--- a/tests/test_revision_store.py	1970-01-01 00:00:00 +0000
+++ b/tests/test_revision_store.py	2009-12-09 20:14:46 +0000
@@ -0,0 +1,147 @@
+# Copyright (C) 2008, 2009 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Direct tests of the revision_store classes."""
+
+from bzrlib import (
+    branch,
+    errors,
+    inventory,
+    osutils,
+    tests,
+    )
+
+from bzrlib.plugins.fastimport import (
+    revision_store,
+    )
+
+
+class Test_TreeShim(tests.TestCase):
+
+    def invAddEntry(self, inv, path, file_id=None):
+        if path.endswith('/'):
+            path = path[:-1]
+            kind = 'directory'
+        else:
+            kind = 'file'
+        parent_path, basename = osutils.split(path)
+        parent_id = inv.path2id(parent_path)
+        inv.add(inventory.make_entry(kind, basename, parent_id, file_id))
+
+    def make_trivial_basis_inv(self):
+        basis_inv = inventory.Inventory('TREE_ROOT')
+        self.invAddEntry(basis_inv, 'foo', 'foo-id')
+        self.invAddEntry(basis_inv, 'bar/', 'bar-id')
+        self.invAddEntry(basis_inv, 'bar/baz', 'baz-id')
+        return basis_inv
+        
+    def test_id2path_no_delta(self):
+        basis_inv = self.make_trivial_basis_inv()
+        shim = revision_store._TreeShim(repo=None, basis_inv=basis_inv,
+                                        inv_delta=[], content_provider=None)
+        self.assertEqual('', shim.id2path('TREE_ROOT'))
+        self.assertEqual('foo', shim.id2path('foo-id'))
+        self.assertEqual('bar', shim.id2path('bar-id'))
+        self.assertEqual('bar/baz', shim.id2path('baz-id'))
+        self.assertRaises(errors.NoSuchId, shim.id2path, 'qux-id')
+
+    def test_id2path_with_delta(self):
+        basis_inv = self.make_trivial_basis_inv()
+        foo_entry = inventory.make_entry('file', 'foo2', 'TREE_ROOT', 'foo-id')
+        inv_delta = [('foo', 'foo2', 'foo-id', foo_entry),
+                     ('bar/baz', None, 'baz-id', None),
+                    ]
+
+        shim = revision_store._TreeShim(repo=None, basis_inv=basis_inv,
+                                        inv_delta=inv_delta,
+                                        content_provider=None)
+        self.assertEqual('', shim.id2path('TREE_ROOT'))
+        self.assertEqual('foo2', shim.id2path('foo-id'))
+        self.assertEqual('bar', shim.id2path('bar-id'))
+        self.assertRaises(errors.NoSuchId, shim.id2path, 'baz-id')
+
+    def test_path2id(self):
+        basis_inv = self.make_trivial_basis_inv()
+        shim = revision_store._TreeShim(repo=None, basis_inv=basis_inv,
+                                        inv_delta=[], content_provider=None)
+        self.assertEqual('TREE_ROOT', shim.path2id(''))
+        # We don't want to ever give a wrong value, so for now we just raise
+        # NotImplementedError
+        self.assertRaises(NotImplementedError, shim.path2id, 'bar')
+
+    def test_get_file_with_stat_content_in_stream(self):
+        basis_inv = self.make_trivial_basis_inv()
+
+        def content_provider(file_id):
+            return 'content of\n' + file_id + '\n'
+        
+        shim = revision_store._TreeShim(repo=None, basis_inv=basis_inv,
+                                        inv_delta=[],
+                                        content_provider=content_provider)
+        f_obj, stat_val = shim.get_file_with_stat('baz-id')
+        self.assertIs(None, stat_val)
+        self.assertEqualDiff('content of\nbaz-id\n', f_obj.read())
+
+    # TODO: Test when the content isn't in the stream, and we fall back to the
+    #       repository that was passed in
+
+    def test_get_symlink_target(self):
+        basis_inv = self.make_trivial_basis_inv()
+        ie = inventory.make_entry('symlink', 'link', 'TREE_ROOT', 'link-id')
+        ie.symlink_target = u'link-target'
+        basis_inv.add(ie)
+        shim = revision_store._TreeShim(repo=None, basis_inv=basis_inv,
+                                        inv_delta=[], content_provider=None)
+        self.assertEqual(u'link-target', shim.get_symlink_target('link-id'))
+
+    def test_get_symlink_target_from_delta(self):
+        basis_inv = self.make_trivial_basis_inv()
+        ie = inventory.make_entry('symlink', 'link', 'TREE_ROOT', 'link-id')
+        ie.symlink_target = u'link-target'
+        inv_delta = [(None, 'link', 'link-id', ie)]
+        shim = revision_store._TreeShim(repo=None, basis_inv=basis_inv,
+                                        inv_delta=inv_delta,
+                                        content_provider=None)
+        self.assertEqual(u'link-target', shim.get_symlink_target('link-id'))
+
+    def test__delta_to_iter_changes(self):
+        basis_inv = self.make_trivial_basis_inv()
+        foo_entry = inventory.make_entry('file', 'foo2', 'bar-id', 'foo-id')
+        link_entry = inventory.make_entry('symlink', 'link', 'TREE_ROOT',
+                                          'link-id')
+        link_entry.symlink_target = u'link-target'
+        inv_delta = [('foo', 'bar/foo2', 'foo-id', foo_entry),
+                     ('bar/baz', None, 'baz-id', None),
+                     (None, 'link', 'link-id', link_entry),
+                    ]
+        shim = revision_store._TreeShim(repo=None, basis_inv=basis_inv,
+                                        inv_delta=inv_delta,
+                                        content_provider=None)
+        changes = list(shim._delta_to_iter_changes())
+        expected = [('foo-id', ('foo', 'bar/foo2'), False, (True, True),
+                     ('TREE_ROOT', 'bar-id'), ('foo', 'foo2'),
+                     ('file', 'file'), (False, False)),
+                    ('baz-id', ('bar/baz', None), True, (True, False),
+                     ('bar-id', None), ('baz', None),
+                     ('file', None), (False, None)),
+                    ('link-id', (None, 'link'), True, (False, True),
+                     (None, 'TREE_ROOT'), (None, 'link'),
+                     (None, 'symlink'), (None, False)),
+                   ]
+        # from pprint import pformat
+        # self.assertEqualDiff(pformat(expected), pformat(changes))
+        self.assertEqual(expected, changes)
+                        



More information about the bazaar-commits mailing list