~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/knit.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-07-06 03:15:29 UTC
  • mfrom: (1711.2.78 jam-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060706031529-e189d8c3f42076be
(jam) allow plugins to include benchmarks

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
70
74
import warnings
71
75
 
72
76
import bzrlib
73
 
from bzrlib import (
74
 
    cache_utf8,
75
 
    errors,
76
 
    osutils,
77
 
    patiencediff,
78
 
    progress,
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
 
    )
 
77
import bzrlib.errors as errors
 
78
from bzrlib.errors import FileExists, NoSuchFile, KnitError, \
 
79
        InvalidRevisionId, KnitCorrupt, KnitHeaderError, \
 
80
        RevisionNotPresent, RevisionAlreadyPresent
91
81
from bzrlib.tuned_gzip import GzipFile
92
82
from bzrlib.trace import mutter
93
 
from bzrlib.osutils import (
94
 
    contains_whitespace,
95
 
    contains_linebreaks,
96
 
    sha_strings,
97
 
    )
 
83
from bzrlib.osutils import contains_whitespace, contains_linebreaks, \
 
84
     sha_strings
 
85
from bzrlib.versionedfile import VersionedFile, InterVersionedFile
98
86
from bzrlib.symbol_versioning import DEPRECATED_PARAMETER, deprecated_passed
99
87
from bzrlib.tsort import topo_sort
100
 
import bzrlib.ui
101
88
import bzrlib.weave
102
 
from bzrlib.versionedfile import VersionedFile, InterVersionedFile
103
89
 
104
90
 
105
91
# TODO: Split out code specific to this format into an associated object.
127
113
 
128
114
    def annotate_iter(self):
129
115
        """Yield tuples of (origin, text) for each content line."""
130
 
        return iter(self._lines)
 
116
        for origin, text in self._lines:
 
117
            yield origin, text
131
118
 
132
119
    def annotate(self):
133
120
        """Return a list of (origin, text) tuples."""
135
122
 
136
123
    def line_delta_iter(self, new_lines):
137
124
        """Generate line-based delta from this content to new_lines."""
138
 
        new_texts = new_lines.text()
139
 
        old_texts = self.text()
 
125
        new_texts = [text for origin, text in new_lines._lines]
 
126
        old_texts = [text for origin, text in self._lines]
140
127
        s = KnitSequenceMatcher(None, old_texts, new_texts)
141
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
142
 
            if tag == 'equal':
 
128
        for op in s.get_opcodes():
 
129
            if op[0] == 'equal':
143
130
                continue
144
 
            # ofrom, oto, length, data
145
 
            yield i1, i2, j2 - j1, new_lines._lines[j1:j2]
 
131
            #     ofrom   oto   length        data
 
132
            yield (op[1], op[2], op[4]-op[3], new_lines._lines[op[3]:op[4]])
146
133
 
147
134
    def line_delta(self, new_lines):
148
135
        return list(self.line_delta_iter(new_lines))
157
144
class _KnitFactory(object):
158
145
    """Base factory for creating content objects."""
159
146
 
160
 
    def make(self, lines, version_id):
 
147
    def make(self, lines, version):
161
148
        num_lines = len(lines)
162
 
        return KnitContent(zip([version_id] * num_lines, lines))
 
149
        return KnitContent(zip([version] * num_lines, lines))
163
150
 
164
151
 
165
152
class KnitAnnotateFactory(_KnitFactory):
167
154
 
168
155
    annotated = True
169
156
 
170
 
    def parse_fulltext(self, content, version_id):
 
157
    def parse_fulltext(self, content, version):
171
158
        """Convert fulltext to internal representation
172
159
 
173
160
        fulltext content is of the format
175
162
        internal representation is of the format:
176
163
        (revid, plaintext)
177
164
        """
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]
 
165
        lines = []
 
166
        for line in content:
 
167
            origin, text = line.split(' ', 1)
 
168
            lines.append((origin.decode('utf-8'), text))
183
169
        return KnitContent(lines)
184
170
 
185
171
    def parse_line_delta_iter(self, lines):
186
 
        return iter(self.parse_line_delta(lines))
 
172
        for result_item in self.parse_line_delta[lines]:
 
173
            yield result_item
187
174
 
188
 
    def parse_line_delta(self, lines, version_id):
 
175
    def parse_line_delta(self, lines, version):
189
176
        """Convert a line based delta into internal representation.
190
177
 
191
178
        line delta is in the form of:
198
185
        result = []
199
186
        lines = iter(lines)
200
187
        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
 
 
207
188
        # walk through the lines parsing.
208
189
        for header in lines:
209
190
            start, end, count = [int(n) for n in header.split(',')]
210
 
            contents = [tuple(next().split(' ', 1)) for i in xrange(count)]
 
191
            contents = []
 
192
            remaining = count
 
193
            while remaining:
 
194
                origin, text = next().split(' ', 1)
 
195
                remaining -= 1
 
196
                contents.append((origin.decode('utf-8'), text))
211
197
            result.append((start, end, count, contents))
212
198
        return result
213
199
 
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):
230
 
                origin, text = next().split(' ', 1)
231
 
                yield text
232
 
 
233
200
    def lower_fulltext(self, content):
234
201
        """convert a fulltext content record into a serializable form.
235
202
 
236
203
        see parse_fulltext which this inverts.
237
204
        """
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]
 
205
        return ['%s %s' % (o.encode('utf-8'), t) for o, t in content._lines]
241
206
 
242
207
    def lower_line_delta(self, delta):
243
208
        """convert a delta into a serializable form.
244
209
 
245
210
        See parse_line_delta which this inverts.
246
211
        """
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
249
212
        out = []
250
213
        for start, end, c, lines in delta:
251
214
            out.append('%d,%d,%d\n' % (start, end, c))
252
 
            out.extend(origin + ' ' + text
253
 
                       for origin, text in lines)
 
215
            for origin, text in lines:
 
216
                out.append('%s %s' % (origin.encode('utf-8'), text))
254
217
        return out
255
218
 
256
219
 
259
222
 
260
223
    annotated = False
261
224
 
262
 
    def parse_fulltext(self, content, version_id):
 
225
    def parse_fulltext(self, content, version):
263
226
        """This parses an unannotated fulltext.
264
227
 
265
228
        Note that this is not a noop - the internal representation
266
229
        has (versionid, line) - its just a constant versionid.
267
230
        """
268
 
        return self.make(content, version_id)
 
231
        return self.make(content, version)
269
232
 
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
 
233
    def parse_line_delta_iter(self, lines, version):
 
234
        while lines:
 
235
            header = lines.pop(0)
276
236
            start, end, c = [int(n) for n in header.split(',')]
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
 
 
 
237
            yield start, end, c, zip([version] * c, lines[:c])
 
238
            del lines[:c]
 
239
 
 
240
    def parse_line_delta(self, lines, version):
 
241
        return list(self.parse_line_delta_iter(lines, version))
 
242
    
301
243
    def lower_fulltext(self, content):
302
244
        return content.text()
303
245
 
330
272
    stored and retrieved.
331
273
    """
