[MERGE] Add get_data_stream/insert_data_stream to KnitVersionedFile

Andrew Bennetts andrew at canonical.com
Fri Aug 3 04:38:26 BST 2007


This bundle adds some new methods to KnitVersionedFile:

  * get_data_stream(versions): returns all the data about those versions from
    this knit, as directly as possible.  So it will return them in the order
    they are read from the file, and does very little processing beyond just
    giving back the bytes straight off disk.

  * insert_data_stream(stream): inserts a data stream from get_data_stream into
    this knit.

  * get_stream_as_bytes(versions): like get_data_stream, but bencodes the stream
    to bytes so that it is suitable for sending directly over e.g. the smart
    server protocol.

  * get_format_signature(): returns a string included in the data stream so that
    data from an incompatible knit won't be erroneously inserted if fed to
    insert_data_stream.

Again, this has been extracted from my work towards smart server support for
transferring sets of revisions in a single request, rather than with lots of
little requests.

-Andrew.

-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.19)
# revision_id: andrew.bennetts at canonical.com-20070803033525-\
#   3pp04fzubrgzlnac
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev
# testament_sha1: c35e0cb747a2562b4663c07b75110b105f6b848f
# timestamp: 2007-08-03 13:37:58 +1000
# source_branch: http://people.ubuntu.com/~andrew/bzr/vf-data-stream
# base_revision_id: pqm at pqm.ubuntu.com-20070802221338-9333q05a8caaciwo
# 
# Begin patch
=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py	2007-07-20 18:59:29 +0000
+++ bzrlib/errors.py	2007-08-03 03:35:25 +0000
@@ -1284,6 +1284,25 @@
     internal_error = True
 
 
+class KnitCorrupt(KnitError):
+
+    _fmt = "Knit %(filename)s corrupt: %(how)s"
+
+    def __init__(self, filename, how):
+        KnitError.__init__(self)
+        self.filename = filename
+        self.how = how
+
+
+class KnitDataStreamIncompatible(KnitError):
+
+    _fmt = "Cannot insert knit data stream of format \"%(stream_format)s\" into knit of format \"%(target_format)s\"."
+
+    def __init__(self, stream_format, target_format):
+        self.stream_format = stream_format
+        self.target_format = target_format
+        
+
 class KnitHeaderError(KnitError):
 
     _fmt = "Knit header error: %(badline)r unexpected for file %(filename)s"
@@ -1294,16 +1313,6 @@
         self.filename = filename
 
 
