~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-08-17 07:52:09 UTC
  • mfrom: (1910.3.4 trivial)
  • Revision ID: pqm@pqm.ubuntu.com-20060817075209-e85a1f9e05ff8b87
(andrew) Trivial fixes to NotImplemented errors.

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
62
66
 
63
67
from copy import copy
64
68
from cStringIO import StringIO
 
69
import difflib
65
70
from itertools import izip, chain
66
71
import operator
67
72
import os
68
73
import sys
69
74
import warnings
70
 
from zlib import Z_DEFAULT_COMPRESSION
71
75
 
72
76
import bzrlib
73
 
from bzrlib.lazy_import import lazy_import
74
 
lazy_import(globals(), """
75
 
from bzrlib import (
76
 
    annotate,
77
 
    pack,
78
 
    trace,
79
 
    )
80
 
""")
81
77
from bzrlib import (
82
78
    cache_utf8,
83
 
    debug,
84
 
    diff,
85
79
    errors,
86
 
    osutils,
87
 
    patiencediff,
88
 
    progress,
89
 
    merge,
90
 
    ui,
91
 
    )
92
 
from bzrlib.errors import (
93
 
    FileExists,
94
 
    NoSuchFile,
95
 
    KnitError,
96
 
    InvalidRevisionId,
97
 
    KnitCorrupt,
98
 
    KnitDataStreamIncompatible,
99
 
    KnitHeaderError,
100
 
    RevisionNotPresent,
101
 
    RevisionAlreadyPresent,
102
 
    )
103
 
from bzrlib.tuned_gzip import GzipFile, bytes_to_gzip
104
 
from bzrlib.osutils import (
105
 
    contains_whitespace,
106
 
    contains_linebreaks,
107
 
    sha_string,
108
 
    sha_strings,
109
 
    )
 
80
    )
 
81
from bzrlib.errors import FileExists, NoSuchFile, KnitError, \
 
82
        InvalidRevisionId, KnitCorrupt, KnitHeaderError, \
 
83
        RevisionNotPresent, RevisionAlreadyPresent
 
84
from bzrlib.tuned_gzip import GzipFile
 
85
from bzrlib.trace import mutter
 
86
from bzrlib.osutils import contains_whitespace, contains_linebreaks, \
 
87
     sha_strings
110
88
from bzrlib.symbol_versioning import DEPRECATED_PARAMETER, deprecated_passed
111
89
from bzrlib.tsort import topo_sort
112
 
import bzrlib.ui
113
90
import bzrlib.weave
114
91
from bzrlib.versionedfile import VersionedFile, InterVersionedFile
115
92
 
134
111
class KnitContent(object):
135
112
    """Content of a knit version to which deltas can be applied."""
136
113
 
 
114
    def __init__(self, lines):
 
115
        self._lines = lines
 
116
 
 
117
    def annotate_iter(self):
 
118
        """Yield tuples of (origin, text) for each content line."""
 
119
        for origin, text in self._lines:
 
120
            yield origin, text
 
121
 
137
122
    def annotate(self):
138
123
        """Return a list of (origin, text) tuples."""
139
124
        return list(self.annotate_iter())
140
125
 
141
 
    def apply_delta(self, delta, new_version_id):
142
 
        """Apply delta to this object to become new_version_id."""
143
 
        raise NotImplementedError(self.apply_delta)
144
 
 
145
126
    def line_delta_iter(self, new_lines):
146
127
        """Generate line-based delta from this content to new_lines."""
147
 
        new_texts = new_lines.text()
148
 
        old_texts = self.text()
149
 
        s = patiencediff.PatienceSequenceMatcher(None, old_texts, new_texts)
150
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
151
 
            if tag == 'equal':
 
128
        new_texts = [text for origin, text in new_lines._lines]
 
129
        old_texts = [text for origin, text in self._lines]
 
130
        s = KnitSequenceMatcher(None, old_texts, new_texts)
 
131
        for op in s.get_opcodes():
 
132
            if op[0] == 'equal':
152
133
                continue
153
 
            # ofrom, oto, length, data
154
 
            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]])
155
136
 
156
137
    def line_delta(self, new_lines):
157
138
        return list(self.line_delta_iter(new_lines))
158
139
 
159
 
    @staticmethod
160
 
    def get_line_delta_blocks(knit_delta, source, target):
161
 
        """Extract SequenceMatcher.get_matching_blocks() from a knit delta"""
162
 
        target_len = len(target)
163
 
        s_pos = 0
164
 
        t_pos = 0
165
 
        for s_begin, s_end, t_len, new_text in knit_delta:
166
 
            true_n = s_begin - s_pos
167
 
            n = true_n
168
 
            if n > 0:
169
 
                # knit deltas do not provide reliable info about whether the
170
 
                # last line of a file matches, due to eol handling.
171
 
                if source[s_pos + n -1] != target[t_pos + n -1]:
172
 
                    n-=1
173
 
                if n > 0:
174
 
                    yield s_pos, t_pos, n
175
 
            t_pos += t_len + true_n
176
 
            s_pos = s_end
177
 
        n = target_len - t_pos
178
 
        if n > 0:
179
 
            if source[s_pos + n -1] != target[t_pos + n -1]:
180
 
                n-=1
181
 
            if n > 0:
182
 
                yield s_pos, t_pos, n
183
 
        yield s_pos + (target_len - t_pos), target_len, 0
184
 
 
185
 
 
186
 
class AnnotatedKnitContent(KnitContent):
187
 
    """Annotated content."""
188
 
 
189
 
    def __init__(self, lines):
190
 
        self._lines = lines
191
 
 
192
 
    def annotate_iter(self):
193
 
        """Yield tuples of (origin, text) for each content line."""
194
 
        return iter(self._lines)
195
 
 
196
 
    def apply_delta(self, delta, new_version_id):
197
 
        """Apply delta to this object to become new_version_id."""
198
 
        offset = 0
199
 
        lines = self._lines
200
 
        for start, end, count, delta_lines in delta:
201
 
            lines[offset+start:offset+end] = delta_lines
202
 
            offset = offset + (start - end) + count
203
 
 
204
 
    def strip_last_line_newline(self):
205
 
        line = self._lines[-1][1].rstrip('\n')
206
 
        self._lines[-1] = (self._lines[-1][0], line)
207
 
 
208
 
    def text(self):
209
 
        try:
210
 
            return [text for origin, text in self._lines]
211
 
        except ValueError, e:
212
 
            # most commonly (only?) caused by the internal form of the knit
213
 
            # missing annotation information because of a bug - see thread
214
 
            # around 20071015
215
 
            raise KnitCorrupt(self,
216
 
                "line in annotated knit missing annotation information: %s"
217
 
                % (e,))
218
 
 
219
 
    def copy(self):
220
 
        return AnnotatedKnitContent(self._lines[:])
221
 
 
222
 
 
223
 
class PlainKnitContent(KnitContent):
224
 
    """Unannotated content.
225
 
    
226
 
    When annotate[_iter] is called on this content, the same version is reported
227
 
    for all lines. Generally, annotate[_iter] is not useful on PlainKnitContent
228
 
    objects.
229
 
    """
230
 
 
231
 
    def __init__(self, lines, version_id):
232
 
        self._lines = lines
233
 
        self._version_id = version_id
234
 
 
235
 
    def annotate_iter(self):
236
 
        """Yield tuples of (origin, text) for each content line."""
237
 
        for line in self._lines:
238
 
            yield self._version_id, line
239
 
 
240
 
    def apply_delta(self, delta, new_version_id):
241
 
        """Apply delta to this object to become new_version_id."""
242
 
        offset = 0
243
 
        lines = self._lines
244
 
        for start, end, count, delta_lines in delta:
245
 
            lines[offset+start:offset+end] = delta_lines
246
 
            offset = offset + (start - end) + count
247
 
        self._version_id = new_version_id
248
 
 
249
 
    def copy(self):
250
 
        return PlainKnitContent(self._lines[:], self._version_id)
251
 
 
252
 
    def strip_last_line_newline(self):
253
 
        self._lines[-1] = self._lines[-1].rstrip('\n')
254
 
 
255
 
    def text(self):
256
 
        return self._lines
257
 
 
258
 
 
259
 
class KnitAnnotateFactory(object):
 
140
    def text(self):
 
141
        return [text for origin, text in self._lines]
 
142
 
 
143
    def copy(self):
 
144
        return KnitContent(self._lines[:])
 
145
 
 
146
 
 
147
class _KnitFactory(object):
 
148
    """Base factory for creating content objects."""
 
149
 
 
150
    def make(self, lines, version):
 
151
        num_lines = len(lines)
 
152
        return KnitContent(zip([version] * num_lines, lines))
 
153
 
 
154
 
 
155
class KnitAnnotateFactory(_KnitFactory):
260
156
    """Factory for creating annotated Content objects."""
261
157
 
262
158
    annotated = True
263
159
 
264
 
    def make(self, lines, version_id):
265
 
        num_lines = len(lines)
266
 
        return AnnotatedKnitContent(zip([version_id] * num_lines, lines))
267
 
 
268
 
    def parse_fulltext(self, content, version_id):
 
160
    def parse_fulltext(self, content, version):
269
161
        """Convert fulltext to internal representation
270
162
 
271
163
        fulltext content is of the format
273
165
        internal representation is of the format:
274
166
        (revid, plaintext)
275
167
        """
276
 
        # TODO: jam 20070209 The tests expect this to be returned as tuples,
277
 
        #       but the code itself doesn't really depend on that.
278
 
        #       Figure out a way to not require the overhead of turning the
279
 
        #       list back into tuples.
280
 
        lines = [tuple(line.split(' ', 1)) for line in content]
281
 
        return AnnotatedKnitContent(lines)
 
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))
 
173
        return KnitContent(lines)
282
174
 
283
175
    def parse_line_delta_iter(self, lines):
284
 
        return iter(self.parse_line_delta(lines))
 
176
        for result_item in self.parse_line_delta[lines]:
 
177
            yield result_item
285
178
 
286
 
    def parse_line_delta(self, lines, version_id, plain=False):
 
179
    def parse_line_delta(self, lines, version):
287
180
        """Convert a line based delta into internal representation.
288
181
 
289
182
        line delta is in the form of:
292
185
        revid(utf8) newline\n
293
186
        internal representation is
294
187
        (start, end, count, [1..count tuples (revid, newline)])
295
 
 
296
 
        :param plain: If True, the lines are returned as a plain
297
 
            list without annotations, not as a list of (origin, content) tuples, i.e.
298
 
            (start, end, count, [1..count newline])
299
188
        """
 
189
        decode_utf8 = cache_utf8.decode
300
190
        result = []
301
191
        lines = iter(lines)
302
192
        next = lines.next
303
 
 
304
 
        cache = {}
305
 
        def cache_and_return(line):
306
 
            origin, text = line.split(' ', 1)
307
 
            return cache.setdefault(origin, origin), text
308
 
 
309
193
        # walk through the lines parsing.
310
 
        # Note that the plain test is explicitly pulled out of the
311
 
        # loop to minimise any performance impact
312
 
        if plain:
313
 
            for header in lines:
314
 
                start, end, count = [int(n) for n in header.split(',')]
315
 
                contents = [next().split(' ', 1)[1] for i in xrange(count)]
316
 
                result.append((start, end, count, contents))
317
 
        else:
318
 
            for header in lines:
319
 
                start, end, count = [int(n) for n in header.split(',')]
320
 
                contents = [tuple(next().split(' ', 1)) for i in xrange(count)]
321
 
                result.append((start, end, count, contents))
322
 
        return result
323
 
 
324
 
    def get_fulltext_content(self, lines):
325
 
        """Extract just the content lines from a fulltext."""
326
 
        return (line.split(' ', 1)[1] for line in lines)
327
 
 
328
 
    def get_linedelta_content(self, lines):
329
 
        """Extract just the content from a line delta.
330
 
 
331
 
        This doesn't return all of the extra information stored in a delta.
332
 
        Only the actual content lines.
333
 
        """
334
 
        lines = iter(lines)
335
 
        next = lines.next
336
194
        for header in lines:
337
 
            header = header.split(',')
338
 
            count = int(header[2])
339
 
            for i in xrange(count):
 
195
            start, end, count = [int(n) for n in header.split(',')]
 
196
            contents = []
 
197
            remaining = count
 
198
            while remaining:
340
199
                origin, text = next().split(' ', 1)
341
 
                yield text
 
200
                remaining -= 1
 
201
                contents.append((decode_utf8(origin), text))
 
202
            result.append((start, end, count, contents))
 
203
        return result
342
204
 
343
205
    def lower_fulltext(self, content):
344
206
        """convert a fulltext content record into a serializable form.
345
207
 
346
208
        see parse_fulltext which this inverts.
347
209
        """
348
 
        # TODO: jam 20070209 We only do the caching thing to make sure that
349
 
        #       the origin is a valid utf-8 line, eventually we could remove it
350
 
        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]
351
212
 
352
213
    def lower_line_delta(self, delta):
353
214
        """convert a delta into a serializable form.
354
215
 
355
216
        See parse_line_delta which this inverts.
356
217
        """
357
 
        # TODO: jam 20070209 We only do the caching thing to make sure that
358
 
        #       the origin is a valid utf-8 line, eventually we could remove it
 
218
        encode_utf8 = cache_utf8.encode
359
219
        out = []
360
220
        for start, end, c, lines in delta:
361
221
            out.append('%d,%d,%d\n' % (start, end, c))