332
274
 
333
 
    def __init__(self, relpath, transport, file_mode=None, access_mode=None,
 
275
    def __init__(self, relpath, transport, file_mode=None, access_mode=None, 
334
276
                 factory=None, basis_knit=DEPRECATED_PARAMETER, delta=True,
335
 
                 create=False, create_parent_dir=False, delay_create=False,
336
 
                 dir_mode=None):
 
277
                 create=False):
337
278
        """Construct a knit at location specified by relpath.
338
279
        
339
280
        :param create: If not True, only open an existing knit.
340
 
        :param create_parent_dir: If True, create the parent directory if 
341
 
            creating the file fails. (This is used for stores with 
342
 
            hash-prefixes that may not exist yet)
343
 
        :param delay_create: The calling code is aware that the knit won't 
344
 
            actually be created until the first data is stored.
345
281
        """
346
282
        if deprecated_passed(basis_knit):
347
283
            warnings.warn("KnitVersionedFile.__(): The basis_knit parameter is"
357
293
        self.writable = (access_mode == 'w')
358
294
        self.delta = delta
359
295
 
360
 
        self._max_delta_chain = 200
361
 
 
362
296
        self._index = _KnitIndex(transport, relpath + INDEX_SUFFIX,
363
 
            access_mode, create=create, file_mode=file_mode,
364
 
            create_parent_dir=create_parent_dir, delay_create=delay_create,
365
 
            dir_mode=dir_mode)
 
297
            access_mode, create=create, file_mode=file_mode)
366
298
        self._data = _KnitData(transport, relpath + DATA_SUFFIX,
367
 
            access_mode, create=create and not len(self), file_mode=file_mode,
368
 
            create_parent_dir=create_parent_dir, delay_create=delay_create,
369
 
            dir_mode=dir_mode)
 
299
            access_mode, create=create and not len(self), file_mode=file_mode)
370
300
 
371
301
    def __repr__(self):
372
302
        return '%s(%s)' % (self.__class__.__name__, 
373
303
                           self.transport.abspath(self.filename))
374
304
    
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
 
 
404
305
    def _add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
405
306
        """See VersionedFile._add_delta()."""
406
307
        self._check_add(version_id, []) # should we check the lines ?
438
339
            # To speed the extract of texts the delta chain is limited
439
340
            # to a fixed number of deltas.  This should minimize both
440
341
            # I/O and the time spend applying deltas.
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.
 
342
            count = 0
 
343
            delta_parents = [delta_parent]
 
344
            while count < 25:
 
345
                parent = delta_parents[0]
 
346
                method = self._index.get_method(parent)
 
347
                if method == 'fulltext':
 
348
                    break
 
349
                delta_parents = self._index.get_parents(parent)
 
350
                count = count + 1
 
351
            if method == 'line-delta':
 
352
                # did not find a fulltext in the delta limit.
 
353
                # just do a normal insertion.
446
354
                return super(KnitVersionedFile, self)._add_delta(version_id,
447
355
                                                                 parents,
448
356
                                                                 delta_parent,
466
374
        """
467
375
        # write all the data
468
376
        pos = self._data.add_raw_record(data)
469
 
        offset = 0
470
377
        index_entries = []
471
378
        for (version_id, options, parents, size) in records:
472
 
            index_entries.append((version_id, options, pos+offset,
473
 
                                  size, parents))
474
 
            if self._data._do_cache:
475
 
                self._data._cache[version_id] = data[offset:offset+size]
476
 
            offset += size
 
379
            index_entries.append((version_id, options, pos, size, parents))
 
380
            pos += size
477
381
        self._index.add_versions(index_entries)
478
382
 
479
 
    def enable_cache(self):
480
 
        """Start caching data for this knit"""
481
 
        self._data.enable_cache()
482
 
 
483
383
    def clear_cache(self):
484
384
        """Clear the data cache only."""
485
385
        self._data.clear_cache()
488
388
        """See VersionedFile.copy_to()."""
489
389
        # copy the current index to a temp index to avoid racing with local
490
390
        # writes
491
 
        transport.put_file_non_atomic(name + INDEX_SUFFIX + '.tmp',
492
 
                self.transport.get(self._index._filename))
 
391
        transport.put(name + INDEX_SUFFIX + '.tmp', self.transport.get(self._index._filename),)
493
392
        # copy the data file
494
393
        f = self._data._open_file()
495
394
        try:
496
 
            transport.put_file(name + DATA_SUFFIX, f)
 
395
            transport.put(name + DATA_SUFFIX, f)
497
396
        finally:
498
397
            f.close()
499
398
        # move the copied index into place
500
399
        transport.move(name + INDEX_SUFFIX + '.tmp', name + INDEX_SUFFIX)
501
400
 
502
401
    def create_empty(self, name, transport, mode=None):
503
 
        return KnitVersionedFile(name, transport, factory=self.factory,
504
 
                                 delta=self.delta, create=True)
 
402
        return KnitVersionedFile(name, transport, factory=self.factory, delta=self.delta, create=True)
505
403
    
506
 
    def _fix_parents(self, version_id, new_parents):
 
404
    def _fix_parents(self, version, new_parents):
507
405
        """Fix the parents list for version.
508
406
        
509
407
        This is done by appending a new version to the index
511
409
        the parents list must be a superset of the current
512
410
        list.
513
411
        """
514
 
        current_values = self._index._cache[version_id]
 
412
        current_values = self._index._cache[version]
515
413
        assert set(current_values[4]).difference(set(new_parents)) == set()
516
 
        self._index.add_version(version_id,
 
414
        self._index.add_version(version,
517
415
                                current_values[1], 
518
416
                                current_values[2],
519
417
                                current_values[3],
521
419
 
522
420
    def get_delta(self, version_id):
523
421
        """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)
526
422
        if not self.has_version(version_id):
527
423
            raise RevisionNotPresent(version_id, self.filename)
528
424
        
533
429
            parent = None
534
430
        data_pos, data_size = self._index.get_position(version_id)
535
431
        data, sha1 = self._data.read_records(((version_id, data_pos, data_size),))[version_id]
 
432
        version_idx = self._index.lookup(version_id)
536
433
        noeol = 'no-eol' in self._index.get_options(version_id)
537
434
        if 'fulltext' == self._index.get_method(version_id):
538
 
            new_content = self.factory.parse_fulltext(data, version_id)
 
435
            new_content = self.factory.parse_fulltext(data, version_idx)
539
436
            if parent is not None:
540
437
                reference_content = self._get_content(parent)
541
438
                old_texts = reference_content.text()
545
442
            delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
546
443
            return parent, sha1, noeol, self._make_line_delta(delta_seq, new_content)
