~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

Initial commit for russian version of documents.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2008 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
2
2
#
3
3
# Authors:
4
4
#   Johan Rydberg <jrydberg@gnu.org>
15
15
#
16
16
# You should have received a copy of the GNU General Public License
17
17
# along with this program; if not, write to the Free Software
18
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
 
20
20
"""Versioned text file storage api."""
21
21
 
22
22
from copy import copy
23
23
from cStringIO import StringIO
24
24
import os
25
 
import struct
 
25
import urllib
26
26
from zlib import adler32
27
27
 
28
28
from bzrlib.lazy_import import lazy_import
29
29
lazy_import(globals(), """
30
 
import urllib
31
30
 
32
31
from bzrlib import (
33
 
    annotate,
34
32
    errors,
35
 
    groupcompress,
36
 
    index,
37
 
    knit,
38
33
    osutils,
39
34
    multiparent,
40
35
    tsort,
41
36
    revision,
42
37
    ui,
43
38
    )
44
 
from bzrlib.graph import DictParentsProvider, Graph, StackedParentsProvider
 
39
from bzrlib.graph import DictParentsProvider, Graph, _StackedParentsProvider
45
40
from bzrlib.transport.memory import MemoryTransport
46
41
""")
47
42
from bzrlib.inter import InterObject
48
43
from bzrlib.registry import Registry
49
44
from bzrlib.symbol_versioning import *
50
45
from bzrlib.textmerge import TextMerge
51
 
from bzrlib import bencode
52
46
 
53
47
 
54
48
adapter_registry = Registry()
64
58
    'bzrlib.knit', 'FTAnnotatedToUnannotated')
65
59
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'fulltext'),
66
60
    'bzrlib.knit', 'FTAnnotatedToFullText')
67
 
# adapter_registry.register_lazy(('knit-annotated-ft-gz', 'chunked'),
68
 
#     'bzrlib.knit', 'FTAnnotatedToChunked')
69
61
 
70
62
 
71
63
class ContentFactory(object):
72
64
    """Abstract interface for insertion and retrieval from a VersionedFile.
73
 
 
 
65
    
74
66
    :ivar sha1: None, or the sha1 of the content fulltext.
75
67
    :ivar storage_kind: The native storage kind of this factory. One of
76
68
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
91
83
        self.parents = None
92
84
 
93
85
 
94
 
class ChunkedContentFactory(ContentFactory):
95
 
    """Static data content factory.
96
 
 
97
 
    This takes a 'chunked' list of strings. The only requirement on 'chunked' is
98
 
    that ''.join(lines) becomes a valid fulltext. A tuple of a single string
99
 
    satisfies this, as does a list of lines.
100
 
 
101
 
    :ivar sha1: None, or the sha1 of the content fulltext.
102
 
    :ivar storage_kind: The native storage kind of this factory. Always
103
 
        'chunked'
104
 
    :ivar key: The key of this content. Each key is a tuple with a single
105
 
        string in it.
106
 
    :ivar parents: A tuple of parent keys for self.key. If the object has
107
 
        no parent information, None (as opposed to () for an empty list of
108
 
        parents).
109
 
     """
110
 
 
111
 
    def __init__(self, key, parents, sha1, chunks):
112
 
        """Create a ContentFactory."""
113
 
        self.sha1 = sha1
114
 
        self.storage_kind = 'chunked'
115
 
        self.key = key
116
 
        self.parents = parents
117
 
        self._chunks = chunks
118
 
 
119
 
    def get_bytes_as(self, storage_kind):
120
 
        if storage_kind == 'chunked':
121
 
            return self._chunks
122
 
        elif storage_kind == 'fulltext':
123
 
            return ''.join(self._chunks)
124
 
        raise errors.UnavailableRepresentation(self.key, storage_kind,
125
 
            self.storage_kind)
126
 
 
127
 
 
128
86
class FulltextContentFactory(ContentFactory):
129
87
    """Static data content factory.
130
88
 
131
89
    This takes a fulltext when created and just returns that during