362
 
            out.extend(origin + ' ' + text
 
222
            out.extend(encode_utf8(origin) + ' ' + text
363
223
                       for origin, text in lines)
364
224
        return out
365
225
 
366
 
    def annotate_iter(self, knit, version_id):
367
 
        content = knit._get_content(version_id)
368
 
        return content.annotate_iter()
369
 
 
370
 
 
371
 
class KnitPlainFactory(object):
 
226
 
 
227
class KnitPlainFactory(_KnitFactory):
372
228
    """Factory for creating plain Content objects."""
373
229
 
374
230
    annotated = False
375
231
 
376
 
    def make(self, lines, version_id):
377
 
        return PlainKnitContent(lines, version_id)
378
 
 
379
 
    def parse_fulltext(self, content, version_id):
 
232
    def parse_fulltext(self, content, version):
380
233
        """This parses an unannotated fulltext.
381
234
 
382
235
        Note that this is not a noop - the internal representation
383
236
        has (versionid, line) - its just a constant versionid.
384
237
        """
385
 
        return self.make(content, version_id)
 
238
        return self.make(content, version)
386
239
 
387
 
    def parse_line_delta_iter(self, lines, version_id):
388
 
        cur = 0
389
 
        num_lines = len(lines)
390
 
        while cur < num_lines:
391
 
            header = lines[cur]
392
 
            cur += 1
 
240
    def parse_line_delta_iter(self, lines, version):
 
241
        while lines:
 
242
            header = lines.pop(0)
393
243
            start, end, c = [int(n) for n in header.split(',')]
394
 
            yield start, end, c, lines[cur:cur+c]
395
 
            cur += c
396
 
 
397
 
    def parse_line_delta(self, lines, version_id):
398
 
        return list(self.parse_line_delta_iter(lines, version_id))
399
 
 
400
 
    def get_fulltext_content(self, lines):
401
 
        """Extract just the content lines from a fulltext."""
402
 
        return iter(lines)
403
 
 
404
 
    def get_linedelta_content(self, lines):
405
 
        """Extract just the content from a line delta.
406
 
 
407
 
        This doesn't return all of the extra information stored in a delta.
408
 
        Only the actual content lines.
409
 
        """
410
 
        lines = iter(lines)
411
 
        next = lines.next
412
 
        for header in lines:
413
 
            header = header.split(',')
414
 
            count = int(header[2])
415
 
            for i in xrange(count):
416
 
                yield next()
417
 
 
 
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
    
418
250
    def lower_fulltext(self, content):
419
251
        return content.text()
420
252
 
422
254
        out = []
423
255
        for start, end, c, lines in delta:
424
256
            out.append('%d,%d,%d\n' % (start, end, c))
425
 
            out.extend(lines)
 
257
            out.extend([text for origin, text in lines])
426
258
        return out
427
259
 
428
 
    def annotate_iter(self, knit, version_id):
429
 
        return annotate_knit(knit, version_id)
430
 
 
431
260
 
432
261
def make_empty_knit(transport, relpath):
433
262
    """Construct a empty knit at the specified location."""
434
263
    k = KnitVersionedFile(transport, relpath, 'w', KnitPlainFactory)
 
264
    k._data._open_file()
435
265
 
436
266
 
437
267
class KnitVersionedFile(VersionedFile):
449
279
    stored and retrieved.
450
280
    """
451
281
 
452
 
    def __init__(self, relpath, transport, file_mode=None, access_mode=None,
453
 
        factory=None, delta=True, create=False, create_parent_dir=False,
454
 
        delay_create=False, dir_mode=None, index=None, access_method=None):
 
282
    def __init__(self, relpath, transport, file_mode=None, access_mode=None, 
 
283
                 factory=None, basis_knit=DEPRECATED_PARAMETER, delta=True,
 
284
                 create=False):
455
285
        """Construct a knit at location specified by relpath.
456
286
        
457
287
        :param create: If not True, only open an existing knit.
458
 
        :param create_parent_dir: If True, create the parent directory if 
459
 
            creating the file fails. (This is used for stores with 
460
 
            hash-prefixes that may not exist yet)
461
 
        :param delay_create: The calling code is aware that the knit won't 
462
 
            actually be created until the first data is stored.
463
 
        :param index: An index to use for the knit.
464
288
        """
 
289
        if deprecated_passed(basis_knit):
 
290
            warnings.warn("KnitVersionedFile.__(): The basis_knit parameter is"
 
291
                 " deprecated as of bzr 0.9.",
 
292
                 DeprecationWarning, stacklevel=2)
465
293
        if access_mode is None:
466
294
            access_mode = 'w'
467
295
        super(KnitVersionedFile, self).__init__(access_mode)
472
300
        self.writable = (access_mode == 'w')
473
301
        self.delta = delta
474
302
 
475
 
        self._max_delta_chain = 200
476
 
 
477
 
        if index is None:
478
 
            self._index = _KnitIndex(transport, relpath + INDEX_SUFFIX,
479
 
                access_mode, create=create, file_mode=file_mode,
480
 
                create_parent_dir=create_parent_dir, delay_create=delay_create,
481
 
                dir_mode=dir_mode)
482
 
        else:
483
 
            self._index = index
484
 
        if access_method is None:
485
 
            _access = _KnitAccess(transport, relpath + DATA_SUFFIX, file_mode, dir_mode,
486
 
                ((create and not len(self)) and delay_create), create_parent_dir)
487
 
        else:
488
 
            _access = access_method
489
 
        if create and not len(self) and not delay_create:
490
 
            _access.create()
491
 
        self._data = _KnitData(_access)
 
303
        self._index = _KnitIndex(transport, relpath + INDEX_SUFFIX,
 
304
            access_mode, create=create, file_mode=file_mode)
 
305
        self._data = _KnitData(transport, relpath + DATA_SUFFIX,
 
306
            access_mode, create=create and not len(self), file_mode=file_mode)
492
307
 
493
308
    def __repr__(self):
494
 
        return '%s(%s)' % (self.__class__.__name__,
 
309
        return '%s(%s)' % (self.__class__.__name__, 
495
310
                           self.transport.abspath(self.filename))
496
311
    
497
 
    def _check_should_delta(self, first_parents):
498
 
        """Iterate back through the parent listing, looking for a fulltext.
499
 
 
500
 
        This is used when we want to decide whether to add a delta or a new
501
 
        fulltext. It searches for _max_delta_chain parents. When it finds a
502
 
        fulltext parent, it sees if the total size of the deltas leading up to
503
 
        it is large enough to indicate that we want a new full text anyway.
504
 
 
505
 
        Return True if we should create a new delta, False if we should use a
506
 
        full text.
507
 
        """
508
 
        delta_size = 0
509
 
        fulltext_size = None
510
 
        delta_parents = first_parents
511
 
        for count in xrange(self._max_delta_chain):
512
 
            parent = delta_parents[0]
513
 
            method = self._index.get_method(parent)
514
 
            index, pos, size = self._index.get_position(parent)
515
 
            if method == 'fulltext':
516
 
                fulltext_size = size
517
 
                break
518
 
            delta_size += size
519
 
            delta_parents = self._index.get_parents(parent)
520
 
        else:
521
 
            # We couldn't find a fulltext, so we must create a new one
522
 
            return False
523
 
 
524
 
        return fulltext_size > delta_size
 
312
    def _add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
 
313
        """See VersionedFile._add_delta()."""
 
314
        self._check_add(version_id, []) # should we check the lines ?
 
315
        self._check_versions_present(parents)
 
316
        present_parents = []
 
317
        ghosts = []
 
318
        parent_texts = {}
 
319
        for parent in parents:
 
320
            if not self.has_version(parent):
 
321
                ghosts.append(parent)
 
322
            else:
 
323
                present_parents.append(parent)
 
324
 
 
325
        if delta_parent is None:
 
326
            # reconstitute as full text.
 
327
            assert len(delta) == 1 or len(delta) == 0
 
328
            if len(delta):
 
329
                assert delta[0][0] == 0
 
330
                assert delta[0][1] == 0, delta[0][1]
 
331
            return super(KnitVersionedFile, self)._add_delta(version_id,
 
332
                                                             parents,
 
333
                                                             delta_parent,
 
334
                                                             sha1,
 
335
                                                             noeol,
 
336
                                                             delta)
 
337
 
 
338
        digest = sha1
 
339
 
 
340
        options = []
 
341
        if noeol:
 
342
            options.append('no-eol')
 
343
 
 
344
        if delta_parent is not None:
 
345
            # determine the current delta chain length.
 
346
            # To speed the extract of texts the delta chain is limited
 
347
            # to a fixed number of deltas.  This should minimize both
 
348
            # I/O and the time spend applying deltas.
 
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.
 
361
                return super(KnitVersionedFile, self)._add_delta(version_id,
 
362
                                                                 parents,
 
363
                                                                 delta_parent,
 
364
                                                                 sha1,
 
365
                                                                 noeol,
 
366
                                                                 delta)
 
367
 
 
368
        options.append('line-delta')
 
369
        store_lines = self.factory.lower_line_delta(delta)
 
370
 
 
371
        where, size = self._data.add_record(version_id, digest, store_lines)
 
372
        self._index.add_version(version_id, options, where, size, parents)
525
373
 
526
374
    def _add_raw_records(self, records, data):
527
375
        """Add all the records 'records' with data pre-joined in 'data'.
532
380
                     the preceding records sizes.
533
381
        """
534
382
        # write all the data
535
 
        raw_record_sizes = [record[3] for record in records]
536
 
        positions = self._data.add_raw_records(raw_record_sizes, data)
 
383
        pos = self._data.add_raw_record(data)
537
384
        offset = 0
538
385
        index_entries = []
539
 
        for (version_id, options, parents, size), access_memo in zip(
540
 
            records, positions):
541
 
            index_entries.append((version_id, options, access_memo, parents))
 
386
        for (version_id, options, parents, size) in records:
 
387
            index_entries.append((version_id, options, pos+offset,
 
388
                                  size, parents))
542
389
            if self._data._do_cache:
543
390
                self._data._cache[version_id] = data[offset:offset+size]
544
391
            offset += size
556
403
        """See VersionedFile.copy_to()."""
557
404
        # copy the current index to a temp index to avoid racing with local
558
405
        # writes
559
 
        transport.put_file_non_atomic(name + INDEX_SUFFIX + '.tmp',
560
 
                self.transport.get(self._index._filename))
 
406
        transport.put(name + INDEX_SUFFIX + '.tmp', self.transport.get(self._index._filename),)
561
407
        # copy the data file
562
408
        f = self._data._open_file()
563
409
        try:
564
 
            transport.put_file(name + DATA_SUFFIX, f)
 
410
            transport.put(name + DATA_SUFFIX, f)
565
411
        finally:
566
412
            f.close()
567
413
        # move the copied index into place
568
414
        transport.move(name + INDEX_SUFFIX + '.tmp', name + INDEX_SUFFIX)
569
415
 
570
416
    def create_empty(self, name, transport, mode=None):
571
 
        return KnitVersionedFile(name, transport, factory=self.factory,
572
 
                                 delta=self.delta, create=True)
 
417
        return KnitVersionedFile(name, transport, factory=self.factory, delta=self.delta, create=True)
573
418
    
574
 
    def get_data_stream(self, required_versions):
575
 
        """Get a data stream for the specified versions.
576
 
 
577
 
        Versions may be returned in any order, not necessarily the order
578
 
        specified.
579
 
 
580
 
        :param required_versions: The exact set of versions to be extracted.
581
 
            Unlike some other knit methods, this is not used to generate a
582
 
            transitive closure, rather it is used precisely as given.
 
419
    def _fix_parents(self, version, new_parents):
 
420
        """Fix the parents list for version.
583
421
        
584
 
        :returns: format_signature, list of (version, options, length, parents),
585
 
            reader_callable.
 
422
        This is done by appending a new version to the index
 
423
        with identical data except for the parents list.
 
424
        the parents list must be a superset of the current
 
425
        list.
586
426
        """
587
 
        if not isinstance(required_versions, set):
588
 
            required_versions = set(required_versions)
589
 
        # we don't care about inclusions, the caller cares.
590
 
        # but we need to setup a list of records to visit.
591
 
        for version_id in required_versions:
592
 
            if not self.has_version(version_id):
593
 
                raise RevisionNotPresent(version_id, self.filename)
594
 
        # Pick the desired versions out of the index in oldest-to-newest order
595
 
        version_list = []
596
 
        for version_id in self.versions():
597
 
            if version_id in required_versions:
598
 
                version_list.append(version_id)
599
 
 
600
 
        # create the list of version information for the result
601
 
        copy_queue_records = []
602
 
        copy_set = set()
603
 
        result_version_list = []
604
 
        for version_id in version_list:
605
 
            options = self._index.get_options(version_id)
606
 
            parents = self._index.get_parents_with_ghosts(version_id)
607
 
            index_memo = self._index.get_position(version_id)
608
 
            copy_queue_records.append((version_id, index_memo))
609
 
            none, data_pos, data_size = index_memo
610
 
            copy_set.add(version_id)
611
 
            # version, options, length, parents
612
 
            result_version_list.append((version_id, options, data_size,
613
 
                parents))
614
 
 
615
 
        # Read the compressed record data.
616
 
        # XXX:
617
 
        # From here down to the return should really be logic in the returned
618
 
        # callable -- in a class that adapts read_records_iter_raw to read
619
 
        # requests.
620
 
        raw_datum = []
621
 
        for (version_id, raw_data), \
622
 
            (version_id2, options, _, parents) in \
623
 
            izip(self._data.read_records_iter_raw(copy_queue_records),
624
 
                 result_version_list):
625
 
            assert version_id == version_id2, 'logic error, inconsistent results'
626
 
            raw_datum.append(raw_data)
627
 
        pseudo_file = StringIO(''.join(raw_datum))
628
 
        def read(length):
629
 
            if length is None:
630
 
                return pseudo_file.read()
631
 
            else:
632
 
                return pseudo_file.read(length)
633
 
        return (self.get_format_signature(), result_version_list, read)
634
 
 
635
 
    def _extract_blocks(self, version_id, source, target):
636
 
        if self._index.get_method(version_id) != 'line-delta':
637
 
            return None
638
 
        parent, sha1, noeol, delta = self.get_delta(version_id)
639
 
        return KnitContent.get_line_delta_blocks(delta, source, target)
 
427
        current_values = self._index._cache[version]
 
428
        assert set(current_values[4]).difference(set(new_parents)) == set()
 
429
        self._index.add_version(version,
 
430
                                current_values[1], 
 
431
                                current_values[2],
 
432
                                current_values[3],
 
433
                                new_parents)
640
434
 
641
435
    def get_delta(self, version_id):
642
436
        """Get a delta for constructing version from some other version."""
643
 
        self.check_not_reserved_id(version_id)
 
437
        if not self.has_version(version_id):
 
438
            raise RevisionNotPresent(version_id, self.filename)
 
439
        
644
440
        parents = self.get_parents(version_id)
645
441
        if len(parents):
646
442
            parent = parents[0]
647
443
        else:
648
444
            parent = None
649
 
        index_memo = self._index.get_position(version_id)
650
 
        data, sha1 = self._data.read_records(((version_id, index_memo),))[version_id]
 
445
        data_pos, data_size = self._index.get_position(version_id)
 
446
        data, sha1 = self._data.read_records(((version_id, data_pos, data_size),))[version_id]
 
447
        version_idx = self._index.lookup(version_id)
651
448
        noeol = 'no-eol' in self._index.get_options(version_id)
652
449
        if 'fulltext' == self._index.get_method(version_id):
653
 
            new_content = self.factory.parse_fulltext(data, version_id)
 
450
            new_content = self.factory.parse_fulltext(data, version_idx)
654
451
            if parent is not None:
655
452
                reference_content = self._get_content(parent)
656
453
                old_texts = reference_content.text()
657
454
            else:
658
455
                old_texts = []
659
456
            new_texts = new_content.text()
660
 
            delta_seq = patiencediff.PatienceSequenceMatcher(None, old_texts,
661
 
                                                             new_texts)
 
457
            delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
662
458
            return parent, sha1, noeol, self._make_line_delta(delta_seq, new_content)
663
459
        else:
664
 
            delta = self.factory.parse_line_delta(data, version_id)
 
460
            delta = self.factory.parse_line_delta(data, version_idx)
665
461
            return parent, sha1, noeol, delta
666
 
 
667
 
    def get_format_signature(self):
668
 
        """See VersionedFile.get_format_signature()."""
669
 
        if self.factory.annotated:
670
 
            annotated_part = "annotated"
671
 
        else:
672
 
            annotated_part = "plain"
673
 
        return "knit-%s" % (annotated_part,)
674
462
        
675
463
    def get_graph_with_ghosts(self):
676
464
        """See VersionedFile.get_graph_with_ghosts()."""
678
466
        return dict(graph_items)
679
467
 
680
468
    def get_sha1(self, version_id):
681
 
        return self.get_sha1s([version_id])[0]
682
 
 
683
 
    def get_sha1s(self, version_ids):
684
469
        """See VersionedFile.get_sha1()."""
685
 
        record_map = self._get_record_map(version_ids)
686
 
        # record entry 2 is the 'digest'.
687
 
        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 
688
473
 
689
474
    @staticmethod
690
475
    def get_suffixes():
705
490
                        return True
706
491
        return False
707
492
 
708
 
    def insert_data_stream(self, (format, data_list, reader_callable)):
709
 
        """Insert knit records from a data stream into this knit.
710
 
 
711
 
        If a version in the stream is already present in this knit, it will not
712
 
        be inserted a second time.  It will be checked for consistency with the
713
 
        stored version however, and may cause a KnitCorrupt error to be raised
714
 
        if the data in the stream disagrees with the already stored data.
715
 
        
716
 
        :seealso: get_data_stream
717
 
        """
718
 
        if format != self.get_format_signature():
719
 
            trace.mutter('incompatible format signature inserting to %r', self)
720
 
            raise KnitDataStreamIncompatible(
721
 
                format, self.get_format_signature())
722
 
 
723
 
        for version_id, options, length, parents in data_list:
724
 
            if self.has_version(version_id):
725
 
                # First check: the list of parents.
726
 
                my_parents = self.get_parents_with_ghosts(version_id)
727
 
                if my_parents != parents:
728
 
                    # XXX: KnitCorrupt is not quite the right exception here.
729
 
                    raise KnitCorrupt(
730
 
                        self.filename,
731
 
                        'parents list %r from data stream does not match '
732
 
                        'already recorded parents %r for %s'
733
 
                        % (parents, my_parents, version_id))
734
 
 
735
 
                # Also check the SHA-1 of the fulltext this content will
736
 
                # produce.
737
 
                raw_data = reader_callable(length)
738
 
                my_fulltext_sha1 = self.get_sha1(version_id)
739
 
                df, rec = self._data._parse_record_header(version_id, raw_data)
740
 
                stream_fulltext_sha1 = rec[3]
741
 
                if my_fulltext_sha1 != stream_fulltext_sha1:
742
 
                    # Actually, we don't know if it's this knit that's corrupt,
743
 
                    # or the data stream we're trying to insert.
744
 
                    raise KnitCorrupt(
745
 
                        self.filename, 'sha-1 does not match %s' % version_id)
746
 
            else:
747
 
                if 'line-delta' in options:
748
 
                    # Make sure that this knit record is actually useful: a
749
 
                    # line-delta is no use unless we have its parent.
750
 
                    # Fetching from a broken repository with this problem
751
 
                    # shouldn't break the target repository.
752
 
                    if not self._index.has_version(parents[0]):
753
 
                        raise KnitCorrupt(
754
 
                            self.filename,
755
 
                            'line-delta from stream references '
756
 
                            'missing parent %s' % parents[0])
757
 
                self._add_raw_records(
758
 
                    [(version_id, options, parents, length)],
759
 
                    reader_callable(length))
760
 
 
761
493
    def versions(self):
762
494
        """See VersionedFile.versions."""
763
 
        if 'evil' in debug.debug_flags:
764
 
            trace.mutter_callsite(2, "versions scales with size of history")
765
495
        return self._index.get_versions()
766
496
 
767
497
    def has_version(self, version_id):
768
498
        """See VersionedFile.has_version."""
769
 
        if 'evil' in debug.debug_flags:
770
 
            trace.mutter_callsite(2, "has_version is a LBYL scenario")
771
499
        return self._index.has_version(version_id)
772
500
 
773
501
    __contains__ = has_version
774
502
 
775
503
    def _merge_annotations(self, content, parents, parent_texts={},
776
 
                           delta=None, annotated=None,
777
 
                           left_matching_blocks=None):
 
504
                           delta=None, annotated=None):