547
444
        else:
548
 
            delta = self.factory.parse_line_delta(data, version_id)
 
445
            delta = self.factory.parse_line_delta(data, version_idx)
549
446
            return parent, sha1, noeol, delta
550
447
        
551
448
    def get_graph_with_ghosts(self):
555
452
 
556
453
    def get_sha1(self, version_id):
557
454
        """See VersionedFile.get_sha1()."""
558
 
        version_id = osutils.safe_revision_id(version_id)
559
455
        record_map = self._get_record_map([version_id])
560
456
        method, content, digest, next = record_map[version_id]
561
457
        return digest 
567
463
 
568
464
    def has_ghost(self, version_id):
569
465
        """True if there is a ghost reference in the file to version_id."""
570
 
        version_id = osutils.safe_revision_id(version_id)
571
466
        # maybe we have it
572
467
        if self.has_version(version_id):
573
468
            return False
586
481
 
587
482
    def has_version(self, version_id):
588
483
        """See VersionedFile.has_version."""
589
 
        version_id = osutils.safe_revision_id(version_id)
590
484
        return self._index.has_version(version_id)
591
485
 
592
486
    __contains__ = has_version
600
494
            delta_seq = None
601
495
            for parent_id in parents:
602
496
                merge_content = self._get_content(parent_id, parent_texts)
603
 
                seq = patiencediff.PatienceSequenceMatcher(
604
 
                                   None, merge_content.text(), content.text())
 
497
                seq = KnitSequenceMatcher(None, merge_content.text(), content.text())
605
498
                if delta_seq is None:
606
499
                    # setup a delta seq to reuse.
607
500
                    delta_seq = seq
618
511
                reference_content = self._get_content(parents[0], parent_texts)
619
512
                new_texts = content.text()
620
513
                old_texts = reference_content.text()
621
 
                delta_seq = patiencediff.PatienceSequenceMatcher(
622
 
                                                 None, old_texts, new_texts)
 
514
                delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
623
515
            return self._make_line_delta(delta_seq, content)
624
516
 
625
517
    def _make_line_delta(self, delta_seq, new_content):
673
565
 
674
566
    def _check_versions_present(self, version_ids):
675
567
        """Check that all specified versions are present."""
676
 
        self._index.check_versions_present(version_ids)
 
568
        version_ids = set(version_ids)
 
569
        for r in list(version_ids):
 
570
            if self._index.has_version(r):
 
571
                version_ids.remove(r)
 
572
        if version_ids:
 
573
            raise RevisionNotPresent(list(version_ids)[0], self.filename)
677
574
 
678
575
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts):
679
576
        """See VersionedFile.add_lines_with_ghosts()."""
692
589
        ### FIXME escape. RBC 20060228
693
590
        if contains_whitespace(version_id):
694
591
            raise InvalidRevisionId(version_id, self.filename)
695
 
        self.check_not_reserved_id(version_id)
696
592
        if self.has_version(version_id):
697
593
            raise RevisionAlreadyPresent(version_id, self.filename)
698
594
        self._check_lines_not_unicode(lines)
742
638
            # To speed the extract of texts the delta chain is limited
743
639
            # to a fixed number of deltas.  This should minimize both
744
640
            # I/O and the time spend applying deltas.
745
 
            delta = self._check_should_delta(present_parents)
 
641
            count = 0
 
642
            delta_parents = present_parents
 
643
            while count < 25:
 
644
                parent = delta_parents[0]
 
645
                method = self._index.get_method(parent)
 
646
                if method == 'fulltext':
 
647
                    break
 
648
                delta_parents = self._index.get_parents(parent)
 
649
                count = count + 1
 
650
            if method == 'line-delta':
 
651
                delta = False
746
652
 
747
 
        assert isinstance(version_id, str)
748
653
        lines = self.factory.make(lines, version_id)
749
654
        if delta or (self.factory.annotated and len(present_parents) > 0):
750
655
            # Merge annotations from parent texts if so is needed.
790
695
        # c = component_id, m = method, p = position, s = size, n = next
791
696
        records = [(c, p, s) for c, (m, p, s, n) in position_map.iteritems()]
792
697
        record_map = {}
793
 
        for component_id, content, digest in \
794
 
                self._data.read_records_iter(records):
 
698
        for component_id, content, digest in\
 
699
            self._data.read_records_iter(records): 
795
700
            method, position, size, next = position_map[component_id]
796
701
            record_map[component_id] = method, content, digest, next
797
702
                          
806
711
 
807
712
    def get_line_list(self, version_ids):