-class KnitCorrupt(KnitError):
-
-    _fmt = "Knit %(filename)s corrupt: %(how)s"
-
-    def __init__(self, filename, how):
-        KnitError.__init__(self)
-        self.filename = filename
-        self.how = how
-
-
 class KnitIndexUnknownMethod(KnitError):
     """Raised when we don't understand the storage method.
 

=== modified file 'bzrlib/knit.py'
--- bzrlib/knit.py	2007-07-30 05:02:10 +0000
+++ bzrlib/knit.py	2007-08-03 03:35:25 +0000
@@ -85,6 +85,7 @@
     KnitError,
     InvalidRevisionId,
     KnitCorrupt,
+    KnitDataStreamIncompatible,
     KnitHeaderError,
     RevisionNotPresent,
     RevisionAlreadyPresent,
@@ -99,6 +100,7 @@
 from bzrlib.symbol_versioning import DEPRECATED_PARAMETER, deprecated_passed
 from bzrlib.tsort import topo_sort
 import bzrlib.ui
+from bzrlib.util import bencode
 import bzrlib.weave
 from bzrlib.versionedfile import VersionedFile, InterVersionedFile
 
@@ -550,6 +552,87 @@
                                 current_values[3],
                                 new_parents)
 
+    def get_data_stream(self, required_versions):
+        """Get a data stream for the specified versions.
+
+        Versions may be returned in any order, not necessarily the order
+        specified.
+
+        :param required_versions: the exact set of versions to be returned, i.e.
+            not a transitive closure.
+        
+        :returns: format_signature, list of (version, options, length, parents),
+            reader_callable.
+        """
+        required_versions = set([osutils.safe_revision_id(v) for v in
+            required_versions])
+        # we don't care about inclusions, the caller cares.
+        # but we need to setup a list of records to visit.
+        for version_id in required_versions:
+            if not self.has_version(version_id):
+                raise RevisionNotPresent(version_id, self.filename)
+        # Pick the desired versions out of the index in oldest-to-newest order
+        version_list = []
+        for version_id in self.versions():
+            if version_id in required_versions:
+                version_list.append(version_id)
+
+        # create the list of version information for the result
+        copy_queue_records = []
+        copy_set = set()
+        result_version_list = []
+        for version_id in version_list:
+            options = self._index.get_options(version_id)
+            parents = self._index.get_parents_with_ghosts(version_id)
+            data_pos, data_size = self._index.get_position(version_id)
+            copy_queue_records.append((version_id, data_pos, data_size))
+            copy_set.add(version_id)
+            # version, options, length, parents
+            result_version_list.append((version_id, options, data_size,
+                parents))
+
+        # Read the compressed record data.
+        # XXX:
+        # From here down to the return should really be logic in the returned
+        # callable -- in a class that adapts read_records_iter_raw to read
+        # requests.
+        raw_datum = []
+        for (version_id, raw_data), \
+            (version_id2, options, _, parents) in \
+            izip(self._data.read_records_iter_raw(copy_queue_records),
+                 result_version_list):
+            assert version_id == version_id2, 'logic error, inconsistent results'
+            raw_datum.append(raw_data)
+        pseudo_file = StringIO(''.join(raw_datum))
+        def read(length):
+            if length is None:
+                return pseudo_file.read()
+            else:
+                return pseudo_file.read(length)
+        return (self.get_format_signature(), result_version_list, read)
+
+    def get_stream_as_bytes(self, required_versions):
+        """Generate a serialised data stream.
+
+        The format is a bencoding of a list.  The first element of the list is a
+        string of the format signature, then each subsequent element is a list
+        corresponding to a record.  Those lists contain:
+
+          * a version id
+          * a list of options
+          * a list of parents
+          * the bytes
+
+        :returns: a bencoded list.
+        """
+        knit_stream = self.get_data_stream(required_versions)
+        format_signature, data_list, callable = knit_stream
+        data = []
+        data.append(format_signature)
+        for version, options, length, parents in data_list:
+            data.append([version, options, parents, callable(length)])
+        return bencode.bencode(data)
+
     def _extract_blocks(self, version_id, source, target):
         if self._index.get_method(version_id) != 'line-delta':
             return None
@@ -584,6 +667,14 @@
         else:
             delta = self.factory.parse_line_delta(data, version_id)
             return parent, sha1, noeol, delta
+
+    def get_format_signature(self):
+        """See VersionedFile.get_format_signature()."""
+        if self.factory.annotated:
+            annotated_part = "annotated"
+        else:
+            annotated_part = "plain"
+        return "knit-%s" % (annotated_part,)
         
     def get_graph_with_ghosts(self):
         """See VersionedFile.get_graph_with_ghosts()."""
@@ -620,6 +711,49 @@
                         return True
         return False
 
+    def insert_data_stream(self, (format, data_list, reader_callable)):
+        """Insert knit records from a data stream into this knit.
+
+        If a version in the stream is already present in this knit, it will not
+        be inserted a second time.  It will be checked for consistency with the
+        stored version however, and may cause a KnitCorrupt error to be raised
+        if the data in the stream disagrees with the already stored data.
+        
+        :seealso: get_data_stream
+        """
+        if format != self.get_format_signature():
+            mutter('incompatible format signature inserting to %r', self)
+            raise KnitDataStreamIncompatible(
+                format, self.get_format_signature())
+
+        for version_id, options, length, parents in data_list:
+            if self.has_version(version_id):
+                # First check: the list of parents.
+                my_parents = self.get_parents_with_ghosts(version_id)
+                if my_parents != parents:
+                    # XXX: KnitCorrupt is not quite the right exception here.
+                    raise KnitCorrupt(
+                        self.filename,
+                        'parents list %r from data stream does not match '
+                        'already recorded parents %r for %s'
+                        % (parents, my_parents, version_id))
+
+                # Also check the SHA-1 of the fulltext this content will
+                # produce.
+                raw_data = reader_callable(length)
+                my_fulltext_sha1 = self.get_sha1(version_id)
+                df, rec = self._data._parse_record_header(version_id, raw_data)
+                stream_fulltext_sha1 = rec[3]
+                if my_fulltext_sha1 != stream_fulltext_sha1:
+                    # Actually, we don't know if it's this knit that's corrupt,
+                    # or the data stream we're trying to insert.
+                    raise KnitCorrupt(
+                        self.filename, 'sha-1 does not match %s' % version_id)
+            else:
+                self._add_raw_records(
+                    [(version_id, options, parents, length)],
+                    reader_callable(length))
+
     def versions(self):
         """See VersionedFile.versions."""
         return self._index.get_versions()

=== modified file 'bzrlib/tests/test_errors.py'
--- bzrlib/tests/test_errors.py	2007-07-20 03:20:20 +0000
+++ bzrlib/tests/test_errors.py	2007-08-03 03:35:25 +0000
@@ -81,6 +81,13 @@
             "cannot be broken.",
             str(error))
 
+    def test_knit_data_stream_incompatible(self):
+        error = errors.KnitDataStreamIncompatible(
+            'stream format', 'target format')
+        self.assertEqual('Cannot insert knit data stream of format '
+                         '"stream format" into knit of format '
+                         '"target format".', str(error))
+
     def test_knit_header_error(self):
         error = errors.KnitHeaderError('line foo\n', 'path/to/file')
         self.assertEqual("Knit header error: 'line foo\\n' unexpected"

=== modified file 'bzrlib/tests/test_knit.py'
--- bzrlib/tests/test_knit.py	2007-07-30 05:02:10 +0000
+++ bzrlib/tests/test_knit.py	2007-08-03 03:35:25 +0000
@@ -49,6 +49,7 @@
 from bzrlib.tests import TestCase, TestCaseWithTransport, Feature
 from bzrlib.transport import TransportLogger, get_transport
 from bzrlib.transport.memory import MemoryTransport
+from bzrlib.util import bencode
 from bzrlib.weave import Weave
 
 
@@ -856,12 +857,13 @@
 class KnitTests(TestCaseWithTransport):
     """Class containing knit test helper routines."""
 
-    def make_test_knit(self, annotate=False, delay_create=False, index=None):
+    def make_test_knit(self, annotate=False, delay_create=False, index=None,
+                       name='test'):
         if not annotate:
             factory = KnitPlainFactory()
         else:
             factory = None
-        return KnitVersionedFile('test', get_transport('.'), access_mode='w',
+        return KnitVersionedFile(name, get_transport('.'), access_mode='w',
                                  factory=factory, create=True,
                                  delay_create=delay_create, index=index)
 
@@ -1328,6 +1330,349 @@
         for plan_line, expected_line in zip(plan, AB_MERGE):
             self.assertEqual(plan_line, expected_line)
 
+    def assertRecordContentEqual(self, knit, version_id, candidate_content):
+        """Assert that some raw record content matches the raw record content
+        for a particular version_id in the given knit.
+        """
+        data_pos, data_size = knit._index.get_position(version_id)
+        record = (version_id, data_pos, data_size)
+        [(_, expected_content)] = list(knit._data.read_records_iter_raw([record]))
+        self.assertEqual(expected_content, candidate_content)
+
+    def test_get_stream_empty(self):
+        """Get a data stream for an empty knit file."""
+        k1 = self.make_test_knit()
+        format, data_list, reader_callable = k1.get_data_stream([])
+        self.assertEqual('knit-plain', format)
+        self.assertEqual([], data_list)
+        content = reader_callable(None)
+        self.assertEqual('', content)
+        self.assertIsInstance(content, str)
+
+    def test_get_stream_one_version(self):
+        """Get a data stream for a single record out of a knit containing just
+        one record.
+        """
+        k1 = self.make_test_knit()
+        test_data = [
+            ('text-a', [], TEXT_1),
+            ]
+        expected_data_list = [
+            # version, options, length, parents
+            ('text-a', ['fulltext'], 122, []),
+           ]
+        for version_id, parents, lines in test_data:
+            k1.add_lines(version_id, parents, split_lines(lines))
+
+        format, data_list, reader_callable = k1.get_data_stream(['text-a'])
+        self.assertEqual('knit-plain', format)
+        self.assertEqual(expected_data_list, data_list)
+        # There's only one record in the knit, so the content should be the
+        # entire knit data file's contents.
+        self.assertEqual(k1.transport.get_bytes(k1._data._filename),
+                         reader_callable(None))
+        
+    def test_get_stream_get_one_version_of_many(self):
+        """Get a data stream for just one version out of a knit containing many
+        versions.
+        """
+        k1 = self.make_test_knit()
+        # Insert the same data as test_knit_join, as they seem to cover a range
+        # of cases (no parents, one parent, multiple parents).
+        test_data = [
+            ('text-a', [], TEXT_1),
+            ('text-b', ['text-a'], TEXT_1),
+            ('text-c', [], TEXT_1),
+            ('text-d', ['text-c'], TEXT_1),
+            ('text-m', ['text-b', 'text-d'], TEXT_1),
+            ]
+        expected_data_list = [
+            # version, options, length, parents
+            ('text-m', ['line-delta'], 84, ['text-b', 'text-d']),
+            ]
+        for version_id, parents, lines in test_data:
+            k1.add_lines(version_id, parents, split_lines(lines))
+
+        format, data_list, reader_callable = k1.get_data_stream(['text-m'])
+        self.assertEqual('knit-plain', format)
+        self.assertEqual(expected_data_list, data_list)
+        self.assertRecordContentEqual(k1, 'text-m', reader_callable(None))
+        
+    def test_get_stream_ghost_parent(self):
+        """Get a data stream for a version with a ghost parent."""
+        k1 = self.make_test_knit()
+        # Test data
+        k1.add_lines('text-a', [], split_lines(TEXT_1))
+        k1.add_lines_with_ghosts('text-b', ['text-a', 'text-ghost'],
+                                 split_lines(TEXT_1))
+        # Expected data
+        expected_data_list = [
+            # version, options, length, parents
+            ('text-b', ['line-delta'], 84, ['text-a', 'text-ghost']),
+            ]
+        
+        format, data_list, reader_callable = k1.get_data_stream(['text-b'])
+        self.assertEqual('knit-plain', format)
+        self.assertEqual(expected_data_list, data_list)
+        self.assertRecordContentEqual(k1, 'text-b', reader_callable(None))
+    
+    def test_get_stream_get_multiple_records(self):
+        """Get a stream for multiple records of a knit."""
+        k1 = self.make_test_knit()
+        # Insert the same data as test_knit_join, as they seem to cover a range
+        # of cases (no parents, one parent, multiple parents).
+        test_data = [
+            ('text-a', [], TEXT_1),
+            ('text-b', ['text-a'], TEXT_1),
+            ('text-c', [], TEXT_1),
+            ('text-d', ['text-c'], TEXT_1),
+            ('text-m', ['text-b', 'text-d'], TEXT_1),
+            ]
+        expected_data_list = [
+            # version, options, length, parents
+            ('text-b', ['line-delta'], 84, ['text-a']),
+            ('text-d', ['line-delta'], 84, ['text-c']),
+            ]
+        for version_id, parents, lines in test_data:
+            k1.add_lines(version_id, parents, split_lines(lines))
+
+        # Note that even though we request the revision IDs in a particular
+        # order, the data stream may return them in any order it likes.  In this
+        # case, they'll be in the order they were inserted into the knit.
+        format, data_list, reader_callable = k1.get_data_stream(
+            ['text-d', 'text-b'])
+        self.assertEqual('knit-plain', format)
+        self.assertEqual(expected_data_list, data_list)
+        self.assertRecordContentEqual(k1, 'text-b', reader_callable(84))
+        self.assertRecordContentEqual(k1, 'text-d', reader_callable(84))
+        self.assertEqual('', reader_callable(None),
+                         "There should be no more bytes left to read.")
+
+    def test_get_stream_all(self):
+        """Get a data stream for all the records in a knit.
+
+        This exercises fulltext records, line-delta records, records with
+        various numbers of parents, and reading multiple records out of the
+        callable.  These cases ought to all be exercised individually by the
+        other test_get_stream_* tests; this test is basically just paranoia.
+        """
+        k1 = self.make_test_knit()
+        # Insert the same data as test_knit_join, as they seem to cover a range
+        # of cases (no parents, one parent, multiple parents).
+        test_data = [
+            ('text-a', [], TEXT_1),
+            ('text-b', ['text-a'], TEXT_1),
+            ('text-c', [], TEXT_1),
+            ('text-d', ['text-c'], TEXT_1),
+            ('text-m', ['text-b', 'text-d'], TEXT_1),
+           ]
+        expected_data_list = [
+            # version, options, length, parents
+            ('text-a', ['fulltext'], 122, []),
+            ('text-b', ['line-delta'], 84, ['text-a']),
+            ('text-c', ['fulltext'], 121, []),
+            ('text-d', ['line-delta'], 84, ['text-c']),
+            ('text-m', ['line-delta'], 84, ['text-b', 'text-d']),
+            ]
+        for version_id, parents, lines in test_data:
+            k1.add_lines(version_id, parents, split_lines(lines))
+
+        format, data_list, reader_callable = k1.get_data_stream(
+            ['text-a', 'text-b', 'text-c', 'text-d', 'text-m'])
+        self.assertEqual('knit-plain', format)
+        self.assertEqual(expected_data_list, data_list)
+        for version_id, options, length, parents in expected_data_list:
+            bytes = reader_callable(length)
+            self.assertRecordContentEqual(k1, version_id, bytes)
+
+    def test_get_data_stream(self):
+        # Make a simple knit
+        k1 = self.make_test_knit()
+        k1.add_lines('text-a', [], split_lines(TEXT_1))
+        
+        # Serialise it, check the output.
+        bytes = k1.get_stream_as_bytes(['text-a'])
+        data = bencode.bdecode(bytes)
+        format, record = data
+        self.assertEqual('knit-plain', format)
+        self.assertEqual(['text-a', ['fulltext'], []], record[:3])
+        self.assertRecordContentEqual(k1, 'text-a', record[3])
+
+    def test_get_stream_as_bytes_all(self):
+        """Get a serialised data stream for all the records in a knit.
+
+        Much like test_get_stream_all, except for get_stream_as_bytes.
+        """
+        k1 = self.make_test_knit()
+        # Insert the same data as test_knit_join, as they seem to cover a range
+        # of cases (no parents, one parent, multiple parents).
+        test_data = [
+            ('text-a', [], TEXT_1),
+            ('text-b', ['text-a'], TEXT_1),
+            ('text-c', [], TEXT_1),
+            ('text-d', ['text-c'], TEXT_1),
+            ('text-m', ['text-b', 'text-d'], TEXT_1),
+           ]
+        expected_data_list = [
+            # version, options, parents
+            ('text-a', ['fulltext'], []),
+            ('text-b', ['line-delta'], ['text-a']),
+            ('text-c', ['fulltext'], []),
+            ('text-d', ['line-delta'], ['text-c']),
+            ('text-m', ['line-delta'], ['text-b', 'text-d']),
+            ]
+        for version_id, parents, lines in test_data:
+            k1.add_lines(version_id, parents, split_lines(lines))
+
+        bytes = k1.get_stream_as_bytes(
+            ['text-a', 'text-b', 'text-c', 'text-d', 'text-m'])
+
+        data = bencode.bdecode(bytes)
+        format = data.pop(0)
+        self.assertEqual('knit-plain', format)
+
+        for expected, actual in zip(expected_data_list, data):
+            expected_version = expected[0]
+            expected_options = expected[1]
+            expected_parents = expected[2]
+            version, options, parents, bytes = actual
+            self.assertEqual(expected_version, version)
+            self.assertEqual(expected_options, options)
+            self.assertEqual(expected_parents, parents)
+            self.assertRecordContentEqual(k1, version, bytes)
+
+    def assertKnitFilesEqual(self, knit1, knit2):
+        """Assert that the contents of the index and data files of two knits are
+        equal.
+        """
+        self.assertEqual(
+            knit1.transport.get_bytes(knit1._data._filename),
+            knit2.transport.get_bytes(knit2._data._filename))
+        self.assertEqual(
+            knit1.transport.get_bytes(knit1._index._filename),
+            knit2.transport.get_bytes(knit2._index._filename))
+
+    def test_insert_data_stream_empty(self):
+        """Inserting a data stream with no records should not put any data into
+        the knit.
+        """
+        k1 = self.make_test_knit()
+        k1.insert_data_stream(
+            (k1.get_format_signature(), [], lambda ignored: ''))
+        self.assertEqual('', k1.transport.get_bytes(k1._data._filename),
+                         "The .knit should be completely empty.")
+        self.assertEqual(k1._index.HEADER,
+                         k1.transport.get_bytes(k1._index._filename),
+                         "The .kndx should have nothing apart from the header.")
+
+    def test_insert_data_stream_one_record(self):
+        """Inserting a data stream with one record from a knit with one record
+        results in byte-identical files.
+        """
+        source = self.make_test_knit(name='source')
+        source.add_lines('text-a', [], split_lines(TEXT_1))
+        data_stream = source.get_data_stream(['text-a'])
+        
+        target = self.make_test_knit(name='target')
+        target.insert_data_stream(data_stream)
+        
+        self.assertKnitFilesEqual(source, target)
+
+    def test_insert_data_stream_records_already_present(self):
+        """Insert a data stream where some records are alreday present in the
+        target, and some not.  Only the new records are inserted.
+        """
+        source = self.make_test_knit(name='source')
+        target = self.make_test_knit(name='target')
+        # Insert 'text-a' into both source and target
+        source.add_lines('text-a', [], split_lines(TEXT_1))
+        target.insert_data_stream(source.get_data_stream(['text-a']))
+        # Insert 'text-b' into just the source.
+        source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
+        # Get a data stream of both text-a and text-b, and insert it.
+        data_stream = source.get_data_stream(['text-a', 'text-b'])
+        target.insert_data_stream(data_stream)
+        # The source and target will now be identical.  This means the text-a
+        # record was not added a second time.
+        self.assertKnitFilesEqual(source, target)
+
+    def test_insert_data_stream_multiple_records(self):
+        """Inserting a data stream of all records from a knit with multiple
+        records results in byte-identical files.
+        """
+        source = self.make_test_knit(name='source')
+        source.add_lines('text-a', [], split_lines(TEXT_1))
+        source.add_lines('text-b', ['text-a'], split_lines(TEXT_1))
+        source.add_lines('text-c', [], split_lines(TEXT_1))
+        data_stream = source.get_data_stream(['text-a', 'text-b', 'text-c'])
+        
+        target = self.make_test_knit(name='target')
+        target.insert_data_stream(data_stream)
+        
+        self.assertKnitFilesEqual(source, target)
+
+    def test_insert_data_stream_ghost_parent(self):
+        """Insert a data stream with a record that has a ghost parent."""
+        # Make a knit with a record, text-a, that has a ghost parent.
+        source = self.make_test_knit(name='source')
+        source.add_lines_with_ghosts('text-a', ['text-ghost'],
+                                     split_lines(TEXT_1))
+        data_stream = source.get_data_stream(['text-a'])
+
+        target = self.make_test_knit(name='target')
+        target.insert_data_stream(data_stream)
+
+        self.assertKnitFilesEqual(source, target)
+
+        # The target knit object is in a consistent state, i.e. the record we
+        # just added is immediately visible.
+        self.assertTrue(target.has_version('text-a'))
+        self.assertTrue(target.has_ghost('text-ghost'))
+        self.assertEqual(split_lines(TEXT_1), target.get_lines('text-a'))
+
+    def test_insert_data_stream_inconsistent_version_lines(self):
+        """Inserting a data stream which has different content for a version_id
+        than already exists in the knit will raise KnitCorrupt.
+        """
+        source = self.make_test_knit(name='source')
+        target = self.make_test_knit(name='target')
+        # Insert a different 'text-a' into both source and target
+        source.add_lines('text-a', [], split_lines(TEXT_1))
+        target.add_lines('text-a', [], split_lines(TEXT_2))
+        # Insert a data stream with conflicting content into the target
+        data_stream = source.get_data_stream(['text-a'])
+        self.assertRaises(
+            errors.KnitCorrupt, target.insert_data_stream, data_stream)
+
+    def test_insert_data_stream_inconsistent_version_parents(self):
+        """Inserting a data stream which has different parents for a version_id
+        than already exists in the knit will raise KnitCorrupt.
+        """
+        source = self.make_test_knit(name='source')
+        target = self.make_test_knit(name='target')
+        # Insert a different 'text-a' into both source and target.  They differ
+        # only by the parents list, the content is the same.
+        source.add_lines_with_ghosts('text-a', [], split_lines(TEXT_1))
+        target.add_lines_with_ghosts('text-a', ['a-ghost'], split_lines(TEXT_1))
+        # Insert a data stream with conflicting content into the target
+        data_stream = source.get_data_stream(['text-a'])
+        self.assertRaises(
+            errors.KnitCorrupt, target.insert_data_stream, data_stream)
+
+    def test_insert_data_stream_incompatible_format(self):
+        """A data stream in a different format to the target knit cannot be
+        inserted.
+
+        It will raise KnitDataStreamIncompatible.
+        """
+        data_stream = ('fake-format-signature', [], lambda _: '')
+        target = self.make_test_knit(name='target')
+        self.assertRaises(
+            errors.KnitDataStreamIncompatible,
+            target.insert_data_stream, data_stream)
+
+    #  * test that a stream of "already present version, then new version"
+    #    inserts correctly.
 
 TEXT_1 = """\
 Banana cup cakes:

