~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: Mark Hammond
  • Date: 2009-01-12 01:55:34 UTC
  • mto: (3995.8.2 prepare-1.12)
  • mto: This revision was merged to the branch mainline in revision 4007.
  • Revision ID: mhammond@skippinet.com.au-20090112015534-yfxg50p7mpds9j4v
Include all .html files from the tortoise doc directory.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
26
25
from zlib import adler32
27
26
 
28
27
from bzrlib.lazy_import import lazy_import
30
29
import urllib
31
30
 
32
31
from bzrlib import (
33
 
    annotate,
34
32
    errors,
35
 
    graph as _mod_graph,
36
 
    groupcompress,
37
33
    index,
38
 
    knit,
39
34
    osutils,
40
35
    multiparent,
41
36
    tsort,
42
37
    revision,
43
38
    ui,
44
39
    )
45
 
from bzrlib.graph import DictParentsProvider, Graph, StackedParentsProvider
 
40
from bzrlib.graph import DictParentsProvider, Graph, _StackedParentsProvider
46
41
from bzrlib.transport.memory import MemoryTransport
47
42
""")
48
43
from bzrlib.inter import InterObject
49
44
from bzrlib.registry import Registry
50
45
from bzrlib.symbol_versioning import *
51
46
from bzrlib.textmerge import TextMerge
52
 
from bzrlib import bencode
53
47
 
54
48
 
55
49
adapter_registry = Registry()
71
65
 
72
66
class ContentFactory(object):
73
67
    """Abstract interface for insertion and retrieval from a VersionedFile.
74
 
 
 
68
    
75
69
    :ivar sha1: None, or the sha1 of the content fulltext.
76
70
    :ivar storage_kind: The native storage kind of this factory. One of
77
71
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
154
148
        if storage_kind == self.storage_kind:
155
149
            return self._text
156
150
        elif storage_kind == 'chunked':
157
 
            return [self._text]
 
151
            return (self._text,)
158
152
        raise errors.UnavailableRepresentation(self.key, storage_kind,
159
153
            self.storage_kind)
160
154
 
161
155
 
162
156
class AbsentContentFactory(ContentFactory):
163
157
    """A placeholder content factory for unavailable texts.
164
 
 
 
158
    
165
159
    :ivar sha1: None.
166
160
    :ivar storage_kind: 'absent'.
167
161
    :ivar key: The key of this content. Each key is a tuple with a single
176
170
        self.key = key
177
171
        self.parents = None
178
172
 
179
 
    def get_bytes_as(self, storage_kind):
180
 
        raise ValueError('A request was made for key: %s, but that'
181
 
                         ' content is not available, and the calling'
182
 
                         ' code does not handle if it is missing.'
183
 
                         % (self.key,))
184
 
 
185
173
 
186
174
class AdapterFactory(ContentFactory):
187
175
    """A content factory to adapt between key prefix's."""
209
197
 
210
198
class VersionedFile(object):
211
199
    """Versioned text file storage.
212
 
 
 
200
    
213
201
    A versioned file manages versions of line-based text files,
214
202
    keeping track of the originating version for each line.
215
203
 
253
241
    def insert_record_stream(self, stream):
254
242
        """Insert a record stream into this versioned file.
255
243
 
256
 
        :param stream: A stream of records to insert.
 
244
        :param stream: A stream of records to insert. 
257
245
        :return: None
258
246
        :seealso VersionedFile.get_record_stream:
259
247
        """
278
266
            the data back accurately. (Checking the lines have been split
279
267
            correctly is expensive and extremely unlikely to catch bugs so it
280
268
            is not done at runtime unless check_content is True.)
281
 
        :param parent_texts: An optional dictionary containing the opaque
 
269
        :param parent_texts: An optional dictionary containing the opaque 
282
270
            representations of some or all of the parents of version_id to
283
271
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
284
272
            returned by add_lines or data corruption can be caused.
312
300
        parent_texts=None, nostore_sha=None, random_id=False,
313
301
        check_content=True, left_matching_blocks=None):
314
302
        """Add lines to the versioned file, allowing ghosts to be present.
315
 
 
 
303
        
316
304
        This takes the same parameters as add_lines and returns the same.
317
305
        """
318
306
        self._check_write_ok()
342
330
 
343
331
    def get_format_signature(self):
344
332
        """Get a text description of the data encoding in this file.
345
 
 
 
333
        
346
334
        :since: 0.90
