~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/knit.py

  • Committer: John Arbash Meinel
  • Date: 2006-08-24 19:17:28 UTC
  • mto: (1946.2.8 reduce-knit-churn)
  • mto: This revision was merged to the branch mainline in revision 1988.
  • Revision ID: john@arbash-meinel.com-20060824191728-2164fd6bb3cc681f
Implement and test 'get_bytes'

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
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>
2
6
#
3
7
# This program is free software; you can redistribute it and/or modify
4
8
# it under the terms of the GNU General Public License as published by
73
77
from bzrlib import (
74
78
    cache_utf8,
75
79
    errors,
76
 
    osutils,
77
 
    patiencediff,
78
 
    progress,
79
 
    merge,
80
 
    ui,
81
 
    )
82
 
from bzrlib.errors import (
83
 
    FileExists,
84
 
    NoSuchFile,
85
 
    KnitError,
86
 
    InvalidRevisionId,
87
 
    KnitCorrupt,
88
 
    KnitHeaderError,
89
 
    RevisionNotPresent,
90
 
    RevisionAlreadyPresent,
91
 
    )
 
80
    )
 
81
from bzrlib.errors import FileExists, NoSuchFile, KnitError, \
 
82
        InvalidRevisionId, KnitCorrupt, KnitHeaderError, \
 
83
        RevisionNotPresent, RevisionAlreadyPresent
92
84
from bzrlib.tuned_gzip import GzipFile
93
85
from bzrlib.trace import mutter
94
 
from bzrlib.osutils import (
95
 
    contains_whitespace,
96
 
    contains_linebreaks,
97
 
    sha_strings,
98
 
    )
 
86
from bzrlib.osutils import contains_whitespace, contains_linebreaks, \
 
87
     sha_strings
99
88
from bzrlib.symbol_versioning import DEPRECATED_PARAMETER, deprecated_passed
100
89
from bzrlib.tsort import topo_sort
101
 
import bzrlib.ui
102
90
import bzrlib.weave
103
91
from bzrlib.versionedfile import VersionedFile, InterVersionedFile
104
92
 
128
116
 
129
117
    def annotate_iter(self):
130
118
        """Yield tuples of (origin, text) for each content line."""
131
 
        return iter(self._lines)
 
119
        for origin, text in self._lines:
 
120
            yield origin, text
132
121
 
133
122
    def annotate(self):
134
123
        """Return a list of (origin, text) tuples."""
136
125
 
137
126
    def line_delta_iter(self, new_lines):
138
127
        """Generate line-based delta from this content to new_lines."""
139
 
        new_texts = new_lines.text()
140
 
        old_texts = self.text()
 
128
        new_texts = [text for origin, text in new_lines._lines]
 
129
        old_texts = [text for origin, text in self._lines]
141
130
        s = KnitSequenceMatcher(None, old_texts, new_texts)
142
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
143
 
            if tag == 'equal':
 
131
        for op in s.get_opcodes():
 
132
            if op[0] == 'equal':
144
133
                continue
145
 
            # ofrom, oto, length, data
146
 
            yield i1, i2, j2 - j1, new_lines._lines[j1:j2]
 
134
            #     ofrom   oto   length        data
 
135
            yield (op[1], op[2], op[4]-op[3], new_lines._lines[op[3]:op[4]])
147
136
 
148
137
    def line_delta(self, new_lines):
149
138
        return list(self.line_delta_iter(new_lines))
154
143
    def copy(self):
155
144
        return KnitContent(self._lines[:])
156
145
 
157
 
    @staticmethod
158
 
    def get_line_delta_blocks(knit_delta, source, target):
159
 
        """Extract SequenceMatcher.get_matching_blocks() from a knit delta"""
160
 
        target_len = len(target)
161
 
        s_pos = 0
162
 
        t_pos = 0
163
 
        for s_begin, s_end, t_len, new_text in knit_delta:
164
 
            true_n = s_begin - s_pos
165
 
            n = true_n
166
 
            if n > 0:
167
 
                # knit deltas do not provide reliable info about whether the
168
 
                # last line of a file matches, due to eol handling.
169
 
                if source[s_pos + n -1] != target[t_pos + n -1]:
170
 
                    n-=1
171
 
                if n > 0:
172
 
                    yield s_pos, t_pos, n
173
 
            t_pos += t_len + true_n
174
 
            s_pos = s_end
175
 
        n = target_len - t_pos
176
 
        if n > 0:
177
 
            if source[s_pos + n -1] != target[t_pos + n -1]:
178
 
                n-=1
179
 
            if n > 0:
180
 
                yield s_pos, t_pos, n
181
 
        yield s_pos + (target_len - t_pos), target_len, 0
182
 
 
183
146
 
184
147
class _KnitFactory(object):
185
148
    """Base factory for creating content objects."""
186
149
 
187
 
    def make(self, lines, version_id):
 
150
    def make(self, lines, version):
188
151
        num_lines = len(lines)
189
 
        return KnitContent(zip([version_id] * num_lines, lines))
 
152
        return KnitContent(zip([version] * num_lines, lines))
190
153
 
191
154
 
192
155
class KnitAnnotateFactory(_KnitFactory):
194
157
 
195
158
    annotated = True
196
159
 
197
 
    def parse_fulltext(self, content, version_id):
 
160
    def parse_fulltext(self, content, version):
198
161
        """Convert fulltext to internal representation
199
162
 
200
163
        fulltext content is of the format
202
165
        internal representation is of the format:
203
166
        (revid, plaintext)
204
167
        """
205
 
        # TODO: jam 20070209 The tests expect this to be returned as tuples,
206
 
        #       but the code itself doesn't really depend on that.
207
 
        #       Figure out a way to not require the overhead of turning the
208
 
        #       list back into tuples.
209
 
        lines = [tuple(line.split(' ', 1)) for line in content]
 
168
        decode_utf8 = cache_utf8.decode
 
169
        lines = []
 
170
        for line in content:
 
171
            origin, text = line.split(' ', 1)
 
172
            lines.append((decode_utf8(origin), text))
210
173
        return KnitContent(lines)
211
174
 
212
175
    def parse_line_delta_iter(self, lines):
213
 
        return iter(self.parse_line_delta(lines))
 
176
        for result_item in self.parse_line_delta[lines]:
 
177
            yield result_item
214
178
 
215
 
    def parse_line_delta(self, lines, version_id):
 
179
    def parse_line_delta(self, lines, version):
216
180
        """Convert a line based delta into internal representation.
217
181
 
218
182
        line delta is in the form of:
222
186
        internal representation is
223
187
        (start, end, count, [1..count tuples (revid, newline)])
224
188
        """
 
189
        decode_utf8 = cache_utf8.decode
225
190
        result = []
226
191
        lines = iter(lines)
227
192
        next = lines.next
228
 
 
229
 
        cache = {}
230
 
        def cache_and_return(line):
231
 
            origin, text = line.split(' ', 1)
232
 
            return cache.setdefault(origin, origin), text
233
 
 
234
193
        # walk through the lines parsing.
235
194
        for header in lines:
236
195
            start, end, count = [int(n) for n in header.split(',')]
237
 
            contents = [tuple(next().split(' ', 1)) for i in xrange(count)]
 
196
            contents = []
 
197
            remaining = count
 
198
            while remaining:
 
199
                origin, text = next().split(' ', 1)
 
200
                remaining -= 1
 
201
                contents.append((decode_utf8(origin), text))
238
202
            result.append((start, end, count, contents))
239
203
        return result
240
204
 
241
 
    def get_fulltext_content(self, lines):
242
 
        """Extract just the content lines from a fulltext."""
243
 
        return (line.split(' ', 1)[1] for line in lines)
244
 
 
245
 
    def get_linedelta_content(self, lines):
246
 
        """Extract just the content from a line delta.
247
 
 
248
 
        This doesn't return all of the extra information stored in a delta.
249
 
        Only the actual content lines.
250
 
        """
251
 
        lines = iter(lines)
252
 
        next = lines.next
253
 
        for header in lines:
254
 
            header = header.split(',')
255
 
            count = int(header[2])
256
 
            for i in xrange(count):
257
 
                origin, text = next().split(' ', 1)
258
 
                yield text
259
 
 
260
205
    def lower_fulltext(self, content):
261
206
        """convert a fulltext content record into a serializable form.
262
207
 
263
208
        see parse_fulltext which this inverts.
264
209
        """
265
 
        # TODO: jam 20070209 We only do the caching thing to make sure that
266
 
        #       the origin is a valid utf-8 line, eventually we could remove it
267
 
        return ['%s %s' % (o, t) for o, t in content._lines]
 
210
        encode_utf8 = cache_utf8.encode
 
211
        return ['%s %s' % (encode_utf8(o), t) for o, t in content._lines]
268
212
 
269
213
    def lower_line_delta(self, delta):
270
214
        """convert a delta into a serializable form.
271
215
 
272
216
        See parse_line_delta which this inverts.
273
217
        """
274
 
        # TODO: jam 20070209 We only do the caching thing to make sure that
275
 
        #       the origin is a valid utf-8 line, eventually we could remove it
 
218
        encode_utf8 = cache_utf8.encode
276
219
        out = []
277
220
        for start, end, c, lines in delta:
278
221
            out.append('%d,%d,%d\n' % (start, end, c))
