/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 breezy/bzr/versionedfile.py

  • Committer: Jelmer Vernooij
  • Date: 2020-06-01 19:35:12 UTC
  • mfrom: (7490.29.10 work)
  • mto: This revision was merged to the branch mainline in revision 7507.
  • Revision ID: jelmer@jelmer.uk-20200601193512-yx77edrbrs12d0qy
Merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Canonical Ltd
2
 
#
3
 
# Authors:
4
 
#   Johan Rydberg <jrydberg@gnu.org>
 
1
# Copyright (C) 2006-2011 Canonical Ltd
5
2
#
6
3
# This program is free software; you can redistribute it and/or modify
7
4
# it under the terms of the GNU General Public License as published by
20
17
"""Versioned text file storage api."""
21
18
 
22
19
from copy import copy
23
 
from cStringIO import StringIO
 
20
from io import BytesIO
 
21
import itertools
24
22
import os
25
23
import struct
26
24
from zlib import adler32
27
25
 
28
 
from bzrlib.lazy_import import lazy_import
 
26
from ..lazy_import import lazy_import
29
27
lazy_import(globals(), """
30
 
import urllib
31
 
 
32
 
from bzrlib import (
 
28
from breezy import (
33
29
    annotate,
34
 
    errors,
 
30
    bencode,
35
31
    graph as _mod_graph,
36
 
    groupcompress,
37
 
    index,
38
 
    knit,
39
32
    osutils,
40
33
    multiparent,
41
34
    tsort,
42
35
    revision,
43
 
    ui,
44
 
    )
45
 
from bzrlib.graph import DictParentsProvider, Graph, StackedParentsProvider
46
 
from bzrlib.transport.memory import MemoryTransport
 
36
    urlutils,
 
37
    )
 
38
from breezy.bzr import (
 
39
    groupcompress,
 
40
    index,
 
41
    knit,
 
42
    )
47
43
""")
48
 
from bzrlib.registry import Registry
49
 
from bzrlib.symbol_versioning import *
50
 
from bzrlib.textmerge import TextMerge
51
 
from bzrlib import bencode
 
44
from .. import (
 
45
    errors,
 
46
    )
 
47
from ..registry import Registry
 
48
from ..textmerge import TextMerge
52
49
 
53
50
 
54
51
adapter_registry = Registry()
55
 
adapter_registry.register_lazy(('knit-delta-gz', 'fulltext'), 'bzrlib.knit',
56
 
    'DeltaPlainToFullText')
57
 
adapter_registry.register_lazy(('knit-ft-gz', 'fulltext'), 'bzrlib.knit',
58
 
    'FTPlainToFullText')
59
52
adapter_registry.register_lazy(('knit-annotated-delta-gz', 'knit-delta-gz'),
60
 
    'bzrlib.knit', 'DeltaAnnotatedToUnannotated')
61
 
adapter_registry.register_lazy(('knit-annotated-delta-gz', 'fulltext'),
62
 
    'bzrlib.knit', 'DeltaAnnotatedToFullText')
 
53
                               'breezy.bzr.knit', 'DeltaAnnotatedToUnannotated')
63
54
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'knit-ft-gz'),
64
 
    'bzrlib.knit', 'FTAnnotatedToUnannotated')
65
 
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'fulltext'),
66
 
    'bzrlib.knit', 'FTAnnotatedToFullText')
67
 
# adapter_registry.register_lazy(('knit-annotated-ft-gz', 'chunked'),
68
 
#     'bzrlib.knit', 'FTAnnotatedToChunked')
 
55
                               'breezy.bzr.knit', 'FTAnnotatedToUnannotated')
 
56
for target_storage_kind in ('fulltext', 'chunked', 'lines'):
 
57
    adapter_registry.register_lazy(('knit-delta-gz', target_storage_kind), 'breezy.bzr.knit',
 
58
                                   'DeltaPlainToFullText')
 
59
    adapter_registry.register_lazy(('knit-ft-gz', target_storage_kind), 'breezy.bzr.knit',
 
60
                                   'FTPlainToFullText')
 
61
    adapter_registry.register_lazy(('knit-annotated-ft-gz', target_storage_kind),
 
62
                                   'breezy.bzr.knit', 'FTAnnotatedToFullText')
 
63
    adapter_registry.register_lazy(('knit-annotated-delta-gz', target_storage_kind),
 
64
                                   'breezy.bzr.knit', 'DeltaAnnotatedToFullText')
69
65
 
70
66
 
71
67
class ContentFactory(object):
72
68
    """Abstract interface for insertion and retrieval from a VersionedFile.
73
69
 
74
70
    :ivar sha1: None, or the sha1 of the content fulltext.
 
71
    :ivar size: None, or the size of the content fulltext.
75
72
    :ivar storage_kind: The native storage kind of this factory. One of
76
73
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
77
74
        'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
86
83
    def __init__(self):
87
84
        """Create a ContentFactory."""
88
85
        self.sha1 = None
 
86
        self.size = None
89
87
        self.storage_kind = None
90
88
        self.key = None
91
89
        self.parents = None
99
97
    satisfies this, as does a list of lines.
100
98
 
101
99
    :ivar sha1: None, or the sha1 of the content fulltext.
 
100
    :ivar size: None, or the size of the content fulltext.
102
101
    :ivar storage_kind: The native storage kind of this factory. Always
103
102
        'chunked'
104
103
    :ivar key: The key of this content. Each key is a tuple with a single
106
105
    :ivar parents: A tuple of parent keys for self.key. If the object has
107
106
        no parent information, None (as opposed to () for an empty list of
108
107
        parents).
 
108
    :ivar chunks_are_lines: Whether chunks are lines.
109
109
     """
110
110
 
111
 
    def __init__(self, key, parents, sha1, chunks):
 
111
    def __init__(self, key, parents, sha1, chunks, chunks_are_lines=None):
112
112
        """Create a ContentFactory."""
113
113
        self.sha1 = sha1
 
114
        self.size = sum(map(len, chunks))
114
115
        self.storage_kind = 'chunked'
115
116
        self.key = key
116
117
        self.parents = parents
117
118
        self._chunks = chunks
 
119
        self._chunks_are_lines = chunks_are_lines
118
120
 
119
121
    def get_bytes_as(self, storage_kind):
120
122
        if storage_kind == 'chunked':
121
123
            return self._chunks
122
124
        elif storage_kind == 'fulltext':
123
 
            return ''.join(self._chunks)
 
125
            return b''.join(self._chunks)
 
126
        elif storage_kind == 'lines':
 
127
            if self._chunks_are_lines:
 
128
                return self._chunks
 
129
            return list(osutils.chunks_to_lines(self._chunks))
124
130
        raise errors.UnavailableRepresentation(self.key, storage_kind,
125
 
            self.storage_kind)
 
131
                                               self.storage_kind)
126
132
 
 
133
    def iter_bytes_as(self, storage_kind):
 
134
        if storage_kind == 'chunked':
 
135
            return iter(self._chunks)
 
136
        elif storage_kind == 'lines':
 
137
            if self._chunks_are_lines:
 
138
                return iter(self._chunks)
 
139
            return iter(osutils.chunks_to_lines(self._chunks))
 
140
        raise errors.UnavailableRepresentation(self.key, storage_kind,
 
141
                                               self.storage_kind)
127
142
 
128
143
class FulltextContentFactory(ContentFactory):
129
144
    """Static data content factory.
144
159
    def __init__(self, key, parents, sha1, text):
145
160
        """Create a ContentFactory."""
146
161
        self.sha1 = sha1
 
162
        self.size = len(text)
147
163
        self.storage_kind = 'fulltext'
148
164
        self.key = key
149
165
        self.parents = parents
 
166
        if not isinstance(text, bytes):
 
167
            raise TypeError(text)
150
168
        self._text = text
151
169
 
152
170
    def get_bytes_as(self, storage_kind):
154
172
            return self._text
155
173
        elif storage_kind == 'chunked':
156
174
            return [self._text]
157
 
        raise errors.UnavailableRepresentation(self.key, storage_kind,
158
 
            self.storage_kind)
 
175
        elif storage_kind == 'lines':
 
176
            return osutils.split_lines(self._text)
 
177
        raise errors.UnavailableRepresentation(self.key, storage_kind,
 
178
                                               self.storage_kind)
 
179
 
 
180
    def iter_bytes_as(self, storage_kind):
 
181
        if storage_kind == 'chunked':
 
182
            return iter([self._text])
 
183
        elif storage_kind == 'lines':
 
184
            return iter(osutils.split_lines(self._text))
 
185
        raise errors.UnavailableRepresentation(self.key, storage_kind,
 
186
                                               self.storage_kind)
 
187
 
 
188
 
 
189
class FileContentFactory(ContentFactory):
 
190
    """File-based content factory.
 