347
335
        """
348
336
        raise NotImplementedError(self.get_format_signature)
469
457
        if isinstance(version_ids, basestring):
470
458
            version_ids = [version_ids]
471
459
        raise NotImplementedError(self.get_ancestry)
472
 
 
 
460
        
473
461
    def get_ancestry_with_ghosts(self, version_ids):
474
462
        """Return a list of all ancestors of given version(s). This
475
463
        will not include the null revision.
476
464
 
477
465
        Must raise RevisionNotPresent if any of the given versions are
478
466
        not present in file history.
479
 
 
 
467
        
480
468
        Ghosts that are known about will be included in ancestry list,
481
469
        but are not explicitly marked.
482
470
        """
483
471
        raise NotImplementedError(self.get_ancestry_with_ghosts)
484
 
 
 
472
    
485
473
    def get_parent_map(self, version_ids):
486
474
        """Get a map of the parents of version_ids.
487
475
 
550
538
        unchanged   Alive in both a and b (possibly created in both)
551
539
        new-a       Created in a
552
540
        new-b       Created in b
553
 
        ghost-a     Killed in a, unborn in b
 
541
        ghost-a     Killed in a, unborn in b    
554
542
        ghost-b     Killed in b, unborn in a
555
543
        irrelevant  Not in either revision
556
544
        """
557
545
        raise NotImplementedError(VersionedFile.plan_merge)
558
 
 
 
546
        
559
547
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
560
548
                    b_marker=TextMerge.B_MARKER):
561
549
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
563
551
 
564
552
class RecordingVersionedFilesDecorator(object):
565
553
    """A minimal versioned files that records calls made on it.
566
 
 
 
554
    
567
555
    Only enough methods have been added to support tests using it to date.
568
556
 
569
557
    :ivar calls: A list of the calls made; can be reset at any time by
572
560
 
573
561
    def __init__(self, backing_vf):
574
562
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
575
 
 
 
563
        
576
564
        :param backing_vf: The versioned file to answer all methods.
577
565
        """
578
566
        self._backing_vf = backing_vf
664
652
 
665
653
    def unmap(self, partition_id):
666
654
        """Map a partitioned storage id back to a key prefix.
667
 
 
 
655
        
668
656
        :param partition_id: The underlying partition id.
669
657
        :return: As much of a key (or prefix) as is derivable from the partition
670
658
            id.
702
690
 
703
691
class PrefixMapper(URLEscapeMapper):
704
692
    """A key mapper that extracts the first component of a key.
705
 
 
 
693
    
706
694
    This mapper is for use with a transport based backend.
707
695
    """
708
696
 
741
729
 
742
730
class HashEscapedPrefixMapper(HashPrefixMapper):
743
731
    """Combines the escaped first component of a key with a hash.
744
 
 
 
732
    
745
733
    This mapper is for use with a transport based backend.
746
734
    """
747
735
 
803
791
        check_content=True):
804
792
        """Add a text to the store.
805
793
 
806
 
        :param key: The key tuple of the text to add. If the last element is
807
 
            None, a CHK string will be generated during the addition.
 
794
        :param key: The key tuple of the text to add.
808
795
        :param parents: The parents key tuples of the text to add.
809
796
        :param lines: A list of lines. Each line must be a bytestring. And all
810
797
            of them except the last must be terminated with \n and contain no
814
801
            the data back accurately. (Checking the lines have been split
815
802
            correctly is expensive and extremely unlikely to catch bugs so it
816
803
            is not done at runtime unless check_content is True.)
817
 
        :param parent_texts: An optional dictionary containing the opaque
 
804
        :param parent_texts: An optional dictionary containing the opaque 
818
805
            representations of some or all of the parents of version_id to
819
806
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
820
807
            returned by add_lines or data corruption can be caused.
837
824
        """
838
825
        raise NotImplementedError(self.add_lines)
839
826
 
840
 
    def _add_text(self, key, parents, text, nostore_sha=None, random_id=False):