778
505
        """Merge annotations for content.  This is done by comparing
779
506
        the annotations based on changed to the text.
780
507
        """
781
 
        if left_matching_blocks is not None:
782
 
            delta_seq = diff._PrematchedMatcher(left_matching_blocks)
783
 
        else:
 
508
        if annotated:
784
509
            delta_seq = None
785
 
        if annotated:
786
510
            for parent_id in parents:
787
511
                merge_content = self._get_content(parent_id, parent_texts)
788
 
                if (parent_id == parents[0] and delta_seq is not None):
789
 
                    seq = delta_seq
790
 
                else:
791
 
                    seq = patiencediff.PatienceSequenceMatcher(
792
 
                        None, merge_content.text(), content.text())
 
512
                seq = KnitSequenceMatcher(None, merge_content.text(), content.text())
 
513
                if delta_seq is None:
 
514
                    # setup a delta seq to reuse.
 
515
                    delta_seq = seq
793
516
                for i, j, n in seq.get_matching_blocks():
794
517
                    if n == 0:
795
518
                        continue
796
 
                    # this appears to copy (origin, text) pairs across to the
797
 
                    # new content for any line that matches the last-checked
798
 
                    # parent.
 
519
                    # this appears to copy (origin, text) pairs across to the new
 
520
                    # content for any line that matches the last-checked parent.
 
521
                    # FIXME: save the sequence control data for delta compression
 
522
                    # against the most relevant parent rather than rediffing.
799
523
                    content._lines[j:j+n] = merge_content._lines[i:i+n]
800
524
        if delta:
801
 
            if delta_seq is None:
 
525
            if not annotated:
802
526
                reference_content = self._get_content(parents[0], parent_texts)
803
527
                new_texts = content.text()
804
528
                old_texts = reference_content.text()
805
 
                delta_seq = patiencediff.PatienceSequenceMatcher(
806
 
                                                 None, old_texts, new_texts)
 
529
                delta_seq = KnitSequenceMatcher(None, old_texts, new_texts)
807
530
            return self._make_line_delta(delta_seq, content)
808
531
 
809
532
    def _make_line_delta(self, delta_seq, new_content):
837
560
                    next = None
838
561
                else:
839
562
                    next = self.get_parents(cursor)[0]
840
 
                index_memo = self._index.get_position(cursor)
841
 
                component_data[cursor] = (method, index_memo, next)
 
563
                data_pos, data_size = self._index.get_position(cursor)
 
564
                component_data[cursor] = (method, data_pos, data_size, next)
842
565
                cursor = next
843
566
        return component_data
844
567
       
845
568
    def _get_content(self, version_id, parent_texts={}):
846
569
        """Returns a content object that makes up the specified
847
570
        version."""
 
571
        if not self.has_version(version_id):
 
572
            raise RevisionNotPresent(version_id, self.filename)
 
573
 
848
574
        cached_version = parent_texts.get(version_id, None)
849
575
        if cached_version is not None:
850
 
            if not self.has_version(version_id):
851
 
                raise RevisionNotPresent(version_id, self.filename)
852
576
            return cached_version
853
577
 
854
578
        text_map, contents_map = self._get_content_maps([version_id])
856
580
 
857
581
    def _check_versions_present(self, version_ids):
858
582
        """Check that all specified versions are present."""
859
 
        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)
860
589
 
861
 
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
862
 
        nostore_sha, random_id, check_content):
 
590
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts):
863
591
        """See VersionedFile.add_lines_with_ghosts()."""
864
 
        self._check_add(version_id, lines, random_id, check_content)
865
 
        return self._add(version_id, lines, parents, self.delta,
866
 
            parent_texts, None, nostore_sha, random_id)
 
592
        self._check_add(version_id, lines)
 
593
        return self._add(version_id, lines[:], parents, self.delta, parent_texts)
867
594
 
868
 
    def _add_lines(self, version_id, parents, lines, parent_texts,
869
 
        left_matching_blocks, nostore_sha, random_id, check_content):
 
595
    def _add_lines(self, version_id, parents, lines, parent_texts):
870
596
        """See VersionedFile.add_lines."""
871
 
        self._check_add(version_id, lines, random_id, check_content)
 
597
        self._check_add(version_id, lines)
872
598
        self._check_versions_present(parents)
873
 
        return self._add(version_id, lines[:], parents, self.delta,
874
 
            parent_texts, left_matching_blocks, nostore_sha, random_id)
 
599
        return self._add(version_id, lines[:], parents, self.delta, parent_texts)
875
600
 
876
 
    def _check_add(self, version_id, lines, random_id, check_content):
 
601
    def _check_add(self, version_id, lines):
877
602
        """check that version_id and lines are safe to add."""
 
603
        assert self.writable, "knit is not opened for write"
 
604
        ### FIXME escape. RBC 20060228
878
605
        if contains_whitespace(version_id):
879
606
            raise InvalidRevisionId(version_id, self.filename)
880
 
        self.check_not_reserved_id(version_id)
881
 
        # Technically this could be avoided if we are happy to allow duplicate
882
 
        # id insertion when other things than bzr core insert texts, but it
883
 
        # seems useful for folk using the knit api directly to have some safety
884
 
        # blanket that we can disable.
885
 
        if not random_id and self.has_version(version_id):
 
607
        if self.has_version(version_id):
886
608
            raise RevisionAlreadyPresent(version_id, self.filename)
887
 
        if check_content:
888
 
            self._check_lines_not_unicode(lines)
889
 
            self._check_lines_are_lines(lines)
 
609
        self._check_lines_not_unicode(lines)
 
610
        self._check_lines_are_lines(lines)
890
611
 
891
 
    def _add(self, version_id, lines, parents, delta, parent_texts,
892
 
        left_matching_blocks, nostore_sha, random_id):
 
612
    def _add(self, version_id, lines, parents, delta, parent_texts):
893
613
        """Add a set of lines on top of version specified by parents.
894
614
 
895
615
        If delta is true, compress the text as a line-delta against
897
617
 
898
618
        Any versions not present will be converted into ghosts.
899
619
        """
900
 
        # first thing, if the content is something we don't need to store, find
901
 
        # that out.
902
 
        line_bytes = ''.join(lines)
903
 
        digest = sha_string(line_bytes)
904
 
        if nostore_sha == digest:
905
 
            raise errors.ExistingContent
 
620
        #  461    0   6546.0390     43.9100   bzrlib.knit:489(_add)
 
621
        # +400    0    889.4890    418.9790   +bzrlib.knit:192(lower_fulltext)
 
622
        # +461    0   1364.8070    108.8030   +bzrlib.knit:996(add_record)
 
623
        # +461    0    193.3940     41.5720   +bzrlib.knit:898(add_version)
 
624
        # +461    0    134.0590     18.3810   +bzrlib.osutils:361(sha_strings)
 
625
        # +461    0     36.3420     15.4540   +bzrlib.knit:146(make)
 
626
        # +1383   0      8.0370      8.0370   +<len>
 
627
        # +61     0     13.5770      7.9190   +bzrlib.knit:199(lower_line_delta)
 
628
        # +61     0    963.3470      7.8740   +bzrlib.knit:427(_get_content)
 
629
        # +61     0    973.9950      5.2950   +bzrlib.knit:136(line_delta)
 
630
        # +61     0   1918.1800      5.2640   +bzrlib.knit:359(_merge_annotations)
906
631
 
907
632
        present_parents = []
 
633
        ghosts = []
908
634
        if parent_texts is None:
909
635
            parent_texts = {}
910
636
        for parent in parents:
911
 
            if self.has_version(parent):
 
637
            if not self.has_version(parent):
 
638
                ghosts.append(parent)
 
639
            else:
912
640
                present_parents.append(parent)
913
641
 
914
 
        # can only compress against the left most present parent.
915
 
        if (delta and
916
 
            (len(present_parents) == 0 or
917
 
             present_parents[0] != parents[0])):
 
642
        if delta and not len(present_parents):
918
643
            delta = False
919
644
 
920
 
        text_length = len(line_bytes)
 
645
        digest = sha_strings(lines)
921
646
        options = []
922
647
        if lines:
923
648
            if lines[-1][-1] != '\n':
924
 
                # copy the contents of lines.
925
 
                lines = lines[:]
926
649
                options.append('no-eol')
927
650
                lines[-1] = lines[-1] + '\n'
928
 
                line_bytes += '\n'
929
651
 
930
 
        if delta:
 
652
        if len(present_parents) and delta:
931
653
            # To speed the extract of texts the delta chain is limited
932
654
            # to a fixed number of deltas.  This should minimize both
933
655
            # I/O and the time spend applying deltas.
934
 
            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
935
667
 
936
 
        assert isinstance(version_id, str)
937
 
        content = self.factory.make(lines, version_id)
 
668
        lines = self.factory.make(lines, version_id)
938
669
        if delta or (self.factory.annotated and len(present_parents) > 0):
939
 
            # Merge annotations from parent texts if needed.
940
 
            delta_hunks = self._merge_annotations(content, present_parents,
941
 
                parent_texts, delta, self.factory.annotated,
942
 
                left_matching_blocks)
 
670
            # Merge annotations from parent texts if so is needed.
 
