/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/groupcompress.py

  • Committer: Marius Kruger
  • Date: 2010-07-10 21:28:56 UTC
  • mto: (5384.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5385.
  • Revision ID: marius.kruger@enerweb.co.za-20100710212856-uq4ji3go0u5se7hx
* Update documentation
* add NEWS

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008-2011 Canonical Ltd
 
1
# Copyright (C) 2008, 2009, 2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
16
16
 
17
17
"""Core compression logic for compressing streams of related files."""
18
18
 
19
 
from __future__ import absolute_import
20
 
 
21
19
import time
22
20
import zlib
 
21
try:
 
22
    import pylzma
 
23
except ImportError:
 
24
    pylzma = None
23
25
 
24
 
from ..lazy_import import lazy_import
25
 
lazy_import(globals(), """
26
 
from breezy import (
 
26
from bzrlib import (
27
27
    annotate,
28
 
    config,
29
28
    debug,
 
29
    errors,
 
30
    graph as _mod_graph,
 
31
    knit,
30
32
    osutils,
 
33
    pack,
31
34
    static_tuple,
32
35
    trace,
33
 
    tsort,
34
 
    )
35
 
from breezy.bzr import (
36
 
    knit,
37
 
    pack,
38
 
    pack_repo,
39
 
    )
40
 
 
41
 
from breezy.i18n import gettext
42
 
""")
43
 
 
44
 
from .. import (
45
 
    errors,
46
 
    )
47
 
from .btree_index import BTreeBuilder
48
 
from ..lru_cache import LRUSizeCache
49
 
from ..sixish import (
50
 
    indexbytes,
51
 
    map,
52
 
    range,
53
 
    viewitems,
54
 
    )
55
 
from .versionedfile import (
56
 
    _KeyRefs,
 
36
    )
 
37
from bzrlib.btree_index import BTreeBuilder
 
38
from bzrlib.lru_cache import LRUSizeCache
 
39
from bzrlib.tsort import topo_sort
 
40
from bzrlib.versionedfile import (
57
41
    adapter_registry,
58
42
    AbsentContentFactory,
59
43
    ChunkedContentFactory,
60
44
    FulltextContentFactory,
61
 
    VersionedFilesWithFallbacks,
 
45
    VersionedFiles,
62
46
    )
63
47
 
64
48
# Minimum number of uncompressed bytes to try fetch at once when retrieving
65
49
# groupcompress blocks.
66
50
BATCH_SIZE = 2**16
67
51
 
68
 
# osutils.sha_string(b'')
69
 
_null_sha1 = b'da39a3ee5e6b4b0d3255bfef95601890afd80709'
 
52
_USE_LZMA = False and (pylzma is not None)
70
53
 
 
54
# osutils.sha_string('')
 
55
_null_sha1 = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
71
56
 
72
57
def sort_gc_optimal(parent_map):
73
58
    """Sort and group the keys in parent_map into groupcompress order.
80
65
    # groupcompress ordering is approximately reverse topological,
81
66
    # properly grouped by file-id.
82
67
    per_prefix_map = {}
83
 
    for key, value in viewitems(parent_map):
84
 
        if isinstance(key, bytes) or len(key) == 1:
85
 
            prefix = b''
 
68
    for key, value in parent_map.iteritems():
 
69
        if isinstance(key, str) or len(key) == 1:
 
70
            prefix = ''
86
71
        else:
87
72
            prefix = key[0]
88
73
        try:
92
77
 
93
78
    present_keys = []
94
79
    for prefix in sorted(per_prefix_map):
95
 
        present_keys.extend(reversed(tsort.topo_sort(per_prefix_map[prefix])))
 
80
        present_keys.extend(reversed(topo_sort(per_prefix_map[prefix])))
96
81
    return present_keys
97
82
 
98
83
 
99
 
class DecompressCorruption(errors.BzrError):
100
 
 
101
 
    _fmt = "Corruption while decompressing repository file%(orig_error)s"
102
 
 
103
 
    def __init__(self, orig_error=None):
104
 
        if orig_error is not None:
105
 
            self.orig_error = ", %s" % (orig_error,)
106
 
        else:
107
 
            self.orig_error = ""
108
 
        errors.BzrError.__init__(self)
109
 
 
110
 
 
111
84
# The max zlib window size is 32kB, so if we set 'max_size' output of the
112
85
# decompressor to the requested bytes + 32kB, then we should guarantee
113
86
# num_bytes coming out.
114
 
_ZLIB_DECOMP_WINDOW = 32 * 1024
115
 
 
 
87
_ZLIB_DECOMP_WINDOW = 32*1024
116
88
 
117
89
class GroupCompressBlock(object):
118
90
    """An object which maintains the internal structure of the compressed data.
121
93
    """
122
94
 
123
95
    # Group Compress Block v1 Zlib
124
 
    GCB_HEADER = b'gcb1z\n'
 
96
    GCB_HEADER = 'gcb1z\n'
125
97
    # Group Compress Block v1 Lzma
126
 
    GCB_LZ_HEADER = b'gcb1l\n'
 
98
    GCB_LZ_HEADER = 'gcb1l\n'
127
99
    GCB_KNOWN_HEADERS = (GCB_HEADER, GCB_LZ_HEADER)
128
100
 
129
101
    def __init__(self):
130
102
        # map by key? or just order in file?
131
103
        self._compressor_name = None
132
 
        self._z_content_chunks = None
 
104
        self._z_content = None
133
105
        self._z_content_decompressor = None
134
106
        self._z_content_length = None
135
107
        self._content_length = None
160
132
        # Expand the content if required
161
133
        if self._content is None:
162
134
            if self._content_chunks is not None:
163
 
                self._content = b''.join(self._content_chunks)
 
135
                self._content = ''.join(self._content_chunks)
164
136
                self._content_chunks = None
165
137
        if self._content is None:
166
 
            # We join self._z_content_chunks here, because if we are
167
 
            # decompressing, then it is *very* likely that we have a single
168
 
            # chunk
169
 
            if self._z_content_chunks is None:
 
138
            if self._z_content is None:
170
139
                raise AssertionError('No content to decompress')
171
 
            z_content = b''.join(self._z_content_chunks)
172
 
            if z_content == b'':
173
 
                self._content = b''
 
140
            if self._z_content == '':
 
141
                self._content = ''
174
142
            elif self._compressor_name == 'lzma':
175
143
                # We don't do partial lzma decomp yet
176
 
                import pylzma
177
 
                self._content = pylzma.decompress(z_content)
 
144
                self._content = pylzma.decompress(self._z_content)
178
145
            elif self._compressor_name == 'zlib':
179
146
                # Start a zlib decompressor
180
147
                if num_bytes * 4 > self._content_length * 3:
181
148
                    # If we are requesting more that 3/4ths of the content,
182
149
                    # just extract the whole thing in a single pass
183
150
                    num_bytes = self._content_length
184
 
                    self._content = zlib.decompress(z_content)
 
151
                    self._content = zlib.decompress(self._z_content)
185
152
                else:
186
153
                    self._z_content_decompressor = zlib.decompressobj()
187
154
                    # Seed the decompressor with the uncompressed bytes, so
188
155
                    # that the rest of the code is simplified
189
156
                    self._content = self._z_content_decompressor.decompress(
190
 
                        z_content, num_bytes + _ZLIB_DECOMP_WINDOW)
 
157
                        self._z_content, num_bytes + _ZLIB_DECOMP_WINDOW)
191
158
                    if not self._z_content_decompressor.unconsumed_tail:
192
159
                        self._z_content_decompressor = None
193
160
            else:
220
187
            # The stream is finished
221
188
            self._z_content_decompressor = None
222
189
 
223
 
    def _parse_bytes(self, data, pos):
 
190
    def _parse_bytes(self, bytes, pos):
224
191
        """Read the various lengths from the header.
225
192
 
226
193
        This also populates the various 'compressed' buffers.
230
197
        # At present, we have 2 integers for the compressed and uncompressed
231
198
        # content. In base10 (ascii) 14 bytes can represent > 1TB, so to avoid
232
199
        # checking too far, cap the search to 14 bytes.
233
 
        pos2 = data.index(b'\n', pos, pos + 14)
234
 
        self._z_content_length = int(data[pos:pos2])
235
 
        pos = pos2 + 1
236
 
        pos2 = data.index(b'\n', pos, pos + 14)
237
 
        self._content_length = int(data[pos:pos2])
238
 
        pos = pos2 + 1
239
 
        if len(data) != (pos + self._z_content_length):
 
200
        pos2 = bytes.index('\n', pos, pos + 14)
 
201
        self._z_content_length = int(bytes[pos:pos2])
 
202
        pos = pos2 + 1
 
203
        pos2 = bytes.index('\n', pos, pos + 14)
 
204
        self._content_length = int(bytes[pos:pos2])
 
205
        pos = pos2 + 1
 
206
        if len(bytes) != (pos + self._z_content_length):
240
207
            # XXX: Define some GCCorrupt error ?
241
208
            raise AssertionError('Invalid bytes: (%d) != %d + %d' %
242
 
                                 (len(data), pos, self._z_content_length))
243
 
        self._z_content_chunks = (data[pos:],)
244
 
 
245
 
    @property
246
 
    def _z_content(self):
247
 
        """Return z_content_chunks as a simple string.
248
 
 
249
 
        Meant only to be used by the test suite.
250
 
        """
251
 
        if self._z_content_chunks is not None:
252
 
            return b''.join(self._z_content_chunks)
253
 
        return None
 
209
                                 (len(bytes), pos, self._z_content_length))
 
210
        self._z_content = bytes[pos:]
254
211
 
255
212
    @classmethod
256
213
    def from_bytes(cls, bytes):
257
214
        out = cls()
258
 
        header = bytes[:6]
259
 
        if header not in cls.GCB_KNOWN_HEADERS:
 
215
        if bytes[:6] not in cls.GCB_KNOWN_HEADERS:
260
216
            raise ValueError('bytes did not start with any of %r'
261
217
                             % (cls.GCB_KNOWN_HEADERS,))
262
 
        if header == cls.GCB_HEADER:
 
218
        # XXX: why not testing the whole header ?
 
219
        if bytes[4] == 'z':
263
220
            out._compressor_name = 'zlib'
264
 
        elif header == cls.GCB_LZ_HEADER:
 
221
        elif bytes[4] == 'l':
265
222
            out._compressor_name = 'lzma'
266
223
        else:
267
 
            raise ValueError('unknown compressor: %r' % (header,))
 
224
            raise ValueError('unknown compressor: %r' % (bytes,))
268
225
        out._parse_bytes(bytes, 6)
269
226
        return out
270
227
 
276
233
        :return: The bytes for the content