191
    """
 
192
 
 
193
    def __init__(self, key, parents, fileobj, sha1=None, size=None):
 
194
        self.key = key
 
195
        self.parents = parents
 
196
        self.file = fileobj
 
197
        self.storage_kind = 'file'
 
198
        self.sha1 = sha1
 
199
        self.size = size
 
200
 
 
201
    def get_bytes_as(self, storage_kind):
 
202
        self.file.seek(0)
 
203
        if storage_kind == 'fulltext':
 
204
            return self.file.read()
 
205
        elif storage_kind == 'chunked':
 
206
            return list(osutils.file_iterator(self.file))
 
207
        elif storage_kind == 'lines':
 
208
            return list(self.file.readlines())
 
209
        raise errors.UnavailableRepresentation(self.key, storage_kind,
 
210
                                               self.storage_kind)
 
211
 
 
212
    def iter_bytes_as(self, storage_kind):
 
213
        self.file.seek(0)
 
214
        if storage_kind == 'chunked':
 
215
            return osutils.file_iterator(self.file)
 
216
        elif storage_kind == 'lines':
 
217
            return self.file
 
218
        raise errors.UnavailableRepresentation(self.key, storage_kind,
 
219
                                               self.storage_kind)
159
220
 
160
221
 
161
222
class AbsentContentFactory(ContentFactory):
171
232
    def __init__(self, key):
172
233
        """Create a ContentFactory."""
173
234
        self.sha1 = None
 
235
        self.size = None
174
236
        self.storage_kind = 'absent'
175
237
        self.key = key
176
238
        self.parents = None
181
243
                         ' code does not handle if it is missing.'
182
244
                         % (self.key,))
183
245
 
 
246
    def iter_bytes_as(self, storage_kind):
 
247
        raise ValueError('A request was made for key: %s, but that'
 
248
                         ' content is not available, and the calling'
 
249
                         ' code does not handle if it is missing.'
 
250
                         % (self.key,))
 
251
 
184
252
 
185
253
class AdapterFactory(ContentFactory):
186
254
    """A content factory to adapt between key prefix's."""
206
274
            yield record
207
275
 
208
276
 
 
277
class _MPDiffGenerator(object):
 
278
    """Pull out the functionality for generating mp_diffs."""
 
279
 
 
280
    def __init__(self, vf, keys):
 
281
        self.vf = vf
 
282
        # This is the order the keys were requested in
 
283
        self.ordered_keys = tuple(keys)
 
284
        # keys + their parents, what we need to compute the diffs
 
285
        self.needed_keys = ()
 
286
        # Map from key: mp_diff
 
287
        self.diffs = {}
 
288
        # Map from key: parents_needed (may have ghosts)
 
289
        self.parent_map = {}
 
290
        # Parents that aren't present
 
291
        self.ghost_parents = ()
 
292
        # Map from parent_key => number of children for this text
 
293
        self.refcounts = {}
 
294
        # Content chunks that are cached while we still need them
 
295
        self.chunks = {}
 
296
 
 
297
    def _find_needed_keys(self):
 
298
        """Find the set of keys we need to request.
 
299
 
 
300
        This includes all the original keys passed in, and the non-ghost
 
301
        parents of those keys.
 
302
 
 
303
        :return: (needed_keys, refcounts)
 
304
            needed_keys is the set of all texts we need to extract
 
305
            refcounts is a dict of {key: num_children} letting us know when we
 
306
                no longer need to cache a given parent text
 
307
        """
 
308
        # All the keys and their parents
 
309
        needed_keys = set(self.ordered_keys)
 
310
        parent_map = self.vf.get_parent_map(needed_keys)
 
311
        self.parent_map = parent_map
 
312
        # TODO: Should we be using a different construct here? I think this
 
313
        #       uses difference_update internally, and we expect the result to
 
314
        #       be tiny
 
315
        missing_keys = needed_keys.difference(parent_map)
 
316
        if missing_keys:
 
317
            raise errors.RevisionNotPresent(list(missing_keys)[0], self.vf)
 
318
        # Parents that might be missing. They are allowed to be ghosts, but we
 
319
        # should check for them
 
320
        refcounts = {}
 
321
        setdefault = refcounts.setdefault
 
322
        just_parents = set()
 
323
        for child_key, parent_keys in parent_map.items():
 
324
            if not parent_keys:
 
325
                # parent_keys may be None if a given VersionedFile claims to
 
326
                # not support graph operations.
 
327
                continue
 
328
            just_parents.update(parent_keys)
 
329
            needed_keys.update(parent_keys)
 
330
            for p in parent_keys:
 
331
                refcounts[p] = setdefault(p, 0) + 1
 
332
        just_parents.difference_update(parent_map)
 
333
        # Remove any parents that are actually ghosts from the needed set
 
334
        self.present_parents = set(self.vf.get_parent_map(just_parents))
 
335
        self.ghost_parents = just_parents.difference(self.present_parents)
 
336
        needed_keys.difference_update(self.ghost_parents)
 
337
        self.needed_keys = needed_keys
 
338
        self.refcounts = refcounts
 
339
        return needed_keys, refcounts
 
340
 
 
341
    def _compute_diff(self, key, parent_lines, lines):
 
342
        """Compute a single mp_diff, and store it in self._diffs"""
 
343
        if len(parent_lines) > 0:
 
344
            # XXX: _extract_blocks is not usefully defined anywhere...
 
345
            #      It was meant to extract the left-parent diff without
 
346
            #      having to recompute it for Knit content (pack-0.92,
 
347
            #      etc). That seems to have regressed somewhere
 
348
            left_parent_blocks = self.vf._extract_blocks(key,
 
349
                                                         parent_lines[0], lines)
 
350
        else:
 
351
            left_parent_blocks = None
 
352
        diff = multiparent.MultiParent.from_lines(lines,
 
353
                                                  parent_lines, left_parent_blocks)
 
354
        self.diffs[key] = diff
 
355
 
 
356
    def _process_one_record(self, key, this_chunks):
 
357
        parent_keys = None
 
358
        if key in self.parent_map:
 
359
            # This record should be ready to diff, since we requested
 
360
            # content in 'topological' order
 
361
            parent_keys = self.parent_map.pop(key)
 
362
            # If a VersionedFile claims 'no-graph' support, then it may return
 
363
            # None for any parent request, so we replace it with an empty tuple
 
364
            if parent_keys is None:
 
365
                parent_keys = ()
 
366
            parent_lines = []
 
367
            for p in parent_keys:
 
368
                # Alternatively we could check p not in self.needed_keys, but
 
369
                # ghost_parents should be tiny versus huge
 
370
                if p in self.ghost_parents:
 
371
                    continue
 
372
                refcount = self.refcounts[p]
 
373
                if refcount == 1:  # Last child reference
 
374
                    self.refcounts.pop(p)
 
375
                    parent_chunks = self.chunks.pop(p)
 
376
                else:
 
377
                    self.refcounts[p] = refcount - 1
 
378
                    parent_chunks = self.chunks[p]
 
379
                p_lines = osutils.chunks_to_lines(parent_chunks)
 
380
                # TODO: Should we cache the line form? We did the
 
381
                #       computation to get it, but storing it this way will
 
382
                #       be less memory efficient...
 
383
                parent_lines.append(p_lines)
 
384
                del p_lines
 
385
            lines = osutils.chunks_to_lines(this_chunks)
 
386
            # Since we needed the lines, we'll go ahead and cache them this way
 
387
            this_chunks = lines
 
388
            self._compute_diff(key, parent_lines, lines)
 
389
            del lines
 
390
        # Is this content required for any more children?
 