841
 
        """Add a text to the store.
842
 
 
843
 
        This is a private function for use by CommitBuilder.
844
 
 
845
 
        :param key: The key tuple of the text to add. If the last element is
846
 
            None, a CHK string will be generated during the addition.
847
 
        :param parents: The parents key tuples of the text to add.
848
 
        :param text: A string containing the text to be committed.
849
 
        :param nostore_sha: Raise ExistingContent and do not add the lines to
850
 
            the versioned file if the digest of the lines matches this.
851
 
        :param random_id: If True a random id has been selected rather than
852
 
            an id determined by some deterministic process such as a converter
853
 
            from a foreign VCS. When True the backend may choose not to check
854
 
            for uniqueness of the resulting key within the versioned file, so
855
 
            this should only be done when the result is expected to be unique
856
 
            anyway.
857
 
        :param check_content: If True, the lines supplied are verified to be
858
 
            bytestrings that are correctly formed lines.
859
 
        :return: The text sha1, the number of bytes in the text, and an opaque
860
 
                 representation of the inserted version which can be provided
861
 
                 back to future _add_text calls in the parent_texts dictionary.
862
 
        """
863
 
        # The default implementation just thunks over to .add_lines(),
864
 
        # inefficient, but it works.
865
 
        return self.add_lines(key, parents, osutils.split_lines(text),
866
 
                              nostore_sha=nostore_sha,
867
 
                              random_id=random_id,
868
 
                              check_content=True)
869
 
 
870
827
    def add_mpdiffs(self, records):
871
828
        """Add mpdiffs to this VersionedFile.
872
829
 
914
871
        raise NotImplementedError(self.annotate)
915
872
 
916
873
    def check(self, progress_bar=None):
917
 
        """Check this object for integrity.
918
 
        
919
 
        :param progress_bar: A progress bar to output as the check progresses.
920
 
        :param keys: Specific keys within the VersionedFiles to check. When
921
 
            this parameter is not None, check() becomes a generator as per
922
 
            get_record_stream. The difference to get_record_stream is that
923
 
            more or deeper checks will be performed.
924
 
        :return: None, or if keys was supplied a generator as per
925
 
            get_record_stream.
926
 
        """
 
874
        """Check this object for integrity."""
927
875
        raise NotImplementedError(self.check)
928
876
 
929
877
    @staticmethod
930
878
    def check_not_reserved_id(version_id):
931
879
        revision.check_not_reserved_id(version_id)
932
880
 
933
 
    def clear_cache(self):
934
 
        """Clear whatever caches this VersionedFile holds.
935
 
 
936
 
        This is generally called after an operation has been performed, when we
937
 
        don't expect to be using this versioned file again soon.
938
 
        """
939
 
 
940
881
    def _check_lines_not_unicode(self, lines):
941
882
        """Check that lines being added to a versioned file are not unicode."""
942
883
        for line in lines:
949
890
            if '\n' in line[:-1]:
950
891
                raise errors.BzrBadParameterContainsNewline("lines")
951
892
 
952
 
    def get_known_graph_ancestry(self, keys):
953
 
        """Get a KnownGraph instance with the ancestry of keys."""
954
 
        # most basic implementation is a loop around get_parent_map
955
 
        pending = set(keys)
956
 
        parent_map = {}
957
 
        while pending:
958
 
            this_parent_map = self.get_parent_map(pending)
959
 
            parent_map.update(this_parent_map)
960
 
            pending = set()
961
 
            map(pending.update, this_parent_map.itervalues())
962
 
            pending = pending.difference(parent_map)
963
 
        kg = _mod_graph.KnownGraph(parent_map)
964
 
        return kg
965
 
 
966
893
    def get_parent_map(self, keys):
967
894
        """Get a map of the parents of keys.
968
895
 
998
925
 
999
926
    has_key = index._has_key_from_parent_map
1000
927
 
1001
 
    def get_missing_compression_parent_keys(self):
1002
 
        """Return an iterable of keys of missing compression parents.
1003
 
 
1004
 
        Check this after calling insert_record_stream to find out if there are
1005
 
        any missing compression parents.  If there are, the records that
1006
 
        depend on them are not able to be inserted safely. The precise
1007
 
        behaviour depends on the concrete VersionedFiles class in use.
1008
 
 
1009
 
        Classes that do not support this will raise NotImplementedError.
1010
 
        """
1011
 
        raise NotImplementedError(self.get_missing_compression_parent_keys)
1012
 
 
1013
928
    def insert_record_stream(self, stream):
1014
929
        """Insert a record stream into this container.
1015
930
 
1016
 
        :param stream: A stream of records to insert.
 
931
        :param stream: A stream of records to insert. 
1017
932
        :return: None
1018
933
        :seealso VersionedFile.get_record_stream:
1019
934
        """
1160
1075
            result.append((prefix + (origin,), line))
1161
1076
        return result
1162
1077
 
1163
 
    def get_annotator(self):
1164
 
        return annotate.Annotator(self)