808
713
        """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)
812
714
        text_map, content_map = self._get_content_maps(version_ids)
813
715
        return [text_map[v] for v in version_ids]
814
716
 
842
744
                if component_id in content_map:
843
745
                    content = content_map[component_id]
844
746
                else:
 
747
                    version_idx = self._index.lookup(component_id)
845
748
                    if method == 'fulltext':
846
749
                        assert content is None
847
 
                        content = self.factory.parse_fulltext(data, version_id)
 
750
                        content = self.factory.parse_fulltext(data, version_idx)
848
751
                    elif method == 'line-delta':
849
 
                        delta = self.factory.parse_line_delta(data, version_id)
 
752
                        delta = self.factory.parse_line_delta(data[:], 
 
753
                                                              version_idx)
850
754
                        content = content.copy()
851
755
                        content._lines = self._apply_delta(content._lines, 
852
756
                                                           delta)
867
771
            text_map[version_id] = text 
868
772
        return text_map, final_content 
869
773
 
870
 
    def iter_lines_added_or_present_in_versions(self, version_ids=None, 
871
 
                                                pb=None):
 
774
    def iter_lines_added_or_present_in_versions(self, version_ids=None):
872
775
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
873
776
        if version_ids is None:
874
777
            version_ids = self.versions()
875
 
        else:
876
 
            version_ids = [osutils.safe_revision_id(v) for v in version_ids]
877
 
        if pb is None:
878
 
            pb = progress.DummyProgress()
879
778
        # we don't care about inclusions, the caller cares.
880
779
        # but we need to setup a list of records to visit.
881
780
        # we need version_id, position, length
882
781
        version_id_records = []
883
 
        requested_versions = set(version_ids)
 
782
        requested_versions = list(version_ids)
884
783
        # filter for available versions
885
784
        for version_id in requested_versions:
886
785
            if not self.has_version(version_id):
887
786
                raise RevisionNotPresent(version_id, self.filename)
888
787
        # get a in-component-order queue:
 
788
        version_ids = []
889
789
        for version_id in self.versions():
890
790
            if version_id in requested_versions:
 
791
                version_ids.append(version_id)
891
792
                data_pos, length = self._index.get_position(version_id)
892
793
                version_id_records.append((version_id, data_pos, length))
893
794
 
 
795
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
796
        count = 0
894
797
        total = len(version_id_records)
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)
898
 
            method = self._index.get_method(version_id)
899
 
 
900
 
            assert method in ('fulltext', 'line-delta')
901
 
            if method == 'fulltext':
902
 
                line_iterator = self.factory.get_fulltext_content(data)
903
 
            else:
904
 
                line_iterator = self.factory.get_linedelta_content(data)
905
 
            for line in line_iterator:
906
 
                yield line
907
 
 
908
 
        pb.update('Walking content.', total, total)
 
798
        try:
 
799
            pb.update('Walking content.', count, total)
 
800
            for version_id, data, sha_value in \
 
801
                self._data.read_records_iter(version_id_records):
 
802
                pb.update('Walking content.', count, total)
 
803
                method = self._index.get_method(version_id)
 
804
                version_idx = self._index.lookup(version_id)
 
805
                assert method in ('fulltext', 'line-delta')
 
806
                if method == 'fulltext':
 
807
                    content = self.factory.parse_fulltext(data, version_idx)
 
808
                    for line in content.text():
 
809
                        yield line
 
810
                else:
 
811
                    delta = self.factory.parse_line_delta(data, version_idx)
 
812
                    for start, end, count, lines in delta:
 
813
                        for origin, line in lines:
 
814
                            yield line
 
815
                count +=1
 
816
            pb.update('Walking content.', total, total)
 
817
            pb.finished()
 
818
        except:
 
819
            pb.update('Walking content.', total, total)
 
820
            pb.finished()
 
821
            raise
909
822
        
910
823
    def num_versions(self):
911
824
        """See VersionedFile.num_versions()."""
915
828
 
916
829
    def annotate_iter(self, version_id):
917
830
        """See VersionedFile.annotate_iter."""
918
 
        version_id = osutils.safe_revision_id(version_id)
919
831
        content = self._get_content(version_id)
920
832
        for origin, text in content.annotate_iter():
921
833
            yield origin, text
925
837
        # perf notes:
926
838
        # optimism counts!
927
839
        # 52554 calls in 1264 872 internal down from 3674
928
 
        version_id = osutils.safe_revision_id(version_id)
929
840
        try:
930
841
            return self._index.get_parents(version_id)
931
842
        except KeyError:
933
844
 
934
845
    def get_parents_with_ghosts(self, version_id):
935
846
        """See VersionedFile.get_parents."""
936
 
        version_id = osutils.safe_revision_id(version_id)
937
847
        try:
938
848
            return self._index.get_parents_with_ghosts(version_id)
939
849
        except KeyError:
940
850
            raise RevisionNotPresent(version_id, self.filename)
941
851
 
942
 
    def get_ancestry(self, versions, topo_sorted=True):
 
852
    def get_ancestry(self, versions):
943
853
        """See VersionedFile.get_ancestry."""
944
854
        if isinstance(versions, basestring):
945
855
            versions = [versions]
946
856
        if not versions:
947
857
            return []
948
 
        versions = [osutils.safe_revision_id(v) for v in versions]
949
 
        return self._index.get_ancestry(versions, topo_sorted)
 
858
        self._check_versions_present(versions)
 
859
        return self._index.get_ancestry(versions)
950
860
 
951
861
    def get_ancestry_with_ghosts(self, versions):
952
862
        """See VersionedFile.get_ancestry_with_ghosts."""
954
864
            versions = [versions]
955
865
        if not versions:
956
866
            return []
957
 
        versions = [osutils.safe_revision_id(v) for v in versions]
 
867
        self._check_versions_present(versions)
958
868
        return self._index.get_ancestry_with_ghosts(versions)
959
869
 
960
870
    #@deprecated_method(zero_eight)
967
877
        from bzrlib.weave import Weave
968
878
 
969
879
        w = Weave(self.filename)
970
 
        ancestry = set(self.get_ancestry(version_ids, topo_sorted=False))
 
880
        ancestry = self.get_ancestry(version_ids)
971
881
        sorted_graph = topo_sort(self._index.get_graph())
972
882
        version_list = [vid for vid in sorted_graph if vid in ancestry]
973
883
        
980
890
 
981
891
    def plan_merge(self, ver_a, ver_b):
982
892
        """See VersionedFile.plan_merge."""
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))
 
893
        ancestors_b = set(self.get_ancestry(ver_b))
986
894
        def status_a(revision, text):
987
895
            if revision in ancestors_b:
988
896
                return 'killed-b', text
989
897
            else:
990
898
                return 'new-a', text
991
899
        
992
 
        ancestors_a = set(self.get_ancestry(ver_a, topo_sorted=False))
 
900
        ancestors_a = set(self.get_ancestry(ver_a))
993
901
        def status_b(revision, text):
994
902
            if revision in ancestors_a:
995
903
                return 'killed-a', text
1023
931
class _KnitComponentFile(object):
1024
932
    """One of the files used to implement a knit database"""
1025
933
 
1026
 
    def __init__(self, transport, filename, mode, file_mode=None,
1027
 
                 create_parent_dir=False, dir_mode=None):
 
934
    def __init__(self, transport, filename, mode, file_mode=None):
1028
935
        self._transport = transport
1029
936
        self._filename = filename
1030
937
        self._mode = mode
1031
 
        self._file_mode = file_mode
1032
 
        self._dir_mode = dir_mode
1033
 
        self._create_parent_dir = create_parent_dir
1034
 
        self._need_to_create = False
 
938
        self._file_mode=file_mode
1035
939
 
1036
 
    def _full_path(self):
1037
 
        """Return the full path to this file."""
1038
 
        return self._transport.base + self._filename
 
940
    def write_header(self):
 
941
        if self._transport.append(self._filename, StringIO(self.HEADER),
 
942
            mode=self._file_mode):
 
943
            raise KnitCorrupt(self._filename, 'misaligned after writing header')
1039
944
 
1040
945
    def check_header(self, fp):
1041
946
        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())
1046
947
        if line != self.HEADER:
1047
 
            raise KnitHeaderError(badline=line,
1048
 
                              filename=self._transport.abspath(self._filename))
 
948
            raise KnitHeaderError(badline=line)
1049
949
 
1050
950
    def commit(self):
1051
951
        """Commit is a nop."""
1096
996
    The ' :' marker is the end of record marker.
1097
997
    
1098
998
    partial writes:
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.
 
999
    when a write is interrupted to the index file, it will result in a line that
 
1000
    does not end in ' :'. If the ' :' is not present at the end of a line, or at
 
1001
    the end of the file, then the record that is missing it will be ignored by
 
1002
    the parser.
1103
1003
 
1104
1004
    When writing new records to the index file, the data is preceded by '\n'
1105
1005
    to ensure that records always start on new lines even if the last write was
1114
1014
 
1115
1015
    def _cache_version(self, version_id, options, pos, size, parents):
1116
1016
        """Cache a version record in the history array and index cache.
1117
 
 
1118
 
        This is inlined into _load_data for performance. KEEP IN SYNC.
 
