~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: Benoît Pierre
  • Date: 2009-02-24 00:25:32 UTC
  • mfrom: (4035 +trunk)
  • mto: (4056.1.1 trunk2)
  • mto: This revision was merged to the branch mainline in revision 4058.
  • Revision ID: benoit.pierre@gmail.com-20090224002532-i2f64ou15pa7if2y
Merge with upstream.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
31
32
from bzrlib import (
32
33
    errors,
33
34
    index,
 
35
    knit,
34
36
    osutils,
35
37
    multiparent,
36
38
    tsort,
44
46
from bzrlib.registry import Registry
45
47
from bzrlib.symbol_versioning import *
46
48
from bzrlib.textmerge import TextMerge
 
49
from bzrlib.util import bencode
47
50
 
48
51
 
49
52
adapter_registry = Registry()
59
62
    'bzrlib.knit', 'FTAnnotatedToUnannotated')
60
63
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'fulltext'),
61
64
    'bzrlib.knit', 'FTAnnotatedToFullText')
 
65
# adapter_registry.register_lazy(('knit-annotated-ft-gz', 'chunked'),
 
66
#     'bzrlib.knit', 'FTAnnotatedToChunked')
62
67
 
63
68
 
64
69
class ContentFactory(object):
65
70
    """Abstract interface for insertion and retrieval from a VersionedFile.
66
 
    
 
71
 
67
72
    :ivar sha1: None, or the sha1 of the content fulltext.
68
73
    :ivar storage_kind: The native storage kind of this factory. One of
69
74
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
84
89
        self.parents = None
85
90
 
86
91
 
 
92
class ChunkedContentFactory(ContentFactory):
 
93
    """Static data content factory.
 
94
 
 
95
    This takes a 'chunked' list of strings. The only requirement on 'chunked' is
 
96
    that ''.join(lines) becomes a valid fulltext. A tuple of a single string
 
97
    satisfies this, as does a list of lines.
 
98
 
 
99
    :ivar sha1: None, or the sha1 of the content fulltext.
 
100
    :ivar storage_kind: The native storage kind of this factory. Always
 
101
        'chunked'
 
102
    :ivar key: The key of this content. Each key is a tuple with a single
 
103
        string in it.
 
104
    :ivar parents: A tuple of parent keys for self.key. If the object has
 
105
        no parent information, None (as opposed to () for an empty list of
 
106
        parents).
 
107
     """
 
108
 
 
109
    def __init__(self, key, parents, sha1, chunks):
 
110
        """Create a ContentFactory."""
 
111
        self.sha1 = sha1
 
112
        self.storage_kind = 'chunked'
 
113
        self.key = key
 
114
        self.parents = parents
 
115
        self._chunks = chunks
 
116
 
 
117
    def get_bytes_as(self, storage_kind):
 
118
        if storage_kind == 'chunked':
 
119
            return self._chunks
 
120
        elif storage_kind == 'fulltext':
 
121
            return ''.join(self._chunks)
 
122
        raise errors.UnavailableRepresentation(self.key, storage_kind,
 
123
            self.storage_kind)
 
124
 
 
125
 
87
126
class FulltextContentFactory(ContentFactory):
88
127
    """Static data content factory.
89
128
 
90
129
    This takes a fulltext when created and just returns that during
91
130
    get_bytes_as('fulltext').
92
 
    
 
131
 
93
132
    :ivar sha1: None, or the sha1 of the content fulltext.
94
133
    :ivar storage_kind: The native storage kind of this factory. Always
95
134
        'fulltext'.
111
150
    def get_bytes_as(self, storage_kind):
112
151
        if storage_kind == self.storage_kind:
113
152
            return self._text
 
153
        elif storage_kind == 'chunked':
 
154
            return [self._text]
114
155
        raise errors.UnavailableRepresentation(self.key, storage_kind,
115
156
            self.storage_kind)
116
157
 
117
158
 
118
159
class AbsentContentFactory(ContentFactory):
119
160
    """A placeholder content factory for unavailable texts.
120
 
    
 
161
 
121
162
    :ivar sha1: None.
122
163
    :ivar storage_kind: 'absent'.
123
164
    :ivar key: The key of this content. Each key is a tuple with a single
159
200
 
160
201
class VersionedFile(object):
161
202
    """Versioned text file storage.