671
            delta_hunks = self._merge_annotations(lines, present_parents, parent_texts,
 
672
                                                  delta, self.factory.annotated)
943
673
 
944
674
        if delta:
945
675
            options.append('line-delta')
946
676
            store_lines = self.factory.lower_line_delta(delta_hunks)
947
 
            size, bytes = self._data._record_to_data(version_id, digest,
948
 
                store_lines)
949
677
        else:
950
678
            options.append('fulltext')
951
 
            # isinstance is slower and we have no hierarchy.
952
 
            if self.factory.__class__ == KnitPlainFactory:
953
 
                # Use the already joined bytes saving iteration time in
954
 
                # _record_to_data.
955
 
                size, bytes = self._data._record_to_data(version_id, digest,
956
 
                    lines, [line_bytes])
957
 
            else:
958
 
                # get mixed annotation + content and feed it into the
959
 
                # serialiser.
960
 
                store_lines = self.factory.lower_fulltext(content)
961
 
                size, bytes = self._data._record_to_data(version_id, digest,
962
 
                    store_lines)
 
679
            store_lines = self.factory.lower_fulltext(lines)
963
680
 
964
 
        access_memo = self._data.add_raw_records([size], bytes)[0]
965
 
        self._index.add_versions(
966
 
            ((version_id, options, access_memo, parents),),
967
 
            random_id=random_id)
968
 
        return digest, text_length, content
 
681
        where, size = self._data.add_record(version_id, digest, store_lines)
 
682
        self._index.add_version(version_id, options, where, size, parents)
 
683
        return lines
969
684
 
970
685
    def check(self, progress_bar=None):
971
686
        """See VersionedFile.check()."""
992
707
        If the method is fulltext, next will be None.
993
708
        """
994
709
        position_map = self._get_components_positions(version_ids)
995
 
        # c = component_id, m = method, i_m = index_memo, n = next
996
 
        records = [(c, i_m) for c, (m, i_m, n) in position_map.iteritems()]
 
710
        # c = component_id, m = method, p = position, s = size, n = next
 
711
        records = [(c, p, s) for c, (m, p, s, n) in position_map.iteritems()]
997
712
        record_map = {}
998
713
        for component_id, content, digest in \
999
714
                self._data.read_records_iter(records):
1000
 
            method, index_memo, next = position_map[component_id]
 
715
            method, position, size, next = position_map[component_id]
1001
716
            record_map[component_id] = method, content, digest, next
1002
717
                          
1003
718
        return record_map
1011
726
 
1012
727
    def get_line_list(self, version_ids):
1013
728
        """Return the texts of listed versions as a list of strings."""
1014
 
        for version_id in version_ids:
1015
 
            self.check_not_reserved_id(version_id)
1016
729
        text_map, content_map = self._get_content_maps(version_ids)
1017
730
        return [text_map[v] for v in version_ids]
1018
731
 
1019
 
    _get_lf_split_line_list = get_line_list
1020
 
 
1021
732
    def _get_content_maps(self, version_ids):
1022
733
        """Produce maps of text and KnitContents
1023
734
        
1025
736
        the requested versions and content_map contains the KnitContents.
1026
737
        Both dicts take version_ids as their keys.
1027
738
        """
1028
 
        # FUTURE: This function could be improved for the 'extract many' case
1029
 
        # by tracking each component and only doing the copy when the number of
1030
 
        # children than need to apply delta's to it is > 1 or it is part of the
1031
 
        # final output.
1032
 
        version_ids = list(version_ids)
1033
 
        multiple_versions = len(version_ids) != 1
 
739
        for version_id in version_ids:
 
740
            if not self.has_version(version_id):
 
741
                raise RevisionNotPresent(version_id, self.filename)
1034
742
        record_map = self._get_record_map(version_ids)
1035
743
 
1036
744
        text_map = {}
1051
759
                if component_id in content_map:
1052
760
                    content = content_map[component_id]
1053
761
                else:
 
762
                    version_idx = self._index.lookup(component_id)
1054
763
                    if method == 'fulltext':
1055
764
                        assert content is None
1056
 
                        content = self.factory.parse_fulltext(data, version_id)
 
765
                        content = self.factory.parse_fulltext(data, version_idx)
1057
766
                    elif method == 'line-delta':
1058
 
                        delta = self.factory.parse_line_delta(data, version_id)
1059
 
                        if multiple_versions:
1060
 
                            # only doing this when we want multiple versions
1061
 
                            # output avoids list copies - which reference and
1062
 
                            # dereference many strings.
1063
 
                            content = content.copy()
1064
 
                        content.apply_delta(delta, version_id)
1065
 
                    if multiple_versions:
1066
 
                        content_map[component_id] = content
 
767
                        delta = self.factory.parse_line_delta(data[:], 
 
768
                                                              version_idx)
 
769
                        content = content.copy()
 
770
                        content._lines = self._apply_delta(content._lines, 
 
771
                                                           delta)
 
772
                    content_map[component_id] = content
1067
773
 
1068
774
            if 'no-eol' in self._index.get_options(version_id):
1069
 
                if multiple_versions:
1070
 
                    content = content.copy()
1071
 
                content.strip_last_line_newline()
 
775
                content = content.copy()
 
776
                line = content._lines[-1][1].rstrip('\n')
 
777
                content._lines[-1] = (content._lines[-1][0], line)
1072
778
            final_content[version_id] = content
1073
779
 
1074
780
            # digest here is the digest from the last applied component.
1075
781
            text = content.text()
1076
 
            actual_sha = sha_strings(text)
1077
 
            if actual_sha != digest:
1078
 
                raise KnitCorrupt(self.filename,
1079
 
                    '\n  sha-1 %s'
1080
 
                    '\n  of reconstructed text does not match'
1081
 
                    '\n  expected %s'
1082
 
                    '\n  for version %s' %
1083
 
                    (actual_sha, digest, version_id))
1084
 
            text_map[version_id] = text
1085
 
        return text_map, final_content
1086
 
 
1087
 
    def iter_lines_added_or_present_in_versions(self, version_ids=None, 
1088
 
                                                pb=None):
 
782
            if sha_strings(text) != digest:
 
783
                raise KnitCorrupt(self.filename, 
 
784
                                  'sha-1 does not match %s' % version_id)
 
785
 
 
786
            text_map[version_id] = text 
 
787
        return text_map, final_content 
 
788
 
 
789
    def iter_lines_added_or_present_in_versions(self, version_ids=None):
1089
790
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
1090
791
        if version_ids is None:
1091
792
            version_ids = self.versions()
1092
 
        if pb is None:
1093
 
            pb = progress.DummyProgress()
1094
793
        # we don't care about inclusions, the caller cares.
1095
794
        # but we need to setup a list of records to visit.
1096
795
        # we need version_id, position, length
1097
796
        version_id_records = []
1098
 
        requested_versions = set(version_ids)
 
797
        requested_versions = list(version_ids)
1099
798
        # filter for available versions
1100
799
        for version_id in requested_versions:
1101
800
            if not self.has_version(version_id):
1102
801
                raise RevisionNotPresent(version_id, self.filename)
1103
802
        # get a in-component-order queue:
 
803
        version_ids = []
1104
804
        for version_id in self.versions():
1105
805
            if version_id in requested_versions:
1106
 
                index_memo = self._index.get_position(version_id)
1107
 
                version_id_records.append((version_id, index_memo))
 
806
                version_ids.append(version_id)
 
807
                data_pos, length = self._index.get_position(version_id)
 
808
                version_id_records.append((version_id, data_pos, length))
1108
809
 
 
810
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
811
        count = 0
1109
812
        total = len(version_id_records)
1110
 
        for version_idx, (version_id, data, sha_value) in \
1111
 
            enumerate(self._data.read_records_iter(version_id_records)):
1112
 
            pb.update('Walking content.', version_idx, total)
1113
 
            method = self._index.get_method(version_id)
1114
 
 
1115
 
            assert method in ('fulltext', 'line-delta')
1116
 
            if method == 'fulltext':
1117
 
                line_iterator = self.factory.get_fulltext_content(data)
1118
 
            else:
1119
 
                line_iterator = self.factory.get_linedelta_content(data)
1120
 
            for line in line_iterator:
1121
 
                yield line
1122
 
 
1123
 
        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
1124
837
        
1125
 
    def iter_parents(self, version_ids):
1126
 
        """Iterate through the parents for many version ids.
1127
 
 
1128
 
        :param version_ids: An iterable yielding version_ids.
1129
 
        :return: An iterator that yields (version_id, parents). Requested 
1130
 
            version_ids not present in the versioned file are simply skipped.
1131
 
            The order is undefined, allowing for different optimisations in
1132
 
            the underlying implementation.
1133
 
        """
1134
 
        return self._index.iter_parents(version_ids)
1135
 
 
1136
838
    def num_versions(self):
1137
839
        """See VersionedFile.num_versions()."""
1138
840
        return self._index.num_versions()
1141
843
 
1142
844
    def annotate_iter(self, version_id):
1143
845
        """See VersionedFile.annotate_iter."""
1144
 
        return self.factory.annotate_iter(self, version_id)
 
846
        content = self._get_content(version_id)
 
847
        for origin, text in content.annotate_iter():
 
848
            yield origin, text
1145
849
 
1146
850
    def get_parents(self, version_id):
1147
851
        """See VersionedFile.get_parents."""
1160
864
        except KeyError:
1161
865
            raise RevisionNotPresent(version_id, self.filename)
1162
866
 
1163
 
    def get_ancestry(self, versions, topo_sorted=True):
 
867
    def get_ancestry(self, versions):
1164
868
        """See VersionedFile.get_ancestry."""
1165
869
        if isinstance(versions, basestring):
1166
870
            versions = [versions]
1167
871
        if not versions:
1168
872
            return []
1169
 
        return self._index.get_ancestry(versions, topo_sorted)
 
873
        self._check_versions_present(versions)
 
874
        return self._index.get_ancestry(versions)
1170
875
 
1171
876
    def get_ancestry_with_ghosts(self, versions):
1172
877
        """See VersionedFile.get_ancestry_with_ghosts."""
1174
879
            versions = [versions]
1175
880
        if not versions:
1176
881
            return []
 
882
        self._check_versions_present(versions)
1177
883
        return self._index.get_ancestry_with_ghosts(versions)
1178
884
 
 
885
    #@deprecated_method(zero_eight)
 
886
    def walk(self, version_ids):
 
887
        """See VersionedFile.walk."""
 
888
        # We take the short path here, and extract all relevant texts
 
889
        # and put them in a weave and let that do all the work.  Far
 
890
        # from optimal, but is much simpler.
 
891
        # FIXME RB 20060228 this really is inefficient!
 
892
        from bzrlib.weave import Weave
 
893
 
 
894
        w = Weave(self.filename)
 
895
        ancestry = self.get_ancestry(version_ids)
 
896
        sorted_graph = topo_sort(self._index.get_graph())
 
897
        version_list = [vid for vid in sorted_graph if vid in ancestry]
 
898
        
 
899
        for version_id in version_list:
 
900
            lines = self.get_lines(version_id)
 
901
            w.add_lines(version_id, self.get_parents(version_id), lines)
 
902
 
 
903
        for lineno, insert_id, dset, line in w.walk(version_ids):
 
904
            yield lineno, insert_id, dset, line
 
905
 
1179
906
    def plan_merge(self, ver_a, ver_b):
1180
907
        """See VersionedFile.plan_merge."""
1181
 
        ancestors_b = set(self.get_ancestry(ver_b, topo_sorted=False))
1182
 
        ancestors_a = set(self.get_ancestry(ver_a, 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
 
914
        
 
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
 
1183
922
        annotated_a = self.annotate(ver_a)
1184
923
        annotated_b = self.annotate(ver_b)
1185
 
        return merge._plan_annotate_merge(annotated_a, annotated_b,
1186
 
                                          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
1187
944
 
1188
945
 
1189
946
class _KnitComponentFile(object):
1190
947
    """One of the files used to implement a knit database"""
1191
948
 
1192
 
    def __init__(self, transport, filename, mode, file_mode=None,
1193
 
                 create_parent_dir=False, dir_mode=None):
 
949
    def __init__(self, transport, filename, mode, file_mode=None):
1194
950
        self._transport = transport
1195
951
        self._filename = filename
1196
952
        self._mode = mode
1197
 
        self._file_mode = file_mode
1198
 
        self._dir_mode = dir_mode
1199
 
        self._create_parent_dir = create_parent_dir
1200
 
        self._need_to_create = False
 
953
        self._file_mode=file_mode
1201
954
 
1202
 
    def _full_path(self):
1203
 
        """Return the full path to this file."""
1204
 
        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')
1205
959
 
1206
960
    def check_header(self, fp):
1207
961
        line = fp.readline()
1208
 
        if line == '':
1209
 
            # An empty file can actually be treated as though the file doesn't
1210
 
            # exist yet.
1211
 
            raise errors.NoSuchFile(self._full_path())
1212
962
        if line != self.HEADER:
1213
 
            raise KnitHeaderError(badline=line,
1214
 
                              filename=self._transport.abspath(self._filename))
 
963
            raise KnitHeaderError(badline=line)
 
964
 
 
965
    def commit(self):
 
966
        """Commit is a nop."""
1215
967
 
1216
968
    def __repr__(self):
1217
969
        return '%s(%s)' % (self.__class__.__name__, self._filename)
1259
1011
    The ' :' marker is the end of record marker.
1260
1012
    
1261
1013
    partial writes:
1262
 
    when a write is interrupted to the index file, it will result in a line
1263
 
    that does not end in ' :'. If the ' :' is not present at the end of a line,
1264
 
    or at the end of the file, then the record that is missing it will be
1265
 
    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.
1266
1018
 
1267
1019
    When writing new records to the index file, the data is preceded by '\n'
1268
1020
    to ensure that records always start on new lines even if the last write was
1277
1029
 
1278
1030
    def _cache_version(self, version_id, options, pos, size, parents):
1279
1031
        """Cache a version record in the history array and index cache.
1280
 
 
1281
 
        This is inlined into _load_data for performance. KEEP IN SYNC.
 
1032
        
 
1033
        This is inlined into __init__ for performance. KEEP IN SYNC.
1282
1034
        (It saves 60ms, 25% of the __init__ overhead on local 4000 record
1283
1035
         indexes).
