~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: John Ferlito
  • Date: 2009-09-02 04:31:45 UTC
  • mto: (4665.7.1 serve-init)
  • mto: This revision was merged to the branch mainline in revision 4913.
  • Revision ID: johnf@inodes.org-20090902043145-gxdsfw03ilcwbyn5
Add a debian init script for bzr --serve

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2008 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 urllib
 
25
import struct
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
30
31
 
31
32
from bzrlib import (
 
33
    annotate,
32
34
    errors,
 
35
    graph as _mod_graph,
 
36
    groupcompress,
 
37
    index,
 
38
    knit,
33
39
    osutils,
34
40
    multiparent,
35
41
    tsort,
36
42
    revision,
37
43
    ui,
38
44
    )
39
 
from bzrlib.graph import DictParentsProvider, Graph, _StackedParentsProvider
 
45
from bzrlib.graph import DictParentsProvider, Graph, StackedParentsProvider
40
46
from bzrlib.transport.memory import MemoryTransport
41
47
""")
42
48
from bzrlib.inter import InterObject
43
49
from bzrlib.registry import Registry
44
50
from bzrlib.symbol_versioning import *
45
51
from bzrlib.textmerge import TextMerge
 
52
from bzrlib import bencode
46
53
 
47
54
 
48
55
adapter_registry = Registry()
58
65
    'bzrlib.knit', 'FTAnnotatedToUnannotated')
59
66
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'fulltext'),
60
67
    'bzrlib.knit', 'FTAnnotatedToFullText')
 
68
# adapter_registry.register_lazy(('knit-annotated-ft-gz', 'chunked'),
 
69
#     'bzrlib.knit', 'FTAnnotatedToChunked')
61
70
 
62
71
 
63
72
class ContentFactory(object):
64
73
    """Abstract interface for insertion and retrieval from a VersionedFile.
65
 
    
 
74
 
66
75
    :ivar sha1: None, or the sha1 of the content fulltext.
67
76
    :ivar storage_kind: The native storage kind of this factory. One of
68
77
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
83
92
        self.parents = None
84
93
 
85
94
 
 
95
class ChunkedContentFactory(ContentFactory):
 
96
    """Static data content factory.
 
97
 
 
98
    This takes a 'chunked' list of strings. The only requirement on 'chunked' is
 
99
    that ''.join(lines) becomes a valid fulltext. A tuple of a single string
 
100
    satisfies this, as does a list of lines.
 
101
 
 
102
    :ivar sha1: None, or the sha1 of the content fulltext.
 
103
    :ivar storage_kind: The native storage kind of this factory. Always
 
104
        'chunked'
 
105
    :ivar key: The key of this content. Each key is a tuple with a single
 
106
        string in it.
 
107
    :ivar parents: A tuple of parent keys for self.key. If the object has
 
108
        no parent information, None (as opposed to () for an empty list of
 
109
        parents).
 
110
     """
 
111
 
 
112
    def __init__(self, key, parents, sha1, chunks):
 
113
        """Create a ContentFactory."""
 
114
        self.sha1 = sha1
 
115
        self.storage_kind = 'chunked'
 
116
        self.key = key
 
117
        self.parents = parents
 
118
        self._chunks = chunks
 
119
 
 
120
    def get_bytes_as(self, storage_kind):
 
121
        if storage_kind == 'chunked':
 
122
            return self._chunks
 
123
        elif storage_kind == 'fulltext':
 
124
            return ''.join(self._chunks)
 
125
        raise errors.UnavailableRepresentation(self.key, storage_kind,
 
126
            self.storage_kind)
 
127
 
 
128
 