162
 
    
 
203
 
163
204
    A versioned file manages versions of line-based text files,
164
205
    keeping track of the originating version for each line.
165
206
 
203
244
    def insert_record_stream(self, stream):
204
245
        """Insert a record stream into this versioned file.
205
246
 
206
 
        :param stream: A stream of records to insert. 
 
247
        :param stream: A stream of records to insert.
207
248
        :return: None
208
249
        :seealso VersionedFile.get_record_stream:
209
250
        """
228
269
            the data back accurately. (Checking the lines have been split
229
270
            correctly is expensive and extremely unlikely to catch bugs so it
230
271
            is not done at runtime unless check_content is True.)
231
 
        :param parent_texts: An optional dictionary containing the opaque 
 
272
        :param parent_texts: An optional dictionary containing the opaque
232
273
            representations of some or all of the parents of version_id to
233
274
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
234
275
            returned by add_lines or data corruption can be caused.
262
303
        parent_texts=None, nostore_sha=None, random_id=False,
263
304
        check_content=True, left_matching_blocks=None):
264
305
        """Add lines to the versioned file, allowing ghosts to be present.
265
 
        
 
306
 
266
307
        This takes the same parameters as add_lines and returns the same.
267
308
        """
268
309
        self._check_write_ok()
292
333
 
293
334
    def get_format_signature(self):
294
335
        """Get a text description of the data encoding in this file.
295
 
        
 
336
 
296
337
        :since: 0.90
297
338
        """
298
339
        raise NotImplementedError(self.get_format_signature)
419
460
        if isinstance(version_ids, basestring):
420
461
            version_ids = [version_ids]
421
462
        raise NotImplementedError(self.get_ancestry)
422
 
        
 
463
 
423
464
    def get_ancestry_with_ghosts(self, version_ids):
424
465
        """Return a list of all ancestors of given version(s). This
425
466
        will not include the null revision.
426
467
 
427
468
        Must raise RevisionNotPresent if any of the given versions are
428
469
        not present in file history.
429
 
        
 
470
 
430
471
        Ghosts that are known about will be included in ancestry list,
431
472
        but are not explicitly marked.
432
473
        """
433
474
        raise NotImplementedError(self.get_ancestry_with_ghosts)
434
 
    
 
475
 
435
476
    def get_parent_map(self, version_ids):
436
477
        """Get a map of the parents of version_ids.
437
478
 
500
541
        unchanged   Alive in both a and b (possibly created in both)
501
542
        new-a       Created in a
502
543
        new-b       Created in b
503
 
        ghost-a     Killed in a, unborn in b    
 
544
        ghost-a     Killed in a, unborn in b
504
545
        ghost-b     Killed in b, unborn in a
505
546
        irrelevant  Not in either revision
506
547
        """
507
548
        raise NotImplementedError(VersionedFile.plan_merge)
508
 
        
 
549
 
509
550
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
510
551
                    b_marker=TextMerge.B_MARKER):
511
552
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
513
554
 
514
555
class RecordingVersionedFilesDecorator(object):
515
556
    """A minimal versioned files that records calls made on it.
516
 
    
 
557
 
517
558
    Only enough methods have been added to support tests using it to date.
518
559
 
519
560
    :ivar calls: A list of the calls made; can be reset at any time by
521
562
    """
522
563
 
523
564
    def __init__(self, backing_vf):
524
 
        """Create a RecordingVersionedFileDsecorator decorating backing_vf.
525
 
        
 
565
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
 
566
 
526
567
        :param backing_vf: The versioned file to answer all methods.
527
568
        """
528
569
        self._backing_vf = backing_vf
562
603
        return self._backing_vf.keys()
563
604
 
564
605
 
 
606
class OrderingVersionedFilesDecorator(RecordingVersionedFilesDecorator):
 
607
    """A VF that records calls, and returns keys in specific order.
 
608
 
 
609
    :ivar calls: A list of the calls made; can be reset at any time by
 
610
        assigning [] to it.
 
611
    """
 
612
 
 
613
    def __init__(self, backing_vf, key_priority):
 