277
234
        """
278
235
        if start == end == 0:
279
 
            return []
 
236
            return ''
280
237
        self._ensure_content(end)
281
238
        # The bytes are 'f' or 'd' for the type, then a variable-length
282
239
        # base128 integer for the content size, then the actual content
283
240
        # We know that the variable-length integer won't be longer than 5
284
241
        # bytes (it takes 5 bytes to encode 2^32)
285
 
        c = self._content[start:start + 1]
286
 
        if c == b'f':
 
242
        c = self._content[start]
 
243
        if c == 'f':
287
244
            type = 'fulltext'
288
245
        else:
289
 
            if c != b'd':
 
246
            if c != 'd':
290
247
                raise ValueError('Unknown content control code: %s'
291
248
                                 % (c,))
292
249
            type = 'delta'
293
250
        content_len, len_len = decode_base128_int(
294
 
            self._content[start + 1:start + 6])
 
251
                            self._content[start + 1:start + 6])
295
252
        content_start = start + 1 + len_len
296
253
        if end != content_start + content_len:
297
254
            raise ValueError('end != len according to field header'
298
 
                             ' %s != %s' % (end, content_start + content_len))
299
 
        if c == b'f':
300
 
            return [self._content[content_start:end]]
301
 
        # Must be type delta as checked above
302
 
        return [apply_delta_to_source(self._content, content_start, end)]
 
255
                ' %s != %s' % (end, content_start + content_len))
 
256
        if c == 'f':
 
257
            bytes = self._content[content_start:end]
 
258
        elif c == 'd':
 
259
            bytes = apply_delta_to_source(self._content, content_start, end)
 
260
        return bytes
303
261
 
304
262
    def set_chunked_content(self, content_chunks, length):
305
263
        """Set the content of this block to the given chunks."""
311
269
        self._content_length = length
312
270
        self._content_chunks = content_chunks
313
271
        self._content = None
314
 
        self._z_content_chunks = None
 
272
        self._z_content = None
315
273
 
316
274
    def set_content(self, content):
317
275
        """Set the content of this block."""
318
276
        self._content_length = len(content)
319
277
        self._content = content
320
 
        self._z_content_chunks = None
321
 
 
322
 
    def _create_z_content_from_chunks(self, chunks):
 
278
        self._z_content = None
 
279
 
 
280
    def _create_z_content_using_lzma(self):
 
281
        if self._content_chunks is not None:
 
282
            self._content = ''.join(self._content_chunks)
 
283
            self._content_chunks = None
 
284
        if self._content is None:
 
285
            raise AssertionError('Nothing to compress')
 
286
        self._z_content = pylzma.compress(self._content)
 
287
        self._z_content_length = len(self._z_content)
 
288
 
 
289
    def _create_z_content_from_chunks(self):
323
290
        compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION)
324
 
        # Peak in this point is 1 fulltext, 1 compressed text, + zlib overhead
325
 
        # (measured peak is maybe 30MB over the above...)
326
 
        compressed_chunks = list(map(compressor.compress, chunks))
 
291
        compressed_chunks = map(compressor.compress, self._content_chunks)
327
292
        compressed_chunks.append(compressor.flush())
328
 
        # Ignore empty chunks
329
 
        self._z_content_chunks = [c for c in compressed_chunks if c]
330
 
        self._z_content_length = sum(map(len, self._z_content_chunks))
 
293
        self._z_content = ''.join(compressed_chunks)
 
294
        self._z_content_length = len(self._z_content)
331
295
 
332
296
    def _create_z_content(self):
333
 
        if self._z_content_chunks is not None:
 
297
        if self._z_content is not None:
 
298
            return
 
299
        if _USE_LZMA:
 
300
            self._create_z_content_using_lzma()
334
301
            return
335
302
        if self._content_chunks is not None:
336
 
            chunks = self._content_chunks
337
 
        else:
338
 
            chunks = (self._content,)
339
 
        self._create_z_content_from_chunks(chunks)
340
 
 
341
 
    def to_chunks(self):
342
 
        """Create the byte stream as a series of 'chunks'"""
343
 
        self._create_z_content()
344
 
        header = self.GCB_HEADER
345
 
        chunks = [b'%s%d\n%d\n'
346
 
                  % (header, self._z_content_length, self._content_length),
347
 
                  ]
348
 
        chunks.extend(self._z_content_chunks)
349
 
        total_len = sum(map(len, chunks))
350
 
        return total_len, chunks
 
303
            self._create_z_content_from_chunks()
 
304
            return
 
305
        self._z_content = zlib.compress(self._content)
 
306
        self._z_content_length = len(self._z_content)
351
307
 
352
308
    def to_bytes(self):
353
309
        """Encode the information into a byte stream."""
354
 
        total_len, chunks = self.to_chunks()
355
 
        return b''.join(chunks)
 
310
        self._create_z_content()
 
311
        if _USE_LZMA:
 
312
            header = self.GCB_LZ_HEADER
 
313
        else:
 
314
            header = self.GCB_HEADER
 
315
        chunks = [header,
 
316
                  '%d\n%d\n' % (self._z_content_length, self._content_length),
 
317
                  self._z_content,
 
318
                 ]
 
319
        return ''.join(chunks)
356
320
 
357
321
    def _dump(self, include_text=False):
358
322
        """Take this block, and spit out a human-readable structure.
368
332
        result = []
369
333
        pos = 0
370
334
        while pos < self._content_length:
371
 
            kind = self._content[pos:pos + 1]
 
335
            kind = self._content[pos]
372
336
            pos += 1
373
 
            if kind not in (b'f', b'd'):
 
337
            if kind not in ('f', 'd'):
374
338
                raise ValueError('invalid kind character: %r' % (kind,))
375
339
            content_len, len_len = decode_base128_int(
376
 
                self._content[pos:pos + 5])
 
340
                                self._content[pos:pos + 5])
377
341
            pos += len_len
378
342
            if content_len + pos > self._content_length:
379
343
                raise ValueError('invalid content_len %d for record @ pos %d'
380
344
                                 % (content_len, pos - len_len - 1))
381
 
            if kind == b'f':  # Fulltext
 
345
            if kind == 'f': # Fulltext
382
346
                if include_text:
383
 
                    text = self._content[pos:pos + content_len]
384
 
                    result.append((b'f', content_len, text))
 
347
                    text = self._content[pos:pos+content_len]
 
348
                    result.append(('f', content_len, text))
385
349
                else:
386
 
                    result.append((b'f', content_len))
387
 
            elif kind == b'd':  # Delta
388
 
                delta_content = self._content[pos:pos + content_len]
 
350
                    result.append(('f', content_len))
 
351
            elif kind == 'd': # Delta
 
352
                delta_content = self._content[pos:pos+content_len]
389
353
                delta_info = []
390
354
                # The first entry in a delta is the decompressed length
391
355
                decomp_len, delta_pos = decode_base128_int(delta_content)
392
 
                result.append((b'd', content_len, decomp_len, delta_info))
 
356
                result.append(('d', content_len, decomp_len, delta_info))
393
357
                measured_len = 0
394
358
                while delta_pos < content_len:
395
 
                    c = indexbytes(delta_content, delta_pos)
 
359
                    c = ord(delta_content[delta_pos])
396
360
                    delta_pos += 1
397
 
                    if c & 0x80:  # Copy
 
361
                    if c & 0x80: # Copy
398
362
                        (offset, length,
399
363
                         delta_pos) = decode_copy_instruction(delta_content, c,
400
364
                                                              delta_pos)
401
365
                        if include_text:
402
 
                            text = self._content[offset:offset + length]
403
 
                            delta_info.append((b'c', offset, length, text))
 
366
                            text = self._content[offset:offset+length]
 
367
                            delta_info.append(('c', offset, length, text))
404
368
                        else:
405
 
                            delta_info.append((b'c', offset, length))
 
369
                            delta_info.append(('c', offset, length))
406
370
                        measured_len += length
407
 
                    else:  # Insert
 
371
                    else: # Insert
408
372
                        if include_text:
409
 
                            txt = delta_content[delta_pos:delta_pos + c]
 
373
                            txt = delta_content[delta_pos:delta_pos+c]
410
374
                        else:
411
 
                            txt = b''
412
 
                        delta_info.append((b'i', c, txt))
 
375
                            txt = ''
 
376
                        delta_info.append(('i', c, txt))
413
377
                        measured_len += c
414
378
                        delta_pos += c
415
379
                if delta_pos != content_len:
441
405
        self.key = key
442
406
        self.parents = parents
443
407
        self.sha1 = None
444
 
        self.size = None
445
408
        # Note: This attribute coupled with Manager._factories creates a
446
409
        #       reference cycle. Perhaps we would rather use a weakref(), or
447
410
        #       find an appropriate time to release the ref. After the first
448
411
        #       get_bytes_as call? After Manager.get_record_stream() returns
449
412
        #       the object?
450
413
        self._manager = manager
451
 
        self._chunks = None
 
414
        self._bytes = None
452
415
        self.storage_kind = 'groupcompress-block'
453
416
        if not first:
454
417
            self.storage_kind = 'groupcompress-block-ref'
458
421
 
459
422
    def __repr__(self):
460
423
        return '%s(%s, first=%s)' % (self.__class__.__name__,
461
 
                                     self.key, self._first)
462
 
 
463
 
    def _extract_bytes(self):
464
 
        # Grab and cache the raw bytes for this entry
465
 
        # and break the ref-cycle with _manager since we don't need it
466
 
        # anymore
467
 
        try:
468
 
            self._manager._prepare_for_extract()
469
 
        except zlib.error as value:
470
 
            raise DecompressCorruption("zlib: " + str(value))
471
 
        block = self._manager._block
472
 
        self._chunks = block.extract(self.key, self._start, self._end)
473
 
        # There are code paths that first extract as fulltext, and then
474
 
        # extract as storage_kind (smart fetch). So we don't break the
475
 
        # refcycle here, but instead in manager.get_record_stream()
 
424
            self.key, self._first)
476
425
 
477
426
    def get_bytes_as(self, storage_kind):
478
427
        if storage_kind == self.storage_kind:
480
429
                # wire bytes, something...
481
430
                return self._manager._wire_bytes()
482
431
            else:
483
 
                return b''
484
 
        if storage_kind in ('fulltext', 'chunked', 'lines'):
485
 
            if self._chunks is None:
486
 
                self._extract_bytes()
 
432
                return ''
 
433
        if storage_kind in ('fulltext', 'chunked'):
 
434
            if self._bytes is None:
 
435
                # Grab and cache the raw bytes for this entry
 
436
                # and break the ref-cycle with _manager since we don't need it
 
437
                # anymore
 
438
                self._manager._prepare_for_extract()
 
439
                block = self._manager._block
 
440
                self._bytes = block.extract(self.key, self._start, self._end)
 
441
                # There are code paths that first extract as fulltext, and then
 
442
                # extract as storage_kind (smart fetch). So we don't break the
 