132
90
    get_bytes_as('fulltext').
133
 
 
 
91
    
134
92
    :ivar sha1: None, or the sha1 of the content fulltext.
135
93
    :ivar storage_kind: The native storage kind of this factory. Always
136
94
        'fulltext'.
152
110
    def get_bytes_as(self, storage_kind):
153
111
        if storage_kind == self.storage_kind:
154
112
            return self._text
155
 
        elif storage_kind == 'chunked':
156
 
            return [self._text]
157
113
        raise errors.UnavailableRepresentation(self.key, storage_kind,
158
114
            self.storage_kind)
159
115
 
160
116
 
161
117
class AbsentContentFactory(ContentFactory):
162
118
    """A placeholder content factory for unavailable texts.
163
 
 
 
119
    
164
120
    :ivar sha1: None.
165
121
    :ivar storage_kind: 'absent'.
166
122
    :ivar key: The key of this content. Each key is a tuple with a single
175
131
        self.key = key
176
132
        self.parents = None
177
133
 
178
 
    def get_bytes_as(self, storage_kind):
179
 
        raise ValueError('A request was made for key: %s, but that'
180
 
                         ' content is not available, and the calling'
181
 
                         ' code does not handle if it is missing.'
182
 
                         % (self.key,))
183
 
 
184
134
 
185
135
class AdapterFactory(ContentFactory):
186
136
    """A content factory to adapt between key prefix's."""
208
158
 
209
159
class VersionedFile(object):
210
160
    """Versioned text file storage.
211
 
 
 
161
    
212
162
    A versioned file manages versions of line-based text files,
213
163
    keeping track of the originating version for each line.
214
164
 
252
202
    def insert_record_stream(self, stream):
253
203
        """Insert a record stream into this versioned file.
254
204
 
255
 
        :param stream: A stream of records to insert.
 
205
        :param stream: A stream of records to insert. 
256
206
        :return: None
257
207
        :seealso VersionedFile.get_record_stream:
258
208
        """
277
227
            the data back accurately. (Checking the lines have been split
278
228
            correctly is expensive and extremely unlikely to catch bugs so it
279
229
            is not done at runtime unless check_content is True.)
280
 
        :param parent_texts: An optional dictionary containing the opaque
 
230
        :param parent_texts: An optional dictionary containing the opaque 
281
231
            representations of some or all of the parents of version_id to
282
232
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
283
233
            returned by add_lines or data corruption can be caused.
311
261
        parent_texts=None, nostore_sha=None, random_id=False,
312
262
        check_content=True, left_matching_blocks=None):
313
263
        """Add lines to the versioned file, allowing ghosts to be present.
314
 
 
 
264
        
315
265
        This takes the same parameters as add_lines and returns the same.
316
266
        """
317
267
        self._check_write_ok()
341
291
 
342
292
    def get_format_signature(self):
343
293
        """Get a text description of the data encoding in this file.
344
 
 
 
294
        
345
295
        :since: 0.90
346
296
        """
347
297
        raise NotImplementedError(self.get_format_signature)
468
418
        if isinstance(version_ids, basestring):
469
419
            version_ids = [version_ids]
470
420
        raise NotImplementedError(self.get_ancestry)
471
 
 
 
421
        
472
422
    def get_ancestry_with_ghosts(self, version_ids):
473
423
        """Return a list of all ancestors of given version(s). This
474
424
        will not include the null revision.
475
425
 
476
426
        Must raise RevisionNotPresent if any of the given versions are
477
427
        not present in file history.
478
 
 
 
428
        
479
429
        Ghosts that are known about will be included in ancestry list,
480
430
        but are not explicitly marked.
481
431
        """
482
432
        raise NotImplementedError(self.get_ancestry_with_ghosts)
483
 
 
 
433
    
484
434
    def get_parent_map(self, version_ids):
485
435
        """Get a map of the parents of version_ids.
486
436
 
549
499
        unchanged   Alive in both a and b (possibly created in both)
550
500
        new-a       Created in a