1284
1036
        """
1289
1041
            self._history.append(version_id)
1290
1042
        else:
1291
1043
            index = self._cache[version_id][5]
1292
 
        self._cache[version_id] = (version_id,
 
1044
        self._cache[version_id] = (version_id, 
1293
1045
                                   options,
1294
1046
                                   pos,
1295
1047
                                   size,
1296
1048
                                   parents,
1297
1049
                                   index)
1298
1050
 
1299
 
    def __init__(self, transport, filename, mode, create=False, file_mode=None,
1300
 
                 create_parent_dir=False, delay_create=False, dir_mode=None):
1301
 
        _KnitComponentFile.__init__(self, transport, filename, mode,
1302
 
                                    file_mode=file_mode,
1303
 
                                    create_parent_dir=create_parent_dir,
1304
 
                                    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)
1305
1053
        self._cache = {}
1306
1054
        # position in _history is the 'official' index for a revision
1307
1055
        # but the values may have come from a newer entry.
1308
1056
        # so - wc -l of a knit index is != the number of unique names
1309
1057
        # in the knit.
1310
1058
        self._history = []
 
1059
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
1311
1060
        try:
1312
 
            fp = self._transport.get(self._filename)
 
1061
            count = 0
 
1062
            total = 1
1313
1063
            try:
1314
 
                # _load_data may raise NoSuchFile if the target knit is
1315
 
                # completely empty.
1316
 
                _load_data(self, fp)
1317
 
            finally:
1318
 
                fp.close()
1319
 
        except NoSuchFile:
1320
 
            if mode != 'w' or not create:
1321
 
                raise
1322
 
            elif delay_create:
1323
 
                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:])
1324
1147
            else:
1325
 
                self._transport.put_bytes_non_atomic(
1326
 
                    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
1327
1154
 
1328
1155
    def get_graph(self):
1329
 
        """Return a list of the node:parents lists from this knit index."""
1330
 
        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
1331
1160
 
1332
 
    def get_ancestry(self, versions, topo_sorted=True):
 
1161
    def get_ancestry(self, versions):
1333
1162
        """See VersionedFile.get_ancestry."""
1334
1163
        # get a graph of all the mentioned versions:
1335
1164
        graph = {}
1336
1165
        pending = set(versions)
1337
 
        cache = self._cache
1338
 
        while pending:
 
1166
        while len(pending):
1339
1167
            version = pending.pop()
 
1168
            parents = self._cache[version][4]
 
1169
            # got the parents ok
1340
1170
            # trim ghosts
1341
 
            try:
1342
 
                parents = [p for p in cache[version][4] if p in cache]
1343
 
            except KeyError:
1344
 
                raise RevisionNotPresent(version, self._filename)
1345
 
            # if not completed and not a ghost
1346
 
            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)
1347
1176
            graph[version] = parents
1348
 
        if not topo_sorted:
1349
 
            return graph.keys()
1350
1177
        return topo_sort(graph.items())
1351
1178
 
1352
1179
    def get_ancestry_with_ghosts(self, versions):
1353
1180
        """See VersionedFile.get_ancestry_with_ghosts."""
1354
1181
        # get a graph of all the mentioned versions:
1355
 
        self.check_versions_present(versions)
1356
 
        cache = self._cache
1357
1182
        graph = {}
1358
1183
        pending = set(versions)
1359
 
        while pending:
 
1184
        while len(pending):
1360
1185
            version = pending.pop()
1361
1186
            try:
1362
 
                parents = cache[version][4]
 
1187
                parents = self._cache[version][4]
1363
1188
            except KeyError:
1364
1189
                # ghost, fake it
1365
1190
                graph[version] = []
 
1191
                pass
1366
1192
            else:
1367
 
                # if not completed
1368
 
                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)
1369
1197
                graph[version] = parents
1370
1198
        return topo_sort(graph.items())
1371
1199
 
1372
 
    def iter_parents(self, version_ids):
1373
 
        """Iterate through the parents for many version ids.
1374
 
 
1375
 
        :param version_ids: An iterable yielding version_ids.
1376
 
        :return: An iterator that yields (version_id, parents). Requested 
1377
 
            version_ids not present in the versioned file are simply skipped.
1378
 
            The order is undefined, allowing for different optimisations in
1379
 
            the underlying implementation.
1380
 
        """
1381
 
        for version_id in version_ids:
1382
 
            try:
1383
 
                yield version_id, tuple(self.get_parents(version_id))
1384
 
            except KeyError:
1385
 
                pass
1386
 
 
1387
1200
    def num_versions(self):
1388
1201
        return len(self._history)
1389
1202
 
1390
1203
    __len__ = num_versions
1391
1204
 
1392
1205
    def get_versions(self):
1393
 
        """Get all the versions in the file. not topologically sorted."""
1394
1206
        return self._history
1395
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
 
1396
1215
    def _version_list_to_index(self, versions):
 
1216
        encode_utf8 = cache_utf8.encode
1397
1217
        result_list = []
1398
 
        cache = self._cache
1399
1218
        for version in versions:
1400
 
            if version in cache:
 
1219
            if version in self._cache:
1401
1220
                # -- inlined lookup() --
1402
 
                result_list.append(str(cache[version][5]))
 
1221
                result_list.append(str(self._cache[version][5]))
1403
1222
                # -- end lookup () --
1404
1223
            else:
1405
 
                result_list.append('.' + version)
 
1224
                result_list.append('.' + encode_utf8(version))
1406
1225
        return ' '.join(result_list)
1407
1226
 
1408
 
    def add_version(self, version_id, options, index_memo, parents):
 
1227
    def add_version(self, version_id, options, pos, size, parents):
1409
1228
        """Add a version record to the index."""
1410
 
        self.add_versions(((version_id, options, index_memo, parents),))
 
1229
        self.add_versions(((version_id, options, pos, size, parents),))
1411
1230
 
1412
 
    def add_versions(self, versions, random_id=False):
 
1231
    def add_versions(self, versions):
1413
1232
        """Add multiple versions to the index.
1414
1233
        
1415
1234
        :param versions: a list of tuples:
1416
1235
                         (version_id, options, pos, size, parents).
1417
 
        :param random_id: If True the ids being added were randomly generated
1418
 
            and no check for existence will be performed.
1419
1236
        """
1420
1237
        lines = []
1421
 
        orig_history = self._history[:]
1422
 
        orig_cache = self._cache.copy()
1423
 
 
1424
 
        try:
1425
 
            for version_id, options, (index, pos, size), parents in versions:
1426
 
                line = "\n%s %s %s %s %s :" % (version_id,
1427
 
                                               ','.join(options),
1428
 
                                               pos,
1429
 
                                               size,
1430
 
                                               self._version_list_to_index(parents))
1431
 
                assert isinstance(line, str), \
1432
 
                    'content must be utf-8 encoded: %r' % (line,)
1433
 
                lines.append(line)
1434
 
                self._cache_version(version_id, options, pos, size, parents)
1435
 
            if not self._need_to_create:
1436
 
                self._transport.append_bytes(self._filename, ''.join(lines))
1437
 
            else:
1438
 
                sio = StringIO()
1439
 
                sio.write(self.HEADER)
1440
 
                sio.writelines(lines)
1441
 
                sio.seek(0)
1442
 
                self._transport.put_file_non_atomic(self._filename, sio,
1443
 
                                    create_parent_dir=self._create_parent_dir,
1444
 
                                    mode=self._file_mode,
1445
 
                                    dir_mode=self._dir_mode)
1446
 
                self._need_to_create = False
1447
 
        except:
1448
 
            # If any problems happen, restore the original values and re-raise
1449
 
            self._history = orig_history
1450
 
            self._cache = orig_cache
1451
 
            raise
1452
 
 
 
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
        
1453
1256
    def has_version(self, version_id):
1454
1257
        """True if the version is in the index."""
1455
 
        return version_id in self._cache
 
1258
        return self._cache.has_key(version_id)
1456
1259
 
1457
1260
    def get_position(self, version_id):
1458
 
        """Return details needed to access the version.
1459
 
        
1460
 
        .kndx indices do not support split-out data, so return None for the 
1461
 
        index field.
1462
 
 
1463
 
        :return: a tuple (None, data position, size) to hand to the access
1464
 
            logic to get the record.
1465
 
        """
1466
 
        entry = self._cache[version_id]
1467
 
        return None, entry[2], entry[3]
 
1261
        """Return data position and size of specified version."""
 
1262
        return (self._cache[version_id][2], \
 
1263
                self._cache[version_id][3])
1468
1264
 
1469
1265
    def get_method(self, version_id):
1470
1266
        """Return compression method of specified version."""
1471
 
        try:
1472
 
            options = self._cache[version_id][1]
1473
 
        except KeyError:
1474
 
            raise RevisionNotPresent(version_id, self._filename)
 
1267
        options = self._cache[version_id][1]
1475
1268
        if 'fulltext' in options:
1476
1269
            return 'fulltext'
1477
1270
        else:
1478
 
            if 'line-delta' not in options:
1479
 
                raise errors.KnitIndexUnknownMethod(self._full_path(), options)
 
1271
            assert 'line-delta' in options
1480
1272
            return 'line-delta'
1481
1273
 
1482
1274
    def get_options(self, version_id):
1483
 
        """Return a string represention options.
1484
 
 
1485
 
        e.g. foo,bar
1486
 
        """
1487
1275
        return self._cache[version_id][1]
1488
1276
 
1489
1277
    def get_parents(self, version_id):
1497
1285
 
1498
1286
    def check_versions_present(self, version_ids):
1499
1287
        """Check that all specified versions are present."""
1500
 
        cache = self._cache
1501
 
        for version_id in version_ids:
1502
 
            if version_id not in cache:
1503
 
                raise RevisionNotPresent(version_id, self._filename)
1504
 
 
1505
 
 
1506
 
class KnitGraphIndex(object):
1507
 
    """A knit index that builds on GraphIndex."""
1508
 
 
1509
 
    def __init__(self, graph_index, deltas=False, parents=True, add_callback=None):
1510
 
        """Construct a KnitGraphIndex on a graph_index.
1511
 
 
1512
 
        :param graph_index: An implementation of bzrlib.index.GraphIndex.
1513
 
        :param deltas: Allow delta-compressed records.
1514
 
        :param add_callback: If not None, allow additions to the index and call
1515
 
            this callback with a list of added GraphIndex nodes:
1516
 
            [(node, value, node_refs), ...]
1517
 
        :param parents: If True, record knits parents, if not do not record 
1518
 
            parents.
1519
 
        """
1520
 
        self._graph_index = graph_index
1521
 
        self._deltas = deltas
1522
 
        self._add_callback = add_callback
1523
 
        self._parents = parents
1524
 
        if deltas and not parents:
1525
 
            raise KnitCorrupt(self, "Cannot do delta compression without "
1526
 
                "parent tracking.")
1527
 
 
1528
 
    def _get_entries(self, keys, check_present=False):
1529
 
        """Get the entries for keys.
1530
 
        
1531
 
        :param keys: An iterable of index keys, - 1-tuples.
1532
 
        """
1533
 
        keys = set(keys)
1534
 
        found_keys = set()
1535
 
        if self._parents:
1536
 
            for node in self._graph_index.iter_entries(keys):
1537
 
                yield node
1538
 
                found_keys.add(node[1])
1539
 
        else:
1540
 
            # adapt parentless index to the rest of the code.
1541
 
            for node in self._graph_index.iter_entries(keys):
1542
 
                yield node[0], node[1], node[2], ()
1543
 
                found_keys.add(node[1])
1544
 
        if check_present:
1545
 
            missing_keys = keys.difference(found_keys)
1546
 
            if missing_keys:
1547
 
                raise RevisionNotPresent(missing_keys.pop(), self)
1548
 
 
1549
 
    def _present_keys(self, version_ids):
1550
 
        return set([
1551
 
            node[1] for node in self._get_entries(version_ids)])
1552
 
 
1553
 
    def _parentless_ancestry(self, versions):
1554
 
        """Honour the get_ancestry API for parentless knit indices."""
1555
 
        wanted_keys = self._version_ids_to_keys(versions)
1556
 
        present_keys = self._present_keys(wanted_keys)
1557
 
        missing = set(wanted_keys).difference(present_keys)
1558
 
        if missing:
1559
 
            raise RevisionNotPresent(missing.pop(), self)
1560
 
        return list(self._keys_to_version_ids(present_keys))
1561
 
 
1562
 
    def get_ancestry(self, versions, topo_sorted=True):
1563
 
        """See VersionedFile.get_ancestry."""
1564
 
        if not self._parents:
1565
 
            return self._parentless_ancestry(versions)
1566
 
        # XXX: This will do len(history) index calls - perhaps
1567
 
        # it should be altered to be a index core feature?
1568
 
        # get a graph of all the mentioned versions:
1569
 
        graph = {}
1570
 
        ghosts = set()
1571
 
        versions = self._version_ids_to_keys(versions)
1572
 
        pending = set(versions)
1573
 
        while pending:
1574
 
            # get all pending nodes
1575
 
            this_iteration = pending
1576
 
            new_nodes = self._get_entries(this_iteration)
1577
 
            found = set()
1578
 
            pending = set()
1579
 
            for (index, key, value, node_refs) in new_nodes:
1580
 
                # dont ask for ghosties - otherwise
1581
 
                # we we can end up looping with pending
1582
 
                # being entirely ghosted.
1583
 
                graph[key] = [parent for parent in node_refs[0]
1584
 
                    if parent not in ghosts]
1585
 
                # queue parents
1586
 
                for parent in graph[key]:
1587
 
                    # dont examine known nodes again
1588
 
                    if parent in graph:
1589
 
                        continue
1590
 
                    pending.add(parent)
1591
 
                found.add(key)
1592
 
            ghosts.update(this_iteration.difference(found))
1593
 
        if versions.difference(graph):
1594
 
            raise RevisionNotPresent(versions.difference(graph).pop(), self)
1595
 
        if topo_sorted:
1596
 
            result_keys = topo_sort(graph.items())
1597
 
        else:
1598
 
            result_keys = graph.iterkeys()
1599
 
        return [key[0] for key in result_keys]
1600
 
 
1601
 
    def get_ancestry_with_ghosts(self, versions):
1602
 
        """See VersionedFile.get_ancestry."""
1603
 
        if not self._parents:
1604
 
            return self._parentless_ancestry(versions)
1605
 
        # XXX: This will do len(history) index calls - perhaps
1606
 
        # it should be altered to be a index core feature?
1607
 
        # get a graph of all the mentioned versions:
1608
 
        graph = {}
1609
 
        versions = self._version_ids_to_keys(versions)
1610
 
        pending = set(versions)
1611
 
        while pending:
1612
 
            # get all pending nodes
1613
 
            this_iteration = pending
1614
 
            new_nodes = self._get_entries(this_iteration)
1615
 
            pending = set()
1616
 
            for (index, key, value, node_refs) in new_nodes:
1617
 
                graph[key] = node_refs[0]
1618
 
                # queue parents 
1619
 
                for parent in graph[key]:
1620
 
                    # dont examine known nodes again
1621
 
                    if parent in graph:
1622
 
                        continue
1623
 
                    pending.add(parent)
1624
 
            missing_versions = this_iteration.difference(graph)
1625
 
            missing_needed = versions.intersection(missing_versions)
1626
 
            if missing_needed:
1627
 
                raise RevisionNotPresent(missing_needed.pop(), self)
1628
 
            for missing_version in missing_versions:
1629
 
                # add a key, no parents
1630
 
                graph[missing_version] = []
1631
 
                pending.discard(missing_version) # don't look for it
1632
 
        result_keys = topo_sort(graph.items())
1633
 
        return [key[0] for key in result_keys]
1634
 
 
1635
 
    def get_graph(self):
1636
 
        """Return a list of the node:parents lists from this knit index."""
1637
 
        if not self._parents:
1638
 
            return [(key, ()) for key in self.get_versions()]
1639
 
        result = []
1640
 
        for index, key, value, refs in self._graph_index.iter_all_entries():
1641
 
            result.append((key[0], tuple([ref[0] for ref in refs[0]])))
1642
 
        return result
1643
 
 
1644
 
    def iter_parents(self, version_ids):
1645
 
        """Iterate through the parents for many version ids.