391
        if key in self.refcounts:
 
392
            self.chunks[key] = this_chunks
 
393
 
 
394
    def _extract_diffs(self):
 
395
        needed_keys, refcounts = self._find_needed_keys()
 
396
        for record in self.vf.get_record_stream(needed_keys,
 
397
                                                'topological', True):
 
398
            if record.storage_kind == 'absent':
 
399
                raise errors.RevisionNotPresent(record.key, self.vf)
 
400
            self._process_one_record(record.key,
 
401
                                     record.get_bytes_as('chunked'))
 
402
 
 
403
    def compute_diffs(self):
 
404
        self._extract_diffs()
 
405
        dpop = self.diffs.pop
 
406
        return [dpop(k) for k in self.ordered_keys]
 
407
 
 
408
 
209
409
class VersionedFile(object):
210
410
    """Versioned text file storage.
211
411
 
259
459
        raise NotImplementedError
260
460
 
261
461
    def add_lines(self, version_id, parents, lines, parent_texts=None,
262
 
        left_matching_blocks=None, nostore_sha=None, random_id=False,
263
 
        check_content=True):
 
462
                  left_matching_blocks=None, nostore_sha=None, random_id=False,
 
463
                  check_content=True):
264
464
        """Add a single text on top of the versioned file.
265
465
 
266
466
        Must raise RevisionAlreadyPresent if the new version is
300
500
        """
301
501
        self._check_write_ok()
302
502
        return self._add_lines(version_id, parents, lines, parent_texts,
303
 
            left_matching_blocks, nostore_sha, random_id, check_content)
 
503
                               left_matching_blocks, nostore_sha, random_id, check_content)
304
504
 
305
505
    def _add_lines(self, version_id, parents, lines, parent_texts,
306
 
        left_matching_blocks, nostore_sha, random_id, check_content):
 
506
                   left_matching_blocks, nostore_sha, random_id, check_content):
307
507
        """Helper to do the class specific add_lines."""
308
508
        raise NotImplementedError(self.add_lines)
309
509
 
310
510
    def add_lines_with_ghosts(self, version_id, parents, lines,
311
 
        parent_texts=None, nostore_sha=None, random_id=False,
312
 
        check_content=True, left_matching_blocks=None):
 
511
                              parent_texts=None, nostore_sha=None, random_id=False,
 
512
                              check_content=True, left_matching_blocks=None):
313
513
        """Add lines to the versioned file, allowing ghosts to be present.
314
514
 
315
515
        This takes the same parameters as add_lines and returns the same.
316
516
        """
317
517
        self._check_write_ok()
318
518
        return self._add_lines_with_ghosts(version_id, parents, lines,
319
 
            parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
 
519
                                           parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
320
520
 
321
521
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
322
 
        nostore_sha, random_id, check_content, left_matching_blocks):
 
522
                               nostore_sha, random_id, check_content, left_matching_blocks):
323
523
        """Helper to do class specific add_lines_with_ghosts."""
324
524
        raise NotImplementedError(self.add_lines_with_ghosts)
325
525
 
330
530
    def _check_lines_not_unicode(self, lines):
331
531
        """Check that lines being added to a versioned file are not unicode."""
332
532
        for line in lines:
333
 
            if line.__class__ is not str:
 
533
            if not isinstance(line, bytes):
334
534
                raise errors.BzrBadParameterUnicode("lines")
335
535
 
336
536
    def _check_lines_are_lines(self, lines):
337
537
        """Check that the lines really are full lines without inline EOL."""
338
538
        for line in lines:
339
 
            if '\n' in line[:-1]:
 
539
            if b'\n' in line[:-1]:
340
540
                raise errors.BzrBadParameterContainsNewline("lines")
341
541
 
342
542
    def get_format_signature(self):
348
548
 
349
549
    def make_mpdiffs(self, version_ids):
350
550
        """Create multiparent diffs for specified versions."""
 
551
        # XXX: Can't use _MPDiffGenerator just yet. This is because version_ids
 
552
        #      is a list of strings, not keys. And while self.get_record_stream
 
553
        #      is supported, it takes *keys*, while self.get_parent_map() takes
 
554
        #      strings... *sigh*
351
555
        knit_versions = set()
352
556
        knit_versions.update(version_ids)
353
557
        parent_map = self.get_parent_map(version_ids)
357
561
            except KeyError:
358
562
                raise errors.RevisionNotPresent(version_id, self)
359
563
        # We need to filter out ghosts, because we can't diff against them.
360
 
        knit_versions = set(self.get_parent_map(knit_versions).keys())
 
564
        knit_versions = set(self.get_parent_map(knit_versions))
361
565
        lines = dict(zip(knit_versions,
362
 
            self._get_lf_split_line_list(knit_versions)))
 
566
                         self._get_lf_split_line_list(knit_versions)))
363
567
        diffs = []
364
568
        for version_id in version_ids:
365
569
            target = lines[version_id]
366
570
            try:
367
571
                parents = [lines[p] for p in parent_map[version_id] if p in
368
 
                    knit_versions]
 
572
                           knit_versions]
369
573
            except KeyError:
370
574
                # I don't know how this could ever trigger.
371
575
                # parent_map[version_id] was already triggered in the previous
378
582
            else:
379
583
                left_parent_blocks = None
380
584
            diffs.append(multiparent.MultiParent.from_lines(target, parents,
381
 
                         left_parent_blocks))
 
585
                                                            left_parent_blocks))
382
586
        return diffs
383
587
 
384
588
    def _extract_blocks(self, version_id, source, target):
401
605
        for version, parent_ids, expected_sha1, mpdiff in records:
402
606
            needed_parents.update(p for p in parent_ids
403
607
                                  if not mpvf.has_version(p))
404
 
        present_parents = set(self.get_parent_map(needed_parents).keys())
 
608
        present_parents = set(self.get_parent_map(needed_parents))
405
609
        for parent_id, lines in zip(present_parents,
406
 
                                 self._get_lf_split_line_list(present_parents)):
 
610
                                    self._get_lf_split_line_list(present_parents)):
407
611
            mpvf.add_version(lines, parent_id, [])
408
 
        for (version, parent_ids, expected_sha1, mpdiff), lines in\
409
 
            zip(records, mpvf.get_line_list(versions)):
 
612
        for (version, parent_ids, expected_sha1, mpdiff), lines in zip(
 
613
                records, mpvf.get_line_list(versions)):
410
614
            if len(parent_ids) == 1:
411
615
                left_matching_blocks = list(mpdiff.get_matching_blocks(0,
412
 
                    mpvf.get_diff(parent_ids[0]).num_lines()))
 
616
                                                                       mpvf.get_diff(parent_ids[0]).num_lines()))
413
617
            else:
414
618
                left_matching_blocks = None
415
619
            try:
416
620
                _, _, version_text = self.add_lines_with_ghosts(version,
417
 
                    parent_ids, lines, vf_parents,
418
 
                    left_matching_blocks=left_matching_blocks)
 
621
                                                                parent_ids, lines, vf_parents,
 
622
                                                                left_matching_blocks=left_matching_blocks)
419
623
            except NotImplementedError:
420
624
                # The vf can't handle ghosts, so add lines normally, which will
421
625
                # (reasonably) fail if there are ghosts in the data.
422
626
                _, _, version_text = self.add_lines(version,
423
 
                    parent_ids, lines, vf_parents,
424
 
                    left_matching_blocks=left_matching_blocks)
 
627
                                                    parent_ids, lines, vf_parents,
 
628
                                                    left_matching_blocks=left_matching_blocks)
425
629
            vf_parents[version] = version_text
426
630
        sha1s = self.get_sha1s(versions)
427
631
        for version, parent_ids, expected_sha1, mpdiff in records:
434
638
        Raises RevisionNotPresent if version is not present in
435
639
        file history.
436
640
        """
437
 
        return ''.join(self.get_lines(version_id))
 
641
        return b''.join(self.get_lines(version_id))
438
642
    get_string = get_text
439
643
 
440
644
    def get_texts(self, version_ids):
443
647
        Raises RevisionNotPresent if version is not present in
444
648
        file history.