86
129
class FulltextContentFactory(ContentFactory):
87
130
    """Static data content factory.
88
131
 
89
132
    This takes a fulltext when created and just returns that during
90
133
    get_bytes_as('fulltext').
91
 
    
 
134
 
92
135
    :ivar sha1: None, or the sha1 of the content fulltext.
93
136
    :ivar storage_kind: The native storage kind of this factory. Always
94
137
        'fulltext'.
110
153
    def get_bytes_as(self, storage_kind):
111
154
        if storage_kind == self.storage_kind:
112
155
            return self._text
 
156
        elif storage_kind == 'chunked':
 
157
            return [self._text]
113
158
        raise errors.UnavailableRepresentation(self.key, storage_kind,
114
159
            self.storage_kind)
115
160
 
116
161
 
117
162
class AbsentContentFactory(ContentFactory):
118
163
    """A placeholder content factory for unavailable texts.
119
 
    
 
164
 
120
165
    :ivar sha1: None.
121
166
    :ivar storage_kind: 'absent'.
122
167
    :ivar key: The key of this content. Each key is a tuple with a single
131
176
        self.key = key
132
177
        self.parents = None
133
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
 
134
185
 
135
186
class AdapterFactory(ContentFactory):
136
187
    """A content factory to adapt between key prefix's."""
158
209
 
159
210
class VersionedFile(object):
160
211
    """Versioned text file storage.
161
 
    
 
212
 
162
213
    A versioned file manages versions of line-based text files,
163
214
    keeping track of the originating version for each line.
164
215
 
202
253
    def insert_record_stream(self, stream):
203
254
        """Insert a record stream into this versioned file.
204
255
 
205
 
        :param stream: A stream of records to insert. 
 
256
        :param stream: A stream of records to insert.
206
257
        :return: None
207
258
        :seealso VersionedFile.get_record_stream:
208
259
        """
227
278
            the data back accurately. (Checking the lines have been split
228
279
            correctly is expensive and extremely unlikely to catch bugs so it
229
280
            is not done at runtime unless check_content is True.)
230
 
        :param parent_texts: An optional dictionary containing the opaque 
 
281
        :param parent_texts: An optional dictionary containing the opaque
231
282
            representations of some or all of the parents of version_id to
232
283
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
233
284
            returned by add_lines or data corruption can be caused.
261
312
        parent_texts=None, nostore_sha=None, random_id=False,
262
313
        check_content=True, left_matching_blocks=None):
263
314
        """Add lines to the versioned file, allowing ghosts to be present.
264
 
        
 
315
 
265
316
        This takes the same parameters as add_lines and returns the same.
266
317
        """
267
318
        self._check_write_ok()
291
342
 
292
343
    def get_format_signature(self):
293
344
        """Get a text description of the data encoding in this file.
294
 
        
 
345
 
295
346
        :since: 0.90
296
347
        """
297
348
        raise NotImplementedError(self.get_format_signature)
418
469
        if isinstance(version_ids, basestring):
419
470
            version_ids = [version_ids]
420
471
        raise NotImplementedError(self.get_ancestry)
421
 
        
 
472
 
422
473
    def get_ancestry_with_ghosts(self, version_ids):
423
474
        """Return a list of all ancestors of given version(s). This
424
475
        will not include the null revision.
425
476
 
426
477
        Must raise RevisionNotPresent if any of the given versions are
427
478
        not present in file history.
428
 
        
 
479
 
429
480
        Ghosts that are known about will be included in ancestry list,
430
481
        but are not explicitly marked.
431
482
        """
432
483
        raise NotImplementedError(self.get_ancestry_with_ghosts)
433
 
    
 
484
 
434
485
    def get_parent_map(self, version_ids):
435
486
        """Get a map of the parents of version_ids.
436
487
 
499
550
        unchanged   Alive in both a and b (possibly created in both)
500
551
        new-a       Created in a
501
552
        new-b       Created in b
502
 
        ghost-a     Killed in a, unborn in b    
 
553
        ghost-a     Killed in a, unborn in b
503
554
        ghost-b     Killed in b, unborn in a
504
555
        irrelevant  Not in either revision
