Rev 4588: (jam) Get bundles working with --2a (bug #393349) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Tue Aug 4 17:20:12 BST 2009


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 4588 [merge]
revision-id: pqm at pqm.ubuntu.com-20090804162005-kyldsbg8c018fknc
parent: pqm at pqm.ubuntu.com-20090804144859-bgjydda2yp4422it
parent: john at arbash-meinel.com-20090804141009-uety2n17v1atk5ok
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Tue 2009-08-04 17:20:05 +0100
message:
  (jam) Get bundles working with --2a (bug #393349)
added:
  bzrlib/tests/per_repository/test_merge_directive.py test_send.py-20090717144100-x6fgufcynx6yu5b6-1
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/bundle/serializer/v4.py v10.py-20070611062757-5ggj7k18s9dej0fr-1
  bzrlib/chk_map.py              chk_map.py-20081001014447-ue6kkuhofvdecvxa-1
  bzrlib/chk_serializer.py       chk_serializer.py-20081002064345-2tofdfj2eqq01h4b-1
  bzrlib/send.py                 send.py-20090521192735-j7cdb33ykmtmzx4w-1
  bzrlib/serializer.py           serializer.py-20090402143702-wmkh9cfjhwpju0qi-1
  bzrlib/tests/per_repository/__init__.py __init__.py-20060131092037-9564957a7d4a841b
  bzrlib/tests/test_bundle.py    test.py-20050630184834-092aa401ab9f039c
=== modified file 'NEWS'
--- a/NEWS	2009-08-04 12:35:07 +0000
+++ b/NEWS	2009-08-04 14:10:09 +0000
@@ -54,6 +54,12 @@
 * ``bzr revert .`` no longer generates an InconsistentDelta error when
   there are missing subtrees. (Robert Collins, #367632)
 
+* ``bzr send`` now generates valid bundles with ``--2a`` formats. However,
+  do to internal changes necessary to support this, older clients will
+  fail when trying to insert them. For newer clients, the bundle can be
+  used to apply the changes to any rich-root compatible format.
+  (John Arbash Meinel, #393349)
+
 * Cope with FTP servers that don't support restart/append by falling back
   to reading and then rewriting the whole file, such as TahoeLAFS.  (This
   fallback may be slow for some access patterns.)  (Nils Durner, #294709)

=== modified file 'bzrlib/bundle/serializer/v4.py'
--- a/bzrlib/bundle/serializer/v4.py	2009-06-10 03:56:49 +0000
+++ b/bzrlib/bundle/serializer/v4.py	2009-08-04 14:08:32 +0000
@@ -22,12 +22,14 @@
     diff,
     errors,
     iterablefile,
+    lru_cache,
     multiparent,
     osutils,
     pack,
     revision as _mod_revision,
+    serializer,
     trace,
-    serializer,
+    ui,
     )
 from bzrlib.bundle import bundle_data, serializer as bundle_serializer
 from bzrlib import bencode
@@ -315,12 +317,83 @@
     def write_revisions(self):
         """Write bundle records for all revisions and signatures"""
         inv_vf = self.repository.inventories
-        revision_order = [key[-1] for key in multiparent.topo_iter_keys(inv_vf,
-            self.revision_keys)]
+        topological_order = [key[-1] for key in multiparent.topo_iter_keys(
+                                inv_vf, self.revision_keys)]
+        revision_order = topological_order
         if self.target is not None and self.target in self.revision_ids:
+            # Make sure the target revision is always the last entry
+            revision_order = list(topological_order)
             revision_order.remove(self.target)
             revision_order.append(self.target)
-        self._add_mp_records_keys('inventory', inv_vf, [(revid,) for revid in revision_order])
+        if self.repository._serializer.support_altered_by_hack:
+            # Repositories that support_altered_by_hack means that
+            # inventories.make_mpdiffs() contains all the data about the tree
+            # shape. Formats without support_altered_by_hack require
+            # chk_bytes/etc, so we use a different code path.
+            self._add_mp_records_keys('inventory', inv_vf,
+                                      [(revid,) for revid in topological_order])
+        else:
+            # Inventories should always be added in pure-topological order, so
+            # that we can apply the mpdiff for the child to the parent texts.
+            self._add_inventory_mpdiffs_from_serializer(topological_order)
+        self._add_revision_texts(revision_order)
+
+    def _add_inventory_mpdiffs_from_serializer(self, revision_order):
+        """Generate mpdiffs by serializing inventories.
+
+        The current repository only has part of the tree shape information in
+        the 'inventories' vf. So we use serializer.write_inventory_to_string to
+        get a 'full' representation of the tree shape, and then generate
+        mpdiffs on that data stream. This stream can then be reconstructed on
+        the other side.
+        """
+        inventory_key_order = [(r,) for r in revision_order]
+        parent_map = self.repository.inventories.get_parent_map(
+                            inventory_key_order)
+        missing_keys = set(inventory_key_order).difference(parent_map)
+        if missing_keys:
+            raise errors.RevisionNotPresent(list(missing_keys)[0],
+                                            self.repository.inventories)
+        inv_to_str = self.repository._serializer.write_inventory_to_string
+        # Make sure that we grab the parent texts first
+        just_parents = set()
+        map(just_parents.update, parent_map.itervalues())
+        just_parents.difference_update(parent_map)
+        # Ignore ghost parents
+        present_parents = self.repository.inventories.get_parent_map(
+                            just_parents)
+        ghost_keys = just_parents.difference(present_parents)
+        needed_inventories = list(present_parents) + inventory_key_order
+        needed_inventories = [k[-1] for k in needed_inventories]
+        all_lines = {}
+        for inv in self.repository.iter_inventories(needed_inventories):
+            revision_id = inv.revision_id
+            key = (revision_id,)
+            as_bytes = inv_to_str(inv)
+            # The sha1 is validated as the xml/textual form, not as the
+            # form-in-the-repository
+            sha1 = osutils.sha_string(as_bytes)
+            as_lines = osutils.split_lines(as_bytes)
+            del as_bytes
+            all_lines[key] = as_lines
+            if key in just_parents:
+                # We don't transmit those entries
+                continue
+            # Create an mpdiff for this text, and add it to the output
+            parent_keys = parent_map[key]
+            # See the comment in VF.make_mpdiffs about how this effects
+            # ordering when there are ghosts present. I think we have a latent
+            # bug
+            parent_lines = [all_lines[p_key] for p_key in parent_keys
+                            if p_key not in ghost_keys]
+            diff = multiparent.MultiParent.from_lines(
+                as_lines, parent_lines)
+            text = ''.join(diff.to_patch())
+            parent_ids = [k[-1] for k in parent_keys]
+            self.bundle.add_multiparent_record(text, sha1, parent_ids,
+                                               'inventory', revision_id, None)
+
+    def _add_revision_texts(self, revision_order):
         parent_map = self.repository.get_parent_map(revision_order)
         revision_to_str = self.repository._serializer.write_revision_to_string
         revisions = self.repository.get_revisions(revision_order)
@@ -543,30 +616,104 @@
             vf_records.append((key, parents, meta['sha1'], d_func(text)))
         versionedfile.add_mpdiffs(vf_records)
 
+    def _get_parent_inventory_texts(self, inventory_text_cache,
+                                    inventory_cache, parent_ids):
+        cached_parent_texts = {}
+        remaining_parent_ids = []
+        for parent_id in parent_ids:
+            p_text = inventory_text_cache.get(parent_id, None)
+            if p_text is None:
+                remaining_parent_ids.append(parent_id)
+            else:
+                cached_parent_texts[parent_id] = p_text
+        ghosts = ()
+        # TODO: Use inventory_cache to grab inventories we already have in
+        #       memory
+        if remaining_parent_ids:
+            # first determine what keys are actually present in the local
+            # inventories object (don't use revisions as they haven't been
+            # installed yet.)
+            parent_keys = [(r,) for r in remaining_parent_ids]
+            present_parent_map = self._repository.inventories.get_parent_map(
+                                        parent_keys)
+            present_parent_ids = []
+            ghosts = set()
+            for p_id in remaining_parent_ids:
+                if (p_id,) in present_parent_map:
+                    present_parent_ids.append(p_id)
+                else:
+                    ghosts.add(p_id)
+            to_string = self._source_serializer.write_inventory_to_string
+            for parent_inv in self._repository.iter_inventories(
+                                    present_parent_ids):
+                p_text = to_string(parent_inv)
+                inventory_cache[parent_inv.revision_id] = parent_inv
+                cached_parent_texts[parent_inv.revision_id] = p_text
+                inventory_text_cache[parent_inv.revision_id] = p_text
+
+        parent_texts = [cached_parent_texts[parent_id]
+                        for parent_id in parent_ids
+                         if parent_id not in ghosts]
+        return parent_texts
+
     def _install_inventory_records(self, records):
-        if self._info['serializer'] == self._repository._serializer.format_num:
+        if (self._info['serializer'] == self._repository._serializer.format_num
+            and self._repository._serializer.support_altered_by_hack):
             return self._install_mp_records_keys(self._repository.inventories,
                 records)
-        for key, metadata, bytes in records:
-            revision_id = key[-1]
-            parent_ids = metadata['parents']
-            parents = [self._repository.get_inventory(p)
-                       for p in parent_ids]
-            p_texts = [self._source_serializer.write_inventory_to_string(p)
-                       for p in parents]
-            target_lines = multiparent.MultiParent.from_patch(bytes).to_lines(
-                p_texts)
-            sha1 = osutils.sha_strings(target_lines)
-            if sha1 != metadata['sha1']:
-                raise errors.BadBundle("Can't convert to target format")
-            target_inv = self._source_serializer.read_inventory_from_string(
-                ''.join(target_lines))
-            self._handle_root(target_inv, parent_ids)
-            try:
-                self._repository.add_inventory(revision_id, target_inv,
-                                               parent_ids)
-            except errors.UnsupportedInventoryKind:
-                raise errors.IncompatibleRevision(repr(self._repository))
+        # Use a 10MB text cache, since these are string xml inventories. Note
+        # that 10MB is fairly small for large projects (a single inventory can
+        # be >5MB). Another possibility is to cache 10-20 inventory texts
+        # instead
+        inventory_text_cache = lru_cache.LRUSizeCache(10*1024*1024)
+        # Also cache the in-memory representation. This allows us to create
+        # inventory deltas to apply rather than calling add_inventory from
+        # scratch each time.
+        inventory_cache = lru_cache.LRUCache(10)
+        pb = ui.ui_factory.nested_progress_bar()
+        try:
+            num_records = len(records)
+            for idx, (key, metadata, bytes) in enumerate(records):
+                pb.update('installing inventory', idx, num_records)
+                revision_id = key[-1]
+                parent_ids = metadata['parents']
+                # Note: This assumes the local ghosts are identical to the
+                #       ghosts in the source, as the Bundle serialization
+                #       format doesn't record ghosts.
+                p_texts = self._get_parent_inventory_texts(inventory_text_cache,
+                                                           inventory_cache,
+                                                           parent_ids)
+                # Why does to_lines() take strings as the source, it seems that
+                # it would have to cast to a list of lines, which we get back
+                # as lines and then cast back to a string.
+                target_lines = multiparent.MultiParent.from_patch(bytes
+                            ).to_lines(p_texts)
+                inv_text = ''.join(target_lines)
+                del target_lines
+                sha1 = osutils.sha_string(inv_text)
+                if sha1 != metadata['sha1']:
+                    raise errors.BadBundle("Can't convert to target format")
+                # Add this to the cache so we don't have to extract it again.
+                inventory_text_cache[revision_id] = inv_text
+                target_inv = self._source_serializer.read_inventory_from_string(
+                    inv_text)
+                self._handle_root(target_inv, parent_ids)
+                parent_inv = None
+                if parent_ids:
+                    parent_inv = inventory_cache.get(parent_ids[0], None)
+                try:
+                    if parent_inv is None:
+                        self._repository.add_inventory(revision_id, target_inv,
+                                                       parent_ids)
+                    else:
+                        delta = target_inv._make_delta(parent_inv)
+                        self._repository.add_inventory_by_delta(parent_ids[0],
+                            delta, revision_id, parent_ids)
+                except errors.UnsupportedInventoryKind:
+                    raise errors.IncompatibleRevision(repr(self._repository))
+                inventory_cache[revision_id] = target_inv
+        finally:
+            pb.finished()
 
     def _handle_root(self, target_inv, parent_ids):
         revision_id = target_inv.revision_id

=== modified file 'bzrlib/chk_map.py'
--- a/bzrlib/chk_map.py	2009-07-16 23:28:49 +0000
+++ b/bzrlib/chk_map.py	2009-08-04 14:10:09 +0000
@@ -60,6 +60,9 @@
 # We are caching bytes so len(value) is perfectly accurate
 _page_cache = lru_cache.LRUSizeCache(_PAGE_CACHE_SIZE)
 
+def clear_cache():
+    _page_cache.clear()
+
 # If a ChildNode falls below this many bytes, we check for a remap
 _INTERESTING_NEW_SIZE = 50
 # If a ChildNode shrinks by more than this amount, we check for a remap

=== modified file 'bzrlib/chk_serializer.py'
--- a/bzrlib/chk_serializer.py	2009-07-01 10:46:27 +0000
+++ b/bzrlib/chk_serializer.py	2009-07-22 20:22:21 +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
@@ -21,8 +21,8 @@
     cache_utf8,
     inventory,
     revision as _mod_revision,
-    xml5,
     xml6,
+    xml7,
     )
 
 
@@ -131,7 +131,7 @@
         return self.read_revision_from_string(f.read())
 
 
-class CHKSerializerSubtree(BEncodeRevisionSerializer1, xml6.Serializer_v6):
+class CHKSerializerSubtree(BEncodeRevisionSerializer1, xml7.Serializer_v7):
     """A CHKInventory based serializer that supports tree references"""
 
     supported_kinds = set(['file', 'directory', 'symlink', 'tree-reference'])
@@ -152,14 +152,14 @@
             return inventory.TreeReference(file_id, name, parent_id, revision,
                                            reference_revision)
         else:
-            return xml6.Serializer_v6._unpack_entry(self, elt)
+            return xml7.Serializer_v7._unpack_entry(self, elt)
 
     def __init__(self, node_size, search_key_name):
         self.maximum_size = node_size
         self.search_key_name = search_key_name
 
 
-class CHKSerializer(xml5.Serializer_v5):
+class CHKSerializer(xml6.Serializer_v6):
     """A CHKInventory based serializer with 'plain' behaviour."""
 
     format_num = '9'

=== modified file 'bzrlib/send.py'
--- a/bzrlib/send.py	2009-07-15 07:32:26 +0000
+++ b/bzrlib/send.py	2009-07-17 14:41:02 +0000
@@ -77,6 +77,9 @@
                        submit_branch)
 
         if mail_to is None or format is None:
+            # TODO: jam 20090716 we open the submit_branch here, but we *don't*
+            #       pass it down into the format creation, so it will have to
+            #       open it again
             submit_br = Branch.open(submit_branch)
             submit_config = submit_br.get_config()
             if mail_to is None:
@@ -126,7 +129,6 @@
         if revision_id == NULL_REVISION:
             raise errors.BzrCommandError('No revisions to submit.')
         if format is None:
-            # TODO: Query submit branch for its preferred format
             format = format_registry.get()
         directive = format(branch, revision_id, submit_branch,
             public_branch, no_patch, no_bundle, message, base_revision_id)

=== modified file 'bzrlib/serializer.py'
--- a/bzrlib/serializer.py	2009-06-15 19:04:38 +0000
+++ b/bzrlib/serializer.py	2009-07-29 17:44:34 +0000
@@ -27,10 +27,26 @@
     squashes_xml_invalid_characters = False
 
     def write_inventory(self, inv, f):
-        """Write inventory to a file"""
+        """Write inventory to a file.
+
+        Note: this is a *whole inventory* operation, and should only be used
+        sparingly, as it does not scale well with large trees.
+        """
         raise NotImplementedError(self.write_inventory)
 
     def write_inventory_to_string(self, inv):
+        """Produce a simple string representation of an inventory.
+
+        Note: this is a *whole inventory* operation, and should only be used
+        sparingly, as it does not scale well with large trees.
+
+        The requirement for the contents of the string is that it can be passed
+        to read_inventory_from_string and the result is an identical inventory
+        in memory.
+
+        (All serializers as of 2009-07-29 produce XML, but this is not mandated
+        by the interface.)
+        """
         raise NotImplementedError(self.write_inventory_to_string)
 
     def read_inventory_from_string(self, string, revision_id=None,
@@ -52,6 +68,7 @@
         raise NotImplementedError(self.read_inventory_from_string)
 
     def read_inventory(self, f, revision_id=None):
+        """See read_inventory_from_string."""
         raise NotImplementedError(self.read_inventory)
 
     def write_revision(self, rev, f):

=== modified file 'bzrlib/tests/per_repository/__init__.py'
--- a/bzrlib/tests/per_repository/__init__.py	2009-07-10 06:45:04 +0000
+++ b/bzrlib/tests/per_repository/__init__.py	2009-07-22 17:22:06 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006, 2007, 2008 Canonical Ltd
+# Copyright (C) 2006, 2007, 2008, 2009 Canonical Ltd
 # Authors: Robert Collins <robert.collins at canonical.com>
 #          and others
 #
@@ -867,6 +867,7 @@
         'test_has_revisions',
         'test_is_write_locked',
         'test_iter_reverse_revision_history',
+        'test_merge_directive',
         'test_pack',
         'test_reconcile',
         'test_refresh_data',

=== added file 'bzrlib/tests/per_repository/test_merge_directive.py'
--- a/bzrlib/tests/per_repository/test_merge_directive.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/per_repository/test_merge_directive.py	2009-07-22 17:22:06 +0000
@@ -0,0 +1,73 @@
+# Copyright (C) 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
+
+"""Tests for how merge directives interact with various repository formats.
+
+Bundles contain the serialized form, so changes in serialization based on
+repository effects the final bundle.
+"""
+
+from bzrlib import (
+    chk_map,
+    errors,
+    merge_directive,
+    tests,
+    )
+
+from bzrlib.tests.per_repository import TestCaseWithRepository
+
+
+class TestMergeDirective(TestCaseWithRepository):
+
+    def make_two_branches(self):
+        builder = self.make_branch_builder('source')
+        builder.start_series()
+        builder.build_snapshot('A', None, [
+            ('add', ('', 'root-id', 'directory', None)),
+            ('add', ('f', 'f-id', 'file', 'initial content\n')),
+            ])
+        builder.build_snapshot('B', 'A', [
+            ('modify', ('f-id', 'new content\n')),
+            ])
+        builder.finish_series()
+        b1 = builder.get_branch()
+        b2 = b1.bzrdir.sprout('target', revision_id='A').open_branch()
+        return b1, b2
+
+    def create_merge_directive(self, source_branch, submit_url):
+        return merge_directive.MergeDirective2.from_objects(
+            source_branch.repository,
+            source_branch.last_revision(),
+            time=1247775710, timezone=0,
+            target_branch=submit_url)
+
+    def test_create_merge_directive(self):
+        source_branch, target_branch = self.make_two_branches()
+        directive = self.create_merge_directive(source_branch,
+                                                target_branch.base)
+        self.assertIsInstance(directive, merge_directive.MergeDirective2)
+
+
+    def test_create_and_install_directive(self):
+        source_branch, target_branch = self.make_two_branches()
+        directive = self.create_merge_directive(source_branch,
+                                                target_branch.base)
+        chk_map.clear_cache()
+        directive.install_revisions(target_branch.repository)
+        rt = target_branch.repository.revision_tree('B')
+        rt.lock_read()
+        self.assertEqualDiff('new content\n', rt.get_file_text('f-id'))
+        rt.unlock()

=== modified file 'bzrlib/tests/test_bundle.py'
--- a/bzrlib/tests/test_bundle.py	2009-07-20 04:26:55 +0000
+++ b/bzrlib/tests/test_bundle.py	2009-08-04 14:10:09 +0000
@@ -50,6 +50,22 @@
 from bzrlib.transform import TreeTransform
 
 
+def get_text(vf, key):
+    """Get the fulltext for a given revision id that is present in the vf"""
+    stream = vf.get_record_stream([key], 'unordered', True)
+    record = stream.next()
+    return record.get_bytes_as('fulltext')
+
+
+def get_inventory_text(repo, revision_id):
+    """Get the fulltext for the inventory at revision id"""
+    repo.lock_read()
+    try:
+        return get_text(repo.inventories, (revision_id,))
+    finally:
+        repo.unlock()
+
+
 class MockTree(object):
     def __init__(self):
         from bzrlib.inventory import InventoryDirectory, ROOT_ID
@@ -558,8 +574,9 @@
         self.tree1 = self.make_branch_and_tree('b1')
         self.b1 = self.tree1.branch
 
-        open('b1/one', 'wb').write('one\n')
-        self.tree1.add('one')
+        self.build_tree_contents([('b1/one', 'one\n')])
+        self.tree1.add('one', 'one-id')
+        self.tree1.set_root_id('root-id')
         self.tree1.commit('add one', rev_id='a at cset-0-1')
 
         bundle = self.get_valid_bundle('null:', 'a at cset-0-1')
@@ -576,8 +593,8 @@
                 , 'b1/sub/sub/'
                 , 'b1/sub/sub/nonempty.txt'
                 ])
-        open('b1/sub/sub/emptyfile.txt', 'wb').close()
-        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
+        self.build_tree_contents([('b1/sub/sub/emptyfile.txt', ''),
+                                  ('b1/dir/nolastnewline.txt', 'bloop')])
         tt = TreeTransform(self.tree1)
         tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
         tt.apply()
@@ -616,7 +633,8 @@
 
         bundle = self.get_valid_bundle('a at cset-0-2', 'a at cset-0-3')
         self.assertRaises((errors.TestamentMismatch,
-            errors.VersionedFileInvalidChecksum), self.get_invalid_bundle,
+            errors.VersionedFileInvalidChecksum,
+            errors.BadBundle), self.get_invalid_bundle,
             'a at cset-0-2', 'a at cset-0-3')
         # Check a rollup bundle
         bundle = self.get_valid_bundle('null:', 'a at cset-0-3')