1165
 
 
1166
 
    def check(self, progress_bar=None, keys=None):
 
1078
    def check(self, progress_bar=None):
1167
1079
        """See VersionedFiles.check()."""
1168
 
        # XXX: This is over-enthusiastic but as we only thunk for Weaves today
1169
 
        # this is tolerable. Ideally we'd pass keys down to check() and 
1170
 
        # have the older VersiondFile interface updated too.
1171
1080
        for prefix, vf in self._iter_all_components():
1172
1081
            vf.check()
1173
 
        if keys is not None:
1174
 
            return self.get_record_stream(keys, 'unordered', True)
1175
1082
 
1176
1083
    def get_parent_map(self, keys):
1177
1084
        """Get a map of the parents of keys.
1255
1162
    def insert_record_stream(self, stream):
1256
1163
        """Insert a record stream into this container.
1257
1164
 
1258
 
        :param stream: A stream of records to insert.
 
1165
        :param stream: A stream of records to insert. 
1259
1166
        :return: None
1260
1167
        :seealso VersionedFile.get_record_stream:
1261
1168
        """
1409
1316
            result[revision.NULL_REVISION] = ()
1410
1317
        self._providers = self._providers[:1] + self.fallback_versionedfiles
1411
1318
        result.update(
1412
 
            StackedParentsProvider(self._providers).get_parent_map(keys))
 
1319
            _StackedParentsProvider(self._providers).get_parent_map(keys))
1413
1320
        for key, parents in result.iteritems():
1414
1321
            if parents == ():
1415
1322
                result[key] = (revision.NULL_REVISION,)
1418
1325
 
1419
1326
class PlanWeaveMerge(TextMerge):
1420
1327
    """Weave merge that takes a plan as its input.
1421
 
 
 
1328
    
1422
1329
    This exists so that VersionedFile.plan_merge is implementable.
1423
1330
    Most callers will want to use WeaveMerge instead.