614
        """Create a RecordingVersionedFilesDecorator decorating backing_vf.
 
615
 
 
616
        :param backing_vf: The versioned file to answer all methods.
 
617
        :param key_priority: A dictionary defining what order keys should be
 
618
            returned from an 'unordered' get_record_stream request.
 
619
            Keys with lower priority are returned first, keys not present in
 
620
            the map get an implicit priority of 0, and are returned in
 
621
            lexicographical order.
 
622
        """
 
623
        RecordingVersionedFilesDecorator.__init__(self, backing_vf)
 
624
        self._key_priority = key_priority
 
625
 
 
626
    def get_record_stream(self, keys, sort_order, include_delta_closure):
 
627
        self.calls.append(("get_record_stream", list(keys), sort_order,
 
628
            include_delta_closure))
 
629
        if sort_order == 'unordered':
 
630
            def sort_key(key):
 
631
                return (self._key_priority.get(key, 0), key)
 
632
            # Use a defined order by asking for the keys one-by-one from the
 
633
            # backing_vf
 
634
            for key in sorted(keys, key=sort_key):
 
635
                for record in self._backing_vf.get_record_stream([key],
 
636
                                'unordered', include_delta_closure):
 
637
                    yield record
 
638
        else:
 
639
            for record in self._backing_vf.get_record_stream(keys, sort_order,
 
640
                            include_delta_closure):
 
641
                yield record
 
642
 
 
643
 
565
644
class KeyMapper(object):
566
645
    """KeyMappers map between keys and underlying partitioned storage."""
567
646
 
576
655
 
577
656
    def unmap(self, partition_id):
578
657
        """Map a partitioned storage id back to a key prefix.
579
 
        
 
658
 
580
659
        :param partition_id: The underlying partition id.
581
660
        :return: As much of a key (or prefix) as is derivable from the partition
582
661
            id.
614
693
 
615
694
class PrefixMapper(URLEscapeMapper):
616
695
    """A key mapper that extracts the first component of a key.
617
 
    
 
696
 
618
697
    This mapper is for use with a transport based backend.
619
698
    """
620
699
 
653
732
 
654
733
class HashEscapedPrefixMapper(HashPrefixMapper):
655
734
    """Combines the escaped first component of a key with a hash.
656
 
    
 
735
 
657
736
    This mapper is for use with a transport based backend.
658
737
    """
659
738
 
725
804
            the data back accurately. (Checking the lines have been split
726
805
            correctly is expensive and extremely unlikely to catch bugs so it
727
806
            is not done at runtime unless check_content is True.)
728
 
        :param parent_texts: An optional dictionary containing the opaque 
 
807
        :param parent_texts: An optional dictionary containing the opaque
729
808
            representations of some or all of the parents of version_id to
730
809
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
731
810
            returned by add_lines or data corruption can be caused.
766
845
                                  if not mpvf.has_version(p))
767
846
        # It seems likely that adding all the present parents as fulltexts can
768
847
        # easily exhaust memory.
769
 
        split_lines = osutils.split_lines
 
848
        chunks_to_lines = osutils.chunks_to_lines
770
849
        for record in self.get_record_stream(needed_parents, 'unordered',
771
850
            True):
772
851
            if record.storage_kind == 'absent':
773
852
                continue
774
 
            mpvf.add_version(split_lines(record.get_bytes_as('fulltext')),
 
853
            mpvf.add_version(chunks_to_lines(record.get_bytes_as('chunked')),
775
854
                record.key, [])
776
855
        for (key, parent_keys, expected_sha1, mpdiff), lines in\
777
856
            zip(records, mpvf.get_line_list(versions)):
849
928
 
850
929
    has_key = index._has_key_from_parent_map
851
930
 
 
931
    def get_missing_compression_parent_keys(self):
 
932
        """Return an iterable of keys of missing compression parents.
 
933
 
 
934
        Check this after calling insert_record_stream to find out if there are
 
935
        any missing compression parents.  If there are, the records that
 
936
        depend on them are not able to be inserted safely. The precise
 
937
        behaviour depends on the concrete VersionedFiles class in use.
 
938
 
 
939
        Classes that do not support this will raise NotImplementedError.
 
940
        """
 
941
        raise NotImplementedError(self.get_missing_compression_parent_keys)
 
942
 
852
943
    def insert_record_stream(self, stream):
853
944
        """Insert a record stream into this container.
854
945
 
855
 
        :param stream: A stream of records to insert. 
 
946
        :param stream: A stream of records to insert.
856
947
        :return: None