505
556
        """
506
557
        raise NotImplementedError(VersionedFile.plan_merge)
507
 
        
 
558
 
508
559
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
509
560
                    b_marker=TextMerge.B_MARKER):
510
561
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
512
563
 
513
564
class RecordingVersionedFilesDecorator(object):
514
565
    """A minimal versioned files that records calls made on it.
515
 
    
 
566
 
516
567
    Only enough methods have been added to support tests using it to date.
517
568
 
518
569
    :ivar calls: A list of the calls made; can be reset at any time by
520
571
    """
521
572
 
522
573
    def __init__(self, backing_vf):
523
 
        """Create a RecordingVersionedFileDsecorator decorating backing_vf.
524
 
        
 
574
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
 
575
 
525
576
        :param backing_vf: The versioned file to answer all methods.
526
577
        """
527
578
        self._backing_vf = backing_vf
561
612
        return self._backing_vf.keys()
562
613
 
563
614
 
 
615
class OrderingVersionedFilesDecorator(RecordingVersionedFilesDecorator):
 
616
    """A VF that records calls, and returns keys in specific order.
 
617
 
 
618
    :ivar calls: A list of the calls made; can be reset at any time by
 
619
        assigning [] to it.
 
620
    """
 
621
 
 
622
    def __init__(self, backing_vf, key_priority):
 
623
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
 
624
 
 
625
        :param backing_vf: The versioned file to answer all methods.
 
626
        :param key_priority: A dictionary defining what order keys should be
 
627
            returned from an 'unordered' get_record_stream request.
 
628
            Keys with lower priority are returned first, keys not present in
 
629
            the map get an implicit priority of 0, and are returned in
 
630
            lexicographical order.
 
631
        """
 
632
        RecordingVersionedFilesDecorator.__init__(self, backing_vf)
 
633
        self._key_priority = key_priority
 
634
 
 
635
    def get_record_stream(self, keys, sort_order, include_delta_closure):
 
636
        self.calls.append(("get_record_stream", list(keys), sort_order,
 
637
            include_delta_closure))
 
638
        if sort_order == 'unordered':
 
639
            def sort_key(key):
 
640
                return (self._key_priority.get(key, 0), key)
 
641
            # Use a defined order by asking for the keys one-by-one from the
 
642
            # backing_vf
 
643
            for key in sorted(keys, key=sort_key):
 
644
                for record in self._backing_vf.get_record_stream([key],
 
645
                                'unordered', include_delta_closure):
 
646
                    yield record
 
647
        else:
 
648
            for record in self._backing_vf.get_record_stream(keys, sort_order,
 
649
                            include_delta_closure):
 
650
                yield record
 
651
 
 
652
 
564
653
class KeyMapper(object):
565
654
    """KeyMappers map between keys and underlying partitioned storage."""
566
655
 
575
664
 
576
665
    def unmap(self, partition_id):
577
666
        """Map a partitioned storage id back to a key prefix.
578
 
        
 
667
 
579
668
        :param partition_id: The underlying partition id.
580
669
        :return: As much of a key (or prefix) as is derivable from the partition
581
670
            id.
613
702
 
614
703
class PrefixMapper(URLEscapeMapper):
615
704
    """A key mapper that extracts the first component of a key.
616
 
    
 
705
 
617
706
    This mapper is for use with a transport based backend.
618
707
    """
619
708
 
652
741
 
653
742
class HashEscapedPrefixMapper(HashPrefixMapper):
654
743
    """Combines the escaped first component of a key with a hash.
655
 
    
 
744
 
656
745
    This mapper is for use with a transport based backend.
657
746
    """
658
747
 
714
803
        check_content=True):
715
804
        """Add a text to the store.
716
805
 
717
 
        :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.
718
808
        :param parents: The parents key tuples of the text to add.
719
809
        :param lines: A list of lines. Each line must be a bytestring. And all
720
810
            of them except the last must be terminated with \n and contain no
724
814
            the data back accurately. (Checking the lines have been split
725
815
            correctly is expensive and extremely unlikely to catch bugs so it
726
816
            is not done at runtime unless check_content is True.)