443
                # refcycle here, but instead in manager.get_record_stream()
487
444
            if storage_kind == 'fulltext':
488
 
                return b''.join(self._chunks)
489
 
            elif storage_kind == 'chunked':
490
 
                return self._chunks
 
445
                return self._bytes
491
446
            else:
492
 
                return osutils.chunks_to_lines(self._chunks)
493
 
        raise errors.UnavailableRepresentation(self.key, storage_kind,
494
 
                                               self.storage_kind)
495
 
 
496
 
    def iter_bytes_as(self, storage_kind):
497
 
        if self._chunks is None:
498
 
            self._extract_bytes()
499
 
        if storage_kind == 'chunked':
500
 
            return iter(self._chunks)
501
 
        elif storage_kind == 'lines':
502
 
            return iter(osutils.chunks_to_lines(self._chunks))
 
447
                return [self._bytes]
503
448
        raise errors.UnavailableRepresentation(self.key, storage_kind,
504
449
                                               self.storage_kind)
505
450
 
507
452
class _LazyGroupContentManager(object):
508
453
    """This manages a group of _LazyGroupCompressFactory objects."""
509
454
 
510
 
    _max_cut_fraction = 0.75  # We allow a block to be trimmed to 75% of
511
 
    # current size, and still be considered
512
 
    # resuable
513
 
    _full_block_size = 4 * 1024 * 1024
514
 
    _full_mixed_block_size = 2 * 1024 * 1024
515
 
    _full_enough_block_size = 3 * 1024 * 1024  # size at which we won't repack
516
 
    _full_enough_mixed_block_size = 2 * 768 * 1024  # 1.5MB
 
455
    _max_cut_fraction = 0.75 # We allow a block to be trimmed to 75% of
 
456
                             # current size, and still be considered
 
457
                             # resuable
 
458
    _full_block_size = 4*1024*1024
 
459
    _full_mixed_block_size = 2*1024*1024
 
460
    _full_enough_block_size = 3*1024*1024 # size at which we won't repack
 
461
    _full_enough_mixed_block_size = 2*768*1024 # 1.5MB
517
462
 
518
 
    def __init__(self, block, get_compressor_settings=None):
 
463
    def __init__(self, block):
519
464
        self._block = block
520
465
        # We need to preserve the ordering
521
466
        self._factories = []
522
467
        self._last_byte = 0
523
 
        self._get_settings = get_compressor_settings
524
 
        self._compressor_settings = None
525
 
 
526
 
    def _get_compressor_settings(self):
527
 
        if self._compressor_settings is not None:
528
 
            return self._compressor_settings
529
 
        settings = None
530
 
        if self._get_settings is not None:
531
 
            settings = self._get_settings()
532
 
        if settings is None:
533
 
            vf = GroupCompressVersionedFiles
534
 
            settings = vf._DEFAULT_COMPRESSOR_SETTINGS
535
 
        self._compressor_settings = settings
536
 
        return self._compressor_settings
537
468
 
538
469
    def add_factory(self, key, parents, start, end):
539
470
        if not self._factories:
542
473
            first = False
543
474
        # Note that this creates a reference cycle....
544
475
        factory = _LazyGroupCompressFactory(key, parents, self,
545
 
                                            start, end, first=first)
 
476
            start, end, first=first)
546
477
        # max() works here, but as a function call, doing a compare seems to be
547
478
        # significantly faster, timeit says 250ms for max() and 100ms for the
548
479
        # comparison
572
503
        new_block.set_content(self._block._content[:last_byte])
573
504
        self._block = new_block
574
505
 
575
 
    def _make_group_compressor(self):
576
 
        return GroupCompressor(self._get_compressor_settings())
577
 
 
578
506
    def _rebuild_block(self):
579
507
        """Create a new GroupCompressBlock with only the referenced texts."""
580
 
        compressor = self._make_group_compressor()
 
508
        compressor = GroupCompressor()
581
509
        tstart = time.time()
582
510
        old_length = self._block._content_length
583
511
        end_point = 0
584
512
        for factory in self._factories:
585
 
            chunks = factory.get_bytes_as('chunked')
586
 
            chunks_len = factory.size
587
 
            if chunks_len is None:
588
 
                chunks_len = sum(map(len, chunks))
 
513
            bytes = factory.get_bytes_as('fulltext')
589
514
            (found_sha1, start_point, end_point,
590
 
             type) = compressor.compress(
591
 
                 factory.key, chunks, chunks_len, factory.sha1)
 
515
             type) = compressor.compress(factory.key, bytes, factory.sha1)
592
516
            # Now update this factory with the new offsets, etc
593
517
            factory.sha1 = found_sha1
594
518
            factory._start = start_point
599
523
        #       block? It seems hard to come up with a method that it would
600
524
        #       expand, since we do full compression again. Perhaps based on a
601
525
        #       request that ends up poorly ordered?
602
 
        # TODO: If the content would have expanded, then we would want to
603
 
        #       handle a case where we need to split the block.
604
 
        #       Now that we have a user-tweakable option
605
 
        #       (max_bytes_to_index), it is possible that one person set it
606
 
        #       to a very low value, causing poor compression.
607
526
        delta = time.time() - tstart
608
527
        self._block = new_block