445
649
        """
446
 
        return [''.join(self.get_lines(v)) for v in version_ids]
 
650
        return [b''.join(self.get_lines(v)) for v in version_ids]
447
651
 
448
652
    def get_lines(self, version_id):
449
653
        """Return version contents as a sequence of lines.
454
658
        raise NotImplementedError(self.get_lines)
455
659
 
456
660
    def _get_lf_split_line_list(self, version_ids):
457
 
        return [StringIO(t).readlines() for t in self.get_texts(version_ids)]
 
661
        return [BytesIO(t).readlines() for t in self.get_texts(version_ids)]
458
662
 
459
663
    def get_ancestry(self, version_ids, topo_sorted=True):
460
664
        """Return a list of all ancestors of given version(s). This
465
669
 
466
670
        Must raise RevisionNotPresent if any of the given versions are
467
671
        not present in file history."""
468
 
        if isinstance(version_ids, basestring):
469
 
            version_ids = [version_ids]
470
672
        raise NotImplementedError(self.get_ancestry)
471
673
 
472
674
    def get_ancestry_with_ghosts(self, version_ids):
533
735
        """
534
736
        raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
535
737
 
536
 
    def plan_merge(self, ver_a, ver_b):
 
738
    def plan_merge(self, ver_a, ver_b, base=None):
537
739
        """Return pseudo-annotation indicating how the two versions merge.
538
740
 
539
741
        This is computed between versions a and b and their common
578
780
        self.calls = []
579
781
 
580
782
    def add_lines(self, key, parents, lines, parent_texts=None,
581
 
        left_matching_blocks=None, nostore_sha=None, random_id=False,
582
 
        check_content=True):
 
783
                  left_matching_blocks=None, nostore_sha=None, random_id=False,
 
784
                  check_content=True):
583
785
        self.calls.append(("add_lines", key, parents, lines, parent_texts,
584
 
            left_matching_blocks, nostore_sha, random_id, check_content))
 
786
                           left_matching_blocks, nostore_sha, random_id, check_content))
585
787
        return self._backing_vf.add_lines(key, parents, lines, parent_texts,
586
 
            left_matching_blocks, nostore_sha, random_id, check_content)
 
788
                                          left_matching_blocks, nostore_sha, random_id, check_content)
 
789
 
 
790
    def add_content(self, factory, parent_texts=None,
 
791
                    left_matching_blocks=None, nostore_sha=None, random_id=False,
 
792
                    check_content=True):
 
793
        self.calls.append(("add_content", factory, parent_texts,
 
794
                           left_matching_blocks, nostore_sha, random_id, check_content))
 
795
        return self._backing_vf.add_content(
 
796
            factory, parent_texts, left_matching_blocks, nostore_sha,
 
797
            random_id, check_content)
587
798
 
588
799
    def check(self):
589
800
        self._backing_vf.check()
594
805
 
595
806
    def get_record_stream(self, keys, sort_order, include_delta_closure):
596
807
        self.calls.append(("get_record_stream", list(keys), sort_order,
597
 
            include_delta_closure))
 
808
                           include_delta_closure))
598
809
        return self._backing_vf.get_record_stream(keys, sort_order,
599
 
            include_delta_closure)
 
810
                                                  include_delta_closure)
600
811
 
601
812
    def get_sha1s(self, keys):
602
813
        self.calls.append(("get_sha1s", copy(keys)))
633
844
 
634
845
    def get_record_stream(self, keys, sort_order, include_delta_closure):
635
846
        self.calls.append(("get_record_stream", list(keys), sort_order,
636
 
            include_delta_closure))
 
847
                           include_delta_closure))
637
848
        if sort_order == 'unordered':
638
849
            def sort_key(key):
639
850
                return (self._key_priority.get(key, 0), key)
641
852
            # backing_vf
642
853
            for key in sorted(keys, key=sort_key):
643
854
                for record in self._backing_vf.get_record_stream([key],
644
 
                                'unordered', include_delta_closure):
 
855
                                                                 'unordered', include_delta_closure):
645
856
                    yield record
646
857
        else:
647
858
            for record in self._backing_vf.get_record_stream(keys, sort_order,
648
 
                            include_delta_closure):
 
859
                                                             include_delta_closure):
649
860
                yield record
650
861
 
651
862
 
655
866
    def map(self, key):
656
867
        """Map key to an underlying storage identifier.
657
868
 
658
 
        :param key: A key tuple e.g. ('file-id', 'revision-id').
 
869
        :param key: A key tuple e.g. (b'file-id', b'revision-id').
659
870
        :return: An underlying storage identifier, specific to the partitioning
660
871
            mechanism.
661
872
        """
692
903
 
693
904
    def map(self, key):
694
905
        """See KeyMapper.map()."""
695
 
        return urllib.quote(self._map(key))
 
906
        return urlutils.quote(self._map(key))
696
907
 
697
908
    def unmap(self, partition_id):
698
909
        """See KeyMapper.unmap()."""
699
 
        return self._unmap(urllib.unquote(partition_id))
 
910
        return self._unmap(urlutils.unquote(partition_id))
700
911
 
701
912
 
702
913
class PrefixMapper(URLEscapeMapper):
707
918
 
708
919
    def _map(self, key):
709
920
        """See KeyMapper.map()."""
710
 
        return key[0]
 
921
        return key[0].decode('utf-8')
711
922
 
712
923
    def _unmap(self, partition_id):
713
924
        """See KeyMapper.unmap()."""
714
 
        return (partition_id,)
 
925
        return (partition_id.encode('utf-8'),)
715
926
 
716
927
 
717
928
class HashPrefixMapper(URLEscapeMapper):
723
934
    def _map(self, key):
724
935
        """See KeyMapper.map()."""
725
936
        prefix = self._escape(key[0])
726
 
        return "%02x/%s" % (adler32(prefix) & 0xff, prefix)
 
937
        return "%02x/%s" % (adler32(prefix) & 0xff, prefix.decode('utf-8'))
727
938
 
728
939
    def _escape(self, prefix):
729
940
        """No escaping needed here."""
731
942
 
732
943
    def _unmap(self, partition_id):
733
944
        """See KeyMapper.unmap()."""
734
 
        return (self._unescape(osutils.basename(partition_id)),)
 
945
        return (self._unescape(osutils.basename(partition_id)).encode('utf-8'),)
735
946
 
736
947
    def _unescape(self, basename):
737
948
        """No unescaping needed for HashPrefixMapper."""
744
955
    This mapper is for use with a transport based backend.
745
956
    """
746
957
 
747
 
    _safe = "abcdefghijklmnopqrstuvwxyz0123456789-_@,."
 
958
    _safe = bytearray(b"abcdefghijklmnopqrstuvwxyz0123456789-_@,.")
748
959
 
749
960
    def _escape(self, prefix):
750
961
        """Turn a key element into a filesystem safe string.
751
962
 
752
 
        This is similar to a plain urllib.quote, except
 
963
        This is similar to a plain urlutils.quote, except
753
964
        it uses specific safe characters, so that it doesn't
754
965
        have to translate a lot of valid file ids.
755
966
        """
756
967
        # @ does not get escaped. This is because it is a valid
757
968
        # filesystem character we use all the time, and it looks
758
969
        # a lot better than seeing %40 all the time.
759
 
        r = [((c in self._safe) and c or ('%%%02x' % ord(c)))
760
 
             for c in prefix]
761
 
        return ''.join(r)
 
970
        r = [(c in self._safe) and chr(c) or ('%%%02x' % c)
 
971
             for c in bytearray(prefix)]
 
972
        return ''.join(r).encode('ascii')
762
973
 
763
974
    def _unescape(self, basename):
764
975
        """Escaped names are easily unescaped by urlutils."""
765
 
        return urllib.unquote(basename)
 
976
        return urlutils.unquote(basename)
766
977
 
767
978
 
768
979
def make_versioned_files_factory(versioned_file_factory, mapper):
774
985
    """
775
986
    def factory(transport):
776
987
        return ThunkedVersionedFiles(transport, versioned_file_factory, mapper,
777
 
            lambda:True)
 
988
                                     lambda: True)
778
989
    return factory
779
990
 
780
991
 
789
1000
 
790
1001
    The keyspace is expressed via simple tuples. Any instance of VersionedFiles
791
1002
    may have a different length key-size, but that size will be constant for