@@ -646,9 +664,10 @@
                           verbose=False)
         bundle = self.get_valid_bundle('a at cset-0-5', 'a at cset-0-6')
         other = self.get_checkout('a at cset-0-5')
-        tree1_inv = self.tree1.branch.repository.get_inventory_xml(
-            'a at cset-0-5')
-        tree2_inv = other.branch.repository.get_inventory_xml('a at cset-0-5')
+        tree1_inv = get_inventory_text(self.tree1.branch.repository,
+                                       'a at cset-0-5')
+        tree2_inv = get_inventory_text(other.branch.repository,
+                                       'a at cset-0-5')
         self.assertEqualDiff(tree1_inv, tree2_inv)
         other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
         other.commit('rename file', rev_id='a at cset-0-6b')
@@ -1317,7 +1336,7 @@
         new_text = self.get_raw(StringIO(''.join(bundle_txt)))
         new_text = new_text.replace('<file file_id="exe-1"',
                                     '<file executable="y" file_id="exe-1"')
-        new_text = new_text.replace('B222', 'B237')
+        new_text = new_text.replace('B260', 'B275')
         bundle_txt = StringIO()
         bundle_txt.write(serializer._get_bundle_header('4'))
         bundle_txt.write('\n')
@@ -1429,6 +1448,200 @@
         return 'metaweave'
 
 