551
501
        new-b       Created in b
552
 
        ghost-a     Killed in a, unborn in b
 
502
        ghost-a     Killed in a, unborn in b    
553
503
        ghost-b     Killed in b, unborn in a
554
504
        irrelevant  Not in either revision
555
505
        """
556
506
        raise NotImplementedError(VersionedFile.plan_merge)
557
 
 
 
507
        
558
508
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
559
509
                    b_marker=TextMerge.B_MARKER):
560
510
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
562
512
 
563
513
class RecordingVersionedFilesDecorator(object):
564
514
    """A minimal versioned files that records calls made on it.
565
 
 
 
515
    
566
516
    Only enough methods have been added to support tests using it to date.
567
517
 
568
518
    :ivar calls: A list of the calls made; can be reset at any time by
570
520
    """
571
521
 
572
522
    def __init__(self, backing_vf):
573
 
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
574
 
 
 
523
        """Create a RecordingVersionedFileDsecorator decorating backing_vf.
 
524
        
575
525
        :param backing_vf: The versioned file to answer all methods.
576
526
        """
577
527
        self._backing_vf = backing_vf
611
561
        return self._backing_vf.keys()
612
562
 
613
563
 
614
 
class OrderingVersionedFilesDecorator(RecordingVersionedFilesDecorator):
615
 
    """A VF that records calls, and returns keys in specific order.
616
 
 
617
 
    :ivar calls: A list of the calls made; can be reset at any time by
618
 
        assigning [] to it.
619
 
    """
620
 
 
621
 
    def __init__(self, backing_vf, key_priority):
622
 
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
623
 
 
624
 
        :param backing_vf: The versioned file to answer all methods.
625
 
        :param key_priority: A dictionary defining what order keys should be
626
 
            returned from an 'unordered' get_record_stream request.
627
 
            Keys with lower priority are returned first, keys not present in
628
 
            the map get an implicit priority of 0, and are returned in
629
 
            lexicographical order.
630
 
        """
631
 
        RecordingVersionedFilesDecorator.__init__(self, backing_vf)
632
 
        self._key_priority = key_priority
633
 
 
634
 
    def get_record_stream(self, keys, sort_order, include_delta_closure):
635
 
        self.calls.append(("get_record_stream", list(keys), sort_order,
636
 
            include_delta_closure))
637
 
        if sort_order == 'unordered':
638
 
            def sort_key(key):
639
 
                return (self._key_priority.get(key, 0), key)
640
 
            # Use a defined order by asking for the keys one-by-one from the
641
 
            # backing_vf
642
 
            for key in sorted(keys, key=sort_key):
643
 
                for record in self._backing_vf.get_record_stream([key],
644
 
                                'unordered', include_delta_closure):
645
 
                    yield record
646
 
        else:
647
 
            for record in self._backing_vf.get_record_stream(keys, sort_order,
648
 
                            include_delta_closure):
649
 
                yield record
650
 
 
651
 
 
652
564
class KeyMapper(object):
653
565
    """KeyMappers map between keys and underlying partitioned storage."""
654
566
 
663
575
 
664
576
    def unmap(self, partition_id):
665
577
        """Map a partitioned storage id back to a key prefix.
666
 
 
 
578
        
667
579
        :param partition_id: The underlying partition id.
668
580
        :return: As much of a key (or prefix) as is derivable from the partition
669
581
            id.
701
613
 
702
614
class PrefixMapper(URLEscapeMapper):
703
615
    """A key mapper that extracts the first component of a key.
704
 
 
 
616
    
705
617
    This mapper is for use with a transport based backend.
706
618
    """
707
619
 
740
652
 
741
653
class HashEscapedPrefixMapper(HashPrefixMapper):
742
654
    """Combines the escaped first component of a key with a hash.
743
 
 
 
655
    
744
656
    This mapper is for use with a transport based backend.
745
657
    """
746
658
 
802
714
        check_content=True):
803
715
        """Add a text to the store.
804
716
 