609
528
        trace.mutter('creating new compressed block on-the-fly in %.3fs'
732
651
        #   <length of gc block>\n
733
652
        #   <header bytes>
734
653
        #   <gc-block>
735
 
        lines = [b'groupcompress-block\n']
 
654
        lines = ['groupcompress-block\n']
736
655
        # The minimal info we need is the key, the start offset, and the
737
656
        # parents. The length and type are encoded in the record itself.
738
657
        # However, passing in the other bits makes it easier.  The list of
743
662
        # 1 line for end byte
744
663
        header_lines = []
745
664
        for factory in self._factories:
746
 
            key_bytes = b'\x00'.join(factory.key)
 
665
            key_bytes = '\x00'.join(factory.key)
747
666
            parents = factory.parents
748
667
            if parents is None:
749
 
                parent_bytes = b'None:'
 
668
                parent_bytes = 'None:'
750
669
            else:
751
 
                parent_bytes = b'\t'.join(b'\x00'.join(key) for key in parents)
752
 
            record_header = b'%s\n%s\n%d\n%d\n' % (
 
670
                parent_bytes = '\t'.join('\x00'.join(key) for key in parents)
 
671
            record_header = '%s\n%s\n%d\n%d\n' % (
753
672
                key_bytes, parent_bytes, factory._start, factory._end)
754
673
            header_lines.append(record_header)
755
674
            # TODO: Can we break the refcycle at this point and set
756
675
            #       factory._manager = None?
757
 
        header_bytes = b''.join(header_lines)
 
676
        header_bytes = ''.join(header_lines)
758
677
        del header_lines
759
678
        header_bytes_len = len(header_bytes)
760
679
        z_header_bytes = zlib.compress(header_bytes)
761
680
        del header_bytes
762
681
        z_header_bytes_len = len(z_header_bytes)
763
 
        block_bytes_len, block_chunks = self._block.to_chunks()
764
 
        lines.append(b'%d\n%d\n%d\n' % (
765
 
            z_header_bytes_len, header_bytes_len, block_bytes_len))
 
682
        block_bytes = self._block.to_bytes()
 
683
        lines.append('%d\n%d\n%d\n' % (z_header_bytes_len, header_bytes_len,
 
684
                                       len(block_bytes)))
766
685
        lines.append(z_header_bytes)
767
 
        lines.extend(block_chunks)
768
 
        del z_header_bytes, block_chunks
769
 
        # TODO: This is a point where we will double the memory consumption. To
770
 
        #       avoid this, we probably have to switch to a 'chunked' api
771
 
        return b''.join(lines)
 
686
        lines.append(block_bytes)
 
687
        del z_header_bytes, block_bytes
 
688
        return ''.join(lines)
772
689
 
773
690
    @classmethod
774
691
    def from_bytes(cls, bytes):
775
692
        # TODO: This does extra string copying, probably better to do it a
776
 
        #       different way. At a minimum this creates 2 copies of the
777
 
        #       compressed content
 
693
        #       different way
778
694
        (storage_kind, z_header_len, header_len,
779
 
         block_len, rest) = bytes.split(b'\n', 4)
 
695
         block_len, rest) = bytes.split('\n', 4)
780
696
        del bytes
781
 
        if storage_kind != b'groupcompress-block':
 
697
        if storage_kind != 'groupcompress-block':
782
698
            raise ValueError('Unknown storage kind: %s' % (storage_kind,))
783
699
        z_header_len = int(z_header_len)
784
700
        if len(rest) < z_header_len:
796
712
        del rest
797
713
        # So now we have a valid GCB, we just need to parse the factories that
798
714
        # were sent to us
799
 
        header_lines = header.split(b'\n')
 
715
        header_lines = header.split('\n')
800
716
        del header
801
717
        last = header_lines.pop()
802
 
        if last != b'':
 
718
        if last != '':
803
719
            raise ValueError('header lines did not end with a trailing'
804
720
                             ' newline')
805
721
        if len(header_lines) % 4 != 0:
807
723
        block = GroupCompressBlock.from_bytes(block_bytes)
808
724
        del block_bytes
809
725
        result = cls(block)
810
 
        for start in range(0, len(header_lines), 4):
 
726
        for start in xrange(0, len(header_lines), 4):
811
727
            # intern()?
812
 
            key = tuple(header_lines[start].split(b'\x00'))
813
 
            parents_line = header_lines[start + 1]
814
 
            if parents_line == b'None:':
 
728
            key = tuple(header_lines[start].split('\x00'))
 
729
            parents_line = header_lines[start+1]
 
730
            if parents_line == 'None:':
815
731
                parents = None
816
732
            else:
817
 
                parents = tuple([tuple(segment.split(b'\x00'))
818
 
                                 for segment in parents_line.split(b'\t')
819
 
                                 if segment])
820
 
            start_offset = int(header_lines[start + 2])
821
 
            end_offset = int(header_lines[start + 3])
 
733
                parents = tuple([tuple(segment.split('\x00'))
 
734
                                 for segment in parents_line.split('\t')
 
735
                                  if segment])
 
736
            start_offset = int(header_lines[start+2])
 
737
            end_offset = int(header_lines[start+3])
822
738
            result.add_factory(key, parents, start_offset, end_offset)
823
739
        return result
824
740
 
832
748
 
833
749
class _CommonGroupCompressor(object):
834
750
 
835
 
    def __init__(self, settings=None):
 
751
    def __init__(self):
836
752
        """Create a GroupCompressor."""
837
753
        self.chunks = []
838
754
        self._last = None
839
755
        self.endpoint = 0
840
756
        self.input_bytes = 0
841
757
        self.labels_deltas = {}
842
 
        self._delta_index = None  # Set by the children
 
758
        self._delta_index = None # Set by the children
843
759
        self._block = GroupCompressBlock()
844
 
        if settings is None:
845
 
            self._settings = {}
846
 
        else:
847
 
            self._settings = settings
848
760
 
849
 
    def compress(self, key, chunks, length, expected_sha, nostore_sha=None,
850
 
                 soft=False):
 
761
    def compress(self, key, bytes, expected_sha, nostore_sha=None, soft=False):
851
762
        """Compress lines with label key.
852
763
 
853
764
        :param key: A key tuple. It is stored in the output
854
765
            for identification of the text during decompression. If the last
855
 
            element is b'None' it is replaced with the sha1 of the text -
 
766
            element is 'None' it is replaced with the sha1 of the text -
856
767
            e.g. sha1:xxxxxxx.
857
 
        :param chunks: Chunks of bytes to be compressed
858
 
        :param length: Length of chunks
 
768
        :param bytes: The bytes to be compressed
859
769
        :param expected_sha: If non-None, the sha the lines are believed to
860
770
            have. During compression the sha is calculated; a mismatch will
861
771
            cause an error.
869
779
 
870
780
        :seealso VersionedFiles.add_lines:
871
781
        """
872
 
        if length == 0:  # empty, like a dir entry, etc
 
782
        if not bytes: # empty, like a dir entry, etc
873
783
            if nostore_sha == _null_sha1:
874
784
                raise errors.ExistingContent()
875
785
            return _null_sha1, 0, 0, 'fulltext'
877
787
        if expected_sha is not None:
878
788
            sha1 = expected_sha
879
789
        else:
880
 
            sha1 = osutils.sha_strings(chunks)
 
790
            sha1 = osutils.sha_string(bytes)
881
791
        if nostore_sha is not None:
882
792
            if sha1 == nostore_sha:
883
793
                raise errors.ExistingContent()
884
794
        if key[-1] is None:
885
 
            key = key[:-1] + (b'sha1:' + sha1,)
 
795
            key = key[:-1] + ('sha1:' + sha1,)
886
796
 
887
 
        start, end, type = self._compress(key, chunks, length, length / 2, soft)
 
797
        start, end, type = self._compress(key, bytes, len(bytes) / 2, soft)
888
798
        return sha1, start, end, type
889
799
 
890
 
    def _compress(self, key, chunks, input_len, max_delta_size, soft=False):
 
800
    def _compress(self, key, bytes, max_delta_size, soft=False):
891
801
        """Compress lines with label key.
892
802
 
893
803
        :param key: A key tuple. It is stored in the output for identification
894
804
            of the text during decompression.
895
805
 
896
 
        :param chunks: The chunks of bytes to be compressed
897
 
 
898
 
        :param input_len: The length of the chunks
 
806
        :param bytes: The bytes to be compressed
899
807
 
900
808
        :param max_delta_size: The size above which we issue a fulltext instead
901
809
            of a delta.
912
820
        """Extract a key previously added to the compressor.
913
821
 
914
822
        :param key: The key to extract.
915
 
        :return: An iterable over chunks and the sha1.
 
823
        :return: An iterable over bytes and the sha1.
916
824
        """
917
 
        (start_byte, start_chunk, end_byte,
918
 
         end_chunk) = self.labels_deltas[key]
 
825
        (start_byte, start_chunk, end_byte, end_chunk) = self.labels_deltas[key]
919
826
        delta_chunks = self.chunks[start_chunk:end_chunk]
920
 
        stored_bytes = b''.join(delta_chunks)
921
 
        kind = stored_bytes[:1]
922
 
        if kind == b'f':
 
827
        stored_bytes = ''.join(delta_chunks)
 
828
        if stored_bytes[0] == 'f':
923
829
            fulltext_len, offset = decode_base128_int(stored_bytes[1:10])
924
830
            data_len = fulltext_len + 1 + offset
925
 
            if data_len != len(stored_bytes):
 
831
            if  data_len != len(stored_bytes):
926
832
                raise ValueError('Index claimed fulltext len, but stored bytes'
927
833
                                 ' claim %s != %s'
928
834
                                 % (len(stored_bytes), data_len))
929
 
            data = [stored_bytes[offset + 1:]]
 
835
            bytes = stored_bytes[offset + 1:]
930
836
        else:
931
 
            if kind != b'd':
932
 
                raise ValueError('Unknown content kind, bytes claim %s' % kind)
933
837
            # XXX: This is inefficient at best
934
 
            source = b''.join(self.chunks[:start_chunk])
 
838
            source = ''.join(self.chunks[:start_chunk])
 
839
            if stored_bytes[0] != 'd':
 
840
                raise ValueError('Unknown content kind, bytes claim %s'
 
841
                                 % (stored_bytes[0],))
935
842
            delta_len, offset = decode_base128_int(stored_bytes[1:10])
936
843
            data_len = delta_len + 1 + offset
937
844
            if data_len != len(stored_bytes):
938
845
                raise ValueError('Index claimed delta len, but stored bytes'
939
846
                                 ' claim %s != %s'
940
847
                                 % (len(stored_bytes), data_len))
941
 
            data = [apply_delta(source, stored_bytes[offset + 1:])]
942
 
        data_sha1 = osutils.sha_strings(data)
943
 
        return data, data_sha1
 
848
            bytes = apply_delta(source, stored_bytes[offset + 1:])
 
849
        bytes_sha1 = osutils.sha_string(bytes)
 
850
        return bytes, bytes_sha1
944
851
 
945
852
    def flush(self):
946
853
        """Finish this group, creating a formatted stream.
947
854
 
948
855
        After calling this, the compressor should no longer be used
949
856
        """
 
857
        # TODO: this causes us to 'bloat' to 2x the size of content in the
 
858
        #       group. This has an impact for 'commit' of large objects.
 
859
        #       One possibility is to use self._content_chunks, and be lazy and
 
860
        #       only fill out self._content as a full string when we actually
 
861
        #       need it. That would at least drop the peak memory consumption
 
862
        #       for 'commit' down to ~1x the size of the largest file, at a
 
863
        #       cost of increased complexity within this code. 2x is still <<
 
864
        #       3x the size of the largest file, so we are doing ok.
950
865
        self._block.set_chunked_content(self.chunks, self.endpoint)
951
866
        self.chunks = None
952
867
        self._delta_index = None
970
885
 
971
886
class PythonGroupCompressor(_CommonGroupCompressor):
972
887
 
973
 
    def __init__(self, settings=None):
 
888
    def __init__(self):
974
889
        """Create a GroupCompressor.
975
890
 
976
891
        Used only if the pyrex version is not available.
977
892
        """
978
 
        super(PythonGroupCompressor, self).__init__(settings)
 
893
        super(PythonGroupCompressor, self).__init__()
979
894
        self._delta_index = LinesDeltaIndex([])
980
895
        # The actual content is managed by LinesDeltaIndex
981
896
        self.chunks = self._delta_index.lines
982
897
 
983
 
    def _compress(self, key, chunks, input_len, max_delta_size, soft=False):
 
898
    def _compress(self, key, bytes, max_delta_size, soft=False):
984
899
        """see _CommonGroupCompressor._compress"""
985
 
        new_lines = osutils.chunks_to_lines(chunks)
 
900
        input_len = len(bytes)
 
901
        new_lines = osutils.split_lines(bytes)
986
902
        out_lines, index_lines = self._delta_index.make_delta(
987
903
            new_lines, bytes_length=input_len, soft=soft)
988
904
        delta_length = sum(map(len, out_lines))
989
905
        if delta_length > max_delta_size:
990
906
            # The delta is longer than the fulltext, insert a fulltext
991
907
            type = 'fulltext'
992
 
            out_lines = [b'f', encode_base128_int(input_len)]
 
908
            out_lines = ['f', encode_base128_int(input_len)]
993
909
            out_lines.extend(new_lines)
994
910
            index_lines = [False, False]
995
911
            index_lines.extend([True] * len(new_lines))
996
912
        else:
997
913
            # this is a worthy delta, output it
998
914
            type = 'delta'
999
 
            out_lines[0] = b'd'
 
915
            out_lines[0] = 'd'
1000
916
            # Update the delta_length to include those two encoded integers
1001
917
            out_lines[1] = encode_base128_int(delta_length)
1002
918
        # Before insertion
1017
933
 
1018
934
    It contains code very similar to SequenceMatcher because of having a similar
1019
935
    task. However some key differences apply:
1020
 
 
1021
 
    * there is no junk, we want a minimal edit not a human readable diff.
1022
 
    * we don't filter very common lines (because we don't know where a good
1023
 
      range will start, and after the first text we want to be emitting minmal
1024
 
      edits only.
1025
 
    * we chain the left side, not the right side
1026
 
    * we incrementally update the adjacency matrix as new lines are provided.
1027
 
    * we look for matches in all of the left side, so the routine which does
1028
 
      the analagous task of find_longest_match does not need to filter on the
1029
 
      left side.
 
936
     - there is no junk, we want a minimal edit not a human readable diff.
 
937
     - we don't filter very common lines (because we don't know where a good
 
938
       range will start, and after the first text we want to be emitting minmal
 
939
       edits only.
 
940
     - we chain the left side, not the right side
 
941
     - we incrementally update the adjacency matrix as new lines are provided.
 
942
     - we look for matches in all of the left side, so the routine which does
 
943
       the analagous task of find_longest_match does not need to filter on the
 
944
       left side.
1030
945
    """
1031
946
 
1032
 
    def __init__(self, settings=None):
1033
 
        super(PyrexGroupCompressor, self).__init__(settings)
1034
 
        max_bytes_to_index = self._settings.get('max_bytes_to_index', 0)
1035
 
        self._delta_index = DeltaIndex(max_bytes_to_index=max_bytes_to_index)
 
947
    def __init__(self):
 
948
        super(PyrexGroupCompressor, self).__init__()
 
949
        self._delta_index = DeltaIndex()
1036
950
 
1037
 
    def _compress(self, key, chunks, input_len, max_delta_size, soft=False):
 
951
    def _compress(self, key, bytes, max_delta_size, soft=False):
1038
952
        """see _CommonGroupCompressor._compress"""
 
953
        input_len = len(bytes)
1039
954
        # By having action/label/sha1/len, we can parse the group if the index
1040
955
        # was ever destroyed, we have the key in 'label', we know the final
1041
956
        # bytes are valid from sha1, and we know where to find the end of this
1047
962
        # new_chunks = ['label:%s\nsha1:%s\n' % (label, sha1)]
1048
963
        if self._delta_index._source_offset != self.endpoint:
1049
964
            raise AssertionError('_source_offset != endpoint'
1050
 
                                 ' somehow the DeltaIndex got out of sync with'
1051
 
                                 ' the output lines')
1052
 
        bytes = b''.join(chunks)
 
965
                ' somehow the DeltaIndex got out of sync with'
 
966
                ' the output lines')