857
948
        :seealso VersionedFile.get_record_stream:
858
949
        """
902
993
        ghosts = maybe_ghosts - set(self.get_parent_map(maybe_ghosts))
903
994
        knit_keys.difference_update(ghosts)
904
995
        lines = {}
905
 
        split_lines = osutils.split_lines
 
996
        chunks_to_lines = osutils.chunks_to_lines
906
997
        for record in self.get_record_stream(knit_keys, 'topological', True):
907
 
            lines[record.key] = split_lines(record.get_bytes_as('fulltext'))
 
998
            lines[record.key] = chunks_to_lines(record.get_bytes_as('chunked'))
908
999
            # line_block_dict = {}
909
1000
            # for parent, blocks in record.extract_line_blocks():
910
1001
            #   line_blocks[parent] = blocks
1086
1177
    def insert_record_stream(self, stream):
1087
1178
        """Insert a record stream into this container.
1088
1179
 
1089
 
        :param stream: A stream of records to insert. 
 
1180
        :param stream: A stream of records to insert.
1090
1181
        :return: None
1091
1182
        :seealso VersionedFile.get_record_stream:
1092
1183
        """
1213
1304
                lines = self._lines[key]
1214
1305
                parents = self._parents[key]
1215
1306
                pending.remove(key)
1216
 
                yield FulltextContentFactory(key, parents, None,
1217
 
                    ''.join(lines))
 
1307
                yield ChunkedContentFactory(key, parents, None, lines)
1218
1308
        for versionedfile in self.fallback_versionedfiles:
1219
1309
            for record in versionedfile.get_record_stream(
1220
1310
                pending, 'unordered', True):
1250
1340
 
1251
1341
class PlanWeaveMerge(TextMerge):
1252
1342
    """Weave merge that takes a plan as its input.
1253
 
    
 
1343
 
1254
1344
    This exists so that VersionedFile.plan_merge is implementable.
1255
1345
    Most callers will want to use WeaveMerge instead.
1256
1346
    """
1277
1367
                yield(lines_a,)
1278
1368
            else:
1279
1369
                yield (lines_a, lines_b)
1280
 
       
 
1370
 
1281
1371
        # We previously considered either 'unchanged' or 'killed-both' lines
1282
1372
        # to be possible places to resynchronize.  However, assuming agreement
1283
1373
        # on killed-both lines may be too aggressive. -- mbp 20060324
1289
1379
                lines_a = []
1290
1380
                lines_b = []
1291
1381
                ch_a = ch_b = False
1292
 
                
 
1382
 
1293
1383
            if state == 'unchanged':
1294
1384
                if line:
1295
1385
                    yield ([line],)
1322
1412
class WeaveMerge(PlanWeaveMerge):
1323
1413
    """Weave merge that takes a VersionedFile and two versions as its input."""
1324
1414
 
1325
 
    def __init__(self, versionedfile, ver_a, ver_b, 
 
1415
    def __init__(self, versionedfile, ver_a, ver_b,
1326
1416
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
1327
1417
        plan = versionedfile.plan_merge(ver_a, ver_b)
1328
1418
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
1329
1419
 
1330
1420
 
1331
1421
class VirtualVersionedFiles(VersionedFiles):
1332
 
    """Dummy implementation for VersionedFiles that uses other functions for 
 
1422
    """Dummy implementation for VersionedFiles that uses other functions for
1333
1423
    obtaining fulltexts and parent maps.
1334
1424
 
1335
 
    This is always on the bottom of the stack and uses string keys 
 
1425
    This is always on the bottom of the stack and uses string keys
1336
1426
    (rather than tuples) internally.
1337
1427
    """
1338
1428
 
1340
1430
        """Create a VirtualVersionedFiles.
1341
1431
 
1342
1432
        :param get_parent_map: Same signature as Repository.get_parent_map.
1343
 
        :param get_lines: Should return lines for specified key or None if 
 
1433
        :param get_lines: Should return lines for specified key or None if
1344
1434
                          not available.
1345
1435
        """
1346
1436
        super(VirtualVersionedFiles, self).__init__()
1347
1437
        self._get_parent_map = get_parent_map
1348
1438
        self._get_lines = get_lines
1349
 
        
 
1439
 
1350
1440
    def check(self, progressbar=None):
