~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-10 15:46:03 UTC
  • mfrom: (4985.3.21 update)
  • mto: This revision was merged to the branch mainline in revision 5021.
  • Revision ID: v.ladeuil+lp@free.fr-20100210154603-k4no1gvfuqpzrw7p
Update performs two merges in a more logical order but stop on conflicts

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., 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
48
from bzrlib.inter import InterObject
44
49
from bzrlib.registry import Registry
45
50
from bzrlib.symbol_versioning import *
46
51
from bzrlib.textmerge import TextMerge
 
52
from bzrlib import bencode
47
53
 
48
54
 
49
55
adapter_registry = Registry()
65
71
 
66
72
class ContentFactory(object):
67
73
    """Abstract interface for insertion and retrieval from a VersionedFile.
68
 
    
 
74
 
69
75
    :ivar sha1: None, or the sha1 of the content fulltext.
70
76
    :ivar storage_kind: The native storage kind of this factory. One of
71
77
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
155
161
 
156
162
class AbsentContentFactory(ContentFactory):
157
163
    """A placeholder content factory for unavailable texts.
158
 
    
 
164
 
159
165
    :ivar sha1: None.
160
166
    :ivar storage_kind: 'absent'.
161
167
    :ivar key: The key of this content. Each key is a tuple with a single
170
176
        self.key = key
171
177
        self.parents = None
172
178
 
 
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
 
173
185
 
174
186
class AdapterFactory(ContentFactory):
175
187
    """A content factory to adapt between key prefix's."""
197
209
 
198
210
class VersionedFile(object):
199
211
    """Versioned text file storage.
200
 
    
 
212
 
201
213
    A versioned file manages versions of line-based text files,
202
214
    keeping track of the originating version for each line.
203
215
 
241
253
    def insert_record_stream(self, stream):
242
254
        """Insert a record stream into this versioned file.
243
255
 
244
 
        :param stream: A stream of records to insert. 
 
256
        :param stream: A stream of records to insert.
245
257
        :return: None
246
258
        :seealso VersionedFile.get_record_stream:
247
259
        """
266
278
            the data back accurately. (Checking the lines have been split
267
279
            correctly is expensive and extremely unlikely to catch bugs so it
268
280
            is not done at runtime unless check_content is True.)
269
 
        :param parent_texts: An optional dictionary containing the opaque 
 
281
        :param parent_texts: An optional dictionary containing the opaque
270
282
            representations of some or all of the parents of version_id to
271
283
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
272
284
            returned by add_lines or data corruption can be caused.
300
312
        parent_texts=None, nostore_sha=None, random_id=False,
301
313
        check_content=True, left_matching_blocks=None):
302
314
        """Add lines to the versioned file, allowing ghosts to be present.
303
 
        
 
315
 
304
316
        This takes the same parameters as add_lines and returns the same.
305
317
        """
306
318
        self._check_write_ok()
330
342
 
331
343
    def get_format_signature(self):
332
344
        """Get a text description of the data encoding in this file.
333
 
        
 
345
 
334
346
        :since: 0.90
335
347
        """
336
348
        raise NotImplementedError(self.get_format_signature)
457
469
        if isinstance(version_ids, basestring):
458
470
            version_ids = [version_ids]
459
471
        raise NotImplementedError(self.get_ancestry)
460
 
        
 
472
 
461
473
    def get_ancestry_with_ghosts(self, version_ids):
462
474
        """Return a list of all ancestors of given version(s). This
463
475
        will not include the null revision.
464
476
 
465
477
        Must raise RevisionNotPresent if any of the given versions are
466
478
        not present in file history.
467
 
        
 
479
 
468
480
        Ghosts that are known about will be included in ancestry list,
469
481
        but are not explicitly marked.
470
482
        """
471
483
        raise NotImplementedError(self.get_ancestry_with_ghosts)
472
 
    
 
484
 
473
485
    def get_parent_map(self, version_ids):
474
486
        """Get a map of the parents of version_ids.
475
487
 
538
550
        unchanged   Alive in both a and b (possibly created in both)
539
551
        new-a       Created in a
540
552
        new-b       Created in b
541
 
        ghost-a     Killed in a, unborn in b    
 
553
        ghost-a     Killed in a, unborn in b
542
554
        ghost-b     Killed in b, unborn in a
543
555
        irrelevant  Not in either revision
544
556
        """
545
557
        raise NotImplementedError(VersionedFile.plan_merge)
546
 
        
 
558
 
547
559
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
548
560
                    b_marker=TextMerge.B_MARKER):
549
561
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
551
563
 
552
564
class RecordingVersionedFilesDecorator(object):
553
565
    """A minimal versioned files that records calls made on it.