1053
967
        delta = self._delta_index.make_delta(bytes, max_delta_size)
1054
 
        if delta is None:
 
968
        if (delta is None):
1055
969
            type = 'fulltext'
1056
 
            enc_length = encode_base128_int(input_len)
 
970
            enc_length = encode_base128_int(len(bytes))
1057
971
            len_mini_header = 1 + len(enc_length)
1058
972
            self._delta_index.add_source(bytes, len_mini_header)
1059
 
            new_chunks = [b'f', enc_length] + chunks
 
973
            new_chunks = ['f', enc_length, bytes]
1060
974
        else:
1061
975
            type = 'delta'
1062
976
            enc_length = encode_base128_int(len(delta))
1063
977
            len_mini_header = 1 + len(enc_length)
1064
 
            new_chunks = [b'd', enc_length, delta]
 
978
            new_chunks = ['d', enc_length, delta]
1065
979
            self._delta_index.add_delta_source(delta, len_mini_header)
1066
980
        # Before insertion
1067
981
        start = self.endpoint
1074
988
                                   self.endpoint, chunk_end)
1075
989
        if not self._delta_index._source_offset == self.endpoint:
1076
990
            raise AssertionError('the delta index is out of sync'
1077
 
                                 'with the output lines %s != %s'
1078
 
                                 % (self._delta_index._source_offset, self.endpoint))
 
991
                'with the output lines %s != %s'
 
992
                % (self._delta_index._source_offset, self.endpoint))
1079
993
        return start, self.endpoint, type
1080
994
 
1081
995
    def _output_chunks(self, new_chunks):
1106
1020
        if graph:
1107
1021
            ref_length = 1
1108
1022
        graph_index = BTreeBuilder(reference_lists=ref_length,
1109
 
                                   key_elements=keylength)
 
1023
            key_elements=keylength)
1110
1024
        stream = transport.open_write_stream('newpack')
1111
1025
        writer = pack.ContainerWriter(stream.write)
1112
1026
        writer.begin()
1113
 
        index = _GCGraphIndex(graph_index, lambda: True, parents=parents,
1114
 
                              add_callback=graph_index.add_nodes,
1115
 
                              inconsistency_fatal=inconsistency_fatal)
1116
 
        access = pack_repo._DirectPackAccess({})
 
1027
        index = _GCGraphIndex(graph_index, lambda:True, parents=parents,
 
1028
            add_callback=graph_index.add_nodes,
 
1029
            inconsistency_fatal=inconsistency_fatal)
 
1030
        access = knit._DirectPackAccess({})
1117
1031
        access.set_writer(writer, graph_index, (transport, 'newpack'))
1118
1032
        result = GroupCompressVersionedFiles(index, access, delta)
1119
1033
        result.stream = stream
1129
1043
 
1130
1044
class _BatchingBlockFetcher(object):
1131
1045
    """Fetch group compress blocks in batches.
1132
 
 
 
1046
    
1133
1047
    :ivar total_bytes: int of expected number of bytes needed to fetch the
1134
1048
        currently pending batch.
1135
1049
    """
1136
1050
 
1137
 
    def __init__(self, gcvf, locations, get_compressor_settings=None):
 
1051
    def __init__(self, gcvf, locations):
1138
1052
        self.gcvf = gcvf
1139
1053
        self.locations = locations
1140
1054
        self.keys = []
1143
1057
        self.total_bytes = 0
1144
1058
        self.last_read_memo = None
1145
1059
        self.manager = None
1146
 
        self._get_compressor_settings = get_compressor_settings
1147
1060
 
1148
1061
    def add_key(self, key):
1149
1062
        """Add another to key to fetch.
1150
 
 
 
1063
        
1151
1064
        :return: The estimated number of bytes needed to fetch the batch so
1152
1065
            far.
1153
1066
        """
1178
1091
            # and then.
1179
1092
            self.batch_memos[read_memo] = cached_block
1180
1093
        return self.total_bytes
1181
 
 
 
1094
        
1182
1095
    def _flush_manager(self):
1183
1096
        if self.manager is not None:
1184
1097
            for factory in self.manager.get_record_stream():
1189
1102
    def yield_factories(self, full_flush=False):
1190
1103
        """Yield factories for keys added since the last yield.  They will be
1191
1104
        returned in the order they were added via add_key.
1192
 
 
 
1105
        
1193
1106
        :param full_flush: by default, some results may not be returned in case
1194
1107
            they can be part of the next batch.  If full_flush is True, then
1195
1108
            all results are returned.
1214
1127
                if memos_to_get_stack and memos_to_get_stack[-1] == read_memo:
1215
1128
                    # The next block from _get_blocks will be the block we
1216
1129
                    # need.
1217
 
                    block_read_memo, block = next(blocks)
 
1130
                    block_read_memo, block = blocks.next()
1218
1131
                    if block_read_memo != read_memo:
1219
1132
                        raise AssertionError(
1220
1133
                            "block_read_memo out of sync with read_memo"
1223
1136
                    memos_to_get_stack.pop()
1224
1137
                else:
1225
1138
                    block = self.batch_memos[read_memo]
1226
 
                self.manager = _LazyGroupContentManager(block,
1227
 
                                                        get_compressor_settings=self._get_compressor_settings)
 
1139
                self.manager = _LazyGroupContentManager(block)
1228
1140
                self.last_read_memo = read_memo
1229
1141
            start, end = index_memo[3:5]
1230
1142
            self.manager.add_factory(key, parents, start, end)
1237
1149
        self.total_bytes = 0
1238
1150
 
1239
1151
 
1240
 
class GroupCompressVersionedFiles(VersionedFilesWithFallbacks):
 
1152
class GroupCompressVersionedFiles(VersionedFiles):
1241
1153
    """A group-compress based VersionedFiles implementation."""
1242
1154
 
1243
 
    # This controls how the GroupCompress DeltaIndex works. Basically, we
1244
 
    # compute hash pointers into the source blocks (so hash(text) => text).
1245
 
    # However each of these references costs some memory in trade against a
1246
 
    # more accurate match result. For very large files, they either are
1247
 
    # pre-compressed and change in bulk whenever they change, or change in just
1248
 
    # local blocks. Either way, 'improved resolution' is not very helpful,
1249
 
    # versus running out of memory trying to track everything. The default max
1250
 
    # gives 100% sampling of a 1MB file.
1251
 
    _DEFAULT_MAX_BYTES_TO_INDEX = 1024 * 1024
1252
 
    _DEFAULT_COMPRESSOR_SETTINGS = {'max_bytes_to_index':
1253
 
                                    _DEFAULT_MAX_BYTES_TO_INDEX}
1254
 
 
1255
 
    def __init__(self, index, access, delta=True, _unadded_refs=None,
1256
 
                 _group_cache=None):
 
1155
    def __init__(self, index, access, delta=True, _unadded_refs=None):
1257
1156
        """Create a GroupCompressVersionedFiles object.
1258
1157
 
1259
1158
        :param index: The index object storing access and graph data.
1260
1159
        :param access: The access object storing raw data.
1261
1160
        :param delta: Whether to delta compress or just entropy compress.
1262
1161
        :param _unadded_refs: private parameter, don't use.
1263
 
        :param _group_cache: private parameter, don't use.
1264
1162
        """
1265
1163
        self._index = index
1266
1164
        self._access = access
1268
1166
        if _unadded_refs is None:
1269
1167
            _unadded_refs = {}
1270
1168
        self._unadded_refs = _unadded_refs
1271
 
        if _group_cache is None:
1272
 
            _group_cache = LRUSizeCache(max_size=50 * 1024 * 1024)
1273
 
        self._group_cache = _group_cache
1274
 
        self._immediate_fallback_vfs = []
1275
 
        self._max_bytes_to_index = None
 
1169
        self._group_cache = LRUSizeCache(max_size=50*1024*1024)
 
1170
        self._fallback_vfs = []
1276
1171
 
1277
1172
    def without_fallbacks(self):
1278
1173
        """Return a clone of this object without any fallbacks configured."""
1279
1174
        return GroupCompressVersionedFiles(self._index, self._access,
1280
 
                                           self._delta, _unadded_refs=dict(
1281
 
                                               self._unadded_refs),
1282
 
                                           _group_cache=self._group_cache)
 
1175
            self._delta, _unadded_refs=dict(self._unadded_refs))
1283
1176
 
1284
1177
    def add_lines(self, key, parents, lines, parent_texts=None,
1285
 
                  left_matching_blocks=None, nostore_sha=None, random_id=False,
1286
 
                  check_content=True):
 
1178
        left_matching_blocks=None, nostore_sha=None, random_id=False,
 
1179
        check_content=True):
1287
1180
        """Add a text to the store.
1288
1181
 
1289
1182
        :param key: The key tuple of the text to add.
1290
1183
        :param parents: The parents key tuples of the text to add.
1291
1184
        :param lines: A list of lines. Each line must be a bytestring. And all
1292
 
            of them except the last must be terminated with \\n and contain no
1293
 
            other \\n's. The last line may either contain no \\n's or a single
1294
 
            terminating \\n. If the lines list does meet this constraint the
1295
 
            add routine may error or may succeed - but you will be unable to