1424
1331
    """
1426
1333
    def __init__(self, plan, a_marker=TextMerge.A_MARKER,
1427
1334
                 b_marker=TextMerge.B_MARKER):
1428
1335
        TextMerge.__init__(self, a_marker, b_marker)
1429
 
        self.plan = list(plan)
 
1336
        self.plan = plan
1430
1337
 
1431
1338
    def _merge_struct(self):
1432
1339
        lines_a = []
1445
1352
                yield(lines_a,)
1446
1353
            else:
1447
1354
                yield (lines_a, lines_b)
1448
 
 
 
1355
       
1449
1356
        # We previously considered either 'unchanged' or 'killed-both' lines
1450
1357
        # to be possible places to resynchronize.  However, assuming agreement
1451
1358
        # on killed-both lines may be too aggressive. -- mbp 20060324
1457
1364
                lines_a = []
1458
1365
                lines_b = []
1459
1366
                ch_a = ch_b = False
1460
 
 
 
1367
                
1461
1368
            if state == 'unchanged':
1462
1369
                if line:
1463
1370
                    yield ([line],)
1479
1386
            elif state == 'conflicted-b':
1480
1387
                ch_b = ch_a = True
1481
1388
                lines_b.append(line)
1482
 
            elif state == 'killed-both':
1483
 
                # This counts as a change, even though there is no associated
1484
 
                # line
1485
 
                ch_b = ch_a = True
1486
1389
            else:
1487
1390
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
1488
 
                        'killed-base'):
 
1391
                        'killed-base', 'killed-both'):
1489
1392
                    raise AssertionError(state)
1490
1393
        for struct in outstanding_struct():
1491
1394
            yield struct
1492
1395
 
1493
 
    def base_from_plan(self):
1494
 
        """Construct a BASE file from the plan text."""
1495
 
        base_lines = []
1496
 
        for state, line in self.plan:
1497
 
            if state in ('killed-a', 'killed-b', 'killed-both', 'unchanged'):
1498
 
                # If unchanged, then this line is straight from base. If a or b
1499
 
                # or both killed the line, then it *used* to be in base.
1500
 
                base_lines.append(line)
1501
 
            else:
1502
 
                if state not in ('killed-base', 'irrelevant',
1503
 
                                 'ghost-a', 'ghost-b',
1504
 
                                 'new-a', 'new-b',
1505
 
                                 'conflicted-a', 'conflicted-b'):
1506
 
                    # killed-base, irrelevant means it doesn't apply
1507
 
                    # ghost-a/ghost-b are harder to say for sure, but they
1508
 
                    # aren't in the 'inc_c' which means they aren't in the
1509
 
                    # shared base of a & b. So we don't include them.  And
1510
 
                    # obviously if the line is newly inserted, it isn't in base
1511
 
 
1512
 
                    # If 'conflicted-a' or b, then it is new vs one base, but
1513
 
                    # old versus another base. However, if we make it present
1514
 
                    # in the base, it will be deleted from the target, and it
1515
 
                    # seems better to get a line doubled in the merge result,
1516
 
                    # rather than have it deleted entirely.
1517
 
                    # Example, each node is the 'text' at that point:
1518
 
                    #           MN
1519
 
                    #          /   \
1520
 
                    #        MaN   MbN
1521
 
                    #         |  X  |
1522
 
                    #        MabN MbaN
1523
 
                    #          \   /
1524
 
                    #           ???
1525
 
                    # There was a criss-cross conflict merge. Both sides
1526
 
                    # include the other, but put themselves first.
1527
 
                    # Weave marks this as a 'clean' merge, picking OTHER over
1528
 
                    # THIS. (Though the details depend on order inserted into
1529
 
                    # weave, etc.)
1530
 
                    # LCA generates a plan:
1531
 
                    # [('unchanged', M),
1532
 
                    #  ('conflicted-b', b),
1533
 
                    #  ('unchanged', a),
1534
 
                    #  ('conflicted-a', b),
1535
 
                    #  ('unchanged', N)]
1536
 
                    # If you mark 'conflicted-*' as part of BASE, then a 3-way
1537
 
                    # merge tool will cleanly generate "MaN" (as BASE vs THIS
1538
 
                    # removes one 'b', and BASE vs OTHER removes the other)
1539
 
                    # If you include neither, 3-way creates a clean "MbabN" as
1540
 
                    # THIS adds one 'b', and OTHER does too.
1541
 
                    # It seems that having the line 2 times is better than
1542
 
                    # having it omitted. (Easier to manually delete than notice
1543
 
                    # it needs to be added.)
1544
 
                    raise AssertionError('Unknown state: %s' % (state,))
1545
 
        return base_lines
1546
 
 
1547
1396
 
1548
1397
class WeaveMerge(PlanWeaveMerge):
1549
1398
    """Weave merge that takes a VersionedFile and two versions as its input."""
1550
1399
 
1551
 
    def __init__(self, versionedfile, ver_a, ver_b,
 
1400
    def __init__(self, versionedfile, ver_a, ver_b, 
1552
1401
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
1553
1402
        plan = versionedfile.plan_merge(ver_a, ver_b)
1554
1403
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
1555
1404
 
1556
1405
 
1557
1406
class VirtualVersionedFiles(VersionedFiles):
1558
 
    """Dummy implementation for VersionedFiles that uses other functions for
 
1407
    """Dummy implementation for VersionedFiles that uses other functions for 
1559
1408
    obtaining fulltexts and parent maps.
1560
1409
 
1561
 
    This is always on the bottom of the stack and uses string keys
 
1410
    This is always on the bottom of the stack and uses string keys 
1562
1411
    (rather than tuples) internally.
1563
1412
    """
1564
1413
 
1566
1415
        """Create a VirtualVersionedFiles.
1567
1416
 
1568
1417
        :param get_parent_map: Same signature as Repository.get_parent_map.
1569
 
        :param get_lines: Should return lines for specified key or None if
 
1418
        :param get_lines: Should return lines for specified key or None if 
1570
1419
                          not available.
1571
1420
        """
1572
1421
        super(VirtualVersionedFiles, self).__init__()
1573
1422
        self._get_parent_map = get_parent_map
1574
1423
        self._get_lines = get_lines
1575
 
 
 
1424
        
1576
1425
    def check(self, progressbar=None):
1577
1426
        """See VersionedFiles.check.
1578
1427
 
1616
1465
            else:
1617
1466
                yield AbsentContentFactory((k,))
1618
1467
 
1619
 
    def iter_lines_added_or_present_in_keys(self, keys, pb=None):
1620
 
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
1621
 
        for i, (key,) in enumerate(keys):
1622
 
            if pb is not None:
1623
 
                pb.update("Finding changed lines", i, len(keys))
1624
 
            for l in self._get_lines(key):
1625
 
                yield (l, key)
1626
 
 
1627
 
 
1628
 
def network_bytes_to_kind_and_offset(network_bytes):
1629
 
    """Strip of a record kind from the front of network_bytes.