279
 
            out.extend(origin + ' ' + text
 
222
            out.extend(encode_utf8(origin) + ' ' + text
280
223
                       for origin, text in lines)
281
224
        return out
282
225
 
286
229
 
287
230
    annotated = False
288
231
 
289
 
    def parse_fulltext(self, content, version_id):
 
232
    def parse_fulltext(self, content, version):
290
233
        """This parses an unannotated fulltext.
291
234
 
292
235
        Note that this is not a noop - the internal representation
293
236
        has (versionid, line) - its just a constant versionid.
294
237
        """
295
 
        return self.make(content, version_id)
 
238
        return self.make(content, version)
296
239
 
297
 
    def parse_line_delta_iter(self, lines, version_id):
298
 
        cur = 0
299
 
        num_lines = len(lines)
300
 
        while cur < num_lines:
301
 
            header = lines[cur]
302
 
            cur += 1
 
240
    def parse_line_delta_iter(self, lines, version):
 
241
        while lines:
 
242
            header = lines.pop(0)
303
243
            start, end, c = [int(n) for n in header.split(',')]
304
 
            yield start, end, c, zip([version_id] * c, lines[cur:cur+c])
305
 
            cur += c
306
 
 
307
 
    def parse_line_delta(self, lines, version_id):
308
 
        return list(self.parse_line_delta_iter(lines, version_id))
309
 
 
310
 
    def get_fulltext_content(self, lines):
311
 
        """Extract just the content lines from a fulltext."""
312
 
        return iter(lines)
313
 
 
314
 
    def get_linedelta_content(self, lines):
315
 
        """Extract just the content from a line delta.
316
 
 
317
 
        This doesn't return all of the extra information stored in a delta.
318
 
        Only the actual content lines.
319
 
        """
320
 
        lines = iter(lines)
321
 
        next = lines.next
322
 
        for header in lines:
323
 
            header = header.split(',')
324
 
            count = int(header[2])
325
 
            for i in xrange(count):
326
 
                yield next()
327
 
 
 
244
            yield start, end, c, zip([version] * c, lines[:c])
 
245
            del lines[:c]
 
246
 
 
247
    def parse_line_delta(self, lines, version):
 
248
        return list(self.parse_line_delta_iter(lines, version))
 
249
    
328
250
    def lower_fulltext(self, content):
329
251
        return content.text()
330
252
 
357
279
    stored and retrieved.
358
280
    """
359
281
 
360
 
    def __init__(self, relpath, transport, file_mode=None, access_mode=None,
 
282
    def __init__(self, relpath, transport, file_mode=None, access_mode=None, 
361
283
                 factory=None, basis_knit=DEPRECATED_PARAMETER, delta=True,
362
 
                 create=False, create_parent_dir=False, delay_create=False,
363
 
                 dir_mode=None, index=None):
 
284
                 create=False):
364
285
        """Construct a knit at location specified by relpath.
365
286
        
366
287
        :param create: If not True, only open an existing knit.
367
 
        :param create_parent_dir: If True, create the parent directory if 
368
 
            creating the file fails. (This is used for stores with 
369
 
            hash-prefixes that may not exist yet)
370
 
        :param delay_create: The calling code is aware that the knit won't 
371
 
            actually be created until the first data is stored.
372
 
        :param index: An index to use for the knit.
373
288
        """
374
289
        if deprecated_passed(basis_knit):
375
290
            warnings.warn("KnitVersionedFile.__(): The basis_knit parameter is"
385
300
        self.writable = (access_mode == 'w')
386
301
        self.delta = delta
387
302
 
388
 
        self._max_delta_chain = 200
389
 
 
390
 
        if index is None:
391
 
            self._index = _KnitIndex(transport, relpath + INDEX_SUFFIX,
392
 
                access_mode, create=create, file_mode=file_mode,
393
 
                create_parent_dir=create_parent_dir, delay_create=delay_create,
394
 
                dir_mode=dir_mode)
395
 
        else:
396
 
            self._index = index
 
303
        self._index = _KnitIndex(transport, relpath + INDEX_SUFFIX,
 
304
            access_mode, create=create, file_mode=file_mode)
397
305
        self._data = _KnitData(transport, relpath + DATA_SUFFIX,
398
 
            access_mode, create=create and not len(self), file_mode=file_mode,
399
 
            create_parent_dir=create_parent_dir, delay_create=delay_create,
400
 
            dir_mode=dir_mode)
 
306
            access_mode, create=create and not len(self), file_mode=file_mode)
401
307
 
402
308
    def __repr__(self):
403
309
        return '%s(%s)' % (self.__class__.__name__, 
404
310
                           self.transport.abspath(self.filename))
405
311
    
406
 
    def _check_should_delta(self, first_parents):
407
 
        """Iterate back through the parent listing, looking for a fulltext.
408
 
 
409
 
        This is used when we want to decide whether to add a delta or a new
410
 
        fulltext. It searches for _max_delta_chain parents. When it finds a
411
 
        fulltext parent, it sees if the total size of the deltas leading up to
412
 
        it is large enough to indicate that we want a new full text anyway.
413
 
 
414
 
        Return True if we should create a new delta, False if we should use a
415
 
        full text.
416
 
        """
417
 
        delta_size = 0
418
 
        fulltext_size = None
419
 
        delta_parents = first_parents
420
 
        for count in xrange(self._max_delta_chain):
421
 
            parent = delta_parents[0]
422
 
            method = self._index.get_method(parent)
423
 
            pos, size = self._index.get_position(parent)
424
 
            if method == 'fulltext':
425
 
                fulltext_size = size
426
 
                break
427
 
            delta_size += size
428
 
            delta_parents = self._index.get_parents(parent)
429
 
        else:
430
 
            # We couldn't find a fulltext, so we must create a new one
431
 
            return False
432
 
 
433
 
        return fulltext_size > delta_size
434
 
 
435
312
    def _add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
436
313
        """See VersionedFile._add_delta()."""
437
314
        self._check_add(version_id, []) # should we check the lines ?
469
346
            # To speed the extract of texts the delta chain is limited
470
347
            # to a fixed number of deltas.  This should minimize both
471
348
            # I/O and the time spend applying deltas.
472
 
            # The window was changed to a maximum of 200 deltas, but also added
473
 
            # was a check that the total compressed size of the deltas is
474
 
            # smaller than the compressed size of the fulltext.
475
 
            if not self._check_should_delta([delta_parent]):
476
 
                # We don't want a delta here, just do a normal insertion.
 
349
            count = 0
 
350
            delta_parents = [delta_parent]
 
351
            while count < 25:
 
352
                parent = delta_parents[0]
 
353
                method = self._index.get_method(parent)
 
354
                if method == 'fulltext':
 
355
                    break
 
356
                delta_parents = self._index.get_parents(parent)
 
357
                count = count + 1
 
358
            if method == 'line-delta':
 
359
                # did not find a fulltext in the delta limit.
 
360
                # just do a normal insertion.
477
361
                return super(KnitVersionedFile, self)._add_delta(version_id,
478
362
                                                                 parents,
479
363
                                                                 delta_parent,
519
403
        """See VersionedFile.copy_to()."""
520
404
        # copy the current index to a temp index to avoid racing with local
521
405
        # writes
522
 
        transport.put_file_non_atomic(name + INDEX_SUFFIX + '.tmp',
523
 
                self.transport.get(self._index._filename))
 
406
        transport.put(name + INDEX_SUFFIX + '.tmp', self.transport.get(self._index._filename),)
524
407
        # copy the data file
525
408
        f = self._data._open_file()
526
409
        try:
527
 
            transport.put_file(name + DATA_SUFFIX, f)
 
410
            transport.put(name + DATA_SUFFIX, f)
528
411
        finally:
529
412
            f.close()
530
413
        # move the copied index into place
531
414
        transport.move(name + INDEX_SUFFIX + '.tmp', name + INDEX_SUFFIX)
532
415
 
533
416
    def create_empty(self, name, transport, mode=None):
534
 
        return KnitVersionedFile(name, transport, factory=self.factory,
535
 
                                 delta=self.delta, create=True)
 
417
        return KnitVersionedFile(name, transport, factory=self.factory, delta=self.delta, create=True)
536
418
    
537
 
    def _fix_parents(self, version_id, new_parents):
 
419
    def _fix_parents(self, version, new_parents):
538
420
        """Fix the parents list for version.
539
421
        
540
422
        This is done by appending a new version to the index
542
424
        the parents list must be a superset of the current
543
425
        list.
544
426
        """
545
 
        current_values = self._index._cache[version_id]
 
427
        current_values = self._index._cache[version]
546
428
        assert set(current_values[4]).difference(set(new_parents)) == set()
547
 
        self._index.add_version(version_id,
 
429
        self._index.add_version(version,
548
430
                                current_values[1], 
549
431
                                current_values[2],
550
432
                                current_values[3],
551
433
                                new_parents)
552
434
 
553
 
    def _extract_blocks(self, version_id, source, target):
554
 
        if self._index.get_method(version_id) != 'line-delta':
555
 
            return None
556
 
        parent, sha1, noeol, delta = self.get_delta(version_id)
557
 
        return KnitContent.get_line_delta_blocks(delta, source, target)
558
 
 
559
435
    def get_delta(self, version_id):
560
436
        """Get a delta for constructing version from some other version."""
561
 
        version_id = osutils.safe_revision_id(version_id)
562
 
        self.check_not_reserved_id(version_id)
563
437
        if not self.has_version(version_id):
564
438
            raise RevisionNotPresent(version_id, self.filename)
565
439
        
570
444
            parent = None
571
445
        data_pos, data_size = self._index.get_position(version_id)
572
446
        data, sha1 = self._data.read_records(((version_id, data_pos, data_size),))[version_id]
 
447
        version_idx = self._index.lookup(version_id)
573
448
        noeol = 'no-eol' in self._index.get_options(version_id)
574
449
        if 'fulltext' == self._index.get_method(version_id):
575
 
            new_content = self.factory.parse_fulltext(data, version_id)
 
450
            new_content = self.factory.parse_fulltext(data, version_idx)
576
451
            if parent is not None:
577
452
                reference_content = self._get_content(parent)
578
453
                old_texts = reference_content.text()
582
457
            delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
583
458
            return parent, sha1, noeol, self._make_line_delta(delta_seq, new_content)
584
459
        else:
585
 
            delta = self.factory.parse_line_delta(data, version_id)
 
460
            delta = self.factory.parse_line_delta(data, version_idx)
586
461
            return parent, sha1, noeol, delta
587
462
        
588
463
    def get_graph_with_ghosts(self):
591
466
        return dict(graph_items)
592
467
 
593
468
    def get_sha1(self, version_id):
594
 
        return self.get_sha1s([version_id])[0]
595
 
 
596
 
    def get_sha1s(self, version_ids):
597
469
        """See VersionedFile.get_sha1()."""
598
 
        version_ids = [osutils.safe_revision_id(v) for v in version_ids]
599
 
        record_map = self._get_record_map(version_ids)
600
 
        # record entry 2 is the 'digest'.
601
 
        return [record_map[v][2] for v in version_ids]
 
470
        record_map = self._get_record_map([version_id])
 
471
        method, content, digest, next = record_map[version_id]
 
472
        return digest 
602
473
 
603
474
    @staticmethod
604
475
    def get_suffixes():
607
478
 
608
479
    def has_ghost(self, version_id):
609
480
        """True if there is a ghost reference in the file to version_id."""
610
 
        version_id = osutils.safe_revision_id(version_id)
611
481
        # maybe we have it
612
482
        if self.has_version(version_id):
613
483
            return False
626
496
 
627
497
    def has_version(self, version_id):
628
498
        """See VersionedFile.has_version."""
629
 
        version_id = osutils.safe_revision_id(version_id)
630
499
        return self._index.has_version(version_id)
631
500
 
632
501
    __contains__ = has_version
640
509
            delta_seq = None
641
510
            for parent_id in parents:
642
511
                merge_content = self._get_content(parent_id, parent_texts)
643
 
                seq = patiencediff.PatienceSequenceMatcher(
644
 
                                   None, merge_content.text(), content.text())
 
512
                seq = KnitSequenceMatcher(None, merge_content.text(), content.text())
645
513
                if delta_seq is None:
646
514
                    # setup a delta seq to reuse.
647
515
                    delta_seq = seq
658
526
                reference_content = self._get_content(parents[0], parent_texts)
659
527
                new_texts = content.text()
660
528
                old_texts = reference_content.text()
661
 
                delta_seq = patiencediff.PatienceSequenceMatcher(
662
 
                                                 None, old_texts, new_texts)
 
529
                delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
663
530
            return self._make_line_delta(delta_seq, content)
664
531
 
665
532
    def _make_line_delta(self, delta_seq, new_content):
713
580
 
714
581
    def _check_versions_present(self, version_ids):
715
582
        """Check that all specified versions are present."""
716
 
        self._index.check_versions_present(version_ids)
 
583
        version_ids = set(version_ids)
 
584
        for r in list(version_ids):
 
585
            if self._index.has_version(r):
 
586
                version_ids.remove(r)
 
587
        if version_ids:
 
588
            raise RevisionNotPresent(list(version_ids)[0], self.filename)
717
589
 
718
590
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts):
719
591
        """See VersionedFile.add_lines_with_ghosts()."""
732
604
        ### FIXME escape. RBC 20060228
733
605
        if contains_whitespace(version_id):
734
606
            raise InvalidRevisionId(version_id, self.filename)
735
 
        self.check_not_reserved_id(version_id)
736
607
        if self.has_version(version_id):
737
608
            raise RevisionAlreadyPresent(version_id, self.filename)
738
609
        self._check_lines_not_unicode(lines)
782
653
            # To speed the extract of texts the delta chain is limited
783
654
            # to a fixed number of deltas.  This should minimize both
784
655
            # I/O and the time spend applying deltas.
785
 
            delta = self._check_should_delta(present_parents)
 
656
            count = 0
 
657
            delta_parents = present_parents
 
658
            while count < 25:
 
659
                parent = delta_parents[0]
 
660
                method = self._index.get_method(parent)
 
661
                if method == 'fulltext':
 
662
                    break
 
663
                delta_parents = self._index.get_parents(parent)
 
664
                count = count + 1
 
665
            if method == 'line-delta':
 
666
                delta = False
786
667
 
787
 
        assert isinstance(version_id, str)
788
668
        lines = self.factory.make(lines, version_id)
789
669
        if delta or (self.factory.annotated and len(present_parents) > 0):
790
670
            # Merge annotations from parent texts if so is needed.
846
726
 
847
727
    def get_line_list(self, version_ids):
848
728
        """Return the texts of listed versions as a list of strings."""
849
 
        version_ids = [osutils.safe_revision_id(v) for v in version_ids]
850
 
        for version_id in version_ids:
851
 
            self.check_not_reserved_id(version_id)
852
729
        text_map, content_map = self._get_content_maps(version_ids)
853
730
        return [text_map[v] for v in version_ids]
854
731
 
855
 
    _get_lf_split_line_list = get_line_list
856
 
 
857
732
    def _get_content_maps(self, version_ids):
858
733
        """Produce maps of text and KnitContents
859
734
        
884
759
                if component_id in content_map:
885
760
                    content = content_map[component_id]
886
761
                else:
 
762
                    version_idx = self._index.lookup(component_id)
887
763
                    if method == 'fulltext':
888
764
                        assert content is None
889
 
                        content = self.factory.parse_fulltext(data, version_id)
 
765
                        content = self.factory.parse_fulltext(data, version_idx)
890
766
                    elif method == 'line-delta':
891
 
                        delta = self.factory.parse_line_delta(data, version_id)
 
767
                        delta = self.factory.parse_line_delta(data[:], 
 
768
                                                              version_idx)
892
769
                        content = content.copy()
893
770
                        content._lines = self._apply_delta(content._lines, 
894
771
                                                           delta)
909
786
            text_map[version_id] = text 
910
787
        return text_map, final_content 
911
788
 
912
 
    def iter_lines_added_or_present_in_versions(self, version_ids=None, 
913
 
                                                pb=None):
 
789
    def iter_lines_added_or_present_in_versions(self, version_ids=None):
914
790
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
915
791
        if version_ids is None:
916
792
            version_ids = self.versions()
917
 
        else:
918
 
            version_ids = [osutils.safe_revision_id(v) for v in version_ids]
919
 
        if pb is None:
920
 
            pb = progress.DummyProgress()
921
793
        # we don't care about inclusions, the caller cares.
922
794
        # but we need to setup a list of records to visit.
923
795
        # we need version_id, position, length
924
796
        version_id_records = []
925
 
        requested_versions = set(version_ids)
 
797
        requested_versions = list(version_ids)
926
798
        # filter for available versions
927
799
        for version_id in requested_versions:
928
800
            if not self.has_version(version_id):
929
801
                raise RevisionNotPresent(version_id, self.filename)
930
802
        # get a in-component-order queue:
 
803
        version_ids = []
931
804
        for version_id in self.versions():
932
805
            if version_id in requested_versions:
 
806
                version_ids.append(version_id)
933
807
                data_pos, length = self._index.get_position(version_id)
934
808
                version_id_records.append((version_id, data_pos, length))
935
809
 
 
810
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
811
        count = 0
936
812
        total = len(version_id_records)
937
 
        for version_idx, (version_id, data, sha_value) in \
938
 
            enumerate(self._data.read_records_iter(version_id_records)):
939
 
            pb.update('Walking content.', version_idx, total)
940
 
            method = self._index.get_method(version_id)
941
 
 
942
 
            assert method in ('fulltext', 'line-delta')
943
 
            if method == 'fulltext':
944
 
                line_iterator = self.factory.get_fulltext_content(data)
945
 
            else:
946
 
                line_iterator = self.factory.get_linedelta_content(data)
947
 
            for line in line_iterator:
948
 
                yield line
949
 
 
950
 
        pb.update('Walking content.', total, total)
 
813
        try:
 
814
            pb.update('Walking content.', count, total)
 
815
            for version_id, data, sha_value in \
 
816
                self._data.read_records_iter(version_id_records):
 
817
                pb.update('Walking content.', count, total)
 
818
                method = self._index.get_method(version_id)
 
819
                version_idx = self._index.lookup(version_id)
 
820
                assert method in ('fulltext', 'line-delta')
 
821
                if method == 'fulltext':
 
822
                    content = self.factory.parse_fulltext(data, version_idx)
 
823
                    for line in content.text():
 
824
                        yield line
 
825
                else:
 
826
                    delta = self.factory.parse_line_delta(data, version_idx)
 
827
                    for start, end, count, lines in delta:
 
828
                        for origin, line in lines:
 
829
                            yield line
 
830
                count +=1
 
831
            pb.update('Walking content.', total, total)
 
832
            pb.finished()
 
833
        except:
 
834
            pb.update('Walking content.', total, total)
 
835
            pb.finished()
 
836
            raise
951
837
        
952
 
    def iter_parents(self, version_ids):
953
 
        """Iterate through the parents for many version ids.
954
 
 
955
 
        :param version_ids: An iterable yielding version_ids.
956
 
        :return: An iterator that yields (version_id, parents). Requested 
957
 
            version_ids not present in the versioned file are simply skipped.
958
 
            The order is undefined, allowing for different optimisations in
959
 
            the underlying implementation.
960
 
        """
961
 
        version_ids = [osutils.safe_revision_id(version_id) for
962
 
            version_id in version_ids]
963
 
        return self._index.iter_parents(version_ids)
964
 
 
965
838
    def num_versions(self):
966
839
        """See VersionedFile.num_versions()."""
967
840
        return self._index.num_versions()
970
843
 
971
844
    def annotate_iter(self, version_id):
972
845
        """See VersionedFile.annotate_iter."""
973
 
        version_id = osutils.safe_revision_id(version_id)
974
846
        content = self._get_content(version_id)
975
847
        for origin, text in content.annotate_iter():
976
848
            yield origin, text
980
852
        # perf notes:
981
853
        # optimism counts!
982
854
        # 52554 calls in 1264 872 internal down from 3674
983
 
        version_id = osutils.safe_revision_id(version_id)
984
855
        try:
985
856
            return self._index.get_parents(version_id)
986
857
        except KeyError:
988
859
 
989
860
    def get_parents_with_ghosts(self, version_id):
990
861
        """See VersionedFile.get_parents."""
991
 
        version_id = osutils.safe_revision_id(version_id)
992
862
        try:
993
863
            return self._index.get_parents_with_ghosts(version_id)
994
864
        except KeyError:
995
865
            raise RevisionNotPresent(version_id, self.filename)
996
866
 
997
 
    def get_ancestry(self, versions, topo_sorted=True):
 
867
    def get_ancestry(self, versions):
998
868
        """See VersionedFile.get_ancestry."""
999
869
        if isinstance(versions, basestring):
1000
870
            versions = [versions]
1001
871
        if not versions:
1002
872
            return []
1003
 
        versions = [osutils.safe_revision_id(v) for v in versions]
1004
 
        return self._index.get_ancestry(versions, topo_sorted)
 
873
        self._check_versions_present(versions)
 
874
        return self._index.get_ancestry(versions)
1005
875
 
1006
876
    def get_ancestry_with_ghosts(self, versions):
1007
877
        """See VersionedFile.get_ancestry_with_ghosts."""
1009
879
            versions = [versions]
1010
880
        if not versions:
1011
881
            return []
1012
 
        versions = [osutils.safe_revision_id(v) for v in versions]
 
882
        self._check_versions_present(versions)
1013
883
        return self._index.get_ancestry_with_ghosts(versions)
1014
884
 
1015
885
    #@deprecated_method(zero_eight)
1022
892
        from bzrlib.weave import Weave
1023
893
 
1024
894
        w = Weave(self.filename)
1025
 
        ancestry = set(self.get_ancestry(version_ids, topo_sorted=False))
 
895
        ancestry = self.get_ancestry(version_ids)
1026
896
        sorted_graph = topo_sort(self._index.get_graph())
1027
897
        version_list = [vid for vid in sorted_graph if vid in ancestry]
1028
898
        
1035
905
 
1036
906
    def plan_merge(self, ver_a, ver_b):
1037
907
        """See VersionedFile.plan_merge."""
1038
 
        ver_a = osutils.safe_revision_id(ver_a)
1039
 
        ver_b = osutils.safe_revision_id(ver_b)
1040
 
        ancestors_b = set(self.get_ancestry(ver_b, topo_sorted=False))
 
908
        ancestors_b = set(self.get_ancestry(ver_b))
 
909
        def status_a(revision, text):
 
910
            if revision in ancestors_b:
 
911
                return 'killed-b', text
 
912
            else:
 
913
                return 'new-a', text
1041
914
        
1042
 
        ancestors_a = set(self.get_ancestry(ver_a, topo_sorted=False))
 
915
        ancestors_a = set(self.get_ancestry(ver_a))
 
916
        def status_b(revision, text):
 
917
            if revision in ancestors_a:
 
918
                return 'killed-a', text
 
919
            else:
 
920
                return 'new-b', text
 
921
 
1043
922
        annotated_a = self.annotate(ver_a)
1044
923
        annotated_b = self.annotate(ver_b)
1045
 
        return merge._plan_annotate_merge(annotated_a, annotated_b,
1046
 
                                          ancestors_a, ancestors_b)
 
924
        plain_a = [t for (a, t) in annotated_a]
 
925
        plain_b = [t for (a, t) in annotated_b]
 
926
        blocks = KnitSequenceMatcher(None, plain_a, plain_b).get_matching_blocks()
 
927
        a_cur = 0
 
928
        b_cur = 0
 
929
        for ai, bi, l in blocks:
 
930
            # process all mismatched sections
 
931
            # (last mismatched section is handled because blocks always
 
932
            # includes a 0-length last block)
 
933
            for revision, text in annotated_a[a_cur:ai]:
 
934
                yield status_a(revision, text)
 
935
            for revision, text in annotated_b[b_cur:bi]:
 
936
                yield status_b(revision, text)
 
937
 
 
938
            # and now the matched section
 
939
            a_cur = ai + l
 
940
            b_cur = bi + l
 
941
            for text_a, text_b in zip(plain_a[ai:a_cur], plain_b[bi:b_cur]):
 
942
                assert text_a == text_b
 
943
                yield "unchanged", text_a
1047
944
 
1048
945
 
1049
946
class _KnitComponentFile(object):
1050
947
    """One of the files used to implement a knit database"""
1051
948
 
1052
 
    def __init__(self, transport, filename, mode, file_mode=None,
1053
 
                 create_parent_dir=False, dir_mode=None):
 
949
    def __init__(self, transport, filename, mode, file_mode=None):
1054
950
        self._transport = transport
1055
951
        self._filename = filename
1056
952
        self._mode = mode
1057
 
        self._file_mode = file_mode
1058
 
        self._dir_mode = dir_mode
1059
 
        self._create_parent_dir = create_parent_dir
1060
 
        self._need_to_create = False
 
953
        self._file_mode=file_mode
1061
954
 
1062
 
    def _full_path(self):
1063
 
        """Return the full path to this file."""
1064
 
        return self._transport.base + self._filename
 
955
    def write_header(self):
 
956
        if self._transport.append(self._filename, StringIO(self.HEADER),
 
957
            mode=self._file_mode):
 
958
            raise KnitCorrupt(self._filename, 'misaligned after writing header')
1065
959
 
1066
960
    def check_header(self, fp):
1067
961
        line = fp.readline()
1068
 
        if line == '':
1069
 
            # An empty file can actually be treated as though the file doesn't
1070
 
            # exist yet.
1071
 
            raise errors.NoSuchFile(self._full_path())
1072
962
        if line != self.HEADER:
1073
 
            raise KnitHeaderError(badline=line,
1074
 
                              filename=self._transport.abspath(self._filename))
 
963
            raise KnitHeaderError(badline=line)
1075
964
 
1076
965
    def commit(self):
1077
966
        """Commit is a nop."""
1122
1011
    The ' :' marker is the end of record marker.
1123
1012
    
1124
1013
    partial writes:
1125
 
    when a write is interrupted to the index file, it will result in a line
1126
 
    that does not end in ' :'. If the ' :' is not present at the end of a line,
1127
 
    or at the end of the file, then the record that is missing it will be
1128
 
    ignored by the parser.
 
1014
    when a write is interrupted to the index file, it will result in a line that
 
1015
    does not end in ' :'. If the ' :' is not present at the end of a line, or at
 
1016
    the end of the file, then the record that is missing it will be ignored by
 
1017
    the parser.
1129
1018
 
1130
1019
    When writing new records to the index file, the data is preceded by '\n'
1131
1020
    to ensure that records always start on new lines even if the last write was
1140
1029
 
1141
1030
    def _cache_version(self, version_id, options, pos, size, parents):
1142
1031
        """Cache a version record in the history array and index cache.
1143
 
 
1144
 
        This is inlined into _load_data for performance. KEEP IN SYNC.
 
1032
        
 
1033
        This is inlined into __init__ for performance. KEEP IN SYNC.
1145
1034
        (It saves 60ms, 25% of the __init__ overhead on local 4000 record
1146
1035
         indexes).
1147
1036
        """
1152
1041
            self._history.append(version_id)
1153
1042
        else:
1154
1043
            index = self._cache[version_id][5]
1155
 
        self._cache[version_id] = (version_id,
 
1044
        self._cache[version_id] = (version_id, 
1156
1045
                                   options,
1157
1046
                                   pos,
1158
1047
                                   size,
1159
1048
                                   parents,
1160
1049
                                   index)
1161
1050
 
1162
 
    def __init__(self, transport, filename, mode, create=False, file_mode=None,
1163
 
                 create_parent_dir=False, delay_create=False, dir_mode=None):
1164
 
        _KnitComponentFile.__init__(self, transport, filename, mode,
1165
 
                                    file_mode=file_mode,
1166
 
                                    create_parent_dir=create_parent_dir,
1167
 
                                    dir_mode=dir_mode)
 
1051
    def __init__(self, transport, filename, mode, create=False, file_mode=None):
 
1052
        _KnitComponentFile.__init__(self, transport, filename, mode, file_mode)
1168
1053
        self._cache = {}
1169
1054
        # position in _history is the 'official' index for a revision
1170
1055
        # but the values may have come from a newer entry.
1171
1056
        # so - wc -l of a knit index is != the number of unique names
1172
1057
        # in the knit.
1173
1058
        self._history = []
 
1059
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1174
1060
        try:
1175
 
            fp = self._transport.get(self._filename)
 
1061
            count = 0
 
1062
            total = 1
1176
1063
            try:
1177
 
                # _load_data may raise NoSuchFile if the target knit is
1178
 
                # completely empty.
1179
 
                _load_data(self, fp)
1180
 
            finally:
1181
 
                fp.close()
1182
 
        except NoSuchFile:
1183
 
            if mode != 'w' or not create:
1184
 
                raise
1185
 
            elif delay_create:
1186
 
                self._need_to_create = True
 
1064
                pb.update('read knit index', count, total)
 
1065
                fp = self._transport.get(self._filename)
 
1066
                try:
 
1067
                    self.check_header(fp)
 
1068
                    # readlines reads the whole file at once:
 
1069
                    # bad for transports like http, good for local disk
 
1070
                    # we save 60 ms doing this one change (
 
1071
                    # from calling readline each time to calling
 
1072
                    # readlines once.
 
1073
                    # probably what we want for nice behaviour on
 
1074
                    # http is a incremental readlines that yields, or
 
1075
                    # a check for local vs non local indexes,
 
1076
                    for l in fp.readlines():
 
1077
                        rec = l.split()
 
1078
                        if len(rec) < 5 or rec[-1] != ':':
 
1079
                            # corrupt line.
 
1080
                            # FIXME: in the future we should determine if its a
 
1081
                            # short write - and ignore it 
 
1082
                            # or a different failure, and raise. RBC 20060407
 
1083
                            continue
 
1084
                        count += 1
 
1085
                        total += 1
 
1086
                        #pb.update('read knit index', count, total)
 
1087
                        # See self._parse_parents
 
1088
                        parents = []
 
1089
                        for value in rec[4:-1]:
 
1090
                            if '.' == value[0]:
 
1091
                                # uncompressed reference
 
1092
                                parents.append(value[1:])
 
1093
                            else:
 
1094
                                # this is 15/4000ms faster than isinstance,
 
1095
                                # (in lsprof)
 
1096
                                # this function is called thousands of times a 
 
1097
                                # second so small variations add up.
 
1098
                                assert value.__class__ is str
 
1099
                                parents.append(self._history[int(value)])
 
1100
                        # end self._parse_parents
 
1101
                        # self._cache_version(rec[0], 
 
1102
                        #                     rec[1].split(','),
 
1103
                        #                     int(rec[2]),
 
1104
                        #                     int(rec[3]),
 
1105
                        #                     parents)
 
1106
                        # --- self._cache_version
 
1107
                        # only want the _history index to reference the 1st 
 
1108
                        # index entry for version_id
 
1109
                        version_id = rec[0]
 
1110
                        if version_id not in self._cache:
 
1111
                            index = len(self._history)
 
1112
                            self._history.append(version_id)
 
1113
                        else:
 
1114
                            index = self._cache[version_id][5]
 
1115
                        self._cache[version_id] = (version_id,
 
1116
                                                   rec[1].split(','),
 
1117
                                                   int(rec[2]),
 
1118
                                                   int(rec[3]),
 
1119
                                                   parents,
 
1120
                                                   index)
 
1121
                        # --- self._cache_version 
 
1122
                finally:
 
1123
                    fp.close()
 
1124
            except NoSuchFile, e:
 
1125
                if mode != 'w' or not create:
 
1126
                    raise
 
1127
                self.write_header()
 
1128
        finally:
 
1129
            pb.update('read knit index', total, total)
 
1130
            pb.finished()
 
1131
 
 
1132
    def _parse_parents(self, compressed_parents):
 
1133
        """convert a list of string parent values into version ids.
 
1134
 
 
1135
        ints are looked up in the index.
 
1136
        .FOO values are ghosts and converted in to FOO.
 
1137
 
 
1138
        NOTE: the function is retained here for clarity, and for possible
 
1139
              use in partial index reads. However bulk processing now has
 
1140
              it inlined in __init__ for inner-loop optimisation.
 
1141
        """
 
1142
        result = []
 
1143
        for value in compressed_parents:
 
1144
            if value[-1] == '.':
 
1145
                # uncompressed reference
 
1146
                result.append(value[1:])
1187
1147
            else:
1188
 
                self._transport.put_bytes_non_atomic(
1189
 
                    self._filename, self.HEADER, mode=self._file_mode)
 
1148
                # this is 15/4000ms faster than isinstance,
 
1149
                # this function is called thousands of times a 
 
1150
                # second so small variations add up.
 
1151
                assert value.__class__ is str
 
1152
                result.append(self._history[int(value)])
 
1153
        return result
1190
1154
 
1191
1155
    def get_graph(self):
1192
 
        """Return a list of the node:parents lists from this knit index."""
1193
 
        return [(vid, idx[4]) for vid, idx in self._cache.iteritems()]
 
1156
        graph = []
 
1157
        for version_id, index in self._cache.iteritems():
 
1158
            graph.append((version_id, index[4]))
 
1159
        return graph
1194
1160
 
1195
 
    def get_ancestry(self, versions, topo_sorted=True):
 
1161
    def get_ancestry(self, versions):
1196
1162
        """See VersionedFile.get_ancestry."""
1197
1163
        # get a graph of all the mentioned versions:
1198
1164
        graph = {}
1199
1165
        pending = set(versions)
1200
 
        cache = self._cache
1201
 
        while pending:
 
1166
        while len(pending):
1202
1167
            version = pending.pop()
 
1168
            parents = self._cache[version][4]
 
1169
            # got the parents ok
1203
1170
            # trim ghosts
1204
 
            try:
1205
 
                parents = [p for p in cache[version][4] if p in cache]
1206
 
            except KeyError:
1207
 
                raise RevisionNotPresent(version, self._filename)
1208
 
            # if not completed and not a ghost
1209
 
            pending.update([p for p in parents if p not in graph])
 
1171
            parents = [parent for parent in parents if parent in self._cache]
 
1172
            for parent in parents:
 
1173
                # if not completed and not a ghost
 
1174
                if parent not in graph:
 
1175
                    pending.add(parent)
1210
1176
            graph[version] = parents
1211
 
        if not topo_sorted:
1212
 
            return graph.keys()
1213
1177
        return topo_sort(graph.items())
1214
1178
 
1215
1179
    def get_ancestry_with_ghosts(self, versions):
1216
1180
        """See VersionedFile.get_ancestry_with_ghosts."""
1217
1181
        # get a graph of all the mentioned versions:
1218
 
        self.check_versions_present(versions)
1219
 
        cache = self._cache
1220
1182
        graph = {}
1221
1183
        pending = set(versions)
1222
 
        while pending:
 
1184
        while len(pending):
1223
1185
            version = pending.pop()
1224
1186
            try:
1225
 
                parents = cache[version][4]
 
1187
                parents = self._cache[version][4]
1226
1188
            except KeyError:
1227
1189
                # ghost, fake it
1228
1190
                graph[version] = []
 
1191
                pass
1229
1192
            else:
1230
 
                # if not completed
1231
 
                pending.update([p for p in parents if p not in graph])
 
1193
                # got the parents ok
 
1194
                for parent in parents:
 
1195
                    if parent not in graph:
 
1196
                        pending.add(parent)
1232
1197
                graph[version] = parents
1233
1198
        return topo_sort(graph.items())
1234
1199
 
1235
 
    def iter_parents(self, version_ids):
1236
 
        """Iterate through the parents for many version ids.
1237
 
 
1238
 
        :param version_ids: An iterable yielding version_ids.
1239
 
        :return: An iterator that yields (version_id, parents). Requested 
1240
 
            version_ids not present in the versioned file are simply skipped.
1241
 
            The order is undefined, allowing for different optimisations in
1242
 
            the underlying implementation.
1243
 
        """
1244
 
        for version_id in version_ids:
1245
 
            try:
1246
 
                yield version_id, tuple(self.get_parents(version_id))
1247
 
            except KeyError:
1248
 
                pass
1249
 
 
1250
1200
    def num_versions(self):
1251
1201
        return len(self._history)
1252
1202
 
1253
1203
    __len__ = num_versions
1254
1204
 
1255
1205
    def get_versions(self):
1256
 
        """Get all the versions in the file. not topologically sorted."""
1257
1206
        return self._history
1258
1207
 
 
1208
    def idx_to_name(self, idx):
 
1209
        return self._history[idx]
 
1210
 
 
1211
    def lookup(self, version_id):
 
1212
        assert version_id in self._cache
 
1213
        return self._cache[version_id][5]
 
1214
 
1259
1215
    def _version_list_to_index(self, versions):
 
1216
        encode_utf8 = cache_utf8.encode
1260
1217
        result_list = []
1261
 
        cache = self._cache
1262
1218
        for version in versions:
1263
 
            if version in cache:
 
1219
            if version in self._cache:
1264
1220
                # -- inlined lookup() --
1265
 
                result_list.append(str(cache[version][5]))
 
1221
                result_list.append(str(self._cache[version][5]))
1266
1222
                # -- end lookup () --
1267
1223
            else:
1268
 
                result_list.append('.' + version)
 
1224
                result_list.append('.' + encode_utf8(version))
1269
1225
        return ' '.join(result_list)
1270
1226
 
1271
1227
    def add_version(self, version_id, options, pos, size, parents):
1279
1235
                         (version_id, options, pos, size, parents).
1280
1236
        """
1281
1237
        lines = []
1282
 
        orig_history = self._history[:]
1283
 
        orig_cache = self._cache.copy()
1284
 
 
1285
 
        try:
1286
 
            for version_id, options, pos, size, parents in versions:
1287
 
                line = "\n%s %s %s %s %s :" % (version_id,
1288
 
                                               ','.join(options),
1289
 
                                               pos,
1290
 
                                               size,
1291
 
                                               self._version_list_to_index(parents))
1292
 
                assert isinstance(line, str), \
1293
 
                    'content must be utf-8 encoded: %r' % (line,)
1294
 
                lines.append(line)
1295
 
                self._cache_version(version_id, options, pos, size, parents)
1296
 
            if not self._need_to_create:
1297
 
                self._transport.append_bytes(self._filename, ''.join(lines))
1298
 
            else:
1299
 
                sio = StringIO()
1300
 
                sio.write(self.HEADER)
1301
 
                sio.writelines(lines)
1302
 
                sio.seek(0)
1303
 
                self._transport.put_file_non_atomic(self._filename, sio,
1304
 
                                    create_parent_dir=self._create_parent_dir,
1305
 
                                    mode=self._file_mode,
1306
 
                                    dir_mode=self._dir_mode)
1307
 
                self._need_to_create = False
1308
 
        except:
1309
 
            # If any problems happen, restore the original values and re-raise
1310
 
            self._history = orig_history
1311
 
            self._cache = orig_cache
1312
 
            raise
1313
 
 
 
1238
        encode_utf8 = cache_utf8.encode
 
1239
        for version_id, options, pos, size, parents in versions:
 
1240
            line = "\n%s %s %s %s %s :" % (encode_utf8(version_id),
 
1241
                                           ','.join(options),
 
1242
                                           pos,
 
1243
                                           size,
 
1244
                                           self._version_list_to_index(parents))
 
1245
            assert isinstance(line, str), \
 
1246
                'content must be utf-8 encoded: %r' % (line,)
 
1247
            lines.append(line)
 
1248
        self._transport.append(self._filename, StringIO(''.join(lines)))
 
1249
        # cache after writing, so that a failed write leads to missing cache
 
1250
        # entries not extra ones. XXX TODO: RBC 20060502 in the event of a 
 
1251
        # failure, reload the index or flush it or some such, to prevent
 
1252
        # writing records that did complete twice.
 
1253
        for version_id, options, pos, size, parents in versions:
 
1254
            self._cache_version(version_id, options, pos, size, parents)
 
1255
        
1314
1256
    def has_version(self, version_id):
1315
1257
        """True if the version is in the index."""
1316
 
        return version_id in self._cache
 
1258
        return self._cache.has_key(version_id)
1317
1259
 
1318
1260
    def get_position(self, version_id):
1319
1261
        """Return data position and size of specified version."""
1320
 
        entry = self._cache[version_id]
1321
 
        return entry[2], entry[3]
 
1262
        return (self._cache[version_id][2], \
 
1263
                self._cache[version_id][3])
1322
1264
 
1323
1265
    def get_method(self, version_id):
1324
1266
        """Return compression method of specified version."""
1326
1268
        if 'fulltext' in options:
1327
1269
            return 'fulltext'
1328
1270
        else:
1329
 
            if 'line-delta' not in options:
1330
 
                raise errors.KnitIndexUnknownMethod(self._full_path(), options)
 
1271
            assert 'line-delta' in options
1331
1272
            return 'line-delta'
1332
1273
 
1333
1274
    def get_options(self, version_id):
1334
 
        """Return a string represention options.
1335
 
 
1336
 
        e.g. foo,bar
1337
 
        """
1338
1275
        return self._cache[version_id][1]
1339
1276
 
1340
1277
    def get_parents(self, version_id):
1348
1285
 
1349
1286
    def check_versions_present(self, version_ids):
1350
1287
        """Check that all specified versions are present."""
1351
 
        cache = self._cache
1352
 
        for version_id in version_ids:
1353
 
            if version_id not in cache:
1354
 
                raise RevisionNotPresent(version_id, self._filename)
1355
 
 
1356
 
 
1357
 
class KnitGraphIndex(object):
1358
 
    """A knit index that builds on GraphIndex."""
1359
 
 
1360
 
    def __init__(self, graph_index, deltas=False, parents=True, add_callback=None):
1361
 
        """Construct a KnitGraphIndex on a graph_index.
1362
 
 
1363
 
        :param graph_index: An implementation of bzrlib.index.GraphIndex.
1364
 
        :param deltas: Allow delta-compressed records.
1365
 
        :param add_callback: If not None, allow additions to the index and call
1366
 
            this callback with a list of added GraphIndex nodes:
1367
 
            [(node, value, node_refs), ...]
1368
 
        :param parents: If True, record knits parents, if not do not record 
1369
 
            parents.
1370
 
        """
1371
 
        self._graph_index = graph_index
1372
 
        self._deltas = deltas
1373
 
        self._add_callback = add_callback
1374
 
        self._parents = parents
1375
 
        if deltas and not parents:
1376
 
            raise KnitCorrupt(self, "Cannot do delta compression without "
1377
 
                "parent tracking.")
1378
 
 
1379
 
    def _get_entries(self, keys, check_present=False):
1380
 
        """Get the entries for keys.
1381
 
        
1382
 
        :param keys: An iterable of index keys, - 1-tuples.
1383
 
        """
1384
 
        keys = set(keys)
1385
 
        found_keys = set()
1386
 
        if self._parents:
1387
 
            for node in self._graph_index.iter_entries(keys):
1388
 
                yield node
1389
 
                found_keys.add(node[0])
1390
 
        else:
1391
 
            # adapt parentless index to the rest of the code.
1392
 
            for node in self._graph_index.iter_entries(keys):
1393
 
                yield node[0], node[1], ()
1394
 
                found_keys.add(node[0])
1395
 
        if check_present:
1396
 
            missing_keys = keys.difference(found_keys)
1397
 
            if missing_keys:
1398
 
                raise RevisionNotPresent(missing_keys.pop(), self)
1399
 
 
1400
 
    def _present_keys(self, version_ids):
1401
 
        return set([
1402
 
            node[0] for node in self._get_entries(version_ids)])
1403
 
 
1404
 
    def _parentless_ancestry(self, versions):
1405
 
        """Honour the get_ancestry API for parentless knit indices."""
1406
 
        wanted_keys = self._version_ids_to_keys(versions)
1407
 
        present_keys = self._present_keys(wanted_keys)
1408
 
        missing = set(wanted_keys).difference(present_keys)
1409
 
        if missing:
1410
 
            raise RevisionNotPresent(missing.pop(), self)
1411
 
        return list(self._keys_to_version_ids(present_keys))
1412
 
 
1413
 
    def get_ancestry(self, versions, topo_sorted=True):
1414
 
        """See VersionedFile.get_ancestry."""
1415
 
        if not self._parents:
1416
 
            return self._parentless_ancestry(versions)
1417
 
        # XXX: This will do len(history) index calls - perhaps
1418
 
        # it should be altered to be a index core feature?
1419
 
        # get a graph of all the mentioned versions:
1420
 
        graph = {}
1421
 
        ghosts = set()
1422
 
        versions = self._version_ids_to_keys(versions)
1423
 
        pending = set(versions)
1424
 
        while pending:
1425
 
            # get all pending nodes
1426
 
            this_iteration = pending
1427
 
            new_nodes = self._get_entries(this_iteration)
1428
 
            found = set()
1429
 
            pending = set()
1430
 
            for (key, value, node_refs) in new_nodes:
1431
 
                # dont ask for ghosties - otherwise
1432
 
                # we we can end up looping with pending
1433
 
                # being entirely ghosted.
1434
 
                graph[key] = [parent for parent in node_refs[0]
1435
 
                    if parent not in ghosts]
1436
 
                # queue parents
1437
 
                for parent in graph[key]:
1438
 
                    # dont examine known nodes again
1439
 
                    if parent in graph:
1440
 
                        continue
1441
 
                    pending.add(parent)
1442
 
                found.add(key)
1443
 
            ghosts.update(this_iteration.difference(found))
1444
 
        if versions.difference(graph):
1445
 
            raise RevisionNotPresent(versions.difference(graph).pop(), self)
1446
 
        if topo_sorted:
1447
 
            result_keys = topo_sort(graph.items())
1448
 
        else:
1449
 
            result_keys = graph.iterkeys()
1450
 
        return [key[0] for key in result_keys]
1451
 
 
1452
 
    def get_ancestry_with_ghosts(self, versions):
1453
 
        """See VersionedFile.get_ancestry."""
1454
 
        if not self._parents:
1455
 
            return self._parentless_ancestry(versions)
1456
 
        # XXX: This will do len(history) index calls - perhaps
1457
 
        # it should be altered to be a index core feature?
1458
 
        # get a graph of all the mentioned versions:
1459
 
        graph = {}
1460
 
        versions = self._version_ids_to_keys(versions)
1461
 
        pending = set(versions)
1462
 
        while pending:
1463
 
            # get all pending nodes
1464
 
            this_iteration = pending
1465
 
            new_nodes = self._get_entries(this_iteration)
1466
 
            pending = set()
1467
 
            for (key, value, node_refs) in new_nodes:
1468
 
                graph[key] = node_refs[0]
1469
 
                # queue parents 
1470
 
                for parent in graph[key]:
1471
 
                    # dont examine known nodes again
1472
 
                    if parent in graph:
1473
 
                        continue
1474
 
                    pending.add(parent)
1475
 
            missing_versions = this_iteration.difference(graph)
1476
 
            missing_needed = versions.intersection(missing_versions)
1477
 
            if missing_needed:
1478
 
                raise RevisionNotPresent(missing_needed.pop(), self)
1479
 
            for missing_version in missing_versions:
1480
 
                # add a key, no parents
1481
 
                graph[missing_version] = []
1482
 
                pending.discard(missing_version) # don't look for it
1483
 
        result_keys = topo_sort(graph.items())
1484
 
        return [key[0] for key in result_keys]
1485
 
 
1486
 
    def get_graph(self):
1487
 
        """Return a list of the node:parents lists from this knit index."""
1488
 
        if not self._parents:
1489
 
            return [(key, ()) for key in self.get_versions()]
1490
 
        result = []
1491
 
        for key, value, refs in self._graph_index.iter_all_entries():
1492
 
            result.append((key[0], tuple([ref[0] for ref in refs[0]])))
1493
 
        return result
1494
 
 
1495
 
    def iter_parents(self, version_ids):
1496
 
        """Iterate through the parents for many version ids.
1497
 
 
1498
 
        :param version_ids: An iterable yielding version_ids.
1499
 
        :return: An iterator that yields (version_id, parents). Requested 
1500
 
            version_ids not present in the versioned file are simply skipped.
1501
 
            The order is undefined, allowing for different optimisations in
1502
 
            the underlying implementation.
1503
 
        """
1504
 
        if self._parents:
1505
 
            all_nodes = set(self._get_entries(self._version_ids_to_keys(version_ids)))
1506
 
            all_parents = set()
1507
 
            present_parents = set()
1508
 
            for node in all_nodes:
1509
 
                all_parents.update(node[2][0])
1510
 
                # any node we are querying must be present
1511
 
                present_parents.add(node[0])
1512
 
            unknown_parents = all_parents.difference(present_parents)
1513
 
            present_parents.update(self._present_keys(unknown_parents))
1514
 
            for node in all_nodes:
1515
 
                parents = []
1516
 
                for parent in node[2][0]:
1517
 
                    if parent in present_parents:
1518
 
                        parents.append(parent[0])
1519
 
                yield node[0][0], tuple(parents)
1520
 
        else:
1521
 
            for node in self._get_entries(self._version_ids_to_keys(version_ids)):
1522
 
                yield node[0][0], ()
1523
 
 
1524
 
    def num_versions(self):
1525
 
        return len(list(self._graph_index.iter_all_entries()))
1526
 
 
1527
 
    __len__ = num_versions
1528
 
 
1529
 
    def get_versions(self):
1530
 
        """Get all the versions in the file. not topologically sorted."""
1531
 
        return [node[0][0] for node in self._graph_index.iter_all_entries()]
1532
 
    
1533
 
    def has_version(self, version_id):
1534
 
        """True if the version is in the index."""
1535
 
        return len(self._present_keys(self._version_ids_to_keys([version_id]))) == 1
1536
 
 
1537
 
    def _keys_to_version_ids(self, keys):
1538
 
        return tuple(key[0] for key in keys)
1539
 
 
1540
 
    def get_position(self, version_id):
1541
 
        """Return data position and size of specified version."""
1542
 
        bits = self._get_node(version_id)[1][1:].split(' ')
1543
 
        return int(bits[0]), int(bits[1])
1544
 
 
1545
 
    def get_method(self, version_id):
1546
 
        """Return compression method of specified version."""
1547
 
        if not self._deltas:
1548
 
            return 'fulltext'
1549
 
        return self._parent_compression(self._get_node(version_id)[2][1])
1550
 
 
1551
 
    def _parent_compression(self, reference_list):
1552
 
        # use the second reference list to decide if this is delta'd or not.
1553
 
        if len(reference_list):
1554
 
            return 'line-delta'
1555
 
        else:
1556
 
            return 'fulltext'
1557
 
 
1558
 
    def _get_node(self, version_id):
1559
 
        return list(self._get_entries(self._version_ids_to_keys([version_id])))[0]
1560
 
 
1561
 
    def get_options(self, version_id):
1562
 
        """Return a string represention options.
1563
 
 
1564
 
        e.g. foo,bar
1565
 
        """
1566
 
        node = self._get_node(version_id)
1567
 
        if not self._deltas:
1568
 
            options = ['fulltext']
1569
 
        else:
1570
 
            options = [self._parent_compression(node[2][1])]
1571
 
        if node[1][0] == 'N':
1572
 
            options.append('no-eol')
1573
 
        return options
1574
 
 
1575
 
    def get_parents(self, version_id):
1576
 
        """Return parents of specified version ignoring ghosts."""
1577
 
        parents = list(self.iter_parents([version_id]))
1578
 
        if not parents:
1579
 
            # missing key
1580
 
            raise errors.RevisionNotPresent(version_id, self)
1581
 
        return parents[0][1]
1582
 
 
1583
 
    def get_parents_with_ghosts(self, version_id):
1584
 
        """Return parents of specified version with ghosts."""
1585
 
        nodes = list(self._get_entries(self._version_ids_to_keys([version_id]),
1586
 
            check_present=True))
1587
 
        if not self._parents:
1588
 
            return ()
1589
 
        return self._keys_to_version_ids(nodes[0][2][0])
1590
 
 
1591
 
    def check_versions_present(self, version_ids):
1592
 
        """Check that all specified versions are present."""
1593
 
        keys = self._version_ids_to_keys(version_ids)
1594
 
        present = self._present_keys(keys)
1595
 
        missing = keys.difference(present)
1596
 
        if missing:
1597
 
            raise RevisionNotPresent(missing.pop(), self)
1598
 
 
1599
 
    def add_version(self, version_id, options, pos, size, parents):
1600
 
        """Add a version record to the index."""
1601
 
        return self.add_versions(((version_id, options, pos, size, parents),))
1602
 
 
1603
 
    def add_versions(self, versions):
1604
 
        """Add multiple versions to the index.
1605
 
        
1606
 
        This function does not insert data into the Immutable GraphIndex
1607
 
        backing the KnitGraphIndex, instead it prepares data for insertion by
1608
 
        the caller and checks that it is safe to insert then calls
1609
 
        self._add_callback with the prepared GraphIndex nodes.
1610
 
 
1611
 
        :param versions: a list of tuples:
1612
 
                         (version_id, options, pos, size, parents).
1613
 
        """
1614
 
        if not self._add_callback:
1615
 
            raise errors.ReadOnlyError(self)
1616
 
        # we hope there are no repositories with inconsistent parentage
1617
 
        # anymore.
1618
 
        # check for dups
1619
 
 
1620
 
        keys = {}
1621
 
        for (version_id, options, pos, size, parents) in versions:
1622
 
            # index keys are tuples:
1623
 
            key = (version_id, )
1624
 
            parents = tuple((parent, ) for parent in parents)
1625
 
            if 'no-eol' in options:
1626
 
                value = 'N'
1627
 
            else:
1628
 
                value = ' '
1629
 
            value += "%d %d" % (pos, size)
1630
 
            if not self._deltas:
1631
 
                if 'line-delta' in options:
1632
 
                    raise KnitCorrupt(self, "attempt to add line-delta in non-delta knit")
1633
 
            if self._parents:
1634
 
                if self._deltas:
1635
 
                    if 'line-delta' in options:
1636
 
                        node_refs = (parents, (parents[0],))
1637
 
                    else:
1638
 
                        node_refs = (parents, ())
1639
 
                else:
1640
 
                    node_refs = (parents, )
1641
 
            else:
1642
 
                if parents:
1643
 
                    raise KnitCorrupt(self, "attempt to add node with parents "
1644
 
                        "in parentless index.")
1645
 
                node_refs = ()
1646
 
            keys[key] = (value, node_refs)
1647
 
        present_nodes = self._get_entries(keys)
1648
 
        for (key, value, node_refs) in present_nodes:
1649
 
            if (value, node_refs) != keys[key]:
1650
 
                raise KnitCorrupt(self, "inconsistent details in add_versions"
1651
 
                    ": %s %s" % ((value, node_refs), keys[key]))
1652
 
            del keys[key]
1653
 
        result = []
1654
 
        if self._parents:
1655
 
            for key, (value, node_refs) in keys.iteritems():
1656
 
                result.append((key, value, node_refs))
1657
 
        else:
1658
 
            for key, (value, node_refs) in keys.iteritems():
1659
 
                result.append((key, value))
1660
 
        self._add_callback(result)
1661
 
        
1662
 
    def _version_ids_to_keys(self, version_ids):
1663
 
        return set((version_id, ) for version_id in version_ids)
1664
 
        
 
1288
        version_ids = set(version_ids)
 
1289
        for version_id in list(version_ids):
 
1290
            if version_id in self._cache:
 
1291
                version_ids.remove(version_id)
 
1292
        if version_ids:
 
1293
            raise RevisionNotPresent(list(version_ids)[0], self.filename)
 
1294
 
1665
1295
 
1666
1296
class _KnitData(_KnitComponentFile):
1667
1297
    """Contents of the knit data file"""
1668
1298
 
1669
 
    def __init__(self, transport, filename, mode, create=False, file_mode=None,
1670
 
                 create_parent_dir=False, delay_create=False,
1671
 
                 dir_mode=None):
1672
 
        _KnitComponentFile.__init__(self, transport, filename, mode,
1673
 
                                    file_mode=file_mode,
1674
 
                                    create_parent_dir=create_parent_dir,
1675
 
                                    dir_mode=dir_mode)
 
1299
    HEADER = "# bzr knit data 8\n"
 
1300
 
 
1301
    def __init__(self, transport, filename, mode, create=False, file_mode=None):
 
1302
        _KnitComponentFile.__init__(self, transport, filename, mode)
1676
1303
        self._checked = False
1677
1304
        # TODO: jam 20060713 conceptually, this could spill to disk
1678
1305
        #       if the cached size gets larger than a certain amount
1681
1308
        self._cache = {}
1682
1309
        self._do_cache = False
1683
1310
        if create:
1684
 
            if delay_create:
1685
 
                self._need_to_create = create
1686
 
            else:
1687
 
                self._transport.put_bytes_non_atomic(self._filename, '',
1688
 
                                                     mode=self._file_mode)
 
1311
            self._transport.put(self._filename, StringIO(''), mode=file_mode)
1689
1312
 
1690
1313
    def enable_cache(self):
1691
1314
        """Enable caching of reads."""
1711
1334
        sio = StringIO()
1712
1335
        data_file = GzipFile(None, mode='wb', fileobj=sio)
1713
1336
 
1714
 
        assert isinstance(version_id, str)
 
1337
        version_id_utf8 = cache_utf8.encode(version_id)
1715
1338
        data_file.writelines(chain(
1716
 
            ["version %s %d %s\n" % (version_id,
 
1339
            ["version %s %d %s\n" % (version_id_utf8,
1717
1340
                                     len(lines),
1718
1341
                                     digest)],
1719
1342
            lines,
1720
 
            ["end %s\n" % version_id]))
 
1343
            ["end %s\n" % version_id_utf8]))
1721
1344
        data_file.close()
1722
1345
        length= sio.tell()
1723
1346
 
1730
1353
        :return: the offset in the data file raw_data was written.
1731
1354
        """
1732
1355
        assert isinstance(raw_data, str), 'data must be plain bytes'
1733
 
        if not self._need_to_create:
1734
 
            return self._transport.append_bytes(self._filename, raw_data)
1735
 
        else:
1736
 
            self._transport.put_bytes_non_atomic(self._filename, raw_data,
1737
 
                                   create_parent_dir=self._create_parent_dir,
1738
 
                                   mode=self._file_mode,
1739
 
                                   dir_mode=self._dir_mode)
1740
 
            self._need_to_create = False
1741
 
            return 0
 
1356
        return self._transport.append(self._filename, StringIO(raw_data))
1742
1357
        
1743
1358
    def add_record(self, version_id, digest, lines):
1744
1359
        """Write new text record to disk.  Returns the position in the
1745
1360
        file where it was written."""
1746
1361
        size, sio = self._record_to_data(version_id, digest, lines)
1747
1362
        # write to disk
1748
 
        if not self._need_to_create:
1749
 
            start_pos = self._transport.append_file(self._filename, sio)
1750
 
        else:
1751
 
            self._transport.put_file_non_atomic(self._filename, sio,
1752
 
                               create_parent_dir=self._create_parent_dir,
1753
 
                               mode=self._file_mode,
1754
 
                               dir_mode=self._dir_mode)
1755
 
            self._need_to_create = False
1756
 
            start_pos = 0
 
1363
        start_pos = self._transport.append(self._filename, sio)
1757
1364
        if self._do_cache:
1758
1365
            self._cache[version_id] = sio.getvalue()
1759
1366
        return start_pos, size
1765
1372
                 as (stream, header_record)
1766
1373
        """
1767
1374
        df = GzipFile(mode='rb', fileobj=StringIO(raw_data))
1768
 
        try:
1769
 
            rec = self._check_header(version_id, df.readline())
1770
 
        except Exception, e:
1771
 
            raise KnitCorrupt(self._filename,
1772
 
                              "While reading {%s} got %s(%s)"
1773
 
                              % (version_id, e.__class__.__name__, str(e)))
 
1375
        rec = df.readline().split()
 
1376
        if len(rec) != 4:
 
1377
            raise KnitCorrupt(self._filename, 'unexpected number of elements in record header')
 
1378
        if cache_utf8.decode(rec[1]) != version_id:
 
1379
            raise KnitCorrupt(self._filename, 
 
1380
                              'unexpected version, wanted %r, got %r' % (
 
1381
                                version_id, rec[1]))
1774
1382
        return df, rec
1775
1383
 
1776
 
    def _check_header(self, version_id, line):
1777
 
        rec = line.split()
1778
 
        if len(rec) != 4:
1779
 
            raise KnitCorrupt(self._filename,
1780
 
                              'unexpected number of elements in record header')
1781
 
        if rec[1] != version_id:
1782
 
            raise KnitCorrupt(self._filename,
1783
 
                              'unexpected version, wanted %r, got %r'
1784
 
                              % (version_id, rec[1]))
1785
 
        return rec
1786
 
 
1787
1384
    def _parse_record(self, version_id, data):
1788
1385
        # profiling notes:
1789
1386
        # 4168 calls in 2880 217 internal
1790
1387
        # 4168 calls to _parse_record_header in 2121
1791
1388
        # 4168 calls to readlines in 330
1792
 
        df = GzipFile(mode='rb', fileobj=StringIO(data))
1793
 
 
1794
 
        try:
1795
 
            record_contents = df.readlines()
1796
 
        except Exception, e:
1797
 
            raise KnitCorrupt(self._filename,
1798
 
                              "While reading {%s} got %s(%s)"
1799
 
                              % (version_id, e.__class__.__name__, str(e)))
1800
 
        header = record_contents.pop(0)
1801
 
        rec = self._check_header(version_id, header)
1802
 
 
1803
 
        last_line = record_contents.pop()
1804
 
        if len(record_contents) != int(rec[2]):
1805
 
            raise KnitCorrupt(self._filename,
1806
 
                              'incorrect number of lines %s != %s'
1807
 
                              ' for version {%s}'
1808
 
                              % (len(record_contents), int(rec[2]),
1809
 
                                 version_id))
1810
 
        if last_line != 'end %s\n' % rec[1]:
1811
 
            raise KnitCorrupt(self._filename,
1812
 
                              'unexpected version end line %r, wanted %r' 
1813
 
                              % (last_line, version_id))
 
1389
        df, rec = self._parse_record_header(version_id, data)
 
1390
        record_contents = df.readlines()
 
1391
        l = record_contents.pop()
 
1392
        assert len(record_contents) == int(rec[2])
 
1393
        if l != 'end %s\n' % cache_utf8.encode(version_id):
 
1394
            raise KnitCorrupt(self._filename, 'unexpected version end line %r, wanted %r' 
 
1395
                        % (l, version_id))
1814
1396
        df.close()
1815
1397
        return record_contents, rec[3]
1816
1398
 
1834
1416
                                               in records]
1835
1417
 
1836
1418
            raw_records = self._transport.readv(self._filename, needed_offsets)
 
1419
                
1837
1420
 
1838
1421
        for version_id, pos, size in records:
1839
1422
            if version_id in self._cache:
1929
1512
        if not version_ids:
1930
1513
            return 0
1931
1514
 
1932
 
        pb = ui.ui_factory.nested_progress_bar()
 
1515
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1933
1516
        try:
1934
1517
            version_ids = list(version_ids)
1935
1518
            if None in version_ids:
2046
1629
        if not version_ids:
2047
1630
            return 0
2048
1631
 
2049
 
        pb = ui.ui_factory.nested_progress_bar()
 
1632
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
2050
1633
        try:
2051
1634
            version_ids = list(version_ids)
2052
1635
    
2247
1830
 
2248
1831
        return besti, bestj, bestsize
2249
1832
 
2250
 
 
2251
 
try:
2252
 
    from bzrlib._knit_load_data_c import _load_data_c as _load_data
2253
 
except ImportError:
2254
 
    from bzrlib._knit_load_data_py import _load_data_py as _load_data