1296
 
            read the data back accurately. (Checking the lines have been split
 
1185
            of them except the last must be terminated with \n and contain no
 
1186
            other \n's. The last line may either contain no \n's or a single
 
1187
            terminating \n. If the lines list does meet this constraint the add
 
1188
            routine may error or may succeed - but you will be unable to read
 
1189
            the data back accurately. (Checking the lines have been split
1297
1190
            correctly is expensive and extremely unlikely to catch bugs so it
1298
1191
            is not done at runtime unless check_content is True.)
1299
1192
        :param parent_texts: An optional dictionary containing the opaque
1318
1211
                 back to future add_lines calls in the parent_texts dictionary.
1319
1212
        """
1320
1213
        self._index._check_write_ok()
1321
 
        if check_content:
1322
 
            self._check_lines_not_unicode(lines)
1323
 
            self._check_lines_are_lines(lines)
1324
 
        return self.add_content(
1325
 
            ChunkedContentFactory(
1326
 
                key, parents, osutils.sha_strings(lines), lines, chunks_are_lines=True),
1327
 
            parent_texts, left_matching_blocks, nostore_sha, random_id)
1328
 
 
1329
 
    def add_content(self, factory, parent_texts=None,
1330
 
                    left_matching_blocks=None, nostore_sha=None,
1331
 
                    random_id=False):
1332
 
        """Add a text to the store.
1333
 
 
1334
 
        :param factory: A ContentFactory that can be used to retrieve the key,
1335
 
            parents and contents.
1336
 
        :param parent_texts: An optional dictionary containing the opaque
1337
 
            representations of some or all of the parents of version_id to
1338
 
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
1339
 
            returned by add_lines or data corruption can be caused.
1340
 
        :param left_matching_blocks: a hint about which areas are common
1341
 
            between the text and its left-hand-parent.  The format is
1342
 
            the SequenceMatcher.get_matching_blocks format.
1343
 
        :param nostore_sha: Raise ExistingContent and do not add the lines to
1344
 
            the versioned file if the digest of the lines matches this.
1345
 
        :param random_id: If True a random id has been selected rather than
1346
 
            an id determined by some deterministic process such as a converter
1347
 
            from a foreign VCS. When True the backend may choose not to check
1348
 
            for uniqueness of the resulting key within the versioned file, so
1349
 
            this should only be done when the result is expected to be unique
1350
 
            anyway.
1351
 
        :return: The text sha1, the number of bytes in the text, and an opaque
1352
 
                 representation of the inserted version which can be provided
1353
 
                 back to future add_lines calls in the parent_texts dictionary.
1354
 
        """
 
1214
        self._check_add(key, lines, random_id, check_content)
 
1215
        if parents is None:
 
1216
            # The caller might pass None if there is no graph data, but kndx
 
1217
            # indexes can't directly store that, so we give them
 
1218
            # an empty tuple instead.
 
1219
            parents = ()
 
1220
        # double handling for now. Make it work until then.
 
1221
        length = sum(map(len, lines))
 
1222
        record = ChunkedContentFactory(key, parents, None, lines)
 
1223
        sha1 = list(self._insert_record_stream([record], random_id=random_id,
 
1224
                                               nostore_sha=nostore_sha))[0]
 
1225
        return sha1, length, None
 
1226
 
 
1227
    def _add_text(self, key, parents, text, nostore_sha=None, random_id=False):
 
1228
        """See VersionedFiles._add_text()."""
1355
1229
        self._index._check_write_ok()
1356
 
        parents = factory.parents
1357
 
        self._check_add(factory.key, random_id)
 
1230
        self._check_add(key, None, random_id, check_content=False)
 
1231
        if text.__class__ is not str:
 
1232
            raise errors.BzrBadParameterUnicode("text")
1358
1233
        if parents is None:
1359
1234
            # The caller might pass None if there is no graph data, but kndx
1360
1235
            # indexes can't directly store that, so we give them
1361
1236
            # an empty tuple instead.
1362
1237
            parents = ()
1363
1238
        # double handling for now. Make it work until then.
1364
 
        sha1, length = list(self._insert_record_stream(
1365
 
            [factory], random_id=random_id, nostore_sha=nostore_sha))[0]
 
1239
        length = len(text)
 
1240
        record = FulltextContentFactory(key, parents, None, text)
 
1241
        sha1 = list(self._insert_record_stream([record], random_id=random_id,
 
1242
                                               nostore_sha=nostore_sha))[0]
1366
1243
        return sha1, length, None
1367
1244
 
1368
1245
    def add_fallback_versioned_files(self, a_versioned_files):
1370
1247
 
1371
1248
        :param a_versioned_files: A VersionedFiles object.
1372
1249
        """
1373
 
        self._immediate_fallback_vfs.append(a_versioned_files)
 
1250
        self._fallback_vfs.append(a_versioned_files)
1374
1251
 
1375
1252
    def annotate(self, key):
1376
1253
        """See VersionedFiles.annotate."""
1385
1262
        if keys is None:
1386
1263
            keys = self.keys()
1387
1264
            for record in self.get_record_stream(keys, 'unordered', True):
1388
 
                for chunk in record.iter_bytes_as('chunked'):
1389
 
                    pass
 
1265
                record.get_bytes_as('fulltext')
1390
1266
        else:
1391
1267
            return self.get_record_stream(keys, 'unordered', True)
1392
1268
 
1396
1272
        self._index._graph_index.clear_cache()
1397
1273
        self._index._int_cache.clear()
1398
1274
 
1399
 
    def _check_add(self, key, random_id):
 
1275
    def _check_add(self, key, lines, random_id, check_content):
1400
1276
        """check that version_id and lines are safe to add."""
1401
1277
        version_id = key[-1]
1402
1278
        if version_id is not None:
1407
1283
        # probably check that the existing content is identical to what is
1408
1284
        # being inserted, and otherwise raise an exception.  This would make
1409
1285
        # the bundle code simpler.
 
1286
        if check_content:
 
1287
            self._check_lines_not_unicode(lines)
 
1288
            self._check_lines_are_lines(lines)
 
1289
 
 
1290
    def get_known_graph_ancestry(self, keys):
 
1291
        """Get a KnownGraph instance with the ancestry of keys."""
 
1292
        # Note that this is identical to
 
1293
        # KnitVersionedFiles.get_known_graph_ancestry, but they don't share
 
1294
        # ancestry.
 
1295
        parent_map, missing_keys = self._index.find_ancestry(keys)
 
1296
        for fallback in self._fallback_vfs:
 
1297
            if not missing_keys:
 
1298
                break
 
1299
            (f_parent_map, f_missing_keys) = fallback._index.find_ancestry(
 
1300
                                                missing_keys)
 
1301
            parent_map.update(f_parent_map)
 
1302
            missing_keys = f_missing_keys
 
1303
        kg = _mod_graph.KnownGraph(parent_map)
 
1304
        return kg
1410
1305
 
1411
1306
    def get_parent_map(self, keys):
1412
1307
        """Get a map of the graph parents of keys.
1428
1323
            and so on.
1429
1324
        """
1430
1325
        result = {}
1431
 
        sources = [self._index] + self._immediate_fallback_vfs
 
1326
        sources = [self._index] + self._fallback_vfs
1432
1327
        source_results = []
1433
1328
        missing = set(keys)
1434
1329
        for source in sources:
1471
1366
                yield read_memo, cached[read_memo]
1472
1367
            except KeyError:
1473
1368
                # Read the block, and cache it.
1474
 
                zdata = next(raw_records)
 
1369
                zdata = raw_records.next()
1475
1370
                block = GroupCompressBlock.from_bytes(zdata)
1476
1371
                self._group_cache[read_memo] = block
1477
1372
                cached[read_memo] = block
1505
1400
        if not keys:
1506
1401
            return
1507
1402
        if (not self._index.has_graph
1508
 
                and ordering in ('topological', 'groupcompress')):
 
1403
            and ordering in ('topological', 'groupcompress')):
1509
1404
            # Cannot topological order when no graph has been stored.
1510
1405
            # but we allow 'as-requested' or 'unordered'
1511
1406
            ordering = 'unordered'
1515
1410
            try:
1516
1411
                keys = set(remaining_keys)
1517
1412
                for content_factory in self._get_remaining_record_stream(keys,
1518
 
                                                                         orig_keys, ordering, include_delta_closure):
 
1413
                        orig_keys, ordering, include_delta_closure):
1519
1414
                    remaining_keys.discard(content_factory.key)
1520
1415
                    yield content_factory
1521
1416
                return
1522
 
            except errors.RetryWithNewPacks as e:
 
1417
            except errors.RetryWithNewPacks, e:
1523
1418
                self._access.reload_or_raise(e)
1524
1419
 
1525
1420
    def _find_from_fallback(self, missing):
1535
1430
        parent_map = {}
1536
1431
        key_to_source_map = {}
1537
1432
        source_results = []
1538
 
        for source in self._immediate_fallback_vfs:
 
1433
        for source in self._fallback_vfs:
1539
1434
            if not missing:
1540
1435
                break
1541
1436
            source_parents = source.get_parent_map(missing)
1551
1446
 
1552
1447
        The returned objects should be in the order defined by 'ordering',
1553
1448
        which can weave between different sources.
1554
 
 
1555
1449
        :param ordering: Must be one of 'topological' or 'groupcompress'
1556
1450
        :return: List of [(source, [keys])] tuples, such that all keys are in
1557
1451
            the defined order, regardless of source.