805
 
        :param key: The key tuple of the text to add. If the last element is
806
 
            None, a CHK string will be generated during the addition.
 
717
        :param key: The key tuple of the text to add.
807
718
        :param parents: The parents key tuples of the text to add.
808
719
        :param lines: A list of lines. Each line must be a bytestring. And all
809
720
            of them except the last must be terminated with \n and contain no
813
724
            the data back accurately. (Checking the lines have been split
814
725
            correctly is expensive and extremely unlikely to catch bugs so it
815
726
            is not done at runtime unless check_content is True.)
816
 
        :param parent_texts: An optional dictionary containing the opaque
 
727
        :param parent_texts: An optional dictionary containing the opaque 
817
728
            representations of some or all of the parents of version_id to
818
729
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
819
730
            returned by add_lines or data corruption can be caused.
836
747
        """
837
748
        raise NotImplementedError(self.add_lines)
838
749
 
839
 
    def _add_text(self, key, parents, text, nostore_sha=None, random_id=False):
840
 
        """Add a text to the store.
841
 
 
842
 
        This is a private function for use by CommitBuilder.
843
 
 
844
 
        :param key: The key tuple of the text to add. If the last element is
845
 
            None, a CHK string will be generated during the addition.
846
 
        :param parents: The parents key tuples of the text to add.
847
 
        :param text: A string containing the text to be committed.
848
 
        :param nostore_sha: Raise ExistingContent and do not add the lines to
849
 
            the versioned file if the digest of the lines matches this.
850
 
        :param random_id: If True a random id has been selected rather than
851
 
            an id determined by some deterministic process such as a converter
852
 
            from a foreign VCS. When True the backend may choose not to check
853
 
            for uniqueness of the resulting key within the versioned file, so
854
 
            this should only be done when the result is expected to be unique
855
 
            anyway.
856
 
        :param check_content: If True, the lines supplied are verified to be
857
 
            bytestrings that are correctly formed lines.
858
 
        :return: The text sha1, the number of bytes in the text, and an opaque
859
 
                 representation of the inserted version which can be provided
860
 
                 back to future _add_text calls in the parent_texts dictionary.
861
 
        """
862
 
        # The default implementation just thunks over to .add_lines(),
863
 
        # inefficient, but it works.
864
 
        return self.add_lines(key, parents, osutils.split_lines(text),
865
 
                              nostore_sha=nostore_sha,
866
 
                              random_id=random_id,
867
 
                              check_content=True)
868
 
 
869
750
    def add_mpdiffs(self, records):
870
751
        """Add mpdiffs to this VersionedFile.
871
752
 
884
765
                                  if not mpvf.has_version(p))
885
766
        # It seems likely that adding all the present parents as fulltexts can
886
767
        # easily exhaust memory.
887
 
        chunks_to_lines = osutils.chunks_to_lines
 
768
        split_lines = osutils.split_lines
888
769
        for record in self.get_record_stream(needed_parents, 'unordered',
889
770
            True):
890
771
            if record.storage_kind == 'absent':
891
772
                continue