727
 
        :param parent_texts: An optional dictionary containing the opaque 
 
817
        :param parent_texts: An optional dictionary containing the opaque
728
818
            representations of some or all of the parents of version_id to
729
819
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
730
820
            returned by add_lines or data corruption can be caused.
747
837
        """
748
838
        raise NotImplementedError(self.add_lines)
749
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
 
750
870
    def add_mpdiffs(self, records):
751
871
        """Add mpdiffs to this VersionedFile.
752
872
 
765
885
                                  if not mpvf.has_version(p))
766
886
        # It seems likely that adding all the present parents as fulltexts can
767
887
        # easily exhaust memory.
768
 
        split_lines = osutils.split_lines
 
888
        chunks_to_lines = osutils.chunks_to_lines
769
889
        for record in self.get_record_stream(needed_parents, 'unordered',
770
890
            True):
771
891
            if record.storage_kind == 'absent':
772
892
                continue
773
 
            mpvf.add_version(split_lines(record.get_bytes_as('fulltext')),
 
893
            mpvf.add_version(chunks_to_lines(record.get_bytes_as('chunked')),
774
894
                record.key, [])
775
895
        for (key, parent_keys, expected_sha1, mpdiff), lines in\
776
896
            zip(records, mpvf.get_line_list(versions)):
794
914
        raise NotImplementedError(self.annotate)
795
915
 
796
916
    def check(self, progress_bar=None):
797
 
        """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
        """
798
927
        raise NotImplementedError(self.check)
799
928
 
800
929
    @staticmethod
813
942
            if '\n' in line[:-1]:
814
943
                raise errors.BzrBadParameterContainsNewline("lines")
815
944
 
 
945
    def get_known_graph_ancestry(self, keys):
 
946
        """Get a KnownGraph instance with the ancestry of keys."""
 
947
        # most basic implementation is a loop around get_parent_map
 
948
        pending = set(keys)
 
949
        parent_map = {}
 
950
        while pending:
 
951
            this_parent_map = self.get_parent_map(pending)
 
952
            parent_map.update(this_parent_map)
 
953
            pending = set()
 
954
            map(pending.update, this_parent_map.itervalues())
 
955
            pending = pending.difference(parent_map)
 
956
        kg = _mod_graph.KnownGraph(parent_map)
 
957
        return kg
 
958
 
816
959
    def get_parent_map(self, keys):
817
960
        """Get a map of the parents of keys.
818
961
 
846
989
        """
847
990
        raise NotImplementedError(self.get_sha1s)
848
991
 
 
992
    has_key = index._has_key_from_parent_map
 
993
 
 
994
    def get_missing_compression_parent_keys(self):
 
995
        """Return an iterable of keys of missing compression parents.
 
996
 
 
997
        Check this after calling insert_record_stream to find out if there are
 
998
        any missing compression parents.  If there are, the records that
 
999
        depend on them are not able to be inserted safely. The precise
 
1000
        behaviour depends on the concrete VersionedFiles class in use.
 
1001
 
 
1002
        Classes that do not support this will raise NotImplementedError.
 
1003
        """
 
1004
        raise NotImplementedError(self.get_missing_compression_parent_keys)
 
1005
 
849
1006
    def insert_record_stream(self, stream):
850
1007
        """Insert a record stream into this container.
851
1008
 
852
 
        :param stream: A stream of records to insert. 
 
1009
        :param stream: A stream of records to insert.
853
1010
        :return: None
854
1011
        :seealso VersionedFile.get_record_stream:
855
1012
        """
899
1056
        ghosts = maybe_ghosts - set(self.get_parent_map(maybe_ghosts))
900
1057
        knit_keys.difference_update(ghosts)
901
1058
        lines = {}
902
 
        split_lines = osutils.split_lines
 
1059
        chunks_to_lines = osutils.chunks_to_lines
903
1060
        for record in self.get_record_stream(knit_keys, 'topological', True):
904
 
            lines[record.key] = split_lines(record.get_bytes_as('fulltext'))
 
1061
            lines[record.key] = chunks_to_lines(record.get_bytes_as('chunked'))
905
1062
            # line_block_dict = {}
906
1063
            # for parent, blocks in record.extract_line_blocks():
907
1064
            #   line_blocks[parent] = blocks
922
1079
                parent_lines, left_parent_blocks))