792
 
    all texts added to or retrieved from it. For instance, bzrlib uses
 
1003
    all texts added to or retrieved from it. For instance, breezy uses
793
1004
    instances with a key-size of 2 for storing user files in a repository, with
794
1005
    the first element the fileid, and the second the version of that file.
795
1006
 
796
1007
    The use of tuples allows a single code base to support several different
797
1008
    uses with only the mapping logic changing from instance to instance.
 
1009
 
 
1010
    :ivar _immediate_fallback_vfs: For subclasses that support stacking,
 
1011
        this is a list of other VersionedFiles immediately underneath this
 
1012
        one.  They may in turn each have further fallbacks.
798
1013
    """
799
1014
 
800
1015
    def add_lines(self, key, parents, lines, parent_texts=None,
801
 
        left_matching_blocks=None, nostore_sha=None, random_id=False,
802
 
        check_content=True):
 
1016
                  left_matching_blocks=None, nostore_sha=None, random_id=False,
 
1017
                  check_content=True):
803
1018
        """Add a text to the store.
804
1019
 
805
1020
        :param key: The key tuple of the text to add. If the last element is
836
1051
        """
837
1052
        raise NotImplementedError(self.add_lines)
838
1053
 
839
 
    def _add_text(self, key, parents, text, nostore_sha=None, random_id=False):
840
 
        """Add a text to the store.
841
 
 
842
 
        This is a private function for use by CommitBuilder.
 
1054
    def add_content(self, factory, parent_texts=None,
 
1055
                    left_matching_blocks=None, nostore_sha=None, random_id=False,
 
1056
                    check_content=True):
 
1057
        """Add a text to the store from a chunk iterable.
843
1058
 
844
1059
        :param key: The key tuple of the text to add. If the last element is
845
1060
            None, a CHK string will be generated during the addition.
846
1061
        :param parents: The parents key tuples of the text to add.
847
 
        :param text: A string containing the text to be committed.
 
1062
        :param chunk_iter: An iterable over bytestrings.
 
1063
        :param parent_texts: An optional dictionary containing the opaque
 
1064
            representations of some or all of the parents of version_id to
 
1065
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
 
1066
            returned by add_lines or data corruption can be caused.
 
1067
        :param left_matching_blocks: a hint about which areas are common
 
1068
            between the text and its left-hand-parent.  The format is
 
1069
            the SequenceMatcher.get_matching_blocks format.
848
1070
        :param nostore_sha: Raise ExistingContent and do not add the lines to
849
1071
            the versioned file if the digest of the lines matches this.
850
1072
        :param random_id: If True a random id has been selected rather than
857
1079
            bytestrings that are correctly formed lines.
858
1080
        :return: The text sha1, the number of bytes in the text, and an opaque
859
1081
                 representation of the inserted version which can be provided
860
 
                 back to future _add_text calls in the parent_texts dictionary.
 
1082
                 back to future add_lines calls in the parent_texts dictionary.
861
1083
        """
862
 
        # The default implementation just thunks over to .add_lines(),
863
 
        # inefficient, but it works.
864
 
        return self.add_lines(key, parents, osutils.split_lines(text),
865
 
                              nostore_sha=nostore_sha,
866
 
                              random_id=random_id,
867
 
                              check_content=True)
 
1084
        raise NotImplementedError(self.add_content)
868
1085
 
869
1086
    def add_mpdiffs(self, records):
870
1087
        """Add mpdiffs to this VersionedFile.
884
1101
                                  if not mpvf.has_version(p))
885
1102
        # It seems likely that adding all the present parents as fulltexts can
886
1103
        # easily exhaust memory.
887
 
        chunks_to_lines = osutils.chunks_to_lines
888
1104
        for record in self.get_record_stream(needed_parents, 'unordered',
889
 
            True):
 
1105
                                             True):
890
1106
            if record.storage_kind == 'absent':
891
1107
                continue
892
 
            mpvf.add_version(chunks_to_lines(record.get_bytes_as('chunked')),
893
 
                record.key, [])
894
 
        for (key, parent_keys, expected_sha1, mpdiff), lines in\
895
 
            zip(records, mpvf.get_line_list(versions)):
 
1108
            mpvf.add_version(record.get_bytes_as('lines'), record.key, [])
 
1109
        for (key, parent_keys, expected_sha1, mpdiff), lines in zip(
 
1110
                records, mpvf.get_line_list(versions)):
896
1111
            if len(parent_keys) == 1:
897
1112
                left_matching_blocks = list(mpdiff.get_matching_blocks(0,
898
 
                    mpvf.get_diff(parent_keys[0]).num_lines()))
 
1113
                                                                       mpvf.get_diff(parent_keys[0]).num_lines()))
899
1114
            else:
900
1115
                left_matching_blocks = None
901
1116
            version_sha1, _, version_text = self.add_lines(key,
902
 
                parent_keys, lines, vf_parents,
903
 
                left_matching_blocks=left_matching_blocks)
 
1117
                                                           parent_keys, lines, vf_parents,
 
1118
                                                           left_matching_blocks=left_matching_blocks)
904
1119
            if version_sha1 != expected_sha1:
905
1120
                raise errors.VersionedFileInvalidChecksum(version)
906
1121
            vf_parents[key] = version_text
914
1129
 
915
1130
    def check(self, progress_bar=None):
916
1131
        """Check this object for integrity.
917
 
        
 
1132
 
918
1133
        :param progress_bar: A progress bar to output as the check progresses.
919
1134
        :param keys: Specific keys within the VersionedFiles to check. When
920
1135
            this parameter is not None, check() becomes a generator as per
939
1154
    def _check_lines_not_unicode(self, lines):
940
1155
        """Check that lines being added to a versioned file are not unicode."""
941
1156
        for line in lines:
942
 
            if line.__class__ is not str:
 
1157
            if line.__class__ is not bytes:
943
1158
                raise errors.BzrBadParameterUnicode("lines")
944
1159
 
945
1160
    def _check_lines_are_lines(self, lines):
946
1161
        """Check that the lines really are full lines without inline EOL."""
947
1162
        for line in lines:
948
 
            if '\n' in line[:-1]:
 
1163
            if b'\n' in line[:-1]:
949
1164
                raise errors.BzrBadParameterContainsNewline("lines")
950
1165
 
951
1166
    def get_known_graph_ancestry(self, keys):
956
1171
        while pending:
957
1172
            this_parent_map = self.get_parent_map(pending)
958
1173
            parent_map.update(this_parent_map)
959
 
            pending = set()
960
 
            map(pending.update, this_parent_map.itervalues())
961
 
            pending = pending.difference(parent_map)
 
1174
            pending = set(itertools.chain.from_iterable(
 
1175
                this_parent_map.values()))
 
1176
            pending.difference_update(parent_map)
962
1177
        kg = _mod_graph.KnownGraph(parent_map)
963
1178
        return kg
964
1179
 
995
1210
        """
996
1211
        raise NotImplementedError(self.get_sha1s)
997
1212
 
998
 
    has_key = index._has_key_from_parent_map
 
1213
    __contains__ = index._has_key_from_parent_map
999
1214
 
1000
1215
    def get_missing_compression_parent_keys(self):
1001
1216
        """Return an iterable of keys of missing compression parents.
1047
1262
 
1048
1263
    def make_mpdiffs(self, keys):
1049
1264
        """Create multiparent diffs for specified keys."""
1050
 
        keys_order = tuple(keys)
1051
 
        keys = frozenset(keys)
1052
 
        knit_keys = set(keys)
1053
 
        parent_map = self.get_parent_map(keys)
1054
 
        for parent_keys in parent_map.itervalues():
1055
 
            if parent_keys:
1056
 
                knit_keys.update(parent_keys)
1057
 
        missing_keys = keys - set(parent_map)
1058
 
        if missing_keys:
1059
 
            raise errors.RevisionNotPresent(list(missing_keys)[0], self)
1060
 
        # We need to filter out ghosts, because we can't diff against them.
1061
 
        maybe_ghosts = knit_keys - keys
1062
 
        ghosts = maybe_ghosts - set(self.get_parent_map(maybe_ghosts))
1063
 
        knit_keys.difference_update(ghosts)
1064
 
        lines = {}