1017
        
 
1018
        This is inlined into __init__ for performance. KEEP IN SYNC.
1119
1019
        (It saves 60ms, 25% of the __init__ overhead on local 4000 record
1120
1020
         indexes).
1121
1021
        """
1126
1026
            self._history.append(version_id)
1127
1027
        else:
1128
1028
            index = self._cache[version_id][5]
1129
 
        self._cache[version_id] = (version_id,
 
1029
        self._cache[version_id] = (version_id, 
1130
1030
                                   options,
1131
1031
                                   pos,
1132
1032
                                   size,
1133
1033
                                   parents,
1134
1034
                                   index)
1135
1035
 
1136
 
    def __init__(self, transport, filename, mode, create=False, file_mode=None,
1137
 
                 create_parent_dir=False, delay_create=False, dir_mode=None):
1138
 
        _KnitComponentFile.__init__(self, transport, filename, mode,
1139
 
                                    file_mode=file_mode,
1140
 
                                    create_parent_dir=create_parent_dir,
1141
 
                                    dir_mode=dir_mode)
 
1036
    def __init__(self, transport, filename, mode, create=False, file_mode=None):
 
1037
        _KnitComponentFile.__init__(self, transport, filename, mode, file_mode)
1142
1038
        self._cache = {}
1143
1039
        # position in _history is the 'official' index for a revision
1144
1040
        # but the values may have come from a newer entry.
1145
1041
        # so - wc -l of a knit index is != the number of unique names
1146
1042
        # in the knit.
1147
1043
        self._history = []
 
1044
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1148
1045
        try:
1149
 
            fp = self._transport.get(self._filename)
 
1046
            count = 0
 
1047
            total = 1
1150
1048
            try:
1151
 
                # _load_data may raise NoSuchFile if the target knit is
1152
 
                # completely empty.
1153
 
                _load_data(self, 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
 
1049
                pb.update('read knit index', count, total)
 
1050
                fp = self._transport.get(self._filename)
 
1051
                try:
 
1052
                    self.check_header(fp)
 
1053
                    # readlines reads the whole file at once:
 
1054
                    # bad for transports like http, good for local disk
 
1055
                    # we save 60 ms doing this one change (
 
1056
                    # from calling readline each time to calling
 
1057
                    # readlines once.
 
1058
                    # probably what we want for nice behaviour on
 
1059
                    # http is a incremental readlines that yields, or
 
1060
                    # a check for local vs non local indexes,
 
1061
                    for l in fp.readlines():
 
1062
                        rec = l.split()
 
1063
                        if len(rec) < 5 or rec[-1] != ':':
 
1064
                            # corrupt line.
 
1065
                            # FIXME: in the future we should determine if its a
 
1066
                            # short write - and ignore it 
 
1067
                            # or a different failure, and raise. RBC 20060407
 
1068
                            continue
 
1069
                        count += 1
 
1070
                        total += 1
 
1071
                        #pb.update('read knit index', count, total)
 
1072
                        # See self._parse_parents
 
1073
                        parents = []
 
1074
                        for value in rec[4:-1]:
 
1075
                            if '.' == value[0]:
 
1076
                                # uncompressed reference
 
1077
                                parents.append(value[1:])
 
1078
                            else:
 
1079
                                # this is 15/4000ms faster than isinstance,
 
1080
                                # (in lsprof)
 
1081
                                # this function is called thousands of times a 
 
1082
                                # second so small variations add up.
 
1083
                                assert value.__class__ is str
 
1084
                                parents.append(self._history[int(value)])
 
1085
                        # end self._parse_parents
 
1086
                        # self._cache_version(rec[0], 
 
1087
                        #                     rec[1].split(','),
 
1088
                        #                     int(rec[2]),
 
1089
                        #                     int(rec[3]),
 
1090
                        #                     parents)
 
1091
                        # --- self._cache_version
 
1092
                        # only want the _history index to reference the 1st 
 
1093
                        # index entry for version_id
 
1094
                        version_id = rec[0]
 
1095
                        if version_id not in self._cache:
 
1096
                            index = len(self._history)
 
1097
                            self._history.append(version_id)
 
1098
                        else:
 
1099
                            index = self._cache[version_id][5]
 
1100
                        self._cache[version_id] = (version_id,
 
1101
                                                   rec[1].split(','),
 
1102
                                                   int(rec[2]),
 
1103
                                                   int(rec[3]),
 
1104
                                                   parents,
 
1105
                                                   index)
 
1106
                        # --- self._cache_version 
 
1107
                finally:
 
1108
                    fp.close()
 
1109
            except NoSuchFile, e:
 
1110
                if mode != 'w' or not create:
 
1111
                    raise
 
1112
                self.write_header()
 
1113
        finally:
 
1114
            pb.update('read knit index', total, total)
 
1115
            pb.finished()
 
1116
 
 
1117
    def _parse_parents(self, compressed_parents):
 
1118
        """convert a list of string parent values into version ids.
 
1119
 
 
1120
        ints are looked up in the index.
 
1121
        .FOO values are ghosts and converted in to FOO.
 
1122
 
 
1123
        NOTE: the function is retained here for clarity, and for possible
 
1124
              use in partial index reads. However bulk processing now has
 
1125
              it inlined in __init__ for inner-loop optimisation.
 