1646
 
 
1647
 
        :param version_ids: An iterable yielding version_ids.
1648
 
        :return: An iterator that yields (version_id, parents). Requested 
1649
 
            version_ids not present in the versioned file are simply skipped.
1650
 
            The order is undefined, allowing for different optimisations in
1651
 
            the underlying implementation.
1652
 
        """
1653
 
        if self._parents:
1654
 
            all_nodes = set(self._get_entries(self._version_ids_to_keys(version_ids)))
1655
 
            all_parents = set()
1656
 
            present_parents = set()
1657
 
            for node in all_nodes:
1658
 
                all_parents.update(node[3][0])
1659
 
                # any node we are querying must be present
1660
 
                present_parents.add(node[1])
1661
 
            unknown_parents = all_parents.difference(present_parents)
1662
 
            present_parents.update(self._present_keys(unknown_parents))
1663
 
            for node in all_nodes:
1664
 
                parents = []
1665
 
                for parent in node[3][0]:
1666
 
                    if parent in present_parents:
1667
 
                        parents.append(parent[0])
1668
 
                yield node[1][0], tuple(parents)
1669
 
        else:
1670
 
            for node in self._get_entries(self._version_ids_to_keys(version_ids)):
1671
 
                yield node[1][0], ()
1672
 
 
1673
 
    def num_versions(self):
1674
 
        return len(list(self._graph_index.iter_all_entries()))
1675
 
 
1676
 
    __len__ = num_versions
1677
 
 
1678
 
    def get_versions(self):
1679
 
        """Get all the versions in the file. not topologically sorted."""
1680
 
        return [node[1][0] for node in self._graph_index.iter_all_entries()]
1681
 
    
1682
 
    def has_version(self, version_id):
1683
 
        """True if the version is in the index."""
1684
 
        return len(self._present_keys(self._version_ids_to_keys([version_id]))) == 1
1685
 
 
1686
 
    def _keys_to_version_ids(self, keys):
1687
 
        return tuple(key[0] for key in keys)
1688
 
 
1689
 
    def get_position(self, version_id):
1690
 
        """Return details needed to access the version.
1691
 
        
1692
 
        :return: a tuple (index, data position, size) to hand to the access
1693
 
            logic to get the record.
1694
 
        """
1695
 
        node = self._get_node(version_id)
1696
 
        bits = node[2][1:].split(' ')
1697
 
        return node[0], int(bits[0]), int(bits[1])
1698
 
 
1699
 
    def get_method(self, version_id):
1700
 
        """Return compression method of specified version."""
1701
 
        if not self._deltas:
1702
 
            return 'fulltext'
1703
 
        return self._parent_compression(self._get_node(version_id)[3][1])
1704
 
 
1705
 
    def _parent_compression(self, reference_list):
1706
 
        # use the second reference list to decide if this is delta'd or not.
1707
 
        if len(reference_list):
1708
 
            return 'line-delta'
1709
 
        else:
1710
 
            return 'fulltext'
1711
 
 
1712
 
    def _get_node(self, version_id):
1713
 
        try:
1714
 
            return list(self._get_entries(self._version_ids_to_keys([version_id])))[0]
1715
 
        except IndexError:
1716
 
            raise RevisionNotPresent(version_id, self)
1717
 
 
1718
 
    def get_options(self, version_id):
1719
 
        """Return a string represention options.
1720
 
 
1721
 
        e.g. foo,bar
1722
 
        """
1723
 
        node = self._get_node(version_id)
1724
 
        if not self._deltas:
1725
 
            options = ['fulltext']
1726
 
        else:
1727
 
            options = [self._parent_compression(node[3][1])]
1728
 
        if node[2][0] == 'N':
1729
 
            options.append('no-eol')
1730
 
        return options
1731
 
 
1732
 
    def get_parents(self, version_id):
1733
 
        """Return parents of specified version ignoring ghosts."""
1734
 
        parents = list(self.iter_parents([version_id]))
1735
 
        if not parents:
1736
 
            # missing key
1737
 
            raise errors.RevisionNotPresent(version_id, self)
1738
 
        return parents[0][1]
1739
 
 
1740
 
    def get_parents_with_ghosts(self, version_id):
1741
 
        """Return parents of specified version with ghosts."""
1742
 
        nodes = list(self._get_entries(self._version_ids_to_keys([version_id]),
1743
 
            check_present=True))
1744
 
        if not self._parents:
1745
 
            return ()
1746
 
        return self._keys_to_version_ids(nodes[0][3][0])
1747
 
 
1748
 
    def check_versions_present(self, version_ids):
1749
 
        """Check that all specified versions are present."""
1750
 
        keys = self._version_ids_to_keys(version_ids)
1751
 
        present = self._present_keys(keys)
1752
 
        missing = keys.difference(present)
1753
 
        if missing:
1754
 
            raise RevisionNotPresent(missing.pop(), self)
1755
 
 
1756
 
    def add_version(self, version_id, options, access_memo, parents):
1757
 
        """Add a version record to the index."""
1758
 
        return self.add_versions(((version_id, options, access_memo, parents),))
1759
 
 
1760
 
    def add_versions(self, versions, random_id=False):
1761
 
        """Add multiple versions to the index.
1762
 
        
1763
 
        This function does not insert data into the Immutable GraphIndex
1764
 
        backing the KnitGraphIndex, instead it prepares data for insertion by
1765
 
        the caller and checks that it is safe to insert then calls
1766
 
        self._add_callback with the prepared GraphIndex nodes.
1767
 
 
1768
 
        :param versions: a list of tuples:
1769
 
                         (version_id, options, pos, size, parents).
1770
 
        :param random_id: If True the ids being added were randomly generated
1771
 
            and no check for existence will be performed.
1772
 
        """
1773
 
        if not self._add_callback:
1774
 
            raise errors.ReadOnlyError(self)
1775
 
        # we hope there are no repositories with inconsistent parentage
1776
 
        # anymore.
1777
 
        # check for dups
1778
 
 
1779
 
        keys = {}
1780
 
        for (version_id, options, access_memo, parents) in versions:
1781
 
            index, pos, size = access_memo
1782
 
            key = (version_id, )
1783
 
            parents = tuple((parent, ) for parent in parents)
1784
 
            if 'no-eol' in options:
1785
 
                value = 'N'
1786
 
            else:
1787
 
                value = ' '
1788
 
            value += "%d %d" % (pos, size)
1789
 
            if not self._deltas:
1790
 
                if 'line-delta' in options:
1791
 
                    raise KnitCorrupt(self, "attempt to add line-delta in non-delta knit")
1792
 
            if self._parents:
1793
 
                if self._deltas:
1794
 
                    if 'line-delta' in options:
1795
 
                        node_refs = (parents, (parents[0],))
1796
 
                    else:
1797
 
                        node_refs = (parents, ())
1798
 
                else:
1799
 
                    node_refs = (parents, )
1800
 
            else:
1801
 
                if parents:
1802
 
                    raise KnitCorrupt(self, "attempt to add node with parents "
1803
 
                        "in parentless index.")
1804
 
                node_refs = ()
1805
 
            keys[key] = (value, node_refs)
1806
 
        if not random_id:
1807
 
            present_nodes = self._get_entries(keys)
1808
 
            for (index, key, value, node_refs) in present_nodes:
1809
 
                if (value, node_refs) != keys[key]:
1810
 
                    raise KnitCorrupt(self, "inconsistent details in add_versions"
1811
 
                        ": %s %s" % ((value, node_refs), keys[key]))
1812
 
                del keys[key]
1813
 
        result = []
1814
 
        if self._parents:
1815
 
            for key, (value, node_refs) in keys.iteritems():
1816
 
                result.append((key, value, node_refs))
1817
 
        else:
1818
 
            for key, (value, node_refs) in keys.iteritems():
1819
 
                result.append((key, value))
1820
 
        self._add_callback(result)
1821
 
        
1822
 
    def _version_ids_to_keys(self, version_ids):
1823
 
        return set((version_id, ) for version_id in version_ids)
1824
 
 
1825
 
 
1826
 
class _KnitAccess(object):
1827
 
    """Access to knit records in a .knit file."""
1828
 
 
1829
 
    def __init__(self, transport, filename, _file_mode, _dir_mode,
1830
 
        _need_to_create, _create_parent_dir):
1831
 
        """Create a _KnitAccess for accessing and inserting data.
1832
 
 
1833
 
        :param transport: The transport the .knit is located on.
1834
 
        :param filename: The filename of the .knit.
1835
 
        """
1836
 
        self._transport = transport
1837
 
        self._filename = filename
1838
 
        self._file_mode = _file_mode
1839
 
        self._dir_mode = _dir_mode
1840
 
        self._need_to_create = _need_to_create
1841
 
        self._create_parent_dir = _create_parent_dir
1842
 
 
1843
 
    def add_raw_records(self, sizes, raw_data):
1844
 
        """Add raw knit bytes to a storage area.
1845
 
 
1846
 
        The data is spooled to whereever the access method is storing data.
1847
 
 
1848
 
        :param sizes: An iterable containing the size of each raw data segment.
1849
 
        :param raw_data: A bytestring containing the data.
1850
 
        :return: A list of memos to retrieve the record later. Each memo is a
1851
 
            tuple - (index, pos, length), where the index field is always None
1852
 
            for the .knit access method.
1853
 
        """
1854
 
        assert type(raw_data) == str, \
1855
 
            'data must be plain bytes was %s' % type(raw_data)
1856
 
        if not self._need_to_create:
1857
 
            base = self._transport.append_bytes(self._filename, raw_data)
1858
 
        else:
1859
 
            self._transport.put_bytes_non_atomic(self._filename, raw_data,
1860
 
                                   create_parent_dir=self._create_parent_dir,
1861
 
                                   mode=self._file_mode,
1862
 
                                   dir_mode=self._dir_mode)
1863
 
            self._need_to_create = False
1864
 
            base = 0
1865
 
        result = []
1866
 
        for size in sizes:
1867
 
            result.append((None, base, size))
1868
 
            base += size
1869
 
        return result
1870
 
 
1871
 
    def create(self):
1872
 
        """IFF this data access has its own storage area, initialise it.
1873
 
 
1874
 
        :return: None.
1875
 
        """
1876
 
        self._transport.put_bytes_non_atomic(self._filename, '',
1877
 
                                             mode=self._file_mode)
1878
 
 
1879
 
    def open_file(self):
1880
 
        """IFF this data access can be represented as a single file, open it.
1881
 
 
1882
 
        For knits that are not mapped to a single file on disk this will
1883
 
        always return None.
1884
 
 
1885
 
        :return: None or a file handle.
1886
 
        """
1887
 
        try:
1888
 
            return self._transport.get(self._filename)
1889
 
        except NoSuchFile:
1890
 
            pass
1891
 
        return None
1892
 
 
1893
 
    def get_raw_records(self, memos_for_retrieval):
1894
 
        """Get the raw bytes for a records.
1895
 
 
1896
 
        :param memos_for_retrieval: An iterable containing the (index, pos, 
1897
 
            length) memo for retrieving the bytes. The .knit method ignores
1898
 
            the index as there is always only a single file.
1899
 
        :return: An iterator over the bytes of the records.
1900
 
        """
1901
 
        read_vector = [(pos, size) for (index, pos, size) in memos_for_retrieval]
1902
 
        for pos, data in self._transport.readv(self._filename, read_vector):
1903
 
            yield data
1904
 
 
1905
 
 
1906
 
class _PackAccess(object):
1907
 
    """Access to knit records via a collection of packs."""
1908
 
 
1909
 
    def __init__(self, index_to_packs, writer=None):
1910
 
        """Create a _PackAccess object.
1911
 
 
1912
 
        :param index_to_packs: A dict mapping index objects to the transport
1913
 
            and file names for obtaining data.
1914
 
        :param writer: A tuple (pack.ContainerWriter, write_index) which
1915
 
            contains the pack to write, and the index that reads from it will
1916
 
            be associated with.
1917
 
        """
1918
 
        if writer:
1919
 
            self.container_writer = writer[0]
1920
 
            self.write_index = writer[1]
1921
 
        else:
1922
 
            self.container_writer = None
1923
 
            self.write_index = None
1924
 
        self.indices = index_to_packs
1925
 
 
1926
 
    def add_raw_records(self, sizes, raw_data):
1927
 
        """Add raw knit bytes to a storage area.
1928
 
 
1929
 
        The data is spooled to the container writer in one bytes-record per
1930
 
        raw data item.
1931
 
 
1932
 
        :param sizes: An iterable containing the size of each raw data segment.
1933
 
        :param raw_data: A bytestring containing the data.
1934
 
        :return: A list of memos to retrieve the record later. Each memo is a
1935
 
            tuple - (index, pos, length), where the index field is the 