1065
 
        chunks_to_lines = osutils.chunks_to_lines
1066
 
        for record in self.get_record_stream(knit_keys, 'topological', True):
1067
 
            lines[record.key] = chunks_to_lines(record.get_bytes_as('chunked'))
1068
 
            # line_block_dict = {}
1069
 
            # for parent, blocks in record.extract_line_blocks():
1070
 
            #   line_blocks[parent] = blocks
1071
 
            # line_blocks[record.key] = line_block_dict
1072
 
        diffs = []
1073
 
        for key in keys_order:
1074
 
            target = lines[key]
1075
 
            parents = parent_map[key] or []
1076
 
            # Note that filtering knit_keys can lead to a parent difference
1077
 
            # between the creation and the application of the mpdiff.
1078
 
            parent_lines = [lines[p] for p in parents if p in knit_keys]
1079
 
            if len(parent_lines) > 0:
1080
 
                left_parent_blocks = self._extract_blocks(key, parent_lines[0],
1081
 
                    target)
1082
 
            else:
1083
 
                left_parent_blocks = None
1084
 
            diffs.append(multiparent.MultiParent.from_lines(target,
1085
 
                parent_lines, left_parent_blocks))
1086
 
        return diffs
 
1265
        generator = _MPDiffGenerator(self, keys)
 
1266
        return generator.compute_diffs()
 
1267
 
 
1268
    def get_annotator(self):
 
1269
        return annotate.Annotator(self)
1087
1270
 
1088
1271
    missing_keys = index._missing_keys_from_parent_map
1089
1272
 
1090
1273
    def _extract_blocks(self, version_id, source, target):
1091
1274
        return None
1092
1275
 
 
1276
    def _transitive_fallbacks(self):
 
1277
        """Return the whole stack of fallback versionedfiles.
 
1278
 
 
1279
        This VersionedFiles may have a list of fallbacks, but it doesn't
 
1280
        necessarily know about the whole stack going down, and it can't know
 
1281
        at open time because they may change after the objects are opened.
 
1282
        """
 
1283
        all_fallbacks = []
 
1284
        for a_vfs in self._immediate_fallback_vfs:
 
1285
            all_fallbacks.append(a_vfs)
 
1286
            all_fallbacks.extend(a_vfs._transitive_fallbacks())
 
1287
        return all_fallbacks
 
1288
 
1093
1289
 
1094
1290
class ThunkedVersionedFiles(VersionedFiles):
1095
1291
    """Storage for many versioned files thunked onto a 'VersionedFile' class.
1108
1304
        self._mapper = mapper
1109
1305
        self._is_locked = is_locked
1110
1306
 
 
1307
    def add_content(self, factory, parent_texts=None,
 
1308
                    left_matching_blocks=None, nostore_sha=None, random_id=False):
 
1309
        """See VersionedFiles.add_content()."""
 
1310
        lines = factory.get_bytes_as('lines')
 
1311
        return self.add_lines(
 
1312
            factory.key, factory.parents, lines,
 
1313
            parent_texts=parent_texts,
 
1314
            left_matching_blocks=left_matching_blocks,
 
1315
            nostore_sha=nostore_sha,
 
1316
            random_id=random_id,
 
1317
            check_content=True)
 
1318
 
1111
1319
    def add_lines(self, key, parents, lines, parent_texts=None,
1112
 
        left_matching_blocks=None, nostore_sha=None, random_id=False,
1113
 
        check_content=True):
 
1320
                  left_matching_blocks=None, nostore_sha=None, random_id=False,
 
1321
                  check_content=True):
1114
1322
        """See VersionedFiles.add_lines()."""
1115
1323
        path = self._mapper.map(key)
1116
1324
        version_id = key[-1]
1119
1327
        try:
1120
1328
            try:
1121
1329
                return vf.add_lines_with_ghosts(version_id, parents, lines,
1122
 
                    parent_texts=parent_texts,
1123
 
                    left_matching_blocks=left_matching_blocks,
1124
 
                    nostore_sha=nostore_sha, random_id=random_id,
1125
 
                    check_content=check_content)
 
1330
                                                parent_texts=parent_texts,
 
1331
                                                left_matching_blocks=left_matching_blocks,
 
1332
                                                nostore_sha=nostore_sha, random_id=random_id,
 
1333
                                                check_content=check_content)
1126
1334
            except NotImplementedError:
1127
1335
                return vf.add_lines(version_id, parents, lines,
1128
 
                    parent_texts=parent_texts,
1129
 
                    left_matching_blocks=left_matching_blocks,
1130
 
                    nostore_sha=nostore_sha, random_id=random_id,
1131
 
                    check_content=check_content)
 
1336
                                    parent_texts=parent_texts,
 
1337
                                    left_matching_blocks=left_matching_blocks,
 
1338
                                    nostore_sha=nostore_sha, random_id=random_id,
 
1339
                                    check_content=check_content)
1132
1340
        except errors.NoSuchFile:
1133
1341
            # parent directory may be missing, try again.
1134
1342
            self._transport.mkdir(osutils.dirname(path))
1135
1343
            try:
1136
1344
                return vf.add_lines_with_ghosts(version_id, parents, lines,
1137
 
                    parent_texts=parent_texts,
1138
 
                    left_matching_blocks=left_matching_blocks,
1139
 
                    nostore_sha=nostore_sha, random_id=random_id,
1140
 
                    check_content=check_content)
 
1345
                                                parent_texts=parent_texts,
 
1346
                                                left_matching_blocks=left_matching_blocks,
 
1347
                                                nostore_sha=nostore_sha, random_id=random_id,
 
1348
                                                check_content=check_content)
1141
1349
            except NotImplementedError:
1142
1350
                return vf.add_lines(version_id, parents, lines,
1143
 
                    parent_texts=parent_texts,
1144
 
                    left_matching_blocks=left_matching_blocks,
1145
 
                    nostore_sha=nostore_sha, random_id=random_id,
1146
 
                    check_content=check_content)
 
1351
                                    parent_texts=parent_texts,
 
1352
                                    left_matching_blocks=left_matching_blocks,
 
1353
                                    nostore_sha=nostore_sha, random_id=random_id,
 
1354
                                    check_content=check_content)
1147
1355
 
1148
1356
    def annotate(self, key):
1149
1357
        """Return a list of (version-key, line) tuples for the text of key.
1159
1367
            result.append((prefix + (origin,), line))
1160
1368
        return result
1161
1369
 
1162
 
    def get_annotator(self):
1163
 
        return annotate.Annotator(self)
1164
 
 
1165
1370
    def check(self, progress_bar=None, keys=None):
1166
1371
        """See VersionedFiles.check()."""
1167
1372
        # XXX: This is over-enthusiastic but as we only thunk for Weaves today
1168
 
        # this is tolerable. Ideally we'd pass keys down to check() and 
 
1373
        # this is tolerable. Ideally we'd pass keys down to check() and
1169
1374
        # have the older VersiondFile interface updated too.
1170
1375
        for prefix, vf in self._iter_all_components():
1171
1376
            vf.check()
1194
1399
        if not self._is_locked():
1195
1400
            raise errors.ObjectNotLocked(self)
1196
1401
        return self._file_factory(path, self._transport, create=True,
1197
 
            get_scope=lambda:None)
 
1402
                                  get_scope=lambda: None)
1198
1403
 
1199
1404
    def _partition_keys(self, keys):
1200
1405
        """Turn keys into a dict of prefix:suffix_list."""
1204
1409
            prefix_keys.append(key[-1])
1205
1410
        return result
1206
1411
 
1207
 
    def _get_all_prefixes(self):
 
1412
    def _iter_all_prefixes(self):
1208
1413
        # Identify all key prefixes.
1209
1414
        # XXX: A bit hacky, needs polish.
1210
 
        if type(self._mapper) == ConstantMapper:
 
1415
        if isinstance(self._mapper, ConstantMapper):
1211
1416
            paths = [self._mapper.map(())]
1212
1417
            prefixes = [()]
1213
1418
        else:
1227
1432
        for prefix, suffixes, vf in self._iter_keys_vf(keys):
1228
1433
            suffixes = [(suffix,) for suffix in suffixes]