554
 
    
 
566
 
555
567
    Only enough methods have been added to support tests using it to date.
556
568
 
557
569
    :ivar calls: A list of the calls made; can be reset at any time by
560
572
 
561
573
    def __init__(self, backing_vf):
562
574
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
563
 
        
 
575
 
564
576
        :param backing_vf: The versioned file to answer all methods.
565
577
        """
566
578
        self._backing_vf = backing_vf
652
664
 
653
665
    def unmap(self, partition_id):
654
666
        """Map a partitioned storage id back to a key prefix.
655
 
        
 
667
 
656
668
        :param partition_id: The underlying partition id.
657
669
        :return: As much of a key (or prefix) as is derivable from the partition
658
670
            id.
690
702
 
691
703
class PrefixMapper(URLEscapeMapper):
692
704
    """A key mapper that extracts the first component of a key.
693
 
    
 
705
 
694
706
    This mapper is for use with a transport based backend.
695
707
    """
696
708
 
729
741
 
730
742
class HashEscapedPrefixMapper(HashPrefixMapper):
731
743
    """Combines the escaped first component of a key with a hash.
732
 
    
 
744
 
733
745
    This mapper is for use with a transport based backend.
734
746
    """
735
747
 
791
803
        check_content=True):
792
804
        """Add a text to the store.
793
805
 
794
 
        :param key: The key tuple of the text to add.
 
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.
795
808
        :param parents: The parents key tuples of the text to add.
796
809
        :param lines: A list of lines. Each line must be a bytestring. And all
797
810
            of them except the last must be terminated with \n and contain no
801
814
            the data back accurately. (Checking the lines have been split
802
815
            correctly is expensive and extremely unlikely to catch bugs so it
803
816
            is not done at runtime unless check_content is True.)
804
 
        :param parent_texts: An optional dictionary containing the opaque 
 
817
        :param parent_texts: An optional dictionary containing the opaque
805
818
            representations of some or all of the parents of version_id to
806
819
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
807
820
            returned by add_lines or data corruption can be caused.
824
837
        """
825
838
        raise NotImplementedError(self.add_lines)
826
839
 
 
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
 
827
870
    def add_mpdiffs(self, records):
828
871
        """Add mpdiffs to this VersionedFile.
829
872
 
871
914
        raise NotImplementedError(self.annotate)
872
915
 
873
916
    def check(self, progress_bar=None):
874
 
        """Check this object for integrity."""
 
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
        """
875
927
        raise NotImplementedError(self.check)
876
928
 
877
929
    @staticmethod
878
930
    def check_not_reserved_id(version_id):
879
931
        revision.check_not_reserved_id(version_id)
880
932
 
 
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
 
881
940
    def _check_lines_not_unicode(self, lines):
882
941
        """Check that lines being added to a versioned file are not unicode."""
883
942
        for line in lines:
890
949
            if '\n' in line[:-1]:
891
950
                raise errors.BzrBadParameterContainsNewline("lines")
892
951
 
 
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
 
893
966
    def get_parent_map(self, keys):
894
967
        """Get a map of the parents of keys.
895
968
 
925
998
 
926
999
    has_key = index._has_key_from_parent_map
927
1000
 
 
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
 
928
1013
    def insert_record_stream(self, stream):
929
1014
        """Insert a record stream into this container.
930
1015
 
931
 
        :param stream: A stream of records to insert. 
 
1016
        :param stream: A stream of records to insert.
932
1017
        :return: None
933
1018
        :seealso VersionedFile.get_record_stream:
934
1019
        """
1075
1160
            result.append((prefix + (origin,), line))
1076
1161
        return result
1077
1162
 
1078
 
    def check(self, progress_bar=None):
 
1163
    def get_annotator(self):
 
1164
        return annotate.Annotator(self)
 
1165
 
 
1166
    def check(self, progress_bar=None, keys=None):
1079
1167
        """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.
1080
1171
        for prefix, vf in self._iter_all_components():
1081
1172
            vf.check()
 
1173
        if keys is not None:
 
1174
            return self.get_record_stream(keys, 'unordered', True)
1082
1175
 
1083
1176
    def get_parent_map(self, keys):
1084
1177
        """Get a map of the parents of keys.
1162
1255
    def insert_record_stream(self, stream):
1163
1256
        """Insert a record stream into this container.
1164
1257
 
1165
 
        :param stream: A stream of records to insert. 
 
