/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/knit.py

  • Committer: Robert Collins
  • Date: 2007-07-04 08:08:13 UTC
  • mfrom: (2572 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2587.
  • Revision ID: robertc@robertcollins.net-20070704080813-wzebx0r88fvwj5rq
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 by Canonical Ltd
2
 
# Written by Martin Pool.
3
 
# Modified by Johan Rydberg <jrydberg@gnu.org>
4
 
# Modified by Robert Collins <robert.collins@canonical.com>
5
 
# Modified by Aaron Bentley <aaron.bentley@utoronto.ca>
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
6
2
#
7
3
# This program is free software; you can redistribute it and/or modify
8
4
# it under the terms of the GNU General Public License as published by
77
73
from bzrlib import (
78
74
    cache_utf8,
79
75
    errors,
 
76
    osutils,
 
77
    patiencediff,
80
78
    progress,
81
 
    )
82
 
from bzrlib.errors import FileExists, NoSuchFile, KnitError, \
83
 
        InvalidRevisionId, KnitCorrupt, KnitHeaderError, \
84
 
        RevisionNotPresent, RevisionAlreadyPresent
 
79
    ui,
 
80
    )
 
81
from bzrlib.errors import (
 
82
    FileExists,
 
83
    NoSuchFile,
 
84
    KnitError,
 
85
    InvalidRevisionId,
 
86
    KnitCorrupt,
 
87
    KnitHeaderError,
 
88
    RevisionNotPresent,
 
89
    RevisionAlreadyPresent,
 
90
    )
85
91
from bzrlib.tuned_gzip import GzipFile
86
92
from bzrlib.trace import mutter
87
 
from bzrlib.osutils import contains_whitespace, contains_linebreaks, \
88
 
     sha_strings
 
93
from bzrlib.osutils import (
 
94
    contains_whitespace,
 
95
    contains_linebreaks,
 
96
    sha_strings,
 
97
    )
89
98
from bzrlib.symbol_versioning import DEPRECATED_PARAMETER, deprecated_passed
90
99
from bzrlib.tsort import topo_sort
 
100
import bzrlib.ui
91
101
import bzrlib.weave
92
102
from bzrlib.versionedfile import VersionedFile, InterVersionedFile
93
103
 
117
127
 
118
128
    def annotate_iter(self):
119
129
        """Yield tuples of (origin, text) for each content line."""
120
 
        for origin, text in self._lines:
121
 
            yield origin, text
 
130
        return iter(self._lines)
122
131
 
123
132
    def annotate(self):
124
133
        """Return a list of (origin, text) tuples."""
126
135
 
127
136
    def line_delta_iter(self, new_lines):
128
137
        """Generate line-based delta from this content to new_lines."""
129
 
        new_texts = [text for origin, text in new_lines._lines]
130
 
        old_texts = [text for origin, text in self._lines]
 
138
        new_texts = new_lines.text()
 
139
        old_texts = self.text()
131
140
        s = KnitSequenceMatcher(None, old_texts, new_texts)
132
 
        for op in s.get_opcodes():
133
 
            if op[0] == 'equal':
 
141
        for tag, i1, i2, j1, j2 in s.get_opcodes():
 
142
            if tag == 'equal':
134
143
                continue
135
 
            #     ofrom   oto   length        data
136
 
            yield (op[1], op[2], op[4]-op[3], new_lines._lines[op[3]:op[4]])
 
144
            # ofrom, oto, length, data
 
145
            yield i1, i2, j2 - j1, new_lines._lines[j1:j2]
137
146
 
138
147
    def line_delta(self, new_lines):
139
148
        return list(self.line_delta_iter(new_lines))
148
157
class _KnitFactory(object):
149
158
    """Base factory for creating content objects."""
150
159
 
151
 
    def make(self, lines, version):
 
160
    def make(self, lines, version_id):
152
161
        num_lines = len(lines)
153
 
        return KnitContent(zip([version] * num_lines, lines))
 
162
        return KnitContent(zip([version_id] * num_lines, lines))
154
163
 
155
164
 
156
165
class KnitAnnotateFactory(_KnitFactory):
158
167
 
159
168
    annotated = True
160
169
 
161
 
    def parse_fulltext(self, content, version):
 
170
    def parse_fulltext(self, content, version_id):
162
171
        """Convert fulltext to internal representation
163
172
 
164
173
        fulltext content is of the format
166
175
        internal representation is of the format:
167
176
        (revid, plaintext)
168
177
        """
169
 
        decode_utf8 = cache_utf8.decode
170
 
        lines = []
171
 
        for line in content:
172
 
            origin, text = line.split(' ', 1)
173
 
            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]
174
183
        return KnitContent(lines)
175
184
 
176
185
    def parse_line_delta_iter(self, lines):
177
 
        for result_item in self.parse_line_delta[lines]:
178
 
            yield result_item
 
186
        return iter(self.parse_line_delta(lines))
179
187
 
180
 
    def parse_line_delta(self, lines, version):
 
188
    def parse_line_delta(self, lines, version_id):
181
189
        """Convert a line based delta into internal representation.
182
190
 
183
191
        line delta is in the form of:
187
195
        internal representation is
188
196
        (start, end, count, [1..count tuples (revid, newline)])
189
197
        """
190
 
        decode_utf8 = cache_utf8.decode
191
198
        result = []
192
199
        lines = iter(lines)
193
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
 
194
207
        # walk through the lines parsing.
195
208
        for header in lines:
196
209
            start, end, count = [int(n) for n in header.split(',')]
197
 
            contents = []
198
 
            remaining = count
199
 
            while remaining:
 
210
            contents = [tuple(next().split(' ', 1)) for i in xrange(count)]
 
211
            result.append((start, end, count, contents))
 
212
        return result
 
213
 
 
214
    def get_fulltext_content(self, lines):
 
215
        """Extract just the content lines from a fulltext."""
 
216
        return (line.split(' ', 1)[1] for line in lines)
 
217
 
 
218
    def get_linedelta_content(self, lines):
 