1558
1452
        """
1559
1453
        if ordering == 'topological':
1560
 
            present_keys = tsort.topo_sort(parent_map)
 
1454
            present_keys = topo_sort(parent_map)
1561
1455
        else:
1562
1456
            # ordering == 'groupcompress'
1563
1457
            # XXX: This only optimizes for the target ordering. We may need
1585
1479
                source = self
1586
1480
            elif key in key_to_source_map:
1587
1481
                source = key_to_source_map[key]
1588
 
            else:  # absent
 
1482
            else: # absent
1589
1483
                continue
1590
1484
            if source is not current_source:
1591
1485
                source_keys.append((source, []))
1599
1493
            # This is the group the bytes are stored in, followed by the
1600
1494
            # location in the group
1601
1495
            return locations[key][0]
 
1496
        present_keys = sorted(locations.iterkeys(), key=get_group)
1602
1497
        # We don't have an ordering for keys in the in-memory object, but
1603
1498
        # lets process the in-memory ones first.
1604
 
        present_keys = list(unadded_keys)
1605
 
        present_keys.extend(sorted(locations, key=get_group))
 
1499
        present_keys = list(unadded_keys) + present_keys
1606
1500
        # Now grab all of the ones from other sources
1607
1501
        source_keys = [(self, present_keys)]
1608
1502
        source_keys.extend(source_result)
1632
1526
            # start with one key, recurse to its oldest parent, then grab
1633
1527
            # everything in the same group, etc.
1634
1528
            parent_map = dict((key, details[2]) for key, details in
1635
 
                              viewitems(locations))
 
1529
                locations.iteritems())
1636
1530
            for key in unadded_keys:
1637
1531
                parent_map[key] = self._unadded_refs[key]
1638
1532
            parent_map.update(fallback_parent_map)
1640
1534
                                                        key_to_source_map)
1641
1535
        elif ordering == 'as-requested':
1642
1536
            source_keys = self._get_as_requested_source_keys(orig_keys,
1643
 
                                                             locations, unadded_keys, key_to_source_map)
 
1537
                locations, unadded_keys, key_to_source_map)
1644
1538
        else:
1645
1539
            # We want to yield the keys in a semi-optimal (read-wise) ordering.
1646
1540
            # Otherwise we thrash the _group_cache and destroy performance
1647
1541
            source_keys = self._get_io_ordered_source_keys(locations,
1648
 
                                                           unadded_keys, source_result)
 
1542
                unadded_keys, source_result)
1649
1543
        for key in missing:
1650
1544
            yield AbsentContentFactory(key)
1651
1545
        # Batch up as many keys as we can until either:
1652
1546
        #  - we encounter an unadded ref, or
1653
1547
        #  - we run out of keys, or
1654
1548
        #  - the total bytes to retrieve for this batch > BATCH_SIZE
1655
 
        batcher = _BatchingBlockFetcher(self, locations,
1656
 
                                        get_compressor_settings=self._get_compressor_settings)
 
1549
        batcher = _BatchingBlockFetcher(self, locations)
1657
1550
        for source, keys in source_keys:
1658
1551
            if source is self:
1659
1552
                for key in keys:
1662
1555
                        # self._compressor.
1663
1556
                        for factory in batcher.yield_factories(full_flush=True):
1664
1557
                            yield factory
1665
 
                        chunks, sha1 = self._compressor.extract(key)
 
1558
                        bytes, sha1 = self._compressor.extract(key)
1666
1559
                        parents = self._unadded_refs[key]
1667
 
                        yield ChunkedContentFactory(key, parents, sha1, chunks)
 
1560
                        yield FulltextContentFactory(key, parents, sha1, bytes)
1668
1561
                        continue
1669
1562
                    if batcher.add_key(key) > BATCH_SIZE:
1670
1563
                        # Ok, this batch is big enough.  Yield some results.
1683
1576
        """See VersionedFiles.get_sha1s()."""
1684
1577
        result = {}
1685
1578
        for record in self.get_record_stream(keys, 'unordered', True):
1686
 
            if record.sha1 is not None:
 
1579
            if record.sha1 != None:
1687
1580
                result[record.key] = record.sha1
1688
1581
            else:
1689
1582
                if record.storage_kind != 'absent':
1690
 
                    result[record.key] = osutils.sha_strings(
1691
 
                        record.iter_bytes_as('chunked'))
 
1583
                    result[record.key] = osutils.sha_string(
 
1584
                        record.get_bytes_as('fulltext'))
1692
1585
        return result
1693
1586
 
1694
1587
    def insert_record_stream(self, stream):
1702
1595
        # test_insert_record_stream_existing_keys fail for groupcompress and
1703
1596
        # groupcompress-nograph, this needs to be revisited while addressing
1704
1597
        # 'bzr branch' performance issues.
1705
 
        for _, _ in self._insert_record_stream(stream, random_id=False):
 
1598
        for _ in self._insert_record_stream(stream, random_id=False):
1706
1599
            pass
1707
1600
 
1708
 
    def _get_compressor_settings(self):
1709
 
        if self._max_bytes_to_index is None:
1710
 
            # TODO: VersionedFiles don't know about their containing
1711
 
            #       repository, so they don't have much of an idea about their
1712
 
            #       location. So for now, this is only a global option.
1713
 
            c = config.GlobalConfig()
1714
 
            val = c.get_user_option('bzr.groupcompress.max_bytes_to_index')
1715
 
            if val is not None:
1716
 
                try:
1717
 
                    val = int(val)
1718
 
                except ValueError as e:
1719
 
                    trace.warning('Value for '
1720
 
                                  '"bzr.groupcompress.max_bytes_to_index"'
1721
 
                                  ' %r is not an integer'
1722
 
                                  % (val,))
1723
 
                    val = None
1724
 
            if val is None:
1725
 
                val = self._DEFAULT_MAX_BYTES_TO_INDEX
1726
 
            self._max_bytes_to_index = val
1727
 
        return {'max_bytes_to_index': self._max_bytes_to_index}
1728
 
 
1729
 
    def _make_group_compressor(self):
1730
 
        return GroupCompressor(self._get_compressor_settings())
1731
 
 
1732
1601
    def _insert_record_stream(self, stream, random_id=False, nostore_sha=None,
1733
1602
                              reuse_blocks=True):
1734
1603
        """Internal core to insert a record stream into this container.
1742
1611
        :param reuse_blocks: If the source is streaming from
1743
1612
            groupcompress-blocks, just insert the blocks as-is, rather than
1744
1613
            expanding the texts and inserting again.
1745
 
        :return: An iterator over (sha1, length) of the inserted records.
 
1614
        :return: An iterator over the sha1 of the inserted records.
1746
1615
        :seealso insert_record_stream:
1747
1616
        :seealso add_lines:
1748
1617
        """
1749
1618
        adapters = {}
1750
 
 
1751
1619
        def get_adapter(adapter_key):
1752
1620
            try:
1753
1621
                return adapters[adapter_key]
1758
1626
                return adapter
1759
1627
        # This will go up to fulltexts for gc to gc fetching, which isn't
1760
1628
        # ideal.
1761
 
        self._compressor = self._make_group_compressor()
 
1629
        self._compressor = GroupCompressor()
1762
1630
        self._unadded_refs = {}
1763
1631
        keys_to_add = []
1764
 
 
1765
1632
        def flush():
1766
 
            bytes_len, chunks = self._compressor.flush().to_chunks()
1767
 
            self._compressor = self._make_group_compressor()
1768
 
            # Note: At this point we still have 1 copy of the fulltext (in
1769
 
            #       record and the var 'bytes'), and this generates 2 copies of
1770
 
            #       the compressed text (one for bytes, one in chunks)
1771
 
            # TODO: Figure out how to indicate that we would be happy to free
1772
 
            #       the fulltext content at this point. Note that sometimes we
1773
 
            #       will want it later (streaming CHK pages), but most of the
1774
 
            #       time we won't (everything else)
1775
 
            index, start, length = self._access.add_raw_record(
1776
 
                None, bytes_len, chunks)
 
1633
            bytes = self._compressor.flush().to_bytes()
 
1634
            self._compressor = GroupCompressor()
 
1635
            index, start, length = self._access.add_raw_records(
 
1636
                [(None, len(bytes))], bytes)[0]
1777
1637
            nodes = []
1778
1638
            for key, reads, refs in keys_to_add:
1779
 
                nodes.append((key, b"%d %d %s" % (start, length, reads), refs))
 
1639
                nodes.append((key, "%d %d %s" % (start, length, reads), refs))
1780
1640
            self._index.add_records(nodes, random_id=random_id)
1781
1641
            self._unadded_refs = {}
1782
1642
            del keys_to_add[:]
1796
1656
                raise errors.RevisionNotPresent(record.key, self)
1797
1657
            if random_id:
1798
1658
                if record.key in inserted_keys:
1799
 
                    trace.note(gettext('Insert claimed random_id=True,'
1800
 
                                       ' but then inserted %r two times'), record.key)
 
1659
                    trace.note('Insert claimed random_id=True,'
 
1660
                               ' but then inserted %r two times', record.key)
1801
1661
                    continue
1802
1662
                inserted_keys.add(record.key)
1803
1663
            if reuse_blocks:
1817
1677
                if record.storage_kind == 'groupcompress-block':
1818
1678
                    # Insert the raw block into the target repo
1819
1679
                    insert_manager = record._manager
1820
 
                    bytes_len, chunks = record._manager._block.to_chunks()
1821
 
                    _, start, length = self._access.add_raw_record(
1822
 
                        None, bytes_len, chunks)
 
1680
                    bytes = record._manager._block.to_bytes()
 
1681
                    _, start, length = self._access.add_raw_records(
 
1682
                        [(None, len(bytes))], bytes)[0]
 
1683
                    del bytes
1823
1684
                    block_start = start
1824
1685
                    block_length = length
1825
1686
                if record.storage_kind in ('groupcompress-block',
1828
1689
                        raise AssertionError('No insert_manager set')
1829
1690
                    if insert_manager is not record._manager:
1830
1691
                        raise AssertionError('insert_manager does not match'
1831
 
                                             ' the current record, we cannot be positive'
1832
 
                                             ' that the appropriate content was inserted.'
1833
 
                                             )
1834
 
                    value = b"%d %d %d %d" % (block_start, block_length,
1835
 
                                              record._start, record._end)
 
1692
                            ' the current record, we cannot be positive'
 
1693
                            ' that the appropriate content was inserted.'
 
1694
                            )
 
1695
                    value = "%d %d %d %d" % (block_start, block_length,
 
1696
                                             record._start, record._end)
1836
1697
                    nodes = [(record.key, value, (record.parents,))]
1837
1698
                    # TODO: Consider buffering up many nodes to be added, not
1838
1699
                    #       sure how much overhead this has, but we're seeing
1840
1701
                    self._index.add_records(nodes, random_id=random_id)
1841
1702
                    continue
1842
1703
            try:
1843
 
                chunks = record.get_bytes_as('chunked')
 
1704
                bytes = record.get_bytes_as('fulltext')
1844
1705
            except errors.UnavailableRepresentation:
1845
 
                adapter_key = record.storage_kind, 'chunked'
 
1706
                adapter_key = record.storage_kind, 'fulltext'
1846
1707
                adapter = get_adapter(adapter_key)
1847
 
                chunks = adapter.get_bytes(record, 'chunked')
1848
 
            chunks_len = record.size
1849
 
            if chunks_len is None:
1850
 
                chunks_len = sum(map(len, chunks))
 
1708
                bytes = adapter.get_bytes(record)
1851
1709
            if len(record.key) > 1:
1852
1710
                prefix = record.key[0]
1853
1711
                soft = (prefix == last_prefix)
1854
1712
            else:
1855
1713
                prefix = None
1856
1714
                soft = False
1857
 
            if max_fulltext_len < chunks_len:
1858
 
                max_fulltext_len = chunks_len
 
1715
            if max_fulltext_len < len(bytes):
 
1716
                max_fulltext_len = len(bytes)
1859
1717
                max_fulltext_prefix = prefix
1860
1718
            (found_sha1, start_point, end_point,
1861
 
             type) = self._compressor.compress(
1862
 
                 record.key, chunks, chunks_len, record.sha1, soft=soft,
1863
 
                 nostore_sha=nostore_sha)
1864
 
            # delta_ratio = float(chunks_len) / (end_point - start_point)
 
1719
             type) = self._compressor.compress(record.key,
 
1720
                                               bytes, record.sha1, soft=soft,
 
1721
                                               nostore_sha=nostore_sha)
 
1722
            # delta_ratio = float(len(bytes)) / (end_point - start_point)
1865
1723
            # Check if we want to continue to include that text
1866
1724
            if (prefix == max_fulltext_prefix
1867
 
                    and end_point < 2 * max_fulltext_len):
 
1725
                and end_point < 2 * max_fulltext_len):
1868
1726
                # As long as we are on the same file_id, we will fill at least
1869
1727
                # 2 * max_fulltext_len
1870
1728
                start_new_block = False
1871
 
            elif end_point > 4 * 1024 * 1024:
 
1729
            elif end_point > 4*1024*1024:
1872
1730
                start_new_block = True
1873
1731
            elif (prefix is not None and prefix != last_prefix
1874
 
                  and end_point > 2 * 1024 * 1024):
 
1732
                  and end_point > 2*1024*1024):
1875
1733
                start_new_block = True
1876
1734
            else:
1877
1735
                start_new_block = False
1879
1737
            if start_new_block:
1880
1738
                self._compressor.pop_last()
1881
1739
                flush()
1882
 
                max_fulltext_len = chunks_len
 
1740
                max_fulltext_len = len(bytes)
1883
1741
                (found_sha1, start_point, end_point,
1884
 
                 type) = self._compressor.compress(
1885
 
                     record.key, chunks, chunks_len, record.sha1)
 
1742
                 type) = self._compressor.compress(record.key, bytes,
 
1743
                                                   record.sha1)
1886
1744
            if record.key[-1] is None:
1887
 
                key = record.key[:-1] + (b'sha1:' + found_sha1,)
 
1745
                key = record.key[:-1] + ('sha1:' + found_sha1,)
1888
1746
            else:
1889
1747
                key = record.key
1890
1748
            self._unadded_refs[key] = record.parents
1891
 
            yield found_sha1, chunks_len
 
1749
            yield found_sha1
1892
1750
            as_st = static_tuple.StaticTuple.from_sequence
1893
1751
            if record.parents is not None:
1894
1752
                parents = as_st([as_st(p) for p in record.parents])
1895
1753
            else:
1896
1754
                parents = None
1897
1755
            refs = static_tuple.StaticTuple(parents)
1898
 
            keys_to_add.append(
1899
 
                (key, b'%d %d' % (start_point, end_point), refs))
 
1756
            keys_to_add.append((key, '%d %d' % (start_point, end_point), refs))
1900
1757
        if len(keys_to_add):
1901
1758
            flush()
1902
1759
        self._compressor = None
1928
1785
        # but we need to setup a list of records to visit.
1929
1786
        # we need key, position, length
1930
1787
        for key_idx, record in enumerate(self.get_record_stream(keys,
1931
 
                                                                'unordered', True)):
 
1788
            'unordered', True)):
1932
1789
            # XXX: todo - optimise to use less than full texts.
1933
1790
            key = record.key
1934
1791
            if pb is not None:
1935
1792
                pb.update('Walking content', key_idx, total)
1936
1793
            if record.storage_kind == 'absent':
1937
1794
                raise errors.RevisionNotPresent(key, self)
1938
 
            for line in record.iter_bytes_as('lines'):
 
1795
            lines = osutils.split_lines(record.get_bytes_as('fulltext'))
 
1796
            for line in lines:
1939
1797
                yield line, key
1940
1798
        if pb is not None:
1941
1799
            pb.update('Walking content', total, total)
1944
1802
        """See VersionedFiles.keys."""
1945
1803
        if 'evil' in debug.debug_flags:
1946
1804
            trace.mutter_callsite(2, "keys scales with size of history")
1947
 
        sources = [self._index] + self._immediate_fallback_vfs
 
1805
        sources = [self._index] + self._fallback_vfs
1948
1806
        result = set()
1949
1807
        for source in sources:
1950
1808
            result.update(source.keys())
1951
1809
        return result
1952
1810
 
1953
1811
 
1954
 
class _GCBuildDetails(object):
1955
 
    """A blob of data about the build details.