1258
        :param stream: A stream of records to insert.
1166
1259
        :return: None
1167
1260
        :seealso VersionedFile.get_record_stream:
1168
1261
        """
1316
1409
            result[revision.NULL_REVISION] = ()
1317
1410
        self._providers = self._providers[:1] + self.fallback_versionedfiles
1318
1411
        result.update(
1319
 
            _StackedParentsProvider(self._providers).get_parent_map(keys))
 
1412
            StackedParentsProvider(self._providers).get_parent_map(keys))
1320
1413
        for key, parents in result.iteritems():
1321
1414
            if parents == ():
1322
1415
                result[key] = (revision.NULL_REVISION,)
1325
1418
 
1326
1419
class PlanWeaveMerge(TextMerge):
1327
1420
    """Weave merge that takes a plan as its input.
1328
 
    
 
1421
 
1329
1422
    This exists so that VersionedFile.plan_merge is implementable.
1330
1423
    Most callers will want to use WeaveMerge instead.
1331
1424
    """
1333
1426
    def __init__(self, plan, a_marker=TextMerge.A_MARKER,
1334
1427
                 b_marker=TextMerge.B_MARKER):
1335
1428
        TextMerge.__init__(self, a_marker, b_marker)
1336
 
        self.plan = plan
 
1429
        self.plan = list(plan)
1337
1430
 
1338
1431
    def _merge_struct(self):
1339
1432
        lines_a = []
1352
1445
                yield(lines_a,)
1353
1446
            else:
1354
1447
                yield (lines_a, lines_b)
1355
 
       
 
1448
 
1356
1449
        # We previously considered either 'unchanged' or 'killed-both' lines
1357
1450
        # to be possible places to resynchronize.  However, assuming agreement
1358
1451
        # on killed-both lines may be too aggressive. -- mbp 20060324
1364
1457
                lines_a = []
1365
1458
                lines_b = []
1366
1459
                ch_a = ch_b = False
1367
 
                
 
1460
 
1368
1461
            if state == 'unchanged':
1369
1462
                if line:
1370
1463
                    yield ([line],)
1386
1479
            elif state == 'conflicted-b':
1387
1480
                ch_b = ch_a = True
1388
1481
                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
1389
1486
            else:
1390
1487
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
1391
 
                        'killed-base', 'killed-both'):
 
1488
                        'killed-base'):
1392
1489
                    raise AssertionError(state)
1393
1490
        for struct in outstanding_struct():
1394
1491
            yield struct
1395
1492
 
 
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
 
1396
1547
 
1397
1548
class WeaveMerge(PlanWeaveMerge):
1398
1549
    """Weave merge that takes a VersionedFile and two versions as its input."""
1399
1550
 
1400
 
    def __init__(self, versionedfile, ver_a, ver_b, 
 
1551
    def __init__(self, versionedfile, ver_a, ver_b,
1401
1552
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
1402
1553
        plan = versionedfile.plan_merge(ver_a, ver_b)
1403
1554
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
1404
1555
 
1405
1556
 
1406
1557
class VirtualVersionedFiles(VersionedFiles):
1407
 
    """Dummy implementation for VersionedFiles that uses other functions for 
 
1558
    """Dummy implementation for VersionedFiles that uses other functions for
1408
1559
    obtaining fulltexts and parent maps.
1409
1560
 
1410
 
    This is always on the bottom of the stack and uses string keys 
 
1561
    This is always on the bottom of the stack and uses string keys
1411
1562
    (rather than tuples) internally.
1412
1563
    """
1413
1564
 
1415
1566
        """Create a VirtualVersionedFiles.
1416
1567
 
1417
1568
        :param get_parent_map: Same signature as Repository.get_parent_map.
1418
 
        :param get_lines: Should return lines for specified key or None if 
 
1569
        :param get_lines: Should return lines for specified key or None if
1419
1570
                          not available.
1420
1571
        """
1421
1572
        super(VirtualVersionedFiles, self).__init__()
1422
1573
        self._get_parent_map = get_parent_map
1423
1574
        self._get_lines = get_lines
1424
 
        
 
1575
 
1425
1576
    def check(self, progressbar=None):
1426
1577
        """See VersionedFiles.check.
1427
1578
 
1469
1620
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
1470
1621
        for i, (key,) in enumerate(keys):
1471
1622
            if pb is not None:
1472
 
                pb.update("iterating texts", i, len(keys))
 
1623
                pb.update("Finding changed lines", i, len(keys))
1473
1624
            for l in self._get_lines(key):
1474
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