1229
1434
            for record in vf.get_record_stream(suffixes, ordering,
1230
 
                include_delta_closure):
 
1435
                                               include_delta_closure):
1231
1436
                if record.parents is not None:
1232
1437
                    record.parents = tuple(
1233
1438
                        prefix + parent for parent in record.parents)
1245
1450
    def get_sha1s(self, keys):
1246
1451
        """See VersionedFiles.get_sha1s()."""
1247
1452
        sha1s = {}
1248
 
        for prefix,suffixes, vf in self._iter_keys_vf(keys):
 
1453
        for prefix, suffixes, vf in self._iter_keys_vf(keys):
1249
1454
            vf_sha1s = vf.get_sha1s(suffixes)
1250
 
            for suffix, sha1 in vf_sha1s.iteritems():
 
1455
            for suffix, sha1 in vf_sha1s.items():
1251
1456
                sha1s[prefix + (suffix,)] = sha1
1252
1457
        return sha1s
1253
1458
 
1299
1504
                yield line, prefix + (version,)
1300
1505
 
1301
1506
    def _iter_all_components(self):
1302
 
        for path, prefix in self._get_all_prefixes():
 
1507
        for path, prefix in self._iter_all_prefixes():
1303
1508
            yield prefix, self._get_vf(path)
1304
1509
 
1305
1510
    def keys(self):
1311
1516
        return result
1312
1517
 
1313
1518
 
 
1519
class VersionedFilesWithFallbacks(VersionedFiles):
 
1520
 
 
1521
    def without_fallbacks(self):
 
1522
        """Return a clone of this object without any fallbacks configured."""
 
1523
        raise NotImplementedError(self.without_fallbacks)
 
1524
 
 
1525
    def add_fallback_versioned_files(self, a_versioned_files):
 
1526
        """Add a source of texts for texts not present in this knit.
 
1527
 
 
1528
        :param a_versioned_files: A VersionedFiles object.
 
1529
        """
 
1530
        raise NotImplementedError(self.add_fallback_versioned_files)
 
1531
 
 
1532
    def get_known_graph_ancestry(self, keys):
 
1533
        """Get a KnownGraph instance with the ancestry of keys."""
 
1534
        parent_map, missing_keys = self._index.find_ancestry(keys)
 
1535
        for fallback in self._transitive_fallbacks():
 
1536
            if not missing_keys:
 
1537
                break
 
1538
            (f_parent_map, f_missing_keys) = fallback._index.find_ancestry(
 
1539
                missing_keys)
 
1540
            parent_map.update(f_parent_map)
 
1541
            missing_keys = f_missing_keys
 
1542
        kg = _mod_graph.KnownGraph(parent_map)
 
1543
        return kg
 
1544
 
 
1545
 
1314
1546
class _PlanMergeVersionedFile(VersionedFiles):
1315
1547
    """A VersionedFile for uncommitted and committed texts.
1316
1548
 
1337
1569
        # line data for locally held keys.
1338
1570
        self._lines = {}
1339
1571
        # key lookup providers
1340
 
        self._providers = [DictParentsProvider(self._parents)]
 
1572
        self._providers = [_mod_graph.DictParentsProvider(self._parents)]
1341
1573
 
1342
1574
    def plan_merge(self, ver_a, ver_b, base=None):
1343
1575
        """See VersionedFile.plan_merge"""
1344
 
        from bzrlib.merge import _PlanMerge
 
1576
        from ..merge import _PlanMerge
1345
1577
        if base is None:
1346
1578
            return _PlanMerge(ver_a, ver_b, self, (self._file_id,)).plan_merge()
1347
 
        old_plan = list(_PlanMerge(ver_a, base, self, (self._file_id,)).plan_merge())
1348
 
        new_plan = list(_PlanMerge(ver_a, ver_b, self, (self._file_id,)).plan_merge())
 
1579
        old_plan = list(_PlanMerge(ver_a, base, self,
 
1580
                                   (self._file_id,)).plan_merge())
 
1581
        new_plan = list(_PlanMerge(ver_a, ver_b, self,
 
1582
                                   (self._file_id,)).plan_merge())
1349
1583
        return _PlanMerge._subtract_plans(old_plan, new_plan)
1350
1584
 
1351
1585
    def plan_lca_merge(self, ver_a, ver_b, base=None):
1352
 
        from bzrlib.merge import _PlanLCAMerge
1353
 
        graph = Graph(self)
1354
 
        new_plan = _PlanLCAMerge(ver_a, ver_b, self, (self._file_id,), graph).plan_merge()
 
1586
        from ..merge import _PlanLCAMerge
 
1587
        graph = _mod_graph.Graph(self)
 
1588
        new_plan = _PlanLCAMerge(
 
1589
            ver_a, ver_b, self, (self._file_id,), graph).plan_merge()
1355
1590
        if base is None:
1356
1591
            return new_plan
1357
 
        old_plan = _PlanLCAMerge(ver_a, base, self, (self._file_id,), graph).plan_merge()
 
1592
        old_plan = _PlanLCAMerge(
 
1593
            ver_a, base, self, (self._file_id,), graph).plan_merge()
1358
1594
        return _PlanLCAMerge._subtract_plans(list(old_plan), list(new_plan))
1359
1595
 
 
1596
    def add_content(self, factory):
 
1597
        return self.add_lines(
 
1598
            factory.key, factory.parents, factory.get_bytes_as('lines'))
 
1599
 
1360
1600
    def add_lines(self, key, parents, lines):
1361
1601
        """See VersionedFiles.add_lines
1362
1602
 
1363
1603
        Lines are added locally, not to fallback versionedfiles.  Also, ghosts
1364
1604
        are permitted.  Only reserved ids are permitted.
1365
1605
        """
1366
 
        if type(key) is not tuple:
 
1606
        if not isinstance(key, tuple):
1367
1607
            raise TypeError(key)
1368
1608
        if not revision.is_reserved_id(key[-1]):
1369
1609
            raise ValueError('Only reserved ids may be used')
1381
1621
                lines = self._lines[key]
1382
1622
                parents = self._parents[key]
1383
1623
                pending.remove(key)
1384
 
                yield ChunkedContentFactory(key, parents, None, lines)
 
1624
                yield ChunkedContentFactory(
 
1625
                    key, parents, None, lines,
 
1626
                    chunks_are_lines=True)
1385
1627
        for versionedfile in self.fallback_versionedfiles:
1386
1628
            for record in versionedfile.get_record_stream(
1387
 
                pending, 'unordered', True):
 
1629
                    pending, 'unordered', True):
1388
1630
                if record.storage_kind == 'absent':
1389
1631
                    continue
1390
1632
                else:
1408
1650
            result[revision.NULL_REVISION] = ()
1409
1651
        self._providers = self._providers[:1] + self.fallback_versionedfiles
1410
1652
        result.update(
1411
 
            StackedParentsProvider(self._providers).get_parent_map(keys))
1412
 
        for key, parents in result.iteritems():
 
1653
            _mod_graph.StackedParentsProvider(
 
1654
                self._providers).get_parent_map(keys))
 
1655
        for key, parents in result.items():
1413
1656
            if parents == ():
1414
1657
                result[key] = (revision.NULL_REVISION,)
1415
1658
        return result
1484
1727
                ch_b = ch_a = True
1485
1728
            else:
1486
1729
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
1487
 
                        'killed-base'):
 
1730
                                 'killed-base'):
1488
1731
                    raise AssertionError(state)
1489
1732
        for struct in outstanding_struct():
1490
1733
            yield struct
1548
1791
    """Weave merge that takes a VersionedFile and two versions as its input."""
1549
1792
 
1550
1793
    def __init__(self, versionedfile, ver_a, ver_b,
1551
 
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
 
1794
                 a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
1552
1795
        plan = versionedfile.plan_merge(ver_a, ver_b)
1553
1796
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
1554
1797
 
1588
1831
 
1589
1832
    def get_parent_map(self, keys):
1590
1833
        """See VersionedFiles.get_parent_map."""
1591
 
        return dict([((k,), tuple([(p,) for p in v]))
1592
 
            for k,v in self._get_parent_map([k for (k,) in keys]).iteritems()])
 
1834
        parent_view = self._get_parent_map(k for (k,) in keys).items()
 
1835
        return dict(((k,), tuple((p,) for p in v)) for k, v in parent_view)
1593
1836
 
1594
1837
    def get_sha1s(self, keys):
1595
1838
        """See VersionedFiles.get_sha1s."""
1609
1852
            if lines is not None:
1610
1853
                if not isinstance(lines, list):
1611
1854
                    raise AssertionError
1612
 
                yield ChunkedContentFactory((k,), None,
1613
 
                        sha1=osutils.sha_strings(lines),
1614
 
                        chunks=lines)
 
1855
                yield ChunkedContentFactory(
 
1856
                    (k,), None, sha1=osutils.sha_strings(lines),
 
1857
                    chunks=lines, chunks_are_lines=True)
1615
1858
            else:
1616
1859
                yield AbsentContentFactory((k,))
1617
1860
 
1624
1867
                yield (l, key)
1625
1868
 
1626
1869
 
 
1870
class NoDupeAddLinesDecorator(object):
 
1871
    """Decorator for a VersionedFiles that skips doing an add_lines if the key
 