923
1080
        return diffs
924
1081
 
 
1082
    missing_keys = index._missing_keys_from_parent_map
 
1083
 
925
1084
    def _extract_blocks(self, version_id, source, target):
926
1085
        return None
927
1086
 
994
1153
            result.append((prefix + (origin,), line))
995
1154
        return result
996
1155
 
997
 
    def check(self, progress_bar=None):
 
1156
    def get_annotator(self):
 
1157
        return annotate.Annotator(self)
 
1158
 
 
1159
    def check(self, progress_bar=None, keys=None):
998
1160
        """See VersionedFiles.check()."""
 
1161
        # XXX: This is over-enthusiastic but as we only thunk for Weaves today
 
1162
        # this is tolerable. Ideally we'd pass keys down to check() and 
 
1163
        # have the older VersiondFile interface updated too.
999
1164
        for prefix, vf in self._iter_all_components():
1000
1165
            vf.check()
 
1166
        if keys is not None:
 
1167
            return self.get_record_stream(keys, 'unordered', True)
1001
1168
 
1002
1169
    def get_parent_map(self, keys):
1003
1170
        """Get a map of the parents of keys.
1081
1248
    def insert_record_stream(self, stream):
1082
1249
        """Insert a record stream into this container.
1083
1250
 
1084
 
        :param stream: A stream of records to insert. 
 
1251
        :param stream: A stream of records to insert.
1085
1252
        :return: None
1086
1253
        :seealso VersionedFile.get_record_stream:
1087
1254
        """
1208
1375
                lines = self._lines[key]
1209
1376
                parents = self._parents[key]
1210
1377
                pending.remove(key)
1211
 
                yield FulltextContentFactory(key, parents, None,
1212
 
                    ''.join(lines))
 
1378
                yield ChunkedContentFactory(key, parents, None, lines)
1213
1379
        for versionedfile in self.fallback_versionedfiles:
1214
1380
            for record in versionedfile.get_record_stream(
1215
1381
                pending, 'unordered', True):
1236
1402
            result[revision.NULL_REVISION] = ()
1237
1403
        self._providers = self._providers[:1] + self.fallback_versionedfiles
1238
1404
        result.update(
1239
 
            _StackedParentsProvider(self._providers).get_parent_map(keys))
 
1405
            StackedParentsProvider(self._providers).get_parent_map(keys))
1240
1406
        for key, parents in result.iteritems():
1241
1407
            if parents == ():
1242
1408
                result[key] = (revision.NULL_REVISION,)
1245
1411
 
1246
1412
class PlanWeaveMerge(TextMerge):
1247
1413
    """Weave merge that takes a plan as its input.
1248
 
    
 
1414
 
1249
1415
    This exists so that VersionedFile.plan_merge is implementable.
1250
1416
    Most callers will want to use WeaveMerge instead.