=== modified file 'bzrlib/tests/test_versionedfile.py'
--- bzrlib/tests/test_versionedfile.py	2007-07-25 00:52:21 +0000
+++ bzrlib/tests/test_versionedfile.py	2007-08-03 03:35:25 +0000
@@ -35,8 +35,8 @@
                            WeaveParentMismatch
                            )
 from bzrlib.knit import KnitVersionedFile, \
-     KnitAnnotateFactory
-from bzrlib.tests import TestCaseWithTransport
+     KnitAnnotateFactory, KnitPlainFactory
+from bzrlib.tests import TestCaseWithMemoryTransport
 from bzrlib.tests.HTTPTestUtil import TestCaseWithWebserver
 from bzrlib.trace import mutter
 from bzrlib.transport import get_transport
@@ -826,7 +826,7 @@
                           vf.get_sha1s(['a', 'c', 'b']))
         
 
-class TestWeave(TestCaseWithTransport, VersionedFileTestMixIn):
+class TestWeave(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
     def get_file(self, name='foo'):
         return WeaveFile(name, get_transport(self.get_url('.')), create=True)
@@ -878,7 +878,7 @@
         return WeaveFile
 
 
-class TestKnit(TestCaseWithTransport, VersionedFileTestMixIn):
+class TestKnit(TestCaseWithMemoryTransport, VersionedFileTestMixIn):
 
     def get_file(self, name='foo'):
         return KnitVersionedFile(name, get_transport(self.get_url('.')),
@@ -927,7 +927,7 @@
 # if we make the registry a separate class though we still need to 
 # test the behaviour in the active registry to catch failure-to-handle-
 # stange-objects
-class TestInterVersionedFile(TestCaseWithTransport):
+class TestInterVersionedFile(TestCaseWithMemoryTransport):
 
     def test_get_default_inter_versionedfile(self):
         # test that the InterVersionedFile.get(a, b) probes
@@ -1247,7 +1247,7 @@
         self._test_merge_from_strings(base, a, b, result)
 
 
-class TestKnitMerge(TestCaseWithTransport, MergeCasesMixin):
+class TestKnitMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
     def get_file(self, name='foo'):
         return KnitVersionedFile(name, get_transport(self.get_url('.')),
@@ -1257,7 +1257,7 @@
         pass
 
 
-class TestWeaveMerge(TestCaseWithTransport, MergeCasesMixin):
+class TestWeaveMerge(TestCaseWithMemoryTransport, MergeCasesMixin):
 
     def get_file(self, name='foo'):
         return WeaveFile(name, get_transport(self.get_url('.')), create=True)
@@ -1270,3 +1270,23 @@
 
     overlappedInsertExpected = ['aaa', '<<<<<<< ', 'xxx', 'yyy', '=======', 
                                 'xxx', '>>>>>>> ', 'bbb']
+
+
+class TestFormatSignatures(TestCaseWithMemoryTransport):
+
+    def get_knit_file(self, name, annotated):
+        if annotated:
+            factory = KnitAnnotateFactory()
+        else:
+            factory = KnitPlainFactory()
+        return KnitVersionedFile(
+            name, get_transport(self.get_url('.')), create=True,
+            factory=factory)
+
+    def test_knit_format_signatures(self):
+        """Different formats of knit have different signature strings."""
+        knit = self.get_knit_file('a', True)
+        self.assertEqual('knit-annotated', knit.get_format_signature())
+        knit = self.get_knit_file('p', False)
+        self.assertEqual('knit-plain', knit.get_format_signature())
+

=== modified file 'bzrlib/versionedfile.py'
--- bzrlib/versionedfile.py	2007-07-25 21:26:30 +0000
+++ bzrlib/versionedfile.py	2007-08-03 03:35:25 +0000
@@ -267,6 +267,13 @@
             result[version_id] = self.get_delta(version_id)
         return result
 
+    def get_format_signature(self):
+        """Get a text description of the data encoding in this file.
+        
+        :since: 0.19
+        """
+        raise NotImplementedError(self.get_format_signature)
+
     def make_mpdiffs(self, version_ids):
         """Create multiparent diffs for specified versions"""
         knit_versions = set()

# Begin bundle
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
ABuM6e2LtnaIZ8Q9gg/8XckU4UJBbmDqtA==


More information about the bazaar mailing list