1872
    is already present.
 
1873
    """
 
1874
 
 
1875
    def __init__(self, store):
 
1876
        self._store = store
 
1877
 
 
1878
    def add_lines(self, key, parents, lines, parent_texts=None,
 
1879
                  left_matching_blocks=None, nostore_sha=None, random_id=False,
 
1880
                  check_content=True):
 
1881
        """See VersionedFiles.add_lines.
 
1882
 
 
1883
        This implementation may return None as the third element of the return
 
1884
        value when the original store wouldn't.
 
1885
        """
 
1886
        if nostore_sha:
 
1887
            raise NotImplementedError(
 
1888
                "NoDupeAddLinesDecorator.add_lines does not implement the "
 
1889
                "nostore_sha behaviour.")
 
1890
        if key[-1] is None:
 
1891
            sha1 = osutils.sha_strings(lines)
 
1892
            key = (b"sha1:" + sha1,)
 
1893
        else:
 
1894
            sha1 = None
 
1895
        if key in self._store.get_parent_map([key]):
 
1896
            # This key has already been inserted, so don't do it again.
 
1897
            if sha1 is None:
 
1898
                sha1 = osutils.sha_strings(lines)
 
1899
            return sha1, sum(map(len, lines)), None
 
1900
        return self._store.add_lines(key, parents, lines,
 
1901
                                     parent_texts=parent_texts,
 
1902
                                     left_matching_blocks=left_matching_blocks,
 
1903
                                     nostore_sha=nostore_sha, random_id=random_id,
 
1904
                                     check_content=check_content)
 
1905
 
 
1906
    def __getattr__(self, name):
 
1907
        return getattr(self._store, name)
 
1908
 
 
1909
 
1627
1910
def network_bytes_to_kind_and_offset(network_bytes):
1628
1911
    """Strip of a record kind from the front of network_bytes.
1629
1912
 
1630
1913
    :param network_bytes: The bytes of a record.
1631
1914
    :return: A tuple (storage_kind, offset_of_remaining_bytes)
1632
1915
    """
1633
 
    line_end = network_bytes.find('\n')
1634
 
    storage_kind = network_bytes[:line_end]
 
1916
    line_end = network_bytes.find(b'\n')
 
1917
    storage_kind = network_bytes[:line_end].decode('ascii')
1635
1918
    return storage_kind, line_end + 1
1636
1919
 
1637
1920
 
1664
1947
        for bytes in self._bytes_iterator:
1665
1948
            storage_kind, line_end = network_bytes_to_kind_and_offset(bytes)
1666
1949
            for record in self._kind_factory[storage_kind](
1667
 
                storage_kind, bytes, line_end):
 
1950
                    storage_kind, bytes, line_end):
1668
1951
                yield record
1669
1952
 
1670
1953
 
1671
1954
def fulltext_network_to_record(kind, bytes, line_end):
1672
1955
    """Convert a network fulltext record to record."""
1673
 
    meta_len, = struct.unpack('!L', bytes[line_end:line_end+4])
1674
 
    record_meta = bytes[line_end+4:line_end+4+meta_len]
 
1956
    meta_len, = struct.unpack('!L', bytes[line_end:line_end + 4])
 
1957
    record_meta = bytes[line_end + 4:line_end + 4 + meta_len]
1675
1958
    key, parents = bencode.bdecode_as_tuple(record_meta)
1676
 
    if parents == 'nil':
 
1959
    if parents == b'nil':
1677
1960
        parents = None
1678
 
    fulltext = bytes[line_end+4+meta_len:]
 
1961
    fulltext = bytes[line_end + 4 + meta_len:]
1679
1962
    return [FulltextContentFactory(key, parents, None, fulltext)]
1680
1963
 
1681
1964
 
1685
1968
 
1686
1969
def record_to_fulltext_bytes(record):
1687
1970
    if record.parents is None:
1688
 
        parents = 'nil'
 
1971
        parents = b'nil'
1689
1972
    else:
1690
1973
        parents = record.parents
1691
1974
    record_meta = bencode.bencode((record.key, parents))
1692
1975
    record_content = record.get_bytes_as('fulltext')
1693
 
    return "fulltext\n%s%s%s" % (
 
1976
    return b"fulltext\n%s%s%s" % (
1694
1977
        _length_prefix(record_meta), record_meta, record_content)
1695
1978
 
1696
1979
 
1705
1988
    # gc-optimal ordering is approximately reverse topological,
1706
1989
    # properly grouped by file-id.
1707
1990
    per_prefix_map = {}
1708
 
    for item in parent_map.iteritems():
 
1991
    for item in parent_map.items():
1709
1992
        key = item[0]
1710
 
        if isinstance(key, str) or len(key) == 1:
1711
 
            prefix = ''
 
1993
        if isinstance(key, bytes) or len(key) == 1:
 
1994
            prefix = b''
1712
1995
        else:
1713
1996
            prefix = key[0]
1714
1997
        try:
1720
2003
    for prefix in sorted(per_prefix_map):
1721
2004
        present_keys.extend(reversed(tsort.topo_sort(per_prefix_map[prefix])))
1722
2005
    return present_keys
 
2006
 
 
2007
 
 
2008
class _KeyRefs(object):
 
2009
 
 
2010
    def __init__(self, track_new_keys=False):
 
2011
        # dict mapping 'key' to 'set of keys referring to that key'
 
2012
        self.refs = {}
 
2013
        if track_new_keys:
 
2014
            # set remembering all new keys
 
2015
            self.new_keys = set()
 
2016
        else:
 
2017
            self.new_keys = None
 
2018
 
 
2019
    def clear(self):
 
2020
        if self.refs:
 
2021
            self.refs.clear()
 
2022
        if self.new_keys:
 
2023
            self.new_keys.clear()
 
2024
 
 
2025
    def add_references(self, key, refs):
 
2026
        # Record the new references
 
2027
        for referenced in refs:
 
2028
            try:
 
2029
                needed_by = self.refs[referenced]
 
2030
            except KeyError:
 
2031
                needed_by = self.refs[referenced] = set()
 
2032
            needed_by.add(key)
 
2033
        # Discard references satisfied by the new key
 
2034
        self.add_key(key)
 
2035
 
 
2036
    def get_new_keys(self):
 
2037
        return self.new_keys
 
2038
 
 
2039
    def get_unsatisfied_refs(self):
 
2040
        return self.refs.keys()
 
2041
 
 
2042
    def _satisfy_refs_for_key(self, key):
 
2043
        try:
 
2044
            del self.refs[key]
 
2045
        except KeyError:
 
2046
            # No keys depended on this key.  That's ok.
 
2047
            pass
 
2048
 
 
2049
    def add_key(self, key):
 
2050
        # satisfy refs for key, and remember that we've seen this key.
 
2051
        self._satisfy_refs_for_key(key)
 
2052
        if self.new_keys is not None:
 
2053
            self.new_keys.add(key)
 
2054
 
 
2055
    def satisfy_refs_for_keys(self, keys):
 
2056
        for key in keys:
 
2057
            self._satisfy_refs_for_key(key)
 
2058
 
 
2059
    def get_referrers(self):
 
2060
        return set(itertools.chain.from_iterable(self.refs.values()))