1251
1417
    """
1272
1438
                yield(lines_a,)
1273
1439
            else:
1274
1440
                yield (lines_a, lines_b)
1275
 
       
 
1441
 
1276
1442
        # We previously considered either 'unchanged' or 'killed-both' lines
1277
1443
        # to be possible places to resynchronize.  However, assuming agreement
1278
1444
        # on killed-both lines may be too aggressive. -- mbp 20060324
1284
1450
                lines_a = []
1285
1451
                lines_b = []
1286
1452
                ch_a = ch_b = False
1287
 
                
 
1453
 
1288
1454
            if state == 'unchanged':
1289
1455
                if line:
1290
1456
                    yield ([line],)
1306
1472
            elif state == 'conflicted-b':
1307
1473
                ch_b = ch_a = True
1308
1474
                lines_b.append(line)
 
1475
            elif state == 'killed-both':
 
1476
                # This counts as a change, even though there is no associated
 
1477
                # line
 
1478
                ch_b = ch_a = True
1309
1479
            else:
1310
1480
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
1311
 
                        'killed-base', 'killed-both'):
 
1481
                        'killed-base'):
1312
1482
                    raise AssertionError(state)
1313
1483
        for struct in outstanding_struct():
1314
1484
            yield struct
1317
1487
class WeaveMerge(PlanWeaveMerge):
1318
1488
    """Weave merge that takes a VersionedFile and two versions as its input."""
1319
1489
 
1320
 
    def __init__(self, versionedfile, ver_a, ver_b, 
 
1490
    def __init__(self, versionedfile, ver_a, ver_b,
1321
1491
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
1322
1492
        plan = versionedfile.plan_merge(ver_a, ver_b)
1323
1493
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
1324
1494
 
1325
1495
 
1326
1496
class VirtualVersionedFiles(VersionedFiles):
1327
 
    """Dummy implementation for VersionedFiles that uses other functions for 
 
1497
    """Dummy implementation for VersionedFiles that uses other functions for
1328
1498
    obtaining fulltexts and parent maps.
1329
1499
 
1330
 
    This is always on the bottom of the stack and uses string keys 
 
1500
    This is always on the bottom of the stack and uses string keys
1331
1501
    (rather than tuples) internally.
1332
1502
    """
1333
1503
 
1335
1505
        """Create a VirtualVersionedFiles.
1336
1506
 
1337
1507
        :param get_parent_map: Same signature as Repository.get_parent_map.
1338
 
        :param get_lines: Should return lines for specified key or None if 
 
1508
        :param get_lines: Should return lines for specified key or None if
1339
1509
                          not available.
1340
1510
        """
1341
1511
        super(VirtualVersionedFiles, self).__init__()
1342
1512
        self._get_parent_map = get_parent_map
1343
1513
        self._get_lines = get_lines
1344
 
        
 
1514
 
1345
1515
    def check(self, progressbar=None):
1346
1516
        """See VersionedFiles.check.
1347
1517
 
1379
1549
            if lines is not None:
1380
1550
                if not isinstance(lines, list):
1381
1551
                    raise AssertionError
1382
 
                yield FulltextContentFactory((k,), None, 
 
1552
                yield ChunkedContentFactory((k,), None,
1383
1553
                        sha1=osutils.sha_strings(lines),
1384
 
                        text=''.join(lines))
 
1554
                        chunks=lines)
1385
1555
            else:
1386
1556
                yield AbsentContentFactory((k,))
1387
1557
 
1388
 
 
1389
 
 
 
1558
    def iter_lines_added_or_present_in_keys(self, keys, pb=None):
 
1559
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
 
1560
        for i, (key,) in enumerate(keys):
 
1561
            if pb is not None:
 
1562
                pb.update("Finding changed lines", i, len(keys))
 
1563
            for l in self._get_lines(key):
 
1564
                yield (l, key)
 
1565
 
 
1566
 
 
1567
def network_bytes_to_kind_and_offset(network_bytes):
 
1568
    """Strip of a record kind from the front of network_bytes.
 
1569
 
 
1570
    :param network_bytes: The bytes of a record.
 
1571
    :return: A tuple (storage_kind, offset_of_remaining_bytes)
 
1572
    """
 
1573
    line_end = network_bytes.find('\n')
 
1574
    storage_kind = network_bytes[:line_end]
 
1575
    return storage_kind, line_end + 1
 
1576
 
 
1577
 
 
1578
class NetworkRecordStream(object):
 
1579
    """A record_stream which reconstitures a serialised stream."""
 
1580
 
 
1581
    def __init__(self, bytes_iterator):
 
1582
        """Create a NetworkRecordStream.
 