892
 
            mpvf.add_version(chunks_to_lines(record.get_bytes_as('chunked')),
 
773
            mpvf.add_version(split_lines(record.get_bytes_as('fulltext')),
893
774
                record.key, [])
894
775
        for (key, parent_keys, expected_sha1, mpdiff), lines in\
895
776
            zip(records, mpvf.get_line_list(versions)):
965
846
        """
966
847
        raise NotImplementedError(self.get_sha1s)
967
848
 
968
 
    has_key = index._has_key_from_parent_map
969
 
 
970
 
    def get_missing_compression_parent_keys(self):
971
 
        """Return an iterable of keys of missing compression parents.
972
 
 
973
 
        Check this after calling insert_record_stream to find out if there are
974
 
        any missing compression parents.  If there are, the records that
975
 
        depend on them are not able to be inserted safely. The precise
976
 
        behaviour depends on the concrete VersionedFiles class in use.
977
 
 
978
 
        Classes that do not support this will raise NotImplementedError.
979
 
        """
980
 
        raise NotImplementedError(self.get_missing_compression_parent_keys)
981
 
 
982
849
    def insert_record_stream(self, stream):
983
850
        """Insert a record stream into this container.
984
851
 
985
 
        :param stream: A stream of records to insert.
 
852
        :param stream: A stream of records to insert. 
986
853
        :return: None
987
854
        :seealso VersionedFile.get_record_stream:
988
855
        """
1032
899
        ghosts = maybe_ghosts - set(self.get_parent_map(maybe_ghosts))
1033
900
        knit_keys.difference_update(ghosts)
1034
901
        lines = {}
1035
 
        chunks_to_lines = osutils.chunks_to_lines
 
902
        split_lines = osutils.split_lines
1036
903
        for record in self.get_record_stream(knit_keys, 'topological', True):
1037
 
            lines[record.key] = chunks_to_lines(record.get_bytes_as('chunked'))
 
904
            lines[record.key] = split_lines(record.get_bytes_as('fulltext'))
1038
905
            # line_block_dict = {}
1039
906
            # for parent, blocks in record.extract_line_blocks():
1040
907
            #   line_blocks[parent] = blocks
1055
922
                parent_lines, left_parent_blocks))
1056
923
        return diffs
1057
924
 
1058
 
    missing_keys = index._missing_keys_from_parent_map
1059
 
 
1060
925
    def _extract_blocks(self, version_id, source, target):
1061
926
        return None
1062
927
 
1129
994
            result.append((prefix + (origin,), line))
1130
995
        return result
1131
996
 
1132
 
    def get_annotator(self):
1133
 
        return annotate.Annotator(self)
1134
 
 
1135
997
    def check(self, progress_bar=None):
1136
998
        """See VersionedFiles.check()."""
1137
999
        for prefix, vf in self._iter_all_components():
1219
1081
    def insert_record_stream(self, stream):
1220
1082
        """Insert a record stream into this container.
1221
1083
 
1222
 
        :param stream: A stream of records to insert.
 
1084
        :param stream: A stream of records to insert. 
1223
1085
        :return: None
1224
1086
        :seealso VersionedFile.get_record_stream:
1225
1087
        """
1346
1208
                lines = self._lines[key]
1347
1209
                parents = self._parents[key]
1348
1210
                pending.remove(key)
1349
 
                yield ChunkedContentFactory(key, parents, None, lines)
 
1211
                yield FulltextContentFactory(key, parents, None,
 
1212
                    ''.join(lines))
1350
1213
        for versionedfile in self.fallback_versionedfiles:
1351
1214
            for record in versionedfile.get_record_stream(
1352
1215
                pending, 'unordered', True):
1373
1236
            result[revision.NULL_REVISION] = ()
1374
1237
        self._providers = self._providers[:1] + self.fallback_versionedfiles
1375
1238
        result.update(
1376
 
            StackedParentsProvider(self._providers).get_parent_map(keys))
 
1239
            _StackedParentsProvider(self._providers).get_parent_map(keys))
1377
1240
        for key, parents in result.iteritems():
1378
1241
            if parents == ():
1379
1242
                result[key] = (revision.NULL_REVISION,)
1382
1245
 
1383
1246
class PlanWeaveMerge(TextMerge):
1384
1247
    """Weave merge that takes a plan as its input.
1385
 
 
 
1248
    
1386
1249
    This exists so that VersionedFile.plan_merge is implementable.
1387
1250
    Most callers will want to use WeaveMerge instead.
1388
1251
    """
1409
1272
                yield(lines_a,)
1410
1273
            else:
1411
1274
                yield (lines_a, lines_b)
1412
 
 
 
1275
       
1413
1276
        # We previously considered either 'unchanged' or 'killed-both' lines
1414
1277
        # to be possible places to resynchronize.  However, assuming agreement
1415
1278
        # on killed-both lines may be too aggressive. -- mbp 20060324
1421
1284
                lines_a = []
1422
1285
                lines_b = []
1423
1286
                ch_a = ch_b = False
1424
 
 
 
1287
                
1425
1288
            if state == 'unchanged':
1426
1289
                if line:
1427
1290
                    yield ([line],)
1443
1306
            elif state == 'conflicted-b':
1444
1307
                ch_b = ch_a = True
1445
1308
                lines_b.append(line)
1446
 
            elif state == 'killed-both':
1447
 
                # This counts as a change, even though there is no associated
1448
 
                # line
1449
 
                ch_b = ch_a = True
1450
1309
            else:
1451
1310
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
1452
 
                        'killed-base'):
 
1311
                        'killed-base', 'killed-both'):
1453
1312
                    raise AssertionError(state)
1454
1313
        for struct in outstanding_struct():
1455
1314
            yield struct
1458
1317
class WeaveMerge(PlanWeaveMerge):
1459
1318
    """Weave merge that takes a VersionedFile and two versions as its input."""
1460
1319
 
1461
 
    def __init__(self, versionedfile, ver_a, ver_b,
 
1320
    def __init__(self, versionedfile, ver_a, ver_b, 
1462
1321
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
1463
1322
        plan = versionedfile.plan_merge(ver_a, ver_b)
1464
1323
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
1465
1324
 
1466
1325
 
1467
1326
class VirtualVersionedFiles(VersionedFiles):
1468
 
    """Dummy implementation for VersionedFiles that uses other functions for
 
1327
    """Dummy implementation for VersionedFiles that uses other functions for 
1469
1328
    obtaining fulltexts and parent maps.
1470
1329
 
1471
 
    This is always on the bottom of the stack and uses string keys
 
1330
    This is always on the bottom of the stack and uses string keys 
1472
1331
    (rather than tuples) internally.
1473
1332
    """
1474
1333
 
1476
1335
        """Create a VirtualVersionedFiles.
1477
1336
 
1478
1337
        :param get_parent_map: Same signature as Repository.get_parent_map.
1479
 
        :param get_lines: Should return lines for specified key or None if
 
1338
        :param get_lines: Should return lines for specified key or None if 
1480
1339
                          not available.
1481
1340
        """
1482
1341
        super(VirtualVersionedFiles, self).__init__()
1483
1342
        self._get_parent_map = get_parent_map
1484
1343
        self._get_lines = get_lines
1485
 
 
 
1344
        
1486
1345
    def check(self, progressbar=None):
1487
1346
        """See VersionedFiles.check.
1488
1347
 
1520
1379
            if lines is not None:
1521
1380
                if not isinstance(lines, list):
1522
1381
                    raise AssertionError
1523
 
                yield ChunkedContentFactory((k,), None,
 
1382
                yield FulltextContentFactory((k,), None, 
1524
1383
                        sha1=osutils.sha_strings(lines),
1525
 
                        chunks=lines)
 
1384
                        text=''.join(lines))
1526
1385
            else:
1527
1386
                yield AbsentContentFactory((k,))
1528
1387
 
1529
 
    def iter_lines_added_or_present_in_keys(self, keys, pb=None):
1530
 
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
1531
 
        for i, (key,) in enumerate(keys):
1532
 
            if pb is not None:
1533
 
                pb.update("Finding changed lines", i, len(keys))
1534
 
            for l in self._get_lines(key):
1535
 
                yield (l, key)
1536
 
 
1537
 
 
1538
 
def network_bytes_to_kind_and_offset(network_bytes):
1539
 
    """Strip of a record kind from the front of network_bytes.
1540
 
 
1541
 
    :param network_bytes: The bytes of a record.
1542
 
    :return: A tuple (storage_kind, offset_of_remaining_bytes)
1543
 
    """
1544
 
    line_end = network_bytes.find('\n')
1545
 
    storage_kind = network_bytes[:line_end]
1546
 
    return storage_kind, line_end + 1
1547
 
 
1548
 
 
1549
 
class NetworkRecordStream(object):
1550
 
    """A record_stream which reconstitures a serialised stream."""
1551
 
 
1552
 
    def __init__(self, bytes_iterator):
1553
 
        """Create a NetworkRecordStream.
1554
 
 
1555
 
        :param bytes_iterator: An iterator of bytes. Each item in this
1556
 
            iterator should have been obtained from a record_streams'
1557
 
            record.get_bytes_as(record.storage_kind) call.
1558
 
        """
1559
 
        self._bytes_iterator = bytes_iterator
1560
 
        self._kind_factory = {'knit-ft-gz':knit.knit_network_to_record,
1561
 
            'knit-delta-gz':knit.knit_network_to_record,
1562
 
            'knit-annotated-ft-gz':knit.knit_network_to_record,
1563
 
            'knit-annotated-delta-gz':knit.knit_network_to_record,
1564
 
            'knit-delta-closure':knit.knit_delta_closure_to_records,
1565
 
            'fulltext':fulltext_network_to_record,
1566
 
            'groupcompress-block':groupcompress.network_block_to_records,
1567
 
            }
1568
 
 
1569
 
    def read(self):
1570
 
        """Read the stream.
1571
 
 
1572
 
        :return: An iterator as per VersionedFiles.get_record_stream().
1573
 
        """
1574
 
        for bytes in self._bytes_iterator:
1575
 
            storage_kind, line_end = network_bytes_to_kind_and_offset(bytes)
1576
 
            for record in self._kind_factory[storage_kind](
1577
 
                storage_kind, bytes, line_end):
1578
 
                yield record
1579
 
 
1580
 
 
1581
 
def fulltext_network_to_record(kind, bytes, line_end):
1582
 
    """Convert a network fulltext record to record."""
1583
 
    meta_len, = struct.unpack('!L', bytes[line_end:line_end+4])
1584
 
    record_meta = bytes[line_end+4:line_end+4+meta_len]
1585
 
    key, parents = bencode.bdecode_as_tuple(record_meta)
1586
 
    if parents == 'nil':
1587
 
        parents = None
1588
 
    fulltext = bytes[line_end+4+meta_len:]
1589
 
    return [FulltextContentFactory(key, parents, None, fulltext)]
1590
 
 
1591
 
 
1592
 
def _length_prefix(bytes):
1593
 
    return struct.pack('!L', len(bytes))
1594
 
 
1595
 
 
1596
 
def record_to_fulltext_bytes(record):
1597
 
    if record.parents is None:
1598
 
        parents = 'nil'
1599
 
    else:
1600
 
        parents = record.parents
1601
 
    record_meta = bencode.bencode((record.key, parents))
1602
 
    record_content = record.get_bytes_as('fulltext')
1603
 
    return "fulltext\n%s%s%s" % (
1604
 
        _length_prefix(record_meta), record_meta, record_content)
1605
 
 
1606
 
 
1607
 
def sort_groupcompress(parent_map):
1608
 
    """Sort and group the keys in parent_map into groupcompress order.
1609
 
 
1610
 
    groupcompress is defined (currently) as reverse-topological order, grouped
1611
 
    by the key prefix.
1612
 
 
1613
 
    :return: A sorted-list of keys
1614
 
    """
1615
 
    # gc-optimal ordering is approximately reverse topological,
1616
 
    # properly grouped by file-id.
1617
 
    per_prefix_map = {}
1618
 
    for item in parent_map.iteritems():
1619
 
        key = item[0]
1620
 
        if isinstance(key, str) or len(key) == 1:
1621
 
            prefix = ''
1622
 
        else:
1623
 
            prefix = key[0]
1624
 
        try:
1625
 
            per_prefix_map[prefix].append(item)
1626
 
        except KeyError:
1627
 
            per_prefix_map[prefix] = [item]
1628
 
 
1629
 
    present_keys = []
1630
 
    for prefix in sorted(per_prefix_map):
1631
 
        present_keys.extend(reversed(tsort.topo_sort(per_prefix_map[prefix])))
1632
 
    return present_keys
 
1388
 
 
1389