219
        """Extract just the content from a line delta.
 
220
 
 
221
        This doesn't return all of the extra information stored in a delta.
 
222
        Only the actual content lines.
 
223
        """
 
224
        lines = iter(lines)
 
225
        next = lines.next
 
226
        for header in lines:
 
227
            header = header.split(',')
 
228
            count = int(header[2])
 
229
            for i in xrange(count):
200
230
                origin, text = next().split(' ', 1)
201
 
                remaining -= 1
202
 
                contents.append((decode_utf8(origin), text))
203
 
            result.append((start, end, count, contents))
204
 
        return result
 
231
                yield text
205
232
 
206
233
    def lower_fulltext(self, content):
207
234
        """convert a fulltext content record into a serializable form.
208
235
 
209
236
        see parse_fulltext which this inverts.
210
237
        """
211
 
        encode_utf8 = cache_utf8.encode
212
 
        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]
213
241
 
214
242
    def lower_line_delta(self, delta):
215
243
        """convert a delta into a serializable form.
216
244
 
217
245
        See parse_line_delta which this inverts.
218
246
        """
219
 
        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
220
249
        out = []
221
250
        for start, end, c, lines in delta:
222
251
            out.append('%d,%d,%d\n' % (start, end, c))
223
 
            out.extend(encode_utf8(origin) + ' ' + text
 
252
            out.extend(origin + ' ' + text
224
253
                       for origin, text in lines)
225
254
        return out
226
255
 
230
259
 
231
260
    annotated = False
232
261
 
233
 
    def parse_fulltext(self, content, version):
 
262
    def parse_fulltext(self, content, version_id):
234
263
        """This parses an unannotated fulltext.
235
264
 
236
265
        Note that this is not a noop - the internal representation
237
266
        has (versionid, line) - its just a constant versionid.
238
267
        """
239
 
        return self.make(content, version)
 
268
        return self.make(content, version_id)
240
269
 
241
 
    def parse_line_delta_iter(self, lines, version):
242
 
        while lines:
243
 
            header = lines.pop(0)
 
270
    def parse_line_delta_iter(self, lines, version_id):
 
271
        cur = 0
 
272
        num_lines = len(lines)
 
273
        while cur < num_lines:
 
274
            header = lines[cur]
 
275
            cur += 1
244
276
            start, end, c = [int(n) for n in header.split(',')]
245
 
            yield start, end, c, zip([version] * c, lines[:c])
246
 
            del lines[:c]
247
 
 
248
 
    def parse_line_delta(self, lines, version):
249
 
        return list(self.parse_line_delta_iter(lines, version))
250
 
    
 
277
            yield start, end, c, zip([version_id] * c, lines[cur:cur+c])
 
278
            cur += c
 
279
 
 
280
    def parse_line_delta(self, lines, version_id):
 
281
        return list(self.parse_line_delta_iter(lines, version_id))
 
282
 
 
283
    def get_fulltext_content(self, lines):
 
284
        """Extract just the content lines from a fulltext."""
 
285
        return iter(lines)
 
286
 
 
287
    def get_linedelta_content(self, lines):
 
288
        """Extract just the content from a line delta.
 
289
 
 
290
        This doesn't return all of the extra information stored in a delta.
 
291
        Only the actual content lines.
 
292
        """
 
293
        lines = iter(lines)
 
294
        next = lines.next
 
295
        for header in lines:
 
296
            header = header.split(',')
 
297
            count = int(header[2])
 
298
            for i in xrange(count):
 
299
                yield next()
 
300
 
251
301
    def lower_fulltext(self, content):
252
302
        return content.text()
253
303
 
307
357
        self.writable = (access_mode == 'w')
308
358
        self.delta = delta
309
359
 
 
360
        self._max_delta_chain = 200
 
361
 
310
362
        self._index = _KnitIndex(transport, relpath + INDEX_SUFFIX,
311
363
            access_mode, create=create, file_mode=file_mode,
312
364
            create_parent_dir=create_parent_dir, delay_create=delay_create,
320
372
        return '%s(%s)' % (self.__class__.__name__, 
321
373
                           self.transport.abspath(self.filename))
322
374
    
 
375
    def _check_should_delta(self, first_parents):
 
376
        """Iterate back through the parent listing, looking for a fulltext.
 
377
 
 
378
        This is used when we want to decide whether to add a delta or a new
 
379
        fulltext. It searches for _max_delta_chain parents. When it finds a
 
380
        fulltext parent, it sees if the total size of the deltas leading up to
 
381
        it is large enough to indicate that we want a new full text anyway.
 
382
 
 
383
        Return True if we should create a new delta, False if we should use a
 
384
        full text.
 
385
        """
 
386
        delta_size = 0
 
387
        fulltext_size = None
 
388
        delta_parents = first_parents
 
389
        for count in xrange(self._max_delta_chain):
 
390
            parent = delta_parents[0]
 
391
            method = self._index.get_method(parent)
 
392
            pos, size = self._index.get_position(parent)
 
393
            if method == 'fulltext':
 
394
                fulltext_size = size
 
395
                break
 
396
            delta_size += size
 
397
            delta_parents = self._index.get_parents(parent)
 
398
        else:
 
399
            # We couldn't find a fulltext, so we must create a new one
 
400
            return False
 
401
 
 
402
        return fulltext_size > delta_size
 
403
 
323
404
    def _add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
324
405
        """See VersionedFile._add_delta()."""
325
406
        self._check_add(version_id, []) # should we check the lines ?
357
438
            # To speed the extract of texts the delta chain is limited
358
439
            # to a fixed number of deltas.  This should minimize both
359
440
            # I/O and the time spend applying deltas.
360
 
            count = 0
361
 
            delta_parents = [delta_parent]
362
 
            while count < 25:
363
 
                parent = delta_parents[0]
364
 
                method = self._index.get_method(parent)
365
 
                if method == 'fulltext':
366
 
                    break
367
 
                delta_parents = self._index.get_parents(parent)
368
 
                count = count + 1
369
 
            if method == 'line-delta':
370
 
                # did not find a fulltext in the delta limit.
371
 
                # just do a normal insertion.
 
441
            # The window was changed to a maximum of 200 deltas, but also added
 
442
            # was a check that the total compressed size of the deltas is
 
443
            # smaller than the compressed size of the fulltext.
 
444
            if not self._check_should_delta([delta_parent]):
 
445
                # We don't want a delta here, just do a normal insertion.
372
446
                return super(KnitVersionedFile, self)._add_delta(version_id,
373
447
                                                                 parents,
374
448
                                                                 delta_parent,
429
503
        return KnitVersionedFile(name, transport, factory=self.factory,
430
504
                                 delta=self.delta, create=True)
431
505
    
432
 
    def _fix_parents(self, version, new_parents):
 
506
    def _fix_parents(self, version_id, new_parents):
433
507
        """Fix the parents list for version.
434
508
        
435
509
        This is done by appending a new version to the index
437
511
        the parents list must be a superset of the current
438
512
        list.
439
513
        """
440
 
        current_values = self._index._cache[version]
 
514
        current_values = self._index._cache[version_id]
441
515
        assert set(current_values[4]).difference(set(new_parents)) == set()
442
 
        self._index.add_version(version,
 
516
        self._index.add_version(version_id,
443
517
                                current_values[1], 
444
518
                                current_values[2],
445
519
                                current_values[3],
447
521
 
448
522
    def get_delta(self, version_id):
449
523
        """Get a delta for constructing version from some other version."""
 
524
        version_id = osutils.safe_revision_id(version_id)
 
525
        self.check_not_reserved_id(version_id)
450
526
        if not self.has_version(version_id):
451
527
            raise RevisionNotPresent(version_id, self.filename)
452
528
        
457
533
            parent = None
458
534
        data_pos, data_size = self._index.get_position(version_id)
459
535
        data, sha1 = self._data.read_records(((version_id, data_pos, data_size),))[version_id]
460
 
        version_idx = self._index.lookup(version_id)
461
536
        noeol = 'no-eol' in self._index.get_options(version_id)
462
537
        if 'fulltext' == self._index.get_method(version_id):
463
 
            new_content = self.factory.parse_fulltext(data, version_idx)
 
538
            new_content = self.factory.parse_fulltext(data, version_id)
464
539
            if parent is not None:
465
540
                reference_content = self._get_content(parent)
466
541
                old_texts = reference_content.text()
470
545
            delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
471
546
            return parent, sha1, noeol, self._make_line_delta(delta_seq, new_content)
472
547
        else:
473
 
            delta = self.factory.parse_line_delta(data, version_idx)
 
548
            delta = self.factory.parse_line_delta(data, version_id)
474
549
            return parent, sha1, noeol, delta
475
550
        
476
551
    def get_graph_with_ghosts(self):
480
555
 
481
556
    def get_sha1(self, version_id):
482
557
        """See VersionedFile.get_sha1()."""
 
558
        version_id = osutils.safe_revision_id(version_id)
483
559
        record_map = self._get_record_map([version_id])
484
560
        method, content, digest, next = record_map[version_id]
485
561
        return digest 
491
567
 
492
568
    def has_ghost(self, version_id):
493
569
        """True if there is a ghost reference in the file to version_id."""
 
570
        version_id = osutils.safe_revision_id(version_id)
494
571
        # maybe we have it
495
572
        if self.has_version(version_id):
496
573
            return False
509
586
 
510
587
    def has_version(self, version_id):
511
588
        """See VersionedFile.has_version."""
 
589
        version_id = osutils.safe_revision_id(version_id)
512
590
        return self._index.has_version(version_id)
513
591
 
514
592
    __contains__ = has_version
522
600
            delta_seq = None
523
601
            for parent_id in parents:
524
602
                merge_content = self._get_content(parent_id, parent_texts)
525
 
                seq = KnitSequenceMatcher(None, merge_content.text(), content.text())
 
603
                seq = patiencediff.PatienceSequenceMatcher(
 
604
                                   None, merge_content.text(), content.text())
526
605
                if delta_seq is None:
527
606
                    # setup a delta seq to reuse.
528
607
                    delta_seq = seq
539
618
                reference_content = self._get_content(parents[0], parent_texts)
540
619
                new_texts = content.text()
541
620
                old_texts = reference_content.text()
542
 
                delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
 
621
                delta_seq = patiencediff.PatienceSequenceMatcher(
 
622
                                                 None, old_texts, new_texts)
543
623
            return self._make_line_delta(delta_seq, content)
544
624
 
545
625
    def _make_line_delta(self, delta_seq, new_content):
593
673
 
594
674
    def _check_versions_present(self, version_ids):
595
675
        """Check that all specified versions are present."""
596
 
        version_ids = set(version_ids)
597
 
        for r in list(version_ids):
598
 
            if self._index.has_version(r):
599
 
                version_ids.remove(r)
600
 
        if version_ids:
601
 
            raise RevisionNotPresent(list(version_ids)[0], self.filename)
 
676
        self._index.check_versions_present(version_ids)
602
677
 
603
678
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts):
604
679
        """See VersionedFile.add_lines_with_ghosts()."""
617
692
        ### FIXME escape. RBC 20060228
618
693
        if contains_whitespace(version_id):
619
694
            raise InvalidRevisionId(version_id, self.filename)
 
695
        self.check_not_reserved_id(version_id)
620
696
        if self.has_version(version_id):
621
697
            raise RevisionAlreadyPresent(version_id, self.filename)
622
698
        self._check_lines_not_unicode(lines)
666
742
            # To speed the extract of texts the delta chain is limited
667
743
            # to a fixed number of deltas.  This should minimize both
668
744
            # I/O and the time spend applying deltas.
669
 
            count = 0
670
 
            delta_parents = present_parents
671
 
            while count < 25:
672
 
                parent = delta_parents[0]
673
 
                method = self._index.get_method(parent)
674
 
                if method == 'fulltext':
675
 
                    break
676
 
                delta_parents = self._index.get_parents(parent)
677
 
                count = count + 1
678
 
            if method == 'line-delta':
679
 
                delta = False
 
745
            delta = self._check_should_delta(present_parents)
680
746
 
 
747
        assert isinstance(version_id, str)
681
748
        lines = self.factory.make(lines, version_id)
682
749
        if delta or (self.factory.annotated and len(present_parents) > 0):
683
750
            # Merge annotations from parent texts if so is needed.
739
806
 
740
807
    def get_line_list(self, version_ids):
741
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]
 
810
        for version_id in version_ids:
 
811
            self.check_not_reserved_id(version_id)
742
812
        text_map, content_map = self._get_content_maps(version_ids)
743
813
        return [text_map[v] for v in version_ids]
744
814
 
772
842
                if component_id in content_map:
773
843
                    content = content_map[component_id]
774
844
                else:
775
 
                    version_idx = self._index.lookup(component_id)
776
845
                    if method == 'fulltext':
777
846
                        assert content is None
778
 
                        content = self.factory.parse_fulltext(data, version_idx)
 
847
                        content = self.factory.parse_fulltext(data, version_id)
779
848
                    elif method == 'line-delta':
780
 
                        delta = self.factory.parse_line_delta(data[:], 
781
 
                                                              version_idx)
 
849
                        delta = self.factory.parse_line_delta(data, version_id)
782
850
                        content = content.copy()
783
851
                        content._lines = self._apply_delta(content._lines, 
784
852
                                                           delta)
804
872
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
805
873
        if version_ids is None:
806
874
            version_ids = self.versions()
 
875
        else:
 
876
            version_ids = [osutils.safe_revision_id(v) for v in version_ids]
807
877
        if pb is None:
808
878
            pb = progress.DummyProgress()
809
879
        # we don't care about inclusions, the caller cares.
810
880
        # but we need to setup a list of records to visit.
811
881
        # we need version_id, position, length
812
882
        version_id_records = []
813
 
        requested_versions = list(version_ids)
 
883
        requested_versions = set(version_ids)
814
884
        # filter for available versions
815
885
        for version_id in requested_versions:
816
886
            if not self.has_version(version_id):
817
887
                raise RevisionNotPresent(version_id, self.filename)
818
888
        # get a in-component-order queue:
819
 
        version_ids = []
820
889
        for version_id in self.versions():
821
890
            if version_id in requested_versions:
822
 
                version_ids.append(version_id)
823
891
                data_pos, length = self._index.get_position(version_id)
824
892
                version_id_records.append((version_id, data_pos, length))
825
893
 
826
 
        count = 0
827
894
        total = len(version_id_records)
828
 
        pb.update('Walking content.', count, total)
829
 
        for version_id, data, sha_value in \
830
 
            self._data.read_records_iter(version_id_records):
831
 
            pb.update('Walking content.', count, total)
 
895
        for version_idx, (version_id, data, sha_value) in \
 
896
            enumerate(self._data.read_records_iter(version_id_records)):
 
897
            pb.update('Walking content.', version_idx, total)
832
898
            method = self._index.get_method(version_id)
833
 
            version_idx = self._index.lookup(version_id)
 
899
 
834
900
            assert method in ('fulltext', 'line-delta')
835
901
            if method == 'fulltext':
836
 
                content = self.factory.parse_fulltext(data, version_idx)
837
 
                for line in content.text():
838
 
                    yield line
 
902
                line_iterator = self.factory.get_fulltext_content(data)
839
903
            else:
840
 
                delta = self.factory.parse_line_delta(data, version_idx)
841
 
                for start, end, count, lines in delta:
842
 
                    for origin, line in lines:
843
 
                        yield line
844
 
            count +=1
 
904
                line_iterator = self.factory.get_linedelta_content(data)
 
905
            for line in line_iterator:
 
906
                yield line
 
907
 
845
908
        pb.update('Walking content.', total, total)
846
909
        
847
910
    def num_versions(self):
852
915
 
853
916
    def annotate_iter(self, version_id):
854
917
        """See VersionedFile.annotate_iter."""
 
918
        version_id = osutils.safe_revision_id(version_id)
855
919
        content = self._get_content(version_id)
856
920
        for origin, text in content.annotate_iter():
857
921
            yield origin, text
861
925
        # perf notes:
862
926
        # optimism counts!
863
927
        # 52554 calls in 1264 872 internal down from 3674
 
928
        version_id = osutils.safe_revision_id(version_id)
864
929
        try:
865
930
            return self._index.get_parents(version_id)
866
931
        except KeyError:
868
933
 
869
934
    def get_parents_with_ghosts(self, version_id):
870
935
        """See VersionedFile.get_parents."""
 
936
        version_id = osutils.safe_revision_id(version_id)
871
937
        try:
872
938
            return self._index.get_parents_with_ghosts(version_id)
873
939
        except KeyError:
874
940
            raise RevisionNotPresent(version_id, self.filename)
875
941
 
876
 
    def get_ancestry(self, versions):
 
942
    def get_ancestry(self, versions, topo_sorted=True):
877
943
        """See VersionedFile.get_ancestry."""
878
944
        if isinstance(versions, basestring):
879
945
            versions = [versions]
880
946
        if not versions:
881
947
            return []
882
 
        self._check_versions_present(versions)
883
 
        return self._index.get_ancestry(versions)
 
948
        versions = [osutils.safe_revision_id(v) for v in versions]
 
949
        return self._index.get_ancestry(versions, topo_sorted)
884
950
 
885
951
    def get_ancestry_with_ghosts(self, versions):
886
952
        """See VersionedFile.get_ancestry_with_ghosts."""
888
954
            versions = [versions]
889
955
        if not versions:
890
956
            return []
891
 
        self._check_versions_present(versions)
 
957
        versions = [osutils.safe_revision_id(v) for v in versions]
892
958
        return self._index.get_ancestry_with_ghosts(versions)
893
959
 
894
960
    #@deprecated_method(zero_eight)
901
967
        from bzrlib.weave import Weave
902
968
 
903
969
        w = Weave(self.filename)
904
 
        ancestry = self.get_ancestry(version_ids)
 
970
        ancestry = set(self.get_ancestry(version_ids, topo_sorted=False))
905
971
        sorted_graph = topo_sort(self._index.get_graph())
906
972
        version_list = [vid for vid in sorted_graph if vid in ancestry]
907
973
        
914
980
 
915
981
    def plan_merge(self, ver_a, ver_b):
916
982
        """See VersionedFile.plan_merge."""
917
 
        ancestors_b = set(self.get_ancestry(ver_b))
 
983
        ver_a = osutils.safe_revision_id(ver_a)
 
984
        ver_b = osutils.safe_revision_id(ver_b)
 
985
        ancestors_b = set(self.get_ancestry(ver_b, topo_sorted=False))
918
986
        def status_a(revision, text):
919
987
            if revision in ancestors_b:
920
988
                return 'killed-b', text
921
989
            else:
922
990
                return 'new-a', text
923
991
        
924
 
        ancestors_a = set(self.get_ancestry(ver_a))
 
992
        ancestors_a = set(self.get_ancestry(ver_a, topo_sorted=False))
925
993
        def status_b(revision, text):
926
994
            if revision in ancestors_a:
927
995
                return 'killed-a', text
965
1033
        self._create_parent_dir = create_parent_dir
966
1034
        self._need_to_create = False
967
1035
 
 
1036
    def _full_path(self):
 
1037
        """Return the full path to this file."""
 
1038
        return self._transport.base + self._filename
 
1039
 
968
1040
    def check_header(self, fp):
969
1041
        line = fp.readline()
 
1042
        if line == '':
 
1043
            # An empty file can actually be treated as though the file doesn't
 
1044
            # exist yet.
 
1045
            raise errors.NoSuchFile(self._full_path())
970
1046
        if line != self.HEADER:
971
 
            raise KnitHeaderError(badline=line)
 
1047
            raise KnitHeaderError(badline=line,
 
1048
                              filename=self._transport.abspath(self._filename))
972
1049
 
973
1050
    def commit(self):
974
1051
        """Commit is a nop."""
1019
1096
    The ' :' marker is the end of record marker.
1020
1097
    
1021
1098
    partial writes:
1022
 
    when a write is interrupted to the index file, it will result in a line that
1023
 
    does not end in ' :'. If the ' :' is not present at the end of a line, or at
1024
 
    the end of the file, then the record that is missing it will be ignored by
1025
 
    the parser.
 
1099
    when a write is interrupted to the index file, it will result in a line
 
1100
    that does not end in ' :'. If the ' :' is not present at the end of a line,
 
1101
    or at the end of the file, then the record that is missing it will be
 
1102
    ignored by the parser.
1026
1103
 
1027
1104
    When writing new records to the index file, the data is preceded by '\n'
1028
1105
    to ensure that records always start on new lines even if the last write was
1037
1114
 
1038
1115
    def _cache_version(self, version_id, options, pos, size, parents):
1039
1116
        """Cache a version record in the history array and index cache.
1040
 
        
1041
 
        This is inlined into __init__ for performance. KEEP IN SYNC.
 
1117
 
 
1118
        This is inlined into _load_data for performance. KEEP IN SYNC.
1042
1119
        (It saves 60ms, 25% of the __init__ overhead on local 4000 record
1043
1120
         indexes).
1044
1121
        """
1049
1126
            self._history.append(version_id)
1050
1127
        else:
1051
1128
            index = self._cache[version_id][5]
1052
 
        self._cache[version_id] = (version_id, 
 
1129
        self._cache[version_id] = (version_id,
1053
1130
                                   options,
1054
1131
                                   pos,
1055
1132
                                   size,
1068
1145
        # so - wc -l of a knit index is != the number of unique names
1069
1146
        # in the knit.
1070
1147
        self._history = []
1071
 
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1072
1148
        try:
1073
 
            count = 0
1074
 
            total = 1
1075
 
            try:
1076
 
                pb.update('read knit index', count, total)
1077
 
                fp = self._transport.get(self._filename)
1078
 
                try:
1079
 
                    self.check_header(fp)
1080
 
                    # readlines reads the whole file at once:
1081
 
                    # bad for transports like http, good for local disk
1082
 
                    # we save 60 ms doing this one change (
1083
 
                    # from calling readline each time to calling
1084
 
                    # readlines once.
1085
 
                    # probably what we want for nice behaviour on
1086
 
                    # http is a incremental readlines that yields, or
1087
 
                    # a check for local vs non local indexes,
1088
 
                    for l in fp.readlines():
1089
 
                        rec = l.split()
1090
 
                        if len(rec) < 5 or rec[-1] != ':':
1091
 
                            # corrupt line.
1092
 
                            # FIXME: in the future we should determine if its a
1093
 
                            # short write - and ignore it 
1094
 
                            # or a different failure, and raise. RBC 20060407
1095
 
                            continue
1096
 
                        count += 1
1097
 
                        total += 1
1098
 
                        #pb.update('read knit index', count, total)
1099
 
                        # See self._parse_parents
1100
 
                        parents = []
1101
 
                        for value in rec[4:-1]:
1102
 
                            if '.' == value[0]:
1103
 
                                # uncompressed reference
1104
 
                                parents.append(value[1:])
1105
 
                            else:
1106
 
                                # this is 15/4000ms faster than isinstance,
1107
 
                                # (in lsprof)
1108
 
                                # this function is called thousands of times a 
1109
 
                                # second so small variations add up.
1110
 
                                assert value.__class__ is str
1111
 
                                parents.append(self._history[int(value)])
1112
 
                        # end self._parse_parents
1113
 
                        # self._cache_version(rec[0], 
1114
 
                        #                     rec[1].split(','),
1115
 
                        #                     int(rec[2]),
1116
 
                        #                     int(rec[3]),
1117
 
                        #                     parents)
1118
 
                        # --- self._cache_version
1119
 
                        # only want the _history index to reference the 1st 
1120
 
                        # index entry for version_id
1121
 
                        version_id = rec[0]
1122
 
                        if version_id not in self._cache:
1123
 
                            index = len(self._history)
1124
 
                            self._history.append(version_id)
1125
 
                        else:
1126
 
                            index = self._cache[version_id][5]
1127
 
                        self._cache[version_id] = (version_id,
1128
 
                                                   rec[1].split(','),
1129
 
                                                   int(rec[2]),
1130
 
                                                   int(rec[3]),
1131
 
                                                   parents,
1132
 
                                                   index)
1133
 
                        # --- self._cache_version 
1134
 
                finally:
1135
 
                    fp.close()
1136
 
            except NoSuchFile, e:
1137
 
                if mode != 'w' or not create:
1138
 
                    raise
1139
 
                if delay_create:
1140
 
                    self._need_to_create = True
1141
 
                else:
1142
 
                    self._transport.put_bytes_non_atomic(self._filename,
1143
 
                        self.HEADER, mode=self._file_mode)
1144
 
 
1145
 
        finally:
1146
 
            pb.update('read knit index', total, total)
1147
 
            pb.finished()
1148
 
 
1149
 
    def _parse_parents(self, compressed_parents):
1150
 
        """convert a list of string parent values into version ids.
1151
 
 
1152
 
        ints are looked up in the index.
1153
 
        .FOO values are ghosts and converted in to FOO.
1154
 
 
1155
 
        NOTE: the function is retained here for clarity, and for possible
1156
 
              use in partial index reads. However bulk processing now has
1157
 
              it inlined in __init__ for inner-loop optimisation.
1158
 
        """
1159
 
        result = []
1160
 
        for value in compressed_parents:
1161
 
            if value[-1] == '.':
1162
 
                # uncompressed reference
1163
 
                result.append(value[1:])
1164
 
            else:
1165
 
                # this is 15/4000ms faster than isinstance,
1166
 
                # this function is called thousands of times a 
1167
 
                # second so small variations add up.
1168
 
                assert value.__class__ is str
1169
 
                result.append(self._history[int(value)])
1170
 
        return result
 
1149
            fp = self._transport.get(self._filename)
 
1150
            try:
 
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)
 
1164
 
 
1165
    def _load_data(self, fp):
 
1166
        cache = self._cache
 
1167
        history = self._history
 
1168
 
 
1169
        self.check_header(fp)
 
1170
        # readlines reads the whole file at once:
 
1171
        # bad for transports like http, good for local disk
 
1172
        # we save 60 ms doing this one change (
 
1173
        # from calling readline each time to calling
 
1174
        # readlines once.
 
1175
        # probably what we want for nice behaviour on
 
1176
        # http is a incremental readlines that yields, or
 
1177
        # a check for local vs non local indexes,
 
1178
        history_top = len(history) - 1
 
1179
        for line in fp.readlines():
 
1180
            rec = line.split()
 
1181
            if len(rec) < 5 or rec[-1] != ':':
 
1182
                # corrupt line.
 
1183
                # FIXME: in the future we should determine if its a
 
1184
                # short write - and ignore it 
 
1185
                # or a different failure, and raise. RBC 20060407
 
1186
                continue
 
1187
 
 
1188
            try:
 
1189
                parents = []
 
1190
                for value in rec[4:-1]:
 
1191
                    if value[0] == '.':
 
1192
                        # uncompressed reference
 
1193
                        parent_id = value[1:]
 
1194
                    else:
 
1195
                        parent_id = history[int(value)]
 
1196
                    parents.append(parent_id)
 
1197
            except (IndexError, ValueError), e:
 
1198
                # The parent could not be decoded to get its parent row. This
 
1199
                # at a minimum will cause this row to have wrong parents, or
 
1200
                # even to apply a delta to the wrong base and decode
 
1201
                # incorrectly. its therefore not usable, and because we have
 
1202
                # encountered a situation where a new knit index had this
 
1203
                # corrupt we can't asssume that no other rows referring to the
 
1204
                # index of this record actually mean the subsequent uncorrupt
 
1205
                # one, so we error.
 
1206
                raise errors.KnitCorrupt(self._filename,
 
1207
                    "line %r: %s" % (rec, e))
 
1208
 
 
1209
            version_id, options, pos, size = rec[:4]
 
1210
            version_id = version_id
 
1211
 
 
1212
            # See self._cache_version
 
1213
            # only want the _history index to reference the 1st 
 
1214
            # index entry for version_id
 
1215
            if version_id not in cache:
 
1216
                history_top += 1
 
1217
                index = history_top
 
1218
                history.append(version_id)
 
1219
            else:
 
1220
                index = cache[version_id][5]
 
1221
            cache[version_id] = (version_id,
 
1222
                                 options.split(','),
 
1223
                                 int(pos),
 
1224
                                 int(size),
 
1225
                                 parents,
 
1226
                                 index)
 
1227
            # end self._cache_version 
1171
1228
 
1172
1229
    def get_graph(self):
1173
 
        graph = []
1174
 
        for version_id, index in self._cache.iteritems():
1175
 
            graph.append((version_id, index[4]))
1176
 
        return graph
 
1230
        return [(vid, idx[4]) for vid, idx in self._cache.iteritems()]
1177
1231
 
1178
 
    def get_ancestry(self, versions):
 
1232
    def get_ancestry(self, versions, topo_sorted=True):
1179
1233
        """See VersionedFile.get_ancestry."""
1180
1234
        # get a graph of all the mentioned versions:
1181
1235
        graph = {}
1182
1236
        pending = set(versions)
1183
 
        while len(pending):
 
1237
        cache = self._cache
 
1238
        while pending:
1184
1239
            version = pending.pop()
1185
 
            parents = self._cache[version][4]
1186
 
            # got the parents ok
1187
1240
            # trim ghosts
1188
 
            parents = [parent for parent in parents if parent in self._cache]
1189
 
            for parent in parents:
1190
 
                # if not completed and not a ghost
1191
 
                if parent not in graph:
1192
 
                    pending.add(parent)
 
1241
            try:
 
1242
                parents = [p for p in cache[version][4] if p in cache]
 
1243
            except KeyError:
 
1244
                raise RevisionNotPresent(version, self._filename)
 
1245
            # if not completed and not a ghost
 
1246
            pending.update([p for p in parents if p not in graph])
1193
1247
            graph[version] = parents
 
1248
        if not topo_sorted:
 
1249
            return graph.keys()
1194
1250
        return topo_sort(graph.items())
1195
1251
 
1196
1252
    def get_ancestry_with_ghosts(self, versions):
1197
1253
        """See VersionedFile.get_ancestry_with_ghosts."""
1198
1254
        # get a graph of all the mentioned versions:
 
1255
        self.check_versions_present(versions)
 
1256
        cache = self._cache
1199
1257
        graph = {}
1200
1258
        pending = set(versions)
1201
 
        while len(pending):
 
1259
        while pending:
1202
1260
            version = pending.pop()
1203
1261
            try:
1204
 
                parents = self._cache[version][4]
 
1262
                parents = cache[version][4]
1205
1263
            except KeyError:
1206
1264
                # ghost, fake it
1207
1265
                graph[version] = []
1208
 
                pass
1209
1266
            else:
1210
 
                # got the parents ok
1211
 
                for parent in parents:
1212
 
                    if parent not in graph:
1213
 
                        pending.add(parent)
 
1267
                # if not completed
 
1268
                pending.update([p for p in parents if p not in graph])
1214
1269
                graph[version] = parents
1215
1270
        return topo_sort(graph.items())
1216
1271
 
1230
1285
        return self._cache[version_id][5]
1231
1286
 
1232
1287
    def _version_list_to_index(self, versions):
1233
 
        encode_utf8 = cache_utf8.encode
1234
1288
        result_list = []
 
1289
        cache = self._cache
1235
1290
        for version in versions:
1236
 
            if version in self._cache:
 
1291
            if version in cache:
1237
1292
                # -- inlined lookup() --
1238
 
                result_list.append(str(self._cache[version][5]))
 
1293
                result_list.append(str(cache[version][5]))
1239
1294
                # -- end lookup () --
1240
1295
            else:
1241
 
                result_list.append('.' + encode_utf8(version))
 
1296
                result_list.append('.' + version)
1242
1297
        return ' '.join(result_list)
1243
1298
 
1244
1299
    def add_version(self, version_id, options, pos, size, parents):
1252
1307
                         (version_id, options, pos, size, parents).
1253
1308
        """
1254
1309
        lines = []
1255
 
        encode_utf8 = cache_utf8.encode
1256
 
        for version_id, options, pos, size, parents in versions:
1257
 
            line = "\n%s %s %s %s %s :" % (encode_utf8(version_id),
1258
 
                                           ','.join(options),
1259
 
                                           pos,
1260
 
                                           size,
1261
 
                                           self._version_list_to_index(parents))
1262
 
            assert isinstance(line, str), \
1263
 
                'content must be utf-8 encoded: %r' % (line,)
1264
 
            lines.append(line)
1265
 
        if not self._need_to_create:
1266
 
            self._transport.append_bytes(self._filename, ''.join(lines))
1267
 
        else:
1268
 
            sio = StringIO()
1269
 
            sio.write(self.HEADER)
1270
 
            sio.writelines(lines)
1271
 
            sio.seek(0)
1272
 
            self._transport.put_file_non_atomic(self._filename, sio,
1273
 
                                create_parent_dir=self._create_parent_dir,
1274
 
                                mode=self._file_mode,
1275
 
                                dir_mode=self._dir_mode)
1276
 
            self._need_to_create = False
1277
 
 
1278
 
        # cache after writing, so that a failed write leads to missing cache
1279
 
        # entries not extra ones. XXX TODO: RBC 20060502 in the event of a 
1280
 
        # failure, reload the index or flush it or some such, to prevent
1281
 
        # writing records that did complete twice.
1282
 
        for version_id, options, pos, size, parents in versions:
1283
 
            self._cache_version(version_id, options, pos, size, parents)
1284
 
        
 
1310
        orig_history = self._history[:]
 
1311
        orig_cache = self._cache.copy()
 
1312
 
 
1313
        try:
 
1314
            for version_id, options, pos, size, parents in versions:
 
1315
                line = "\n%s %s %s %s %s :" % (version_id,
 
1316
                                               ','.join(options),
 
1317
                                               pos,
 
1318
                                               size,
 
1319
                                               self._version_list_to_index(parents))
 
1320
                assert isinstance(line, str), \
 
1321
                    'content must be utf-8 encoded: %r' % (line,)
 
1322
                lines.append(line)
 
1323
                self._cache_version(version_id, options, pos, size, parents)
 
1324
            if not self._need_to_create:
 
1325
                self._transport.append_bytes(self._filename, ''.join(lines))
 
1326
            else:
 
1327
                sio = StringIO()
 
1328
                sio.write(self.HEADER)
 
1329
                sio.writelines(lines)
 
1330
                sio.seek(0)
 
1331
                self._transport.put_file_non_atomic(self._filename, sio,
 
1332
                                    create_parent_dir=self._create_parent_dir,
 
1333
                                    mode=self._file_mode,
 
1334
                                    dir_mode=self._dir_mode)
 
1335
                self._need_to_create = False
 
1336
        except:
 
1337
            # If any problems happen, restore the original values and re-raise
 
1338
            self._history = orig_history
 
1339
            self._cache = orig_cache
 
1340
            raise
 
1341
 
1285
1342
    def has_version(self, version_id):
1286
1343
        """True if the version is in the index."""
1287
 
        return (version_id in self._cache)
 
1344
        return version_id in self._cache
1288
1345
 
1289
1346
    def get_position(self, version_id):
1290
1347
        """Return data position and size of specified version."""
1291
 
        return (self._cache[version_id][2], \
1292
 
                self._cache[version_id][3])
 
1348
        entry = self._cache[version_id]
 
1349
        return entry[2], entry[3]
1293
1350
 
1294
1351
    def get_method(self, version_id):
1295
1352
        """Return compression method of specified version."""
1297
1354
        if 'fulltext' in options:
1298
1355
            return 'fulltext'
1299
1356
        else:
1300
 
            assert 'line-delta' in options
 
1357
            if 'line-delta' not in options:
 
1358
                raise errors.KnitIndexUnknownMethod(self._full_path(), options)
1301
1359
            return 'line-delta'
1302
1360
 
1303
1361
    def get_options(self, version_id):
1314
1372
 
1315
1373
    def check_versions_present(self, version_ids):
1316
1374
        """Check that all specified versions are present."""
1317
 
        version_ids = set(version_ids)
1318
 
        for version_id in list(version_ids):
1319
 
            if version_id in self._cache:
1320
 
                version_ids.remove(version_id)
1321
 
        if version_ids:
1322
 
            raise RevisionNotPresent(list(version_ids)[0], self.filename)
 
1375
        cache = self._cache
 
1376
        for version_id in version_ids:
 
1377
            if version_id not in cache:
 
1378
                raise RevisionNotPresent(version_id, self._filename)
1323
1379
 
1324
1380
 
1325
1381
class _KnitData(_KnitComponentFile):
1370
1426
        sio = StringIO()
1371
1427
        data_file = GzipFile(None, mode='wb', fileobj=sio)
1372
1428
 
1373
 
        version_id_utf8 = cache_utf8.encode(version_id)
 
1429
        assert isinstance(version_id, str)
1374
1430
        data_file.writelines(chain(
1375
 
            ["version %s %d %s\n" % (version_id_utf8,
 
1431
            ["version %s %d %s\n" % (version_id,
1376
1432
                                     len(lines),
1377
1433
                                     digest)],
1378
1434
            lines,
1379
 
            ["end %s\n" % version_id_utf8]))
 
1435
            ["end %s\n" % version_id]))
1380
1436
        data_file.close()
1381
1437
        length= sio.tell()
1382
1438
 
1424
1480
                 as (stream, header_record)
1425
1481
        """
1426
1482
        df = GzipFile(mode='rb', fileobj=StringIO(raw_data))
1427
 
        rec = df.readline().split()
 
1483
        try:
 
1484
            rec = self._check_header(version_id, df.readline())
 
1485
        except Exception, e:
 
1486
            raise KnitCorrupt(self._filename,
 
1487
                              "While reading {%s} got %s(%s)"
 
1488
                              % (version_id, e.__class__.__name__, str(e)))
 
1489
        return df, rec
 
1490
 
 
1491
    def _check_header(self, version_id, line):
 
1492
        rec = line.split()
1428
1493
        if len(rec) != 4:
1429
 
            raise KnitCorrupt(self._filename, 'unexpected number of elements in record header')
1430
 
        if cache_utf8.decode(rec[1]) != version_id:
1431
 
            raise KnitCorrupt(self._filename, 
1432
 
                              'unexpected version, wanted %r, got %r' % (
1433
 
                                version_id, rec[1]))
1434
 
        return df, rec
 
1494
            raise KnitCorrupt(self._filename,
 
1495
                              'unexpected number of elements in record header')
 
1496
        if rec[1] != version_id:
 
1497
            raise KnitCorrupt(self._filename,
 
1498
                              'unexpected version, wanted %r, got %r'
 
1499
                              % (version_id, rec[1]))
 
1500
        return rec
1435
1501
 
1436
1502
    def _parse_record(self, version_id, data):
1437
1503
        # profiling notes:
1438
1504
        # 4168 calls in 2880 217 internal
1439
1505
        # 4168 calls to _parse_record_header in 2121
1440
1506
        # 4168 calls to readlines in 330
1441
 
        df, rec = self._parse_record_header(version_id, data)
1442
 
        record_contents = df.readlines()
1443
 
        l = record_contents.pop()
1444
 
        assert len(record_contents) == int(rec[2])
1445
 
        if l != 'end %s\n' % cache_utf8.encode(version_id):
1446
 
            raise KnitCorrupt(self._filename, 'unexpected version end line %r, wanted %r' 
1447
 
                        % (l, version_id))
 
1507
        df = GzipFile(mode='rb', fileobj=StringIO(data))
 
1508
 
 
1509
        try:
 
1510
            record_contents = df.readlines()
 
1511
        except Exception, e:
 
1512
            raise KnitCorrupt(self._filename,
 
1513
                              "While reading {%s} got %s(%s)"
 
1514
                              % (version_id, e.__class__.__name__, str(e)))
 
1515
        header = record_contents.pop(0)
 
1516
        rec = self._check_header(version_id, header)
 
1517
 
 
1518
        last_line = record_contents.pop()
 
1519
        if len(record_contents) != int(rec[2]):
 
1520
            raise KnitCorrupt(self._filename,
 
1521
                              'incorrect number of lines %s != %s'
 
1522
                              ' for version {%s}'
 
1523
                              % (len(record_contents), int(rec[2]),
 
1524
                                 version_id))
 
1525
        if last_line != 'end %s\n' % rec[1]:
 
1526
            raise KnitCorrupt(self._filename,
 
1527
                              'unexpected version end line %r, wanted %r' 
 
1528
                              % (last_line, version_id))
1448
1529
        df.close()
1449
1530
        return record_contents, rec[3]
1450
1531
 
1468
1549
                                               in records]
1469
1550
 
1470
1551
            raw_records = self._transport.readv(self._filename, needed_offsets)
1471
 
                
1472
1552
 
1473
1553
        for version_id, pos, size in records:
1474
1554
            if version_id in self._cache:
1564
1644
        if not version_ids:
1565
1645
            return 0
1566
1646
 
1567
 
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1647
        pb = ui.ui_factory.nested_progress_bar()
1568
1648
        try:
1569
1649
            version_ids = list(version_ids)
1570
1650
            if None in version_ids:
1681
1761
        if not version_ids:
1682
1762
            return 0
1683
1763
 
1684
 
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1764
        pb = ui.ui_factory.nested_progress_bar()
1685
1765
        try:
1686
1766
            version_ids = list(version_ids)
1687
1767
    
1881
1961
            bestsize = bestsize + 1
1882
1962
 
1883
1963
        return besti, bestj, bestsize
1884