1351
1441
        """See VersionedFiles.check.
1352
1442
 
1384
1474
            if lines is not None:
1385
1475
                if not isinstance(lines, list):
1386
1476
                    raise AssertionError
1387
 
                yield FulltextContentFactory((k,), None, 
 
1477
                yield ChunkedContentFactory((k,), None,
1388
1478
                        sha1=osutils.sha_strings(lines),
1389
 
                        text=''.join(lines))
 
1479
                        chunks=lines)
1390
1480
            else:
1391
1481
                yield AbsentContentFactory((k,))
1392
1482
 
1393
 
 
1394
 
 
 
1483
    def iter_lines_added_or_present_in_keys(self, keys, pb=None):
 
1484
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
 
1485
        for i, (key,) in enumerate(keys):
 
1486
            if pb is not None:
 
1487
                pb.update("iterating texts", i, len(keys))
 
1488
            for l in self._get_lines(key):
 
1489
                yield (l, key)
 
1490
 
 
1491
 
 
1492
def network_bytes_to_kind_and_offset(network_bytes):
 
1493
    """Strip of a record kind from the front of network_bytes.
 
1494
 
 
1495
    :param network_bytes: The bytes of a record.
 
1496
    :return: A tuple (storage_kind, offset_of_remaining_bytes)
 
1497
    """
 
1498
    line_end = network_bytes.find('\n')
 
1499
    storage_kind = network_bytes[:line_end]
 
1500
    return storage_kind, line_end + 1
 
1501
 
 
1502
 
 
1503
class NetworkRecordStream(object):
 
1504
    """A record_stream which reconstitures a serialised stream."""
 
1505
 
 
1506
    def __init__(self, bytes_iterator):
 
1507
        """Create a NetworkRecordStream.
 
1508
 
 
1509
        :param bytes_iterator: An iterator of bytes. Each item in this
 
1510
            iterator should have been obtained from a record_streams'
 
1511
            record.get_bytes_as(record.storage_kind) call.
 
1512
        """
 
1513
        self._bytes_iterator = bytes_iterator
 
1514
        self._kind_factory = {'knit-ft-gz':knit.knit_network_to_record,
 
1515
            'knit-delta-gz':knit.knit_network_to_record,
 
1516
            'knit-annotated-ft-gz':knit.knit_network_to_record,
 
1517
            'knit-annotated-delta-gz':knit.knit_network_to_record,
 
1518
            'knit-delta-closure':knit.knit_delta_closure_to_records,
 
1519
            'fulltext':fulltext_network_to_record,
 
1520
            }
 
1521
 
 
1522
    def read(self):
 
1523
        """Read the stream.
 
1524
 
 
1525
        :return: An iterator as per VersionedFiles.get_record_stream().
 
1526
        """
 
1527
        for bytes in self._bytes_iterator:
 
1528
            storage_kind, line_end = network_bytes_to_kind_and_offset(bytes)
 
1529
            for record in self._kind_factory[storage_kind](
 
1530
                storage_kind, bytes, line_end):
 
1531
                yield record
 
1532
 
 
1533
 
 
1534
def fulltext_network_to_record(kind, bytes, line_end):
 
1535
    """Convert a network fulltext record to record."""
 
1536
    meta_len, = struct.unpack('!L', bytes[line_end:line_end+4])
 
1537
    record_meta = record_bytes[line_end+4:line_end+4+meta_len]
 
1538
    key, parents = bencode.bdecode_as_tuple(record_meta)
 
1539
    if parents == 'nil':
 
1540
        parents = None
 
1541
    fulltext = record_bytes[line_end+4+meta_len:]
 
1542
    return FulltextContentFactory(key, parents, None, fulltext)
 
1543
 
 
1544
 
 
1545
def _length_prefix(bytes):
 
1546
    return struct.pack('!L', len(bytes))
 
1547
 
 
1548
 
 
1549
def record_to_fulltext_bytes(self, record):
 
1550
    if record.parents is None:
 
1551
        parents = 'nil'
 
1552
    else:
 
1553
        parents = record.parents
 
1554
    record_meta = bencode.bencode((record.key, parents))
 
1555
    record_content = record.get_bytes_as('fulltext')
 
1556
    return "fulltext\n%s%s%s" % (
 
1557
        _length_prefix(record_meta), record_meta, record_content)