1126
        """
 
1127
        result = []
 
1128
        for value in compressed_parents:
 
1129
            if value[-1] == '.':
 
1130
                # uncompressed reference
 
1131
                result.append(value[1:])
1161
1132
            else:
1162
 
                self._transport.put_bytes_non_atomic(
1163
 
                    self._filename, self.HEADER, mode=self._file_mode)
 
1133
                # this is 15/4000ms faster than isinstance,
 
1134
                # this function is called thousands of times a 
 
1135
                # second so small variations add up.
 
1136
                assert value.__class__ is str
 
1137
                result.append(self._history[int(value)])
 
1138
        return result
1164
1139
 
1165
1140
    def get_graph(self):
1166
 
        return [(vid, idx[4]) for vid, idx in self._cache.iteritems()]
 
1141
        graph = []
 
1142
        for version_id, index in self._cache.iteritems():
 
1143
            graph.append((version_id, index[4]))
 
1144
        return graph
1167
1145
 
1168
 
    def get_ancestry(self, versions, topo_sorted=True):
 
1146
    def get_ancestry(self, versions):
1169
1147
        """See VersionedFile.get_ancestry."""
1170
1148
        # get a graph of all the mentioned versions:
1171
1149
        graph = {}
1172
1150
        pending = set(versions)
1173
 
        cache = self._cache
1174
 
        while pending:
 
1151
        while len(pending):
1175
1152
            version = pending.pop()
 
1153
            parents = self._cache[version][4]
 
1154
            # got the parents ok
1176
1155
            # trim ghosts
1177
 
            try:
1178
 
                parents = [p for p in cache[version][4] if p in cache]
1179
 
            except KeyError:
1180
 
                raise RevisionNotPresent(version, self._filename)
1181
 
            # if not completed and not a ghost
1182
 
            pending.update([p for p in parents if p not in graph])
 
1156
            parents = [parent for parent in parents if parent in self._cache]
 
1157
            for parent in parents:
 
1158
                # if not completed and not a ghost
 
1159
                if parent not in graph:
 
1160
                    pending.add(parent)
1183
1161
            graph[version] = parents
1184
 
        if not topo_sorted:
1185
 
            return graph.keys()
1186
1162
        return topo_sort(graph.items())
1187
1163
 
1188
1164
    def get_ancestry_with_ghosts(self, versions):
1189
1165
        """See VersionedFile.get_ancestry_with_ghosts."""
1190
1166
        # get a graph of all the mentioned versions:
1191
 
        self.check_versions_present(versions)
1192
 
        cache = self._cache
1193
1167
        graph = {}
1194
1168
        pending = set(versions)
1195
 
        while pending:
 
1169
        while len(pending):
1196
1170
            version = pending.pop()
1197
1171
            try:
1198
 
                parents = cache[version][4]
 
1172
                parents = self._cache[version][4]
1199
1173
            except KeyError:
1200
1174
                # ghost, fake it
1201
1175
                graph[version] = []
 
1176
                pass
1202
1177
            else:
1203
 
                # if not completed
1204
 
                pending.update([p for p in parents if p not in graph])
 
1178
                # got the parents ok
 
1179
                for parent in parents:
 
1180
                    if parent not in graph:
 
1181
                        pending.add(parent)
1205
1182
                graph[version] = parents
1206
1183
        return topo_sort(graph.items())
1207
1184
 
1222
1199
 
1223
1200
    def _version_list_to_index(self, versions):
1224
1201
        result_list = []
1225
 
        cache = self._cache
1226
1202
        for version in versions:
1227
 
            if version in cache:
 
1203
            if version in self._cache:
1228
1204
                # -- inlined lookup() --
1229
 
                result_list.append(str(cache[version][5]))
 
1205
                result_list.append(str(self._cache[version][5]))
1230
1206
                # -- end lookup () --
1231
1207
            else:
1232
 
                result_list.append('.' + version)
 
1208
                result_list.append('.' + version.encode('utf-8'))
1233
1209
        return ' '.join(result_list)
1234
1210
 
1235
1211
    def add_version(self, version_id, options, pos, size, parents):
1243
1219
                         (version_id, options, pos, size, parents).
1244
1220
        """
1245
1221
        lines = []
1246
 
        orig_history = self._history[:]
1247
 
        orig_cache = self._cache.copy()
1248
 
 
1249
 
        try:
1250
 
            for version_id, options, pos, size, parents in versions:
1251
 
                line = "\n%s %s %s %s %s :" % (version_id,
1252
 
                                               ','.join(options),
1253
 
                                               pos,
1254
 
                                               size,
1255
 
                                               self._version_list_to_index(parents))
1256
 
                assert isinstance(line, str), \
1257
 
                    'content must be utf-8 encoded: %r' % (line,)
1258
 
                lines.append(line)
1259
 
                self._cache_version(version_id, options, pos, size, parents)
1260
 
            if not self._need_to_create:
1261
 
                self._transport.append_bytes(self._filename, ''.join(lines))
1262
 
            else:
1263
 
                sio = StringIO()
1264
 
                sio.write(self.HEADER)
1265
 
                sio.writelines(lines)
1266
 
                sio.seek(0)
1267
 
                self._transport.put_file_non_atomic(self._filename, sio,
1268
 
                                    create_parent_dir=self._create_parent_dir,
1269
 
                                    mode=self._file_mode,
1270
 
                                    dir_mode=self._dir_mode)
1271
 
                self._need_to_create = False
1272
 
        except:
1273
 
            # If any problems happen, restore the original values and re-raise
1274
 
            self._history = orig_history
1275
 
            self._cache = orig_cache
1276
 
            raise
1277
 
 
 
1222
        for version_id, options, pos, size, parents in versions:
 
1223
            line = "\n%s %s %s %s %s :" % (version_id.encode('utf-8'),
 
1224
                                           ','.join(options),
 
1225
                                           pos,
 
1226
                                           size,
 
1227
                                           self._version_list_to_index(parents))
 
1228
            assert isinstance(line, str), \
 
1229
                'content must be utf-8 encoded: %r' % (line,)
 
1230
            lines.append(line)
 
1231
        self._transport.append(self._filename, StringIO(''.join(lines)))
 
1232
        # cache after writing, so that a failed write leads to missing cache
 
1233
        # entries not extra ones. XXX TODO: RBC 20060502 in the event of a 
 
1234
        # failure, reload the index or flush it or some such, to prevent
 
1235
        # writing records that did complete twice.
 
1236
        for version_id, options, pos, size, parents in versions:
 
1237
            self._cache_version(version_id, options, pos, size, parents)
 
1238
        
1278
1239
    def has_version(self, version_id):
1279
1240
        """True if the version is in the index."""
1280
 
        return version_id in self._cache
 
1241
        return self._cache.has_key(version_id)
1281
1242
 
1282
1243
    def get_position(self, version_id):
1283
1244
        """Return data position and size of specified version."""
1284
 
        entry = self._cache[version_id]
1285
 
        return entry[2], entry[3]
 
1245
        return (self._cache[version_id][2], \
 
1246
                self._cache[version_id][3])
1286
1247
 
1287
1248
    def get_method(self, version_id):
1288
1249
        """Return compression method of specified version."""
1290
1251
        if 'fulltext' in options:
1291
1252
            return 'fulltext'
1292
1253
        else:
1293
 
            if 'line-delta' not in options:
1294
 
                raise errors.KnitIndexUnknownMethod(self._full_path(), options)
 
1254
            assert 'line-delta' in options
1295
1255
            return 'line-delta'
1296
1256
 
1297
1257
    def get_options(self, version_id):
1308
1268
 
1309
1269
    def check_versions_present(self, version_ids):
1310
1270
        """Check that all specified versions are present."""
1311
 
        cache = self._cache
1312
 
        for version_id in version_ids:
1313
 
            if version_id not in cache:
1314
 
                raise RevisionNotPresent(version_id, self._filename)
 
1271
        version_ids = set(version_ids)
 
1272
        for version_id in list(version_ids):
 
1273
            if version_id in self._cache:
 
1274
                version_ids.remove(version_id)
 
1275
        if version_ids:
 
1276
            raise RevisionNotPresent(list(version_ids)[0], self.filename)
1315
1277
 
1316
1278
 
1317
1279
class _KnitData(_KnitComponentFile):
1318
1280
    """Contents of the knit data file"""
1319
1281
 