1936
 
            write_index object supplied to the PackAccess object.
1937
 
        """
1938
 
        assert type(raw_data) == str, \
1939
 
            'data must be plain bytes was %s' % type(raw_data)
1940
 
        result = []
1941
 
        offset = 0
1942
 
        for size in sizes:
1943
 
            p_offset, p_length = self.container_writer.add_bytes_record(
1944
 
                raw_data[offset:offset+size], [])
1945
 
            offset += size
1946
 
            result.append((self.write_index, p_offset, p_length))
1947
 
        return result
1948
 
 
1949
 
    def create(self):
1950
 
        """Pack based knits do not get individually created."""
1951
 
 
1952
 
    def get_raw_records(self, memos_for_retrieval):
1953
 
        """Get the raw bytes for a records.
1954
 
 
1955
 
        :param memos_for_retrieval: An iterable containing the (index, pos, 
1956
 
            length) memo for retrieving the bytes. The Pack access method
1957
 
            looks up the pack to use for a given record in its index_to_pack
1958
 
            map.
1959
 
        :return: An iterator over the bytes of the records.
1960
 
        """
1961
 
        # first pass, group into same-index requests
1962
 
        request_lists = []
1963
 
        current_index = None
1964
 
        for (index, offset, length) in memos_for_retrieval:
1965
 
            if current_index == index:
1966
 
                current_list.append((offset, length))
1967
 
            else:
1968
 
                if current_index is not None:
1969
 
                    request_lists.append((current_index, current_list))
1970
 
                current_index = index
1971
 
                current_list = [(offset, length)]
1972
 
        # handle the last entry
1973
 
        if current_index is not None:
1974
 
            request_lists.append((current_index, current_list))
1975
 
        for index, offsets in request_lists:
1976
 
            transport, path = self.indices[index]
1977
 
            reader = pack.make_readv_reader(transport, path, offsets)
1978
 
            for names, read_func in reader.iter_records():
1979
 
                yield read_func(None)
1980
 
 
1981
 
    def open_file(self):
1982
 
        """Pack based knits have no single file."""
1983
 
        return None
1984
 
 
1985
 
    def set_writer(self, writer, index, (transport, packname)):
1986
 
        """Set a writer to use for adding data."""
1987
 
        if index is not None:
1988
 
            self.indices[index] = (transport, packname)
1989
 
        self.container_writer = writer
1990
 
        self.write_index = index
1991
 
 
1992
 
 
1993
 
class _KnitData(object):
1994
 
    """Manage extraction of data from a KnitAccess, caching and decompressing.
1995
 
    
1996
 
    The KnitData class provides the logic for parsing and using knit records,
1997
 
    making use of an access method for the low level read and write operations.
1998
 
    """
1999
 
 
2000
 
    def __init__(self, access):
2001
 
        """Create a KnitData object.
2002
 
 
2003
 
        :param access: The access method to use. Access methods such as
2004
 
            _KnitAccess manage the insertion of raw records and the subsequent
2005
 
            retrieval of the same.
2006
 
        """
2007
 
        self._access = access
 
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
 
 
1295
 
 
1296
class _KnitData(_KnitComponentFile):
 
1297
    """Contents of the knit data file"""
 
1298
 
 
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)
2008
1303
        self._checked = False
2009
1304
        # TODO: jam 20060713 conceptually, this could spill to disk
2010
1305
        #       if the cached size gets larger than a certain amount
2012
1307
        #       a simple dictionary
2013
1308
        self._cache = {}
2014
1309
        self._do_cache = False
 
1310
        if create:
 
1311
            self._transport.put(self._filename, StringIO(''), mode=file_mode)
2015
1312
 
2016
1313
    def enable_cache(self):
2017
1314
        """Enable caching of reads."""
2023
1320
        self._cache = {}
2024
1321
 
2025
1322
    def _open_file(self):
2026
 
        return self._access.open_file()
 
1323
        try:
 
1324
            return self._transport.get(self._filename)
 
1325
        except NoSuchFile:
 
1326
            pass
 
1327
        return None
2027
1328
 
2028
 
    def _record_to_data(self, version_id, digest, lines, dense_lines=None):
 
1329
    def _record_to_data(self, version_id, digest, lines):
2029
1330
        """Convert version_id, digest, lines into a raw data block.
2030
1331
        
2031
 
        :param dense_lines: The bytes of lines but in a denser form. For
2032
 
            instance, if lines is a list of 1000 bytestrings each ending in \n,
2033
 
            dense_lines may be a list with one line in it, containing all the
2034
 
            1000's lines and their \n's. Using dense_lines if it is already
2035
 
            known is a win because the string join to create bytes in this
2036
 
            function spends less time resizing the final string.
2037
1332
        :return: (len, a StringIO instance with the raw data ready to read.)
2038
1333
        """
2039
 
        # Note: using a string copy here increases memory pressure with e.g.
2040
 
        # ISO's, but it is about 3 seconds faster on a 1.2Ghz intel machine
2041
 
        # when doing the initial commit of a mozilla tree. RBC 20070921
2042
 
        bytes = ''.join(chain(
2043
 
            ["version %s %d %s\n" % (version_id,
 
1334
        sio = StringIO()
 
1335
        data_file = GzipFile(None, mode='wb', fileobj=sio)
 
1336
 
 
1337
        version_id_utf8 = cache_utf8.encode(version_id)
 
1338
        data_file.writelines(chain(
 
1339
            ["version %s %d %s\n" % (version_id_utf8,
2044
1340
                                     len(lines),
2045
1341
                                     digest)],
2046
 
            dense_lines or lines,
2047
 
            ["end %s\n" % version_id]))
2048
 
        assert bytes.__class__ == str
2049
 
        compressed_bytes = bytes_to_gzip(bytes)
2050
 
        return len(compressed_bytes), compressed_bytes
2051
 
 
2052
 
    def add_raw_records(self, sizes, raw_data):
 
1342
            lines,
 
1343
            ["end %s\n" % version_id_utf8]))
 
1344
        data_file.close()
 
1345
        length= sio.tell()
 
1346
 
 
1347
        sio.seek(0)
 
1348
        return length, sio
 
1349
 
 
1350
    def add_raw_record(self, raw_data):
2053
1351
        """Append a prepared record to the data file.
2054
1352
        
2055
 
        :param sizes: An iterable containing the size of each raw data segment.
2056
 
        :param raw_data: A bytestring containing the data.
2057
 
        :return: a list of index data for the way the data was stored.
2058
 
            See the access method add_raw_records documentation for more
2059
 
            details.
 
1353
        :return: the offset in the data file raw_data was written.
2060
1354
        """
2061
 
        return self._access.add_raw_records(sizes, raw_data)
 
1355
        assert isinstance(raw_data, str), 'data must be plain bytes'
 
1356
        return self._transport.append(self._filename, StringIO(raw_data))
2062
1357
        
 
1358
    def add_record(self, version_id, digest, lines):
 
1359
        """Write new text record to disk.  Returns the position in the
 
1360
        file where it was written."""
 
1361
        size, sio = self._record_to_data(version_id, digest, lines)
 
1362
        # write to disk
 
1363
        start_pos = self._transport.append(self._filename, sio)
 
1364
        if self._do_cache:
 
1365
            self._cache[version_id] = sio.getvalue()
 
1366
        return start_pos, size
 
1367
 
2063
1368
    def _parse_record_header(self, version_id, raw_data):
2064
1369
        """Parse a record header for consistency.
2065
1370
 
2067
1372
                 as (stream, header_record)
2068
1373
        """
2069
1374
        df = GzipFile(mode='rb', fileobj=StringIO(raw_data))
2070
 
        try:
2071
 
            rec = self._check_header(version_id, df.readline())
2072
 
        except Exception, e:
2073
 
            raise KnitCorrupt(self._access,
2074
 
                              "While reading {%s} got %s(%s)"
2075
 
                              % (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]))
2076
1382
        return df, rec
2077
1383
 
2078
 
    def _check_header(self, version_id, line):
2079
 
        rec = line.split()
2080
 
        if len(rec) != 4:
2081
 
            raise KnitCorrupt(self._access,
2082
 
                              'unexpected number of elements in record header')
2083
 
        if rec[1] != version_id:
2084
 
            raise KnitCorrupt(self._access,
2085
 
                              'unexpected version, wanted %r, got %r'
2086
 
                              % (version_id, rec[1]))
2087
 
        return rec
2088
 
 
2089
1384
    def _parse_record(self, version_id, data):
2090
1385
        # profiling notes:
2091
1386
        # 4168 calls in 2880 217 internal
2092
1387
        # 4168 calls to _parse_record_header in 2121
2093
1388
        # 4168 calls to readlines in 330
2094
 
        df = GzipFile(mode='rb', fileobj=StringIO(data))
2095
 
 
2096
 
        try:
2097
 
            record_contents = df.readlines()
2098
 
        except Exception, e:
2099
 
            raise KnitCorrupt(self._access,
2100
 
                              "While reading {%s} got %s(%s)"
2101
 
                              % (version_id, e.__class__.__name__, str(e)))
2102
 
        header = record_contents.pop(0)
2103
 
        rec = self._check_header(version_id, header)
2104
 
 
2105
 
        last_line = record_contents.pop()
2106
 
        if len(record_contents) != int(rec[2]):
2107
 
            raise KnitCorrupt(self._access,
2108
 
                              'incorrect number of lines %s != %s'
2109
 
                              ' for version {%s}'
2110
 
                              % (len(record_contents), int(rec[2]),
2111
 
                                 version_id))
2112
 
        if last_line != 'end %s\n' % rec[1]:
2113
 
            raise KnitCorrupt(self._access,
2114
 
                              'unexpected version end line %r, wanted %r' 
2115
 
                              % (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))
2116
1396
        df.close()
2117
1397
        return record_contents, rec[3]
2118
1398
 
2128
1408
            # grab the disk data needed.
2129
1409
            if self._cache:
2130
1410
                # Don't check _cache if it is empty
2131
 
                needed_offsets = [index_memo for version_id, index_memo
 
1411
                needed_offsets = [(pos, size) for version_id, pos, size
2132
1412
                                              in records
2133
1413
                                              if version_id not in self._cache]
2134
1414
            else:
2135
 
                needed_offsets = [index_memo for version_id, index_memo
 
1415
                needed_offsets = [(pos, size) for version_id, pos, size
2136
1416
                                               in records]
2137
1417
 
2138
 
            raw_records = self._access.get_raw_records(needed_offsets)
 
1418
            raw_records = self._transport.readv(self._filename, needed_offsets)
 
1419
                
2139
1420
 
2140
 
        for version_id, index_memo in records:
 
1421
        for version_id, pos, size in records:
2141
1422
            if version_id in self._cache:
2142
1423
                # This data has already been validated
2143
1424
                data = self._cache[version_id]
2144
1425
            else:
2145
 
                data = raw_records.next()
 
1426
                pos, data = raw_records.next()
2146
1427
                if self._do_cache:
2147
1428
                    self._cache[version_id] = data
2148
1429
 
2187
1468
 
2188
1469
        # The transport optimizes the fetching as well 
2189
1470
        # (ie, reads continuous ranges.)
2190
 
        raw_data = self._access.get_raw_records(
2191
 
            [index_memo for version_id, index_memo in needed_records])
 
1471
        readv_response = self._transport.readv(self._filename,
 
1472
            [(pos, size) for version_id, pos, size in needed_records])
2192
1473
 
2193
 
        for (version_id, index_memo), data in \
2194
 
                izip(iter(needed_records), raw_data):
 
1474
        for (version_id, pos, size), (pos, data) in \
 
1475
                izip(iter(needed_records), readv_response):
2195
1476
            content, digest = self._parse_record(version_id, data)
2196
1477
            if self._do_cache:
2197
1478
                self._cache[version_id] = data
2226
1507
        assert isinstance(self.source, KnitVersionedFile)
2227
1508
        assert isinstance(self.target, KnitVersionedFile)
2228
1509
 
2229
 
        # If the source and target are mismatched w.r.t. annotations vs
2230
 
        # plain, the data needs to be converted accordingly
2231
 
        if self.source.factory.annotated == self.target.factory.annotated:
2232
 
            converter = None
2233
 
        elif self.source.factory.annotated:
2234
 
            converter = self._anno_to_plain_converter
2235
 
        else:
2236
 
            # We're converting from a plain to an annotated knit. This requires
2237
 
            # building the annotations from scratch. The generic join code
2238
 
            # handles this implicitly so we delegate to it.
2239
 
            return super(InterKnit, self).join(pb, msg, version_ids,
2240
 
                ignore_missing)
2241
 
 
2242
1510
        version_ids = self._get_source_version_ids(version_ids, ignore_missing)
 
1511
 
2243
1512
        if not version_ids:
2244
1513
            return 0
2245
1514
 
2246
 
        pb = ui.ui_factory.nested_progress_bar()
 
1515
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
2247
1516
        try:
2248
1517
            version_ids = list(version_ids)
2249
1518
            if None in version_ids:
2251
1520
    
2252
1521
            self.source_ancestry = set(self.source.get_ancestry(version_ids))
2253
1522
            this_versions = set(self.target._index.get_versions())
2254
 
            # XXX: For efficiency we should not look at the whole index,
2255
 
            #      we only need to consider the referenced revisions - they
2256
 
            #      must all be present, or the method must be full-text.
2257
 
            #      TODO, RBC 20070919
2258
1523
            needed_versions = self.source_ancestry - this_versions
 
1524
            cross_check_versions = self.source_ancestry.intersection(this_versions)
 
1525
            mismatched_versions = set()
 
1526
            for version in cross_check_versions:
 
1527
                # scan to include needed parents.
 
1528
                n1 = set(self.target.get_parents_with_ghosts(version))
 
1529
                n2 = set(self.source.get_parents_with_ghosts(version))
 
1530
                if n1 != n2:
 
1531
                    # FIXME TEST this check for cycles being introduced works
 
1532
                    # the logic is we have a cycle if in our graph we are an
 
1533
                    # ancestor of any of the n2 revisions.
 
1534
                    for parent in n2:
 
1535
                        if parent in n1:
 
1536
                            # safe
 
1537
                            continue
 
1538
                        else:
 
1539
                            parent_ancestors = self.source.get_ancestry(parent)
 
1540
                            if version in parent_ancestors:
 
1541
                                raise errors.GraphCycleError([parent, version])
 
1542
                    # ensure this parent will be available later.
 
1543
                    new_parents = n2.difference(n1)
 
1544
                    needed_versions.update(new_parents.difference(this_versions))
 
1545
                    mismatched_versions.add(version)
2259
1546
    
2260
 
            if not needed_versions:
 
1547
            if not needed_versions and not mismatched_versions:
2261
1548
                return 0
2262
1549
            full_list = topo_sort(self.source.get_graph())
2263
1550
    
2280
1567
                    assert (self.target.has_version(parent) or
2281
1568
                            parent in copy_set or
2282
1569
                            not self.source.has_version(parent))
2283
 
                index_memo = self.source._index.get_position(version_id)
2284
 
                copy_queue_records.append((version_id, index_memo))
 
1570
                data_pos, data_size = self.source._index.get_position(version_id)
 
1571
                copy_queue_records.append((version_id, data_pos, data_size))
2285
1572
                copy_queue.append((version_id, options, parents))
2286
1573
                copy_set.add(version_id)
2287
1574
 
2297
1584
                assert version_id == version_id2, 'logic error, inconsistent results'
2298
1585
                count = count + 1
2299
1586
                pb.update("Joining knit", count, total)
2300
 
                if converter:
2301
 
                    size, raw_data = converter(raw_data, version_id, options,
2302
 
                        parents)
2303
 
                else:
2304
 
                    size = len(raw_data)
2305
 
                raw_records.append((version_id, options, parents, size))
 
1587
                raw_records.append((version_id, options, parents, len(raw_data)))
2306
1588
                raw_datum.append(raw_data)
2307
1589
            self.target._add_raw_records(raw_records, ''.join(raw_datum))
 
1590
 
 
1591
            for version in mismatched_versions:
 
1592
                # FIXME RBC 20060309 is this needed?
 
1593
                n1 = set(self.target.get_parents_with_ghosts(version))
 
1594
                n2 = set(self.source.get_parents_with_ghosts(version))
 
1595
                # write a combined record to our history preserving the current 
 
1596
                # parents as first in the list
 
1597
                new_parents = self.target.get_parents_with_ghosts(version) + list(n2.difference(n1))
 
1598
                self.target.fix_parents(version, new_parents)
2308
1599
            return count
2309
1600
        finally:
2310
1601
            pb.finished()
2311
1602
 
2312
 
    def _anno_to_plain_converter(self, raw_data, version_id, options,
2313
 
                                 parents):
2314
 
        """Convert annotated content to plain content."""
2315
 
        data, digest = self.source._data._parse_record(version_id, raw_data)
2316
 
        if 'fulltext' in options:
2317
 
            content = self.source.factory.parse_fulltext(data, version_id)
2318
 
            lines = self.target.factory.lower_fulltext(content)
2319
 
        else:
2320
 
            delta = self.source.factory.parse_line_delta(data, version_id,
2321
 
                plain=True)
2322
 
            lines = self.target.factory.lower_line_delta(delta)
2323
 
        return self.target._data._record_to_data(version_id, digest, lines)
2324
 
 
2325
1603
 
2326
1604
InterVersionedFile.register_optimiser(InterKnit)
2327
1605
 
2351
1629
        if not version_ids:
2352
1630
            return 0
2353
1631
 
2354
 
        pb = ui.ui_factory.nested_progress_bar()
 
1632
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
2355
1633
        try:
2356
1634
            version_ids = list(version_ids)
2357
1635
    
2358
1636
            self.source_ancestry = set(self.source.get_ancestry(version_ids))
2359
1637
            this_versions = set(self.target._index.get_versions())
2360
1638
            needed_versions = self.source_ancestry - this_versions
 
1639
            cross_check_versions = self.source_ancestry.intersection(this_versions)
 
1640
            mismatched_versions = set()
 
1641
            for version in cross_check_versions:
 
1642
                # scan to include needed parents.
 
1643
                n1 = set(self.target.get_parents_with_ghosts(version))
 
1644
                n2 = set(self.source.get_parents(version))
 
1645
                # if all of n2's parents are in n1, then its fine.
 
1646
                if n2.difference(n1):
 
1647
                    # FIXME TEST this check for cycles being introduced works
 
1648
                    # the logic is we have a cycle if in our graph we are an
 
1649
                    # ancestor of any of the n2 revisions.
 
1650
                    for parent in n2:
 
1651
                        if parent in n1:
 
1652
                            # safe
 
1653
                            continue
 
1654
                        else:
 
1655
                            parent_ancestors = self.source.get_ancestry(parent)
 
1656
                            if version in parent_ancestors:
 
1657
                                raise errors.GraphCycleError([parent, version])
 
1658
                    # ensure this parent will be available later.
 
1659
                    new_parents = n2.difference(n1)
 
1660
                    needed_versions.update(new_parents.difference(this_versions))
 
1661
                    mismatched_versions.add(version)
2361
1662
    
2362
 
            if not needed_versions:
 
1663
            if not needed_versions and not mismatched_versions:
2363
1664
                return 0
2364
1665
            full_list = topo_sort(self.source.get_graph())
2365
1666
    
2379
1680
                self.target.add_lines(
2380
1681
                    version_id, parents, self.source.get_lines(version_id))
2381
1682
                count = count + 1
 
1683
 
 
1684
            for version in mismatched_versions:
 
1685
                # FIXME RBC 20060309 is this needed?
 
1686
                n1 = set(self.target.get_parents_with_ghosts(version))
 
1687
                n2 = set(self.source.get_parents(version))
 
1688
                # write a combined record to our history preserving the current 
 
1689
                # parents as first in the list
 
1690
                new_parents = self.target.get_parents_with_ghosts(version) + list(n2.difference(n1))
 
1691
                self.target.fix_parents(version, new_parents)
2382
1692
            return count
2383
1693
        finally:
2384
1694
            pb.finished()
2387
1697
InterVersionedFile.register_optimiser(WeaveToKnit)
2388
1698
 
2389
1699
 
2390
 
# Deprecated, use PatienceSequenceMatcher instead
2391
 
KnitSequenceMatcher = patiencediff.PatienceSequenceMatcher
2392
 
 
2393
 
 
2394
 
def annotate_knit(knit, revision_id):
2395
 
    """Annotate a knit with no cached annotations.
