~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/knit.py

[merge] bzr.dev 2294

Show diffs side-by-side

added added

removed removed

Lines of Context:
73
73
from bzrlib import (
74
74
    cache_utf8,
75
75
    errors,
 
76
    osutils,
76
77
    patiencediff,
77
78
    progress,
78
79
    ui,
156
157
class _KnitFactory(object):
157
158
    """Base factory for creating content objects."""
158
159
 
159
 
    def make(self, lines, version):
 
160
    def make(self, lines, version_id):
160
161
        num_lines = len(lines)
161
 
        return KnitContent(zip([version] * num_lines, lines))
 
162
        return KnitContent(zip([version_id] * num_lines, lines))
162
163
 
163
164
 
164
165
class KnitAnnotateFactory(_KnitFactory):
166
167
 
167
168
    annotated = True
168
169
 
169
 
    def parse_fulltext(self, content, version):
 
170
    def parse_fulltext(self, content, version_id):
170
171
        """Convert fulltext to internal representation
171
172
 
172
173
        fulltext content is of the format
174
175
        internal representation is of the format:
175
176
        (revid, plaintext)
176
177
        """
177
 
        decode_utf8 = cache_utf8.decode
178
 
        lines = []
179
 
        for line in content:
180
 
            origin, text = line.split(' ', 1)
181
 
            lines.append((decode_utf8(origin), text))
 
178
        # TODO: jam 20070209 The tests expect this to be returned as tuples,
 
179
        #       but the code itself doesn't really depend on that.
 
180
        #       Figure out a way to not require the overhead of turning the
 
181
        #       list back into tuples.
 
182
        lines = [tuple(line.split(' ', 1)) for line in content]
182
183
        return KnitContent(lines)
183
184
 
184
185
    def parse_line_delta_iter(self, lines):
185
186
        return iter(self.parse_line_delta(lines))
186
187
 
187
 
    def parse_line_delta(self, lines, version):
 
188
    def parse_line_delta(self, lines, version_id):
188
189
        """Convert a line based delta into internal representation.
189
190
 
190
191
        line delta is in the form of:
194
195
        internal representation is
195
196
        (start, end, count, [1..count tuples (revid, newline)])
196
197
        """
197
 
        decode_utf8 = cache_utf8.decode
198
198
        result = []
199
199
        lines = iter(lines)
200
200
        next = lines.next
 
201
 
 
202
        cache = {}
 
203
        def cache_and_return(line):
 
204
            origin, text = line.split(' ', 1)
 
205
            return cache.setdefault(origin, origin), text
 
206
 
201
207
        # walk through the lines parsing.
202
208
        for header in lines:
203
209
            start, end, count = [int(n) for n in header.split(',')]
204
 
            contents = []
205
 
            remaining = count
206
 
            while remaining:
207
 
                origin, text = next().split(' ', 1)
208
 
                remaining -= 1
209
 
                contents.append((decode_utf8(origin), text))
 
210
            contents = [tuple(next().split(' ', 1)) for i in xrange(count)]
210
211
            result.append((start, end, count, contents))
211
212
        return result
212
213
 
234
235
 
235
236
        see parse_fulltext which this inverts.
236
237
        """
237
 
        encode_utf8 = cache_utf8.encode
238
 
        return ['%s %s' % (encode_utf8(o), t) for o, t in content._lines]
 
238
        # TODO: jam 20070209 We only do the caching thing to make sure that
 
239
        #       the origin is a valid utf-8 line, eventually we could remove it
 
240
        return ['%s %s' % (o, t) for o, t in content._lines]
239
241
 
240
242
    def lower_line_delta(self, delta):
241
243
        """convert a delta into a serializable form.
242
244
 
243
245
        See parse_line_delta which this inverts.
244
246
        """
245
 
        encode_utf8 = cache_utf8.encode
 
247
        # TODO: jam 20070209 We only do the caching thing to make sure that
 
248
        #       the origin is a valid utf-8 line, eventually we could remove it
246
249
        out = []
247
250
        for start, end, c, lines in delta:
248
251
            out.append('%d,%d,%d\n' % (start, end, c))
249
 
            out.extend(encode_utf8(origin) + ' ' + text
 
252
            out.extend(origin + ' ' + text
250
253
                       for origin, text in lines)
251
254
        return out
252
255
 
256
259
 
257
260
    annotated = False
258
261
 
259
 
    def parse_fulltext(self, content, version):
 
262
    def parse_fulltext(self, content, version_id):
260
263
        """This parses an unannotated fulltext.
261
264
 
262
265
        Note that this is not a noop - the internal representation
263
266
        has (versionid, line) - its just a constant versionid.
264
267
        """
265
 
        return self.make(content, version)
 
268
        return self.make(content, version_id)
266
269
 
267
 
    def parse_line_delta_iter(self, lines, version):
 
270
    def parse_line_delta_iter(self, lines, version_id):
268
271
        cur = 0
269
272
        num_lines = len(lines)
270
273
        while cur < num_lines:
271
274
            header = lines[cur]
272
275
            cur += 1
273
276
            start, end, c = [int(n) for n in header.split(',')]
274
 
            yield start, end, c, zip([version] * c, lines[cur:cur+c])
 
277
            yield start, end, c, zip([version_id] * c, lines[cur:cur+c])
275
278
            cur += c
276
279
 
277
 
    def parse_line_delta(self, lines, version):
278
 
        return list(self.parse_line_delta_iter(lines, version))
 
280
    def parse_line_delta(self, lines, version_id):
 
281
        return list(self.parse_line_delta_iter(lines, version_id))
279
282
 
280
283
    def get_fulltext_content(self, lines):
281
284
        """Extract just the content lines from a fulltext."""
500
503
        return KnitVersionedFile(name, transport, factory=self.factory,
501
504
                                 delta=self.delta, create=True)
502
505
    
503
 
    def _fix_parents(self, version, new_parents):
 
506
    def _fix_parents(self, version_id, new_parents):
504
507
        """Fix the parents list for version.
505
508
        
506
509
        This is done by appending a new version to the index
508
511
        the parents list must be a superset of the current
509
512
        list.
510
513
        """
511
 
        current_values = self._index._cache[version]
 
514
        current_values = self._index._cache[version_id]
512
515
        assert set(current_values[4]).difference(set(new_parents)) == set()
513
 
        self._index.add_version(version,
 
516
        self._index.add_version(version_id,
514
517
                                current_values[1], 
515
518
                                current_values[2],
516
519
                                current_values[3],
518
521
 
519
522
    def get_delta(self, version_id):
520
523
        """Get a delta for constructing version from some other version."""
 
524
        version_id = osutils.safe_revision_id(version_id)
521
525
        self.check_not_reserved_id(version_id)
522
526
        if not self.has_version(version_id):
523
527
            raise RevisionNotPresent(version_id, self.filename)
529
533
            parent = None
530
534
        data_pos, data_size = self._index.get_position(version_id)
531
535
        data, sha1 = self._data.read_records(((version_id, data_pos, data_size),))[version_id]
532
 
        version_idx = self._index.lookup(version_id)
533
536
        noeol = 'no-eol' in self._index.get_options(version_id)
534
537
        if 'fulltext' == self._index.get_method(version_id):
535
 
            new_content = self.factory.parse_fulltext(data, version_idx)
 
538
            new_content = self.factory.parse_fulltext(data, version_id)
536
539
            if parent is not None:
537
540
                reference_content = self._get_content(parent)
538
541
                old_texts = reference_content.text()
542
545
            delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
543
546
            return parent, sha1, noeol, self._make_line_delta(delta_seq, new_content)
544
547
        else:
545
 
            delta = self.factory.parse_line_delta(data, version_idx)
 
548
            delta = self.factory.parse_line_delta(data, version_id)
546
549
            return parent, sha1, noeol, delta
547
550
        
548
551
    def get_graph_with_ghosts(self):
552
555
 
553
556
    def get_sha1(self, version_id):
554
557
        """See VersionedFile.get_sha1()."""
 
558
        version_id = osutils.safe_revision_id(version_id)
555
559
        record_map = self._get_record_map([version_id])
556
560
        method, content, digest, next = record_map[version_id]
557
561
        return digest 
563
567
 
564
568
    def has_ghost(self, version_id):
565
569
        """True if there is a ghost reference in the file to version_id."""
 
570
        version_id = osutils.safe_revision_id(version_id)
566
571
        # maybe we have it
567
572
        if self.has_version(version_id):
568
573
            return False
581
586
 
582
587
    def has_version(self, version_id):
583
588
        """See VersionedFile.has_version."""
 
589
        version_id = osutils.safe_revision_id(version_id)
584
590
        return self._index.has_version(version_id)
585
591
 
586
592
    __contains__ = has_version
738
744
            # I/O and the time spend applying deltas.
739
745
            delta = self._check_should_delta(present_parents)
740
746
 
 
747
        assert isinstance(version_id, str)
741
748
        lines = self.factory.make(lines, version_id)
742
749
        if delta or (self.factory.annotated and len(present_parents) > 0):
743
750
            # Merge annotations from parent texts if so is needed.
799
806
 
800
807
    def get_line_list(self, version_ids):
801
808
        """Return the texts of listed versions as a list of strings."""
 
809
        version_ids = [osutils.safe_revision_id(v) for v in version_ids]
802
810
        for version_id in version_ids:
803
811
            self.check_not_reserved_id(version_id)
804
812
        text_map, content_map = self._get_content_maps(version_ids)
834
842
                if component_id in content_map:
835
843
                    content = content_map[component_id]
836
844
                else:
837
 
                    version_idx = self._index.lookup(component_id)
838
845
                    if method == 'fulltext':
839
846
                        assert content is None
840
 
                        content = self.factory.parse_fulltext(data, version_idx)
 
847
                        content = self.factory.parse_fulltext(data, version_id)
841
848
                    elif method == 'line-delta':
842
 
                        delta = self.factory.parse_line_delta(data, version_idx)
 
849
                        delta = self.factory.parse_line_delta(data, version_id)
843
850
                        content = content.copy()
844
851
                        content._lines = self._apply_delta(content._lines, 
845
852
                                                           delta)
865
872
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
866
873
        if version_ids is None:
867
874
            version_ids = self.versions()
 
875
        else:
 
876
            version_ids = [osutils.safe_revision_id(v) for v in version_ids]
868
877
        if pb is None:
869
878
            pb = progress.DummyProgress()
870
879
        # we don't care about inclusions, the caller cares.
887
896
            enumerate(self._data.read_records_iter(version_id_records)):
888
897
            pb.update('Walking content.', version_idx, total)
889
898
            method = self._index.get_method(version_id)
890
 
            version_idx = self._index.lookup(version_id)
891
899
 
892
900
            assert method in ('fulltext', 'line-delta')
893
901
            if method == 'fulltext':
907
915
 
908
916
    def annotate_iter(self, version_id):
909
917
        """See VersionedFile.annotate_iter."""
 
918
        version_id = osutils.safe_revision_id(version_id)
910
919
        content = self._get_content(version_id)
911
920
        for origin, text in content.annotate_iter():
912
921
            yield origin, text
916
925
        # perf notes:
917
926
        # optimism counts!
918
927
        # 52554 calls in 1264 872 internal down from 3674
 
928
        version_id = osutils.safe_revision_id(version_id)
919
929
        try:
920
930
            return self._index.get_parents(version_id)
921
931
        except KeyError:
923
933
 
924
934
    def get_parents_with_ghosts(self, version_id):
925
935
        """See VersionedFile.get_parents."""
 
936
        version_id = osutils.safe_revision_id(version_id)
926
937
        try:
927
938
            return self._index.get_parents_with_ghosts(version_id)
928
939
        except KeyError:
934
945
            versions = [versions]
935
946
        if not versions:
936
947
            return []
 
948
        versions = [osutils.safe_revision_id(v) for v in versions]
937
949
        return self._index.get_ancestry(versions)
938
950
 
939
951
    def get_ancestry_with_ghosts(self, versions):
942
954
            versions = [versions]
943
955
        if not versions:
944
956
            return []
 
957
        versions = [osutils.safe_revision_id(v) for v in versions]
945
958
        return self._index.get_ancestry_with_ghosts(versions)
946
959
 
947
960
    #@deprecated_method(zero_eight)
967
980
 
968
981
    def plan_merge(self, ver_a, ver_b):
969
982
        """See VersionedFile.plan_merge."""
 
983
        ver_a = osutils.safe_revision_id(ver_a)
 
984
        ver_b = osutils.safe_revision_id(ver_b)
970
985
        ancestors_b = set(self.get_ancestry(ver_b))
971
986
        def status_a(revision, text):
972
987
            if revision in ancestors_b:
1130
1145
        # so - wc -l of a knit index is != the number of unique names
1131
1146
        # in the knit.
1132
1147
        self._history = []
1133
 
        decode_utf8 = cache_utf8.decode
1134
 
        pb = ui.ui_factory.nested_progress_bar()
1135
1148
        try:
1136
 
            pb.update('read knit index', 0, 1)
 
1149
            fp = self._transport.get(self._filename)
1137
1150
            try:
1138
 
                fp = self._transport.get(self._filename)
1139
 
                try:
1140
 
                    # _load_data may raise NoSuchFile if the target knit is
1141
 
                    # completely empty.
1142
 
                    self._load_data(fp)
1143
 
                finally:
1144
 
                    fp.close()
1145
 
            except NoSuchFile:
1146
 
                if mode != 'w' or not create:
1147
 
                    raise
1148
 
                elif delay_create:
1149
 
                    self._need_to_create = True
1150
 
                else:
1151
 
                    self._transport.put_bytes_non_atomic(
1152
 
                        self._filename, self.HEADER, mode=self._file_mode)
1153
 
        finally:
1154
 
            pb.update('read knit index', 1, 1)
1155
 
            pb.finished()
 
1151
                # _load_data may raise NoSuchFile if the target knit is
 
1152
                # completely empty.
 
1153
                self._load_data(fp)
 
1154
            finally:
 
1155
                fp.close()
 
1156
        except NoSuchFile:
 
1157
            if mode != 'w' or not create:
 
1158
                raise
 
1159
            elif delay_create:
 
1160
                self._need_to_create = True
 
1161
            else:
 
1162
                self._transport.put_bytes_non_atomic(
 
1163
                    self._filename, self.HEADER, mode=self._file_mode)
1156
1164
 
1157
1165
    def _load_data(self, fp):
1158
1166
        cache = self._cache
1159
1167
        history = self._history
1160
 
        decode_utf8 = cache_utf8.decode
1161
1168
 
1162
1169
        self.check_header(fp)
1163
1170
        # readlines reads the whole file at once:
1182
1189
            for value in rec[4:-1]:
1183
1190
                if value[0] == '.':
1184
1191
                    # uncompressed reference
1185
 
                    parents.append(decode_utf8(value[1:]))
 
1192
                    parent_id = value[1:]
1186
1193
                else:
1187
 
                    parents.append(history[int(value)])
 
1194
                    parent_id = history[int(value)]
 
1195
                parents.append(parent_id)
1188
1196
 
1189
1197
            version_id, options, pos, size = rec[:4]
1190
 
            version_id = decode_utf8(version_id)
 
1198
            version_id = version_id
1191
1199
 
1192
1200
            # See self._cache_version
1193
1201
            # only want the _history index to reference the 1st 
1263
1271
        return self._cache[version_id][5]
1264
1272
 
1265
1273
    def _version_list_to_index(self, versions):
1266
 
        encode_utf8 = cache_utf8.encode
1267
1274
        result_list = []
1268
1275
        cache = self._cache
1269
1276
        for version in versions:
1272
1279
                result_list.append(str(cache[version][5]))
1273
1280
                # -- end lookup () --
1274
1281
            else:
1275
 
                result_list.append('.' + encode_utf8(version))
 
1282
                result_list.append('.' + version)
1276
1283
        return ' '.join(result_list)
1277
1284
 
1278
1285
    def add_version(self, version_id, options, pos, size, parents):
1286
1293
                         (version_id, options, pos, size, parents).
1287
1294
        """
1288
1295
        lines = []
1289
 
        encode_utf8 = cache_utf8.encode
1290
1296
        orig_history = self._history[:]
1291
1297
        orig_cache = self._cache.copy()
1292
1298
 
1293
1299
        try:
1294
1300
            for version_id, options, pos, size, parents in versions:
1295
 
                line = "\n%s %s %s %s %s :" % (encode_utf8(version_id),
 
1301
                line = "\n%s %s %s %s %s :" % (version_id,
1296
1302
                                               ','.join(options),
1297
1303
                                               pos,
1298
1304
                                               size,
1406
1412
        sio = StringIO()
1407
1413
        data_file = GzipFile(None, mode='wb', fileobj=sio)
1408
1414
 
1409
 
        version_id_utf8 = cache_utf8.encode(version_id)
 
1415
        assert isinstance(version_id, str)
1410
1416
        data_file.writelines(chain(
1411
 
            ["version %s %d %s\n" % (version_id_utf8,
 
1417
            ["version %s %d %s\n" % (version_id,
1412
1418
                                     len(lines),
1413
1419
                                     digest)],
1414
1420
            lines,
1415
 
            ["end %s\n" % version_id_utf8]))
 
1421
            ["end %s\n" % version_id]))
1416
1422
        data_file.close()
1417
1423
        length= sio.tell()
1418
1424
 
1468
1474
        if len(rec) != 4:
1469
1475
            raise KnitCorrupt(self._filename,
1470
1476
                              'unexpected number of elements in record header')
1471
 
        if cache_utf8.decode(rec[1]) != version_id:
 
1477
        if rec[1] != version_id:
1472
1478
            raise KnitCorrupt(self._filename,
1473
1479
                              'unexpected version, wanted %r, got %r'
1474
1480
                              % (version_id, rec[1]))