1320
 
    def __init__(self, transport, filename, mode, create=False, file_mode=None,
1321
 
                 create_parent_dir=False, delay_create=False,
1322
 
                 dir_mode=None):
1323
 
        _KnitComponentFile.__init__(self, transport, filename, mode,
1324
 
                                    file_mode=file_mode,
1325
 
                                    create_parent_dir=create_parent_dir,
1326
 
                                    dir_mode=dir_mode)
 
1282
    HEADER = "# bzr knit data 8\n"
 
1283
 
 
1284
    def __init__(self, transport, filename, mode, create=False, file_mode=None):
 
1285
        _KnitComponentFile.__init__(self, transport, filename, mode)
1327
1286
        self._checked = False
1328
 
        # TODO: jam 20060713 conceptually, this could spill to disk
1329
 
        #       if the cached size gets larger than a certain amount
1330
 
        #       but it complicates the model a bit, so for now just use
1331
 
        #       a simple dictionary
1332
 
        self._cache = {}
1333
 
        self._do_cache = False
1334
1287
        if create:
1335
 
            if delay_create:
1336
 
                self._need_to_create = create
1337
 
            else:
1338
 
                self._transport.put_bytes_non_atomic(self._filename, '',
1339
 
                                                     mode=self._file_mode)
1340
 
 
1341
 
    def enable_cache(self):
1342
 
        """Enable caching of reads."""
1343
 
        self._do_cache = True
 
1288
            self._transport.put(self._filename, StringIO(''), mode=file_mode)
1344
1289
 
1345
1290
    def clear_cache(self):
1346
1291
        """Clear the record cache."""
1347
 
        self._do_cache = False
1348
 
        self._cache = {}
 
1292
        pass
1349
1293
 
1350
1294
    def _open_file(self):
1351
1295
        try:
1361
1305
        """
1362
1306
        sio = StringIO()
1363
1307
        data_file = GzipFile(None, mode='wb', fileobj=sio)
1364
 
 
1365
 
        assert isinstance(version_id, str)
1366
1308
        data_file.writelines(chain(
1367
 
            ["version %s %d %s\n" % (version_id,
 
1309
            ["version %s %d %s\n" % (version_id.encode('utf-8'), 
1368
1310
                                     len(lines),
1369
1311
                                     digest)],
1370
1312
            lines,
1371
 
            ["end %s\n" % version_id]))
 
1313
            ["end %s\n" % version_id.encode('utf-8')]))
1372
1314
        data_file.close()
1373
1315
        length= sio.tell()
1374
1316
 
1381
1323
        :return: the offset in the data file raw_data was written.
1382
1324
        """
1383
1325
        assert isinstance(raw_data, str), 'data must be plain bytes'
1384
 
        if not self._need_to_create:
1385
 
            return self._transport.append_bytes(self._filename, raw_data)
1386
 
        else:
1387
 
            self._transport.put_bytes_non_atomic(self._filename, raw_data,
1388
 
                                   create_parent_dir=self._create_parent_dir,
1389
 
                                   mode=self._file_mode,
1390
 
                                   dir_mode=self._dir_mode)
1391
 
            self._need_to_create = False
1392
 
            return 0
 
1326
        return self._transport.append(self._filename, StringIO(raw_data))
1393
1327
        
1394
1328
    def add_record(self, version_id, digest, lines):
1395
1329
        """Write new text record to disk.  Returns the position in the
1396
1330
        file where it was written."""
1397
1331
        size, sio = self._record_to_data(version_id, digest, lines)
1398
1332
        # write to disk
1399
 
        if not self._need_to_create:
1400
 
            start_pos = self._transport.append_file(self._filename, sio)
1401
 
        else:
1402
 
            self._transport.put_file_non_atomic(self._filename, sio,
1403
 
                               create_parent_dir=self._create_parent_dir,
1404
 
                               mode=self._file_mode,
1405
 
                               dir_mode=self._dir_mode)
1406
 
            self._need_to_create = False
1407
 
            start_pos = 0
1408
 
        if self._do_cache:
1409
 
            self._cache[version_id] = sio.getvalue()
 
1333
        start_pos = self._transport.append(self._filename, sio)
1410
1334
        return start_pos, size
1411
1335
 
1412
1336
    def _parse_record_header(self, version_id, raw_data):
1416
1340
                 as (stream, header_record)
1417
1341
        """
1418
1342
        df = GzipFile(mode='rb', fileobj=StringIO(raw_data))
1419
 
        try:
1420
 
            rec = self._check_header(version_id, df.readline())
1421
 
        except Exception, e:
1422
 
            raise KnitCorrupt(self._filename,
1423
 
                              "While reading {%s} got %s(%s)"
1424
 
                              % (version_id, e.__class__.__name__, str(e)))
 
1343
        rec = df.readline().split()
 
1344
        if len(rec) != 4:
 
1345
            raise KnitCorrupt(self._filename, 'unexpected number of elements in record header')
 
1346
        if rec[1].decode('utf-8')!= version_id:
 
1347
            raise KnitCorrupt(self._filename, 
 
1348
                              'unexpected version, wanted %r, got %r' % (
 
1349
                                version_id, rec[1]))
1425
1350
        return df, rec
1426
1351
 
1427
 
    def _check_header(self, version_id, line):
1428
 
        rec = line.split()
1429
 
        if len(rec) != 4:
1430
 
            raise KnitCorrupt(self._filename,
1431
 
                              'unexpected number of elements in record header')
1432
 
        if rec[1] != version_id:
1433
 
            raise KnitCorrupt(self._filename,
1434
 
                              'unexpected version, wanted %r, got %r'
1435
 
                              % (version_id, rec[1]))
1436
 
        return rec
1437
 
 
1438
1352
    def _parse_record(self, version_id, data):
1439
1353
        # profiling notes:
1440
1354
        # 4168 calls in 2880 217 internal
1441
1355
        # 4168 calls to _parse_record_header in 2121
1442
1356
        # 4168 calls to readlines in 330
1443
 
        df = GzipFile(mode='rb', fileobj=StringIO(data))
1444
 
 
1445
 
        try:
1446
 
            record_contents = df.readlines()
1447
 
        except Exception, e:
1448
 
            raise KnitCorrupt(self._filename,
1449
 
                              "While reading {%s} got %s(%s)"
1450
 
                              % (version_id, e.__class__.__name__, str(e)))
1451
 
        header = record_contents.pop(0)
1452
 
        rec = self._check_header(version_id, header)
1453
 
 
1454
 
        last_line = record_contents.pop()
1455
 
        if len(record_contents) != int(rec[2]):
1456
 
            raise KnitCorrupt(self._filename,
1457
 
                              'incorrect number of lines %s != %s'
1458
 
                              ' for version {%s}'
1459
 
                              % (len(record_contents), int(rec[2]),
1460
 
                                 version_id))
1461
 
        if last_line != 'end %s\n' % rec[1]:
1462
 
            raise KnitCorrupt(self._filename,
1463
 
                              'unexpected version end line %r, wanted %r' 
1464
 
                              % (last_line, version_id))
 
1357
        df, rec = self._parse_record_header(version_id, data)
 
1358
        record_contents = df.readlines()
 
1359
        l = record_contents.pop()
 
1360
        assert len(record_contents) == int(rec[2])
 
1361
        if l.decode('utf-8') != 'end %s\n' % version_id:
 
1362
            raise KnitCorrupt(self._filename, 'unexpected version end line %r, wanted %r' 
 
1363
                        % (l, version_id))
1465
1364
        df.close()
1466
1365
        return record_contents, rec[3]
1467
1366
 
1470
1369
 
1471
1370
        This unpacks enough of the text record to validate the id is
1472
1371
        as expected but thats all.
 
1372
 
 
1373
        It will actively recompress currently cached records on the
 
1374
        basis that that is cheaper than I/O activity.
1473
1375
        """