1583
 
 
1584
        :param bytes_iterator: An iterator of bytes. Each item in this
 
1585
            iterator should have been obtained from a record_streams'
 
1586
            record.get_bytes_as(record.storage_kind) call.
 
1587
        """
 
1588
        self._bytes_iterator = bytes_iterator
 
1589
        self._kind_factory = {
 
1590
            'fulltext': fulltext_network_to_record,
 
1591
            'groupcompress-block': groupcompress.network_block_to_records,
 
1592
            'knit-ft-gz': knit.knit_network_to_record,
 
1593
            'knit-delta-gz': knit.knit_network_to_record,
 
1594
            'knit-annotated-ft-gz': knit.knit_network_to_record,
 
1595
            'knit-annotated-delta-gz': knit.knit_network_to_record,
 
1596
            'knit-delta-closure': knit.knit_delta_closure_to_records,
 
1597
            }
 
1598
 
 
1599
    def read(self):
 
1600
        """Read the stream.
 
1601
 
 
1602
        :return: An iterator as per VersionedFiles.get_record_stream().
 
1603
        """
 
1604
        for bytes in self._bytes_iterator:
 
1605
            storage_kind, line_end = network_bytes_to_kind_and_offset(bytes)
 
1606
            for record in self._kind_factory[storage_kind](
 
1607
                storage_kind, bytes, line_end):
 
1608
                yield record
 
1609
 
 
1610
 
 
1611
def fulltext_network_to_record(kind, bytes, line_end):
 
1612
    """Convert a network fulltext record to record."""
 
1613
    meta_len, = struct.unpack('!L', bytes[line_end:line_end+4])
 
1614
    record_meta = bytes[line_end+4:line_end+4+meta_len]
 
1615
    key, parents = bencode.bdecode_as_tuple(record_meta)
 
1616
    if parents == 'nil':
 
1617
        parents = None
 
1618
    fulltext = bytes[line_end+4+meta_len:]
 
1619
    return [FulltextContentFactory(key, parents, None, fulltext)]
 
1620
 
 
1621
 
 
1622
def _length_prefix(bytes):
 
1623
    return struct.pack('!L', len(bytes))
 
1624
 
 
1625
 
 
1626
def record_to_fulltext_bytes(record):
 
1627
    if record.parents is None:
 
1628
        parents = 'nil'
 
1629
    else:
 
1630
        parents = record.parents
 
1631
    record_meta = bencode.bencode((record.key, parents))
 
1632
    record_content = record.get_bytes_as('fulltext')
 
1633
    return "fulltext\n%s%s%s" % (
 
1634
        _length_prefix(record_meta), record_meta, record_content)
 
1635
 
 
1636
 
 
1637
def sort_groupcompress(parent_map):
 
1638
    """Sort and group the keys in parent_map into groupcompress order.
 
1639
 
 
1640
    groupcompress is defined (currently) as reverse-topological order, grouped
 
1641
    by the key prefix.
 
1642
 
 
1643
    :return: A sorted-list of keys
 
1644
    """
 
1645
    # gc-optimal ordering is approximately reverse topological,
 
1646
    # properly grouped by file-id.
 
1647
    per_prefix_map = {}
 
1648
    for item in parent_map.iteritems():
 
1649
        key = item[0]
 
1650
        if isinstance(key, str) or len(key) == 1:
 
1651
            prefix = ''
 
1652
        else:
 
1653
            prefix = key[0]
 
1654
        try:
 
1655
            per_prefix_map[prefix].append(item)
 
1656
        except KeyError:
 
1657
            per_prefix_map[prefix] = [item]
 
1658
 
 
1659
    present_keys = []
 
1660
    for prefix in sorted(per_prefix_map):
 
1661
        present_keys.extend(reversed(tsort.topo_sort(per_prefix_map[prefix])))
 
1662
    return present_keys