+class V4_2aBundleTester(V4BundleTester):
+
+    def bzrdir_format(self):
+        return '2a'
+
+    def get_invalid_bundle(self, base_rev_id, rev_id):
+        """Create a bundle from base_rev_id -> rev_id in built-in branch.
+        Munge the text so that it's invalid.
+
+        :return: The in-memory bundle
+        """
+        from bzrlib.bundle import serializer
+        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
+        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
+        # We are going to be replacing some text to set the executable bit on a
+        # file. Make sure the text replacement actually works correctly.
+        self.assertContainsRe(new_text, '(?m)B244\n\ni 1\n<inventory')
+        new_text = new_text.replace('<file file_id="exe-1"',
+                                    '<file executable="y" file_id="exe-1"')
+        new_text = new_text.replace('B244', 'B259')
+        bundle_txt = StringIO()
+        bundle_txt.write(serializer._get_bundle_header('4'))
+        bundle_txt.write('\n')
+        bundle_txt.write(new_text.encode('bz2'))
+        bundle_txt.seek(0)
+        bundle = read_bundle(bundle_txt)
+        self.valid_apply_bundle(base_rev_id, bundle)
+        return bundle
+
+    def make_merged_branch(self):
+        builder = self.make_branch_builder('source')
+        builder.start_series()
+        builder.build_snapshot('a at cset-0-1', None, [
+            ('add', ('', 'root-id', 'directory', None)),
+            ('add', ('file', 'file-id', 'file', 'original content\n')),
+            ])
+        builder.build_snapshot('a at cset-0-2a', ['a at cset-0-1'], [
+            ('modify', ('file-id', 'new-content\n')),
+            ])
+        builder.build_snapshot('a at cset-0-2b', ['a at cset-0-1'], [
+            ('add', ('other-file', 'file2-id', 'file', 'file2-content\n')),
+            ])
+        builder.build_snapshot('a at cset-0-3', ['a at cset-0-2a', 'a at cset-0-2b'], [
+            ('add', ('other-file', 'file2-id', 'file', 'file2-content\n')),
+            ])
+        builder.finish_series()
+        self.b1 = builder.get_branch()
+        self.b1.lock_read()
+        self.addCleanup(self.b1.unlock)
+
+    def make_bundle_just_inventories(self, base_revision_id,
+                                     target_revision_id,
+                                     revision_ids):
+        sio = StringIO()
+        writer = v4.BundleWriteOperation(base_revision_id, target_revision_id,
+                                         self.b1.repository, sio)
+        writer.bundle.begin()
+        writer._add_inventory_mpdiffs_from_serializer(revision_ids)
+        writer.bundle.end()
+        sio.seek(0)
+        return sio
+
+    def test_single_inventory_multiple_parents_as_xml(self):
+        self.make_merged_branch()
+        sio = self.make_bundle_just_inventories('a at cset-0-1', 'a at cset-0-3',
+                                                ['a at cset-0-3'])
+        reader = v4.BundleReader(sio, stream_input=False)
+        records = list(reader.iter_records())
+        self.assertEqual(1, len(records))
+        (bytes, metadata, repo_kind, revision_id,
+         file_id) = records[0]
+        self.assertIs(None, file_id)
+        self.assertEqual('a at cset-0-3', revision_id)
+        self.assertEqual('inventory', repo_kind)
+        self.assertEqual({'parents': ['a at cset-0-2a', 'a at cset-0-2b'],
+                          'sha1': '09c53b0c4de0895e11a2aacc34fef60a6e70865c',
+                          'storage_kind': 'mpdiff',
+                         }, metadata)
+        # We should have an mpdiff that takes some lines from both parents.
+        self.assertEqualDiff(
+            'i 1\n'
+            '<inventory format="10" revision_id="a at cset-0-3">\n'
+            '\n'
+            'c 0 1 1 2\n'
+            'c 1 3 3 2\n', bytes)
+
+    def test_single_inv_no_parents_as_xml(self):
+        self.make_merged_branch()
+        sio = self.make_bundle_just_inventories('null:', 'a at cset-0-1',
+                                                ['a at cset-0-1'])
+        reader = v4.BundleReader(sio, stream_input=False)
+        records = list(reader.iter_records())
+        self.assertEqual(1, len(records))
+        (bytes, metadata, repo_kind, revision_id,
+         file_id) = records[0]
+        self.assertIs(None, file_id)
+        self.assertEqual('a at cset-0-1', revision_id)
+        self.assertEqual('inventory', repo_kind)
+        self.assertEqual({'parents': [],
+                          'sha1': 'a13f42b142d544aac9b085c42595d304150e31a2',
+                          'storage_kind': 'mpdiff',
+                         }, metadata)
+        # We should have an mpdiff that takes some lines from both parents.
+        self.assertEqualDiff(
+            'i 4\n'
+            '<inventory format="10" revision_id="a at cset-0-1">\n'
+            '<directory file_id="root-id" name=""'
+                ' revision="a at cset-0-1" />\n'
+            '<file file_id="file-id" name="file" parent_id="root-id"'
+                ' revision="a at cset-0-1"'
+                ' text_sha1="09c2f8647e14e49e922b955c194102070597c2d1"'
+                ' text_size="17" />\n'
+            '</inventory>\n'
+            '\n', bytes)
+
+    def test_multiple_inventories_as_xml(self):
+        self.make_merged_branch()
+        sio = self.make_bundle_just_inventories('a at cset-0-1', 'a at cset-0-3',
+            ['a at cset-0-2a', 'a at cset-0-2b', 'a at cset-0-3'])
+        reader = v4.BundleReader(sio, stream_input=False)
+        records = list(reader.iter_records())
+        self.assertEqual(3, len(records))
+        revision_ids = [rev_id for b, m, k, rev_id, f in records]
+        self.assertEqual(['a at cset-0-2a', 'a at cset-0-2b', 'a at cset-0-3'],
+                         revision_ids)
+        metadata_2a = records[0][1]
+        self.assertEqual({'parents': ['a at cset-0-1'],
+                          'sha1': '1e105886d62d510763e22885eec733b66f5f09bf',
+                          'storage_kind': 'mpdiff',
+                         }, metadata_2a)
+        metadata_2b = records[1][1]
+        self.assertEqual({'parents': ['a at cset-0-1'],
+                          'sha1': 'f03f12574bdb5ed2204c28636c98a8547544ccd8',
+                          'storage_kind': 'mpdiff',
+                         }, metadata_2b)
+        metadata_3 = records[2][1]
+        self.assertEqual({'parents': ['a at cset-0-2a', 'a at cset-0-2b'],
+                          'sha1': '09c53b0c4de0895e11a2aacc34fef60a6e70865c',
+                          'storage_kind': 'mpdiff',
+                         }, metadata_3)
+        bytes_2a = records[0][0]
+        self.assertEqualDiff(
+            'i 1\n'
+            '<inventory format="10" revision_id="a at cset-0-2a">\n'
+            '\n'
+            'c 0 1 1 1\n'
+            'i 1\n'
+            '<file file_id="file-id" name="file" parent_id="root-id"'
+                ' revision="a at cset-0-2a"'
+                ' text_sha1="50f545ff40e57b6924b1f3174b267ffc4576e9a9"'
+                ' text_size="12" />\n'
+            '\n'
+            'c 0 3 3 1\n', bytes_2a)
+        bytes_2b = records[1][0]
+        self.assertEqualDiff(
+            'i 1\n'
+            '<inventory format="10" revision_id="a at cset-0-2b">\n'
+            '\n'
+            'c 0 1 1 2\n'
+            'i 1\n'
+            '<file file_id="file2-id" name="other-file" parent_id="root-id"'
+                ' revision="a at cset-0-2b"'
+                ' text_sha1="b46c0c8ea1e5ef8e46fc8894bfd4752a88ec939e"'
+                ' text_size="14" />\n'
+            '\n'
+            'c 0 3 4 1\n', bytes_2b)
+        bytes_3 = records[2][0]
+        self.assertEqualDiff(
+            'i 1\n'
+            '<inventory format="10" revision_id="a at cset-0-3">\n'
+            '\n'
+            'c 0 1 1 2\n'
+            'c 1 3 3 2\n', bytes_3)
+
+    def test_creating_bundle_preserves_chk_pages(self):
+        self.make_merged_branch()
+        target = self.b1.bzrdir.sprout('target',
+                                       revision_id='a at cset-0-2a').open_branch()
+        bundle_txt, rev_ids = self.create_bundle_text('a at cset-0-2a',
+                                                      'a at cset-0-3')
+        self.assertEqual(['a at cset-0-2b', 'a at cset-0-3'], rev_ids)
+        bundle = read_bundle(bundle_txt)
+        target.lock_write()
+        self.addCleanup(target.unlock)
+        install_bundle(target.repository, bundle)
+        inv1 = self.b1.repository.inventories.get_record_stream([
+            ('a at cset-0-3',)], 'unordered',
+            True).next().get_bytes_as('fulltext')
+        inv2 = target.repository.inventories.get_record_stream([
+            ('a at cset-0-3',)], 'unordered',
+            True).next().get_bytes_as('fulltext')
+        self.assertEqualDiff(inv1, inv2)
+
+
 class MungedBundleTester(object):
 
     def build_test_bundle(self):




More information about the bazaar-commits mailing list