1474
1376
        # setup an iterator of the external records:
1475
1377
        # uses readv so nice and fast we hope.
1476
1378
        if len(records):
1477
1379
            # grab the disk data needed.
1478
 
            if self._cache:
1479
 
                # Don't check _cache if it is empty
1480
 
                needed_offsets = [(pos, size) for version_id, pos, size
1481
 
                                              in records
1482
 
                                              if version_id not in self._cache]
1483
 
            else:
1484
 
                needed_offsets = [(pos, size) for version_id, pos, size
1485
 
                                               in records]
1486
 
 
1487
 
            raw_records = self._transport.readv(self._filename, needed_offsets)
 
1380
            raw_records = self._transport.readv(self._filename,
 
1381
                [(pos, size) for version_id, pos, size in records])
1488
1382
 
1489
1383
        for version_id, pos, size in records:
1490
 
            if version_id in self._cache:
1491
 
                # This data has already been validated
1492
 
                data = self._cache[version_id]
1493
 
            else:
1494
 
                pos, data = raw_records.next()
1495
 
                if self._do_cache:
1496
 
                    self._cache[version_id] = data
1497
 
 
1498
 
                # validate the header
1499
 
                df, rec = self._parse_record_header(version_id, data)
1500
 
                df.close()
 
1384
            pos, data = raw_records.next()
 
1385
            # validate the header
 
1386
            df, rec = self._parse_record_header(version_id, data)
 
1387
            df.close()
1501
1388
            yield version_id, data
1502
1389
 
1503
1390
    def read_records_iter(self, records):
1504
1391
        """Read text records from data file and yield result.
1505
1392
 
1506
 
        The result will be returned in whatever is the fastest to read.
1507
 
        Not by the order requested. Also, multiple requests for the same
1508
 
        record will only yield 1 response.
1509
 
        :param records: A list of (version_id, pos, len) entries
1510
 
        :return: Yields (version_id, contents, digest) in the order
1511
 
                 read, not the order requested
 
1393
        Each passed record is a tuple of (version_id, pos, len) and
 
1394
        will be read in the given order.  Yields (version_id,
 
1395
        contents, digest).
1512
1396
        """
1513
 
        if not records:
1514
 
            return
1515
 
 
1516
 
        if self._cache:
1517
 
            # Skip records we have alread seen
1518
 
            yielded_records = set()
1519
 
            needed_records = set()
1520
 
            for record in records:
1521
 
                if record[0] in self._cache:
1522
 
                    if record[0] in yielded_records:
1523
 
                        continue
1524
 
                    yielded_records.add(record[0])
1525
 
                    data = self._cache[record[0]]
1526
 
                    content, digest = self._parse_record(record[0], data)
1527
 
                    yield (record[0], content, digest)
1528
 
                else:
1529
 
                    needed_records.add(record)
1530
 
            needed_records = sorted(needed_records, key=operator.itemgetter(1))
1531
 
        else:
1532
 
            needed_records = sorted(set(records), key=operator.itemgetter(1))
1533
 
 
1534
 
        if not needed_records:
1535
 
            return
1536
 
 
1537
 
        # The transport optimizes the fetching as well 
1538
 
        # (ie, reads continuous ranges.)
1539
 
        readv_response = self._transport.readv(self._filename,
 
1397
        if len(records) == 0:
 
1398
            return
 
1399
        # profiling notes:
 
1400
        # 60890  calls for 4168 extractions in 5045, 683 internal.
 
1401
        # 4168   calls to readv              in 1411
 
1402
        # 4168   calls to parse_record       in 2880
 
1403
 
 
1404
        # Get unique records, sorted by position
 
1405
        needed_records = sorted(set(records), key=operator.itemgetter(1))
 
1406
 
 
1407
        # We take it that the transport optimizes the fetching as good
 
1408
        # as possible (ie, reads continuous ranges.)
 
1409
        response = self._transport.readv(self._filename,
1540
1410
            [(pos, size) for version_id, pos, size in needed_records])
1541
1411
 
1542
 
        for (version_id, pos, size), (pos, data) in \
1543
 
                izip(iter(needed_records), readv_response):
1544
 
            content, digest = self._parse_record(version_id, data)
1545
 
            if self._do_cache:
1546
 
                self._cache[version_id] = data
 
1412
        record_map = {}
 
1413
        for (record_id, pos, size), (pos, data) in \
 
1414
            izip(iter(needed_records), response):
 
1415
            content, digest = self._parse_record(record_id, data)
 
1416
            record_map[record_id] = (digest, content)
 
1417
 
 
1418
        for version_id, pos, size in records:
 
1419
            digest, content = record_map[version_id]
1547
1420
            yield version_id, content, digest
1548
1421
 
1549
1422
    def read_records(self, records):
1550
1423
        """Read records into a dictionary."""
1551
1424
        components = {}
1552
 
        for record_id, content, digest in \
1553
 
                self.read_records_iter(records):
 
1425
        for record_id, content, digest in self.read_records_iter(records):
1554
1426
            components[record_id] = (content, digest)
1555
1427
        return components
1556
1428
 
1580
1452
        if not version_ids:
1581
1453
            return 0
1582
1454
 
1583
 
        pb = ui.ui_factory.nested_progress_bar()
 
1455
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1584
1456
        try:
1585
1457
            version_ids = list(version_ids)
1586
1458
            if None in version_ids:
1697
1569
        if not version_ids:
1698
1570
            return 0
1699
1571
 
1700
 
        pb = ui.ui_factory.nested_progress_bar()
 
1572
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1701
1573
        try:
1702
1574
            version_ids = list(version_ids)
1703
1575
    
1898
1770
 
1899
1771
        return besti, bestj, bestsize
1900
1772
 
1901
 
 
1902
 
try:
1903
 
    from bzrlib._knit_load_data_c import _load_data_c as _load_data
1904
 
except ImportError:
1905
 
    from bzrlib._knit_load_data_py import _load_data_py as _load_data