1956
 
 
1957
 
    This stores the minimal data, which then allows compatibility with the old
1958
 
    api, without taking as much memory.
1959
 
    """
1960
 
 
1961
 
    __slots__ = ('_index', '_group_start', '_group_end', '_basis_end',
1962
 
                 '_delta_end', '_parents')
1963
 
 
1964
 
    method = 'group'
1965
 
    compression_parent = None
1966
 
 
1967
 
    def __init__(self, parents, position_info):
1968
 
        self._parents = parents
1969
 
        (self._index, self._group_start, self._group_end, self._basis_end,
1970
 
         self._delta_end) = position_info
1971
 
 
1972
 
    def __repr__(self):
1973
 
        return '%s(%s, %s)' % (self.__class__.__name__,
1974
 
                               self.index_memo, self._parents)
1975
 
 
1976
 
    @property
1977
 
    def index_memo(self):
1978
 
        return (self._index, self._group_start, self._group_end,
1979
 
                self._basis_end, self._delta_end)
1980
 
 
1981
 
    @property
1982
 
    def record_details(self):
1983
 
        return static_tuple.StaticTuple(self.method, None)
1984
 
 
1985
 
    def __getitem__(self, offset):
1986
 
        """Compatibility thunk to act like a tuple."""
1987
 
        if offset == 0:
1988
 
            return self.index_memo
1989
 
        elif offset == 1:
1990
 
            return self.compression_parent  # Always None
1991
 
        elif offset == 2:
1992
 
            return self._parents
1993
 
        elif offset == 3:
1994
 
            return self.record_details
1995
 
        else:
1996
 
            raise IndexError('offset out of range')
1997
 
 
1998
 
    def __len__(self):
1999
 
        return 4
2000
 
 
2001
 
 
2002
1812
class _GCGraphIndex(object):
2003
1813
    """Mapper from GroupCompressVersionedFiles needs into GraphIndex storage."""
2004
1814
 
2005
1815
    def __init__(self, graph_index, is_locked, parents=True,
2006
 
                 add_callback=None, track_external_parent_refs=False,
2007
 
                 inconsistency_fatal=True, track_new_keys=False):
 
1816
        add_callback=None, track_external_parent_refs=False,
 
1817
        inconsistency_fatal=True, track_new_keys=False):
2008
1818
        """Construct a _GCGraphIndex on a graph_index.
2009
1819
 
2010
 
        :param graph_index: An implementation of breezy.index.GraphIndex.
 
1820
        :param graph_index: An implementation of bzrlib.index.GraphIndex.
2011
1821
        :param is_locked: A callback, returns True if the index is locked and
2012
1822
            thus usable.
2013
1823
        :param parents: If True, record knits parents, if not do not record
2033
1843
        # repeated over and over, this creates a surplus of ints
2034
1844
        self._int_cache = {}
2035
1845
        if track_external_parent_refs:
2036
 
            self._key_dependencies = _KeyRefs(
 
1846
            self._key_dependencies = knit._KeyRefs(
2037
1847
                track_new_keys=track_new_keys)
2038
1848
        else:
2039
1849
            self._key_dependencies = None
2063
1873
                if refs:
2064
1874
                    for ref in refs:
2065
1875
                        if ref:
2066
 
                            raise knit.KnitCorrupt(self,
2067
 
                                                   "attempt to add node with parents "
2068
 
                                                   "in parentless index.")
 
1876
                            raise errors.KnitCorrupt(self,
 
1877
                                "attempt to add node with parents "
 
1878
                                "in parentless index.")
2069
1879
                    refs = ()
2070
1880
                    changed = True
2071
1881
            keys[key] = (value, refs)
2079
1889
                if node_refs != passed[1]:
2080
1890
                    details = '%s %s %s' % (key, (value, node_refs), passed)
2081
1891
                    if self._inconsistency_fatal:
2082
 
                        raise knit.KnitCorrupt(self, "inconsistent details"
2083
 
                                               " in add_records: %s" %
2084
 
                                               details)
 
1892
                        raise errors.KnitCorrupt(self, "inconsistent details"
 
1893
                                                 " in add_records: %s" %
 
1894
                                                 details)
2085
1895
                    else:
2086
1896
                        trace.warning("inconsistent details in skipped"
2087
1897
                                      " record: %s", details)
2090
1900
        if changed:
2091
1901
            result = []
2092
1902
            if self._parents:
2093
 
                for key, (value, node_refs) in viewitems(keys):
 
1903
                for key, (value, node_refs) in keys.iteritems():
2094
1904
                    result.append((key, value, node_refs))
2095
1905
            else:
2096
 
                for key, (value, node_refs) in viewitems(keys):
 
1906
                for key, (value, node_refs) in keys.iteritems():
2097
1907
                    result.append((key, value))
2098
1908
            records = result
2099
1909
        key_dependencies = self._key_dependencies
2179
1989
        :param keys: An iterable of keys.
2180
1990
        :return: A dict of key:
2181
1991
            (index_memo, compression_parent, parents, record_details).
2182
 
 
2183
 
            * index_memo: opaque structure to pass to read_records to extract
2184
 
              the raw data
2185
 
            * compression_parent: Content that this record is built upon, may
2186
 
              be None
2187
 
            * parents: Logical parents of this node
2188
 
            * record_details: extra information about the content which needs
2189
 
              to be passed to Factory.parse_record
 
1992
            index_memo
 
1993
                opaque structure to pass to read_records to extract the raw
 
1994
                data
 
1995
            compression_parent
 
1996
                Content that this record is built upon, may be None
 
1997
            parents
 
1998
                Logical parents of this node
 
1999
            record_details
 
2000
                extra information about the content which needs to be passed to
 
2001
                Factory.parse_record
2190
2002
        """
2191
2003
        self._check_read()
2192
2004
        result = {}
2197
2009
                parents = None
2198
2010
            else:
2199
2011
                parents = entry[3][0]
2200
 
            details = _GCBuildDetails(parents, self._node_to_position(entry))
2201
 
            result[key] = details
 
2012
            method = 'group'
 
2013
            result[key] = (self._node_to_position(entry),
 
2014
                                  None, parents, (method, None))
2202
2015
        return result
2203
2016
 
2204
2017
    def keys(self):
2211
2024
 
2212
2025
    def _node_to_position(self, node):
2213
2026
        """Convert an index value to position details."""
2214
 
        bits = node[2].split(b' ')
 
2027
        bits = node[2].split(' ')
2215
2028
        # It would be nice not to read the entire gzip.
2216
2029
        # start and stop are put into _int_cache because they are very common.
2217
2030
        # They define the 'group' that an entry is in, and many groups can have
2220
2033
        # each, or about 7MB. Note that it might be even more when you consider
2221
2034
        # how PyInt is allocated in separate slabs. And you can't return a slab
2222
2035
        # to the OS if even 1 int on it is in use. Note though that Python uses
2223
 
        # a LIFO when re-using PyInt slots, which might cause more
 
2036
        # a LIFO when re-using PyInt slots, which probably causes more
2224
2037
        # fragmentation.
2225
2038
        start = int(bits[0])
2226
2039
        start = self._int_cache.setdefault(start, start)
2250
2063
            key_dependencies.add_references(node[1], node[3][0])
2251
2064
 
2252
2065
 
2253
 
from ._groupcompress_py import (
 
2066
from bzrlib._groupcompress_py import (
2254
2067
    apply_delta,
2255
2068
    apply_delta_to_source,
2256
2069
    encode_base128_int,
2259
2072
    LinesDeltaIndex,
2260
2073
    )
2261
2074
try:
2262
 
    from ._groupcompress_pyx import (
 
2075
    from bzrlib._groupcompress_pyx import (
2263
2076
        apply_delta,
2264
2077
        apply_delta_to_source,
2265
2078
        DeltaIndex,
2267
2080
        decode_base128_int,
2268
2081
        )
2269
2082
    GroupCompressor = PyrexGroupCompressor
2270
 
except ImportError as e:
 
2083
except ImportError, e:
2271
2084
    osutils.failed_to_load_extension(e)
2272
2085
    GroupCompressor = PythonGroupCompressor
 
2086