2396
 
 
2397
 
    This implementation is for knits with no cached annotations.
2398
 
    It will work for knits with cached annotations, but this is not
2399
 
    recommended.
 
1700
class KnitSequenceMatcher(difflib.SequenceMatcher):
 
1701
    """Knit tuned sequence matcher.
 
1702
 
 
1703
    This is based on profiling of difflib which indicated some improvements
 
1704
    for our usage pattern.
2400
1705
    """
2401
 
    ancestry = knit.get_ancestry(revision_id)
2402
 
    fulltext = dict(zip(ancestry, knit.get_line_list(ancestry)))
2403
 
    annotations = {}
2404
 
    for candidate in ancestry:
2405
 
        if candidate in annotations:
2406
 
            continue
2407
 
        parents = knit.get_parents(candidate)
2408
 
        if len(parents) == 0:
2409
 
            blocks = None
2410
 
        elif knit._index.get_method(candidate) != 'line-delta':
2411
 
            blocks = None
2412
 
        else:
2413
 
            parent, sha1, noeol, delta = knit.get_delta(candidate)
2414
 
            blocks = KnitContent.get_line_delta_blocks(delta,
2415
 
                fulltext[parents[0]], fulltext[candidate])
2416
 
        annotations[candidate] = list(annotate.reannotate([annotations[p]
2417
 
            for p in parents], fulltext[candidate], candidate, blocks))
2418
 
    return iter(annotations[revision_id])
2419
 
 
2420
 
 
2421
 
try:
2422
 
    from bzrlib._knit_load_data_c import _load_data_c as _load_data
2423
 
except ImportError:
2424
 
    from bzrlib._knit_load_data_py import _load_data_py as _load_data
 
1706
 
 
1707
    def find_longest_match(self, alo, ahi, blo, bhi):
 
1708
        """Find longest matching block in a[alo:ahi] and b[blo:bhi].
 
1709
 
 
1710
        If isjunk is not defined:
 
1711
 
 
1712
        Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
 
1713
            alo <= i <= i+k <= ahi
 
1714
            blo <= j <= j+k <= bhi
 
1715
        and for all (i',j',k') meeting those conditions,
 
1716
            k >= k'
 
1717
            i <= i'
 
1718
            and if i == i', j <= j'
 
1719
 
 
1720
        In other words, of all maximal matching blocks, return one that
 
1721
        starts earliest in a, and of all those maximal matching blocks that
 
1722
        start earliest in a, return the one that starts earliest in b.
 
1723
 
 
1724
        >>> s = SequenceMatcher(None, " abcd", "abcd abcd")
 
1725
        >>> s.find_longest_match(0, 5, 0, 9)
 
1726
        (0, 4, 5)
 
1727
 
 
1728
        If isjunk is defined, first the longest matching block is
 
1729
        determined as above, but with the additional restriction that no
 
1730
        junk element appears in the block.  Then that block is extended as
 
1731
        far as possible by matching (only) junk elements on both sides.  So
 
1732
        the resulting block never matches on junk except as identical junk
 
1733
        happens to be adjacent to an "interesting" match.
 
1734
 
 
1735
        Here's the same example as before, but considering blanks to be
 
1736
        junk.  That prevents " abcd" from matching the " abcd" at the tail
 
1737
        end of the second sequence directly.  Instead only the "abcd" can
 
1738
        match, and matches the leftmost "abcd" in the second sequence:
 
1739
 
 
1740
        >>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd")
 
1741
        >>> s.find_longest_match(0, 5, 0, 9)
 
1742
        (1, 0, 4)
 
1743
 
 
1744
        If no blocks match, return (alo, blo, 0).
 
1745
 
 
1746
        >>> s = SequenceMatcher(None, "ab", "c")
 
1747
        >>> s.find_longest_match(0, 2, 0, 1)
 
1748
        (0, 0, 0)
 
1749
        """
 
1750
 
 
1751
        # CAUTION:  stripping common prefix or suffix would be incorrect.
 
1752
        # E.g.,
 
1753
        #    ab
 
1754
        #    acab
 
1755
        # Longest matching block is "ab", but if common prefix is
 
1756
        # stripped, it's "a" (tied with "b").  UNIX(tm) diff does so
 
1757
        # strip, so ends up claiming that ab is changed to acab by
 
1758
        # inserting "ca" in the middle.  That's minimal but unintuitive:
 
1759
        # "it's obvious" that someone inserted "ac" at the front.
 
1760
        # Windiff ends up at the same place as diff, but by pairing up
 
1761
        # the unique 'b's and then matching the first two 'a's.
 
1762
 
 
1763
        a, b, b2j, isbjunk = self.a, self.b, self.b2j, self.isbjunk
 
1764
        besti, bestj, bestsize = alo, blo, 0
 
1765
        # find longest junk-free match
 
1766
        # during an iteration of the loop, j2len[j] = length of longest
 
1767
        # junk-free match ending with a[i-1] and b[j]
 
1768
        j2len = {}
 
1769
        # nothing = []
 
1770
        b2jget = b2j.get
 
1771
        for i in xrange(alo, ahi):
 
1772
            # look at all instances of a[i] in b; note that because
 
1773
            # b2j has no junk keys, the loop is skipped if a[i] is junk
 
1774
            j2lenget = j2len.get
 
1775
            newj2len = {}
 
1776
            
 
1777
            # changing b2j.get(a[i], nothing) to a try:KeyError pair produced the
 
1778
            # following improvement
 
1779
            #     704  0   4650.5320   2620.7410   bzrlib.knit:1336(find_longest_match)
 
1780
            # +326674  0   1655.1210   1655.1210   +<method 'get' of 'dict' objects>
 
1781
            #  +76519  0    374.6700    374.6700   +<method 'has_key' of 'dict' objects>
 
1782
            # to 
 
1783
            #     704  0   3733.2820   2209.6520   bzrlib.knit:1336(find_longest_match)
 
1784
            #  +211400 0   1147.3520   1147.3520   +<method 'get' of 'dict' objects>
 
1785
            #  +76519  0    376.2780    376.2780   +<method 'has_key' of 'dict' objects>
 
1786
 
 
1787
            try:
 
1788
                js = b2j[a[i]]
 
1789
            except KeyError:
 
1790
                pass
 
1791
            else:
 
1792
                for j in js:
 
1793
                    # a[i] matches b[j]
 
1794
                    if j >= blo:
 
1795
                        if j >= bhi:
 
1796
                            break
 
1797
                        k = newj2len[j] = 1 + j2lenget(-1 + j, 0)
 
1798
                        if k > bestsize:
 
1799
                            besti, bestj, bestsize = 1 + i-k, 1 + j-k, k
 
1800
            j2len = newj2len
 
1801
 
 
1802
        # Extend the best by non-junk elements on each end.  In particular,
 
1803
        # "popular" non-junk elements aren't in b2j, which greatly speeds
 
1804
        # the inner loop above, but also means "the best" match so far
 
1805
        # doesn't contain any junk *or* popular non-junk elements.
 
1806
        while besti > alo and bestj > blo and \
 
1807
              not isbjunk(b[bestj-1]) and \
 
1808
              a[besti-1] == b[bestj-1]:
 
1809
            besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
 
1810
        while besti+bestsize < ahi and bestj+bestsize < bhi and \
 
1811
              not isbjunk(b[bestj+bestsize]) and \
 
1812
              a[besti+bestsize] == b[bestj+bestsize]:
 
1813
            bestsize += 1
 
1814
 
 
1815
        # Now that we have a wholly interesting match (albeit possibly
 
1816
        # empty!), we may as well suck up the matching junk on each
 
1817
        # side of it too.  Can't think of a good reason not to, and it
 
1818
        # saves post-processing the (possibly considerable) expense of
 
1819
        # figuring out what to do with it.  In the case of an empty
 
1820
        # interesting match, this is clearly the right thing to do,
 
1821
        # because no other kind of match is possible in the regions.
 
1822
        while besti > alo and bestj > blo and \
 
1823
              isbjunk(b[bestj-1]) and \
 
1824
              a[besti-1] == b[bestj-1]:
 
1825
            besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
 
1826
        while besti+bestsize < ahi and bestj+bestsize < bhi and \
 
1827
              isbjunk(b[bestj+bestsize]) and \
 
1828
              a[besti+bestsize] == b[bestj+bestsize]:
 
1829
            bestsize = bestsize + 1
 
1830
 
 
1831
        return besti, bestj, bestsize
 
1832