1630
 
 
1631
 
    :param network_bytes: The bytes of a record.
1632
 
    :return: A tuple (storage_kind, offset_of_remaining_bytes)
1633
 
    """
1634
 
    line_end = network_bytes.find('\n')
1635
 
    storage_kind = network_bytes[:line_end]
1636
 
    return storage_kind, line_end + 1
1637
 
 
1638
 
 
1639
 
class NetworkRecordStream(object):
1640
 
    """A record_stream which reconstitures a serialised stream."""
1641
 
 
1642
 
    def __init__(self, bytes_iterator):
1643
 
        """Create a NetworkRecordStream.
1644
 
 
1645
 
        :param bytes_iterator: An iterator of bytes. Each item in this
1646
 
            iterator should have been obtained from a record_streams'
1647
 
            record.get_bytes_as(record.storage_kind) call.
1648
 
        """
1649
 
        self._bytes_iterator = bytes_iterator
1650
 
        self._kind_factory = {
1651
 
            'fulltext': fulltext_network_to_record,
1652
 
            'groupcompress-block': groupcompress.network_block_to_records,
1653
 
            'knit-ft-gz': knit.knit_network_to_record,
1654
 
            'knit-delta-gz': knit.knit_network_to_record,
1655
 
            'knit-annotated-ft-gz': knit.knit_network_to_record,
1656
 
            'knit-annotated-delta-gz': knit.knit_network_to_record,
1657
 
            'knit-delta-closure': knit.knit_delta_closure_to_records,
1658
 
            }
1659
 
 
1660
 
    def read(self):
1661
 
        """Read the stream.
1662
 
 
1663
 
        :return: An iterator as per VersionedFiles.get_record_stream().
1664
 
        """
1665
 
        for bytes in self._bytes_iterator:
1666
 
            storage_kind, line_end = network_bytes_to_kind_and_offset(bytes)
1667
 
            for record in self._kind_factory[storage_kind](
1668
 
                storage_kind, bytes, line_end):
1669
 
                yield record
1670
 
 
1671
 
 
1672
 
def fulltext_network_to_record(kind, bytes, line_end):
1673
 
    """Convert a network fulltext record to record."""
1674
 
    meta_len, = struct.unpack('!L', bytes[line_end:line_end+4])
1675
 
    record_meta = bytes[line_end+4:line_end+4+meta_len]
1676
 
    key, parents = bencode.bdecode_as_tuple(record_meta)
1677
 
    if parents == 'nil':
1678
 
        parents = None
1679
 
    fulltext = bytes[line_end+4+meta_len:]
1680
 
    return [FulltextContentFactory(key, parents, None, fulltext)]
1681
 
 
1682
 
 
1683
 
def _length_prefix(bytes):
1684
 
    return struct.pack('!L', len(bytes))
1685
 
 
1686
 
 
1687
 
def record_to_fulltext_bytes(record):
1688
 
    if record.parents is None:
1689
 
        parents = 'nil'
1690
 
    else:
1691
 
        parents = record.parents
1692
 
    record_meta = bencode.bencode((record.key, parents))
1693
 
    record_content = record.get_bytes_as('fulltext')
1694
 
    return "fulltext\n%s%s%s" % (
1695
 
        _length_prefix(record_meta), record_meta, record_content)
1696
 
 
1697
 
 
1698
 
def sort_groupcompress(parent_map):
1699
 
    """Sort and group the keys in parent_map into groupcompress order.
1700
 
 
1701
 
    groupcompress is defined (currently) as reverse-topological order, grouped
1702
 
    by the key prefix.
1703
 
 
1704
 
    :return: A sorted-list of keys
1705
 
    """
1706
 
    # gc-optimal ordering is approximately reverse topological,
1707
 
    # properly grouped by file-id.
1708
 
    per_prefix_map = {}
1709
 
    for item in parent_map.iteritems():
1710
 
        key = item[0]
1711
 
        if isinstance(key, str) or len(key) == 1:
1712
 
            prefix = ''
1713
 
        else:
1714
 
            prefix = key[0]
1715
 
        try:
1716
 
            per_prefix_map[prefix].append(item)
1717
 
        except KeyError:
1718
 
            per_prefix_map[prefix] = [item]
1719
 
 
1720
 
    present_keys = []
1721
 
    for prefix in sorted(per_prefix_map):
1722
 
        present_keys.extend(reversed(tsort.topo_sort(per_prefix_map[prefix])))
1723
 
    return present_keys
 
1468
 
 
1469