~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

Replace remaining to unittest.TestResult methods with super

Show diffs side-by-side

added added

removed removed

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