/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/weave.py

  • Committer: Jelmer Vernooij
  • Date: 2020-05-05 23:32:39 UTC
  • mto: (7490.7.21 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200505233239-kdmnmscn8eisltk6
Add a breezy.__main__ module.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2009 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
# Author: Martin Pool <mbp@canonical.com>
 
18
 
 
19
"""Weave - storage of related text file versions"""
 
20
 
 
21
from __future__ import absolute_import
 
22
 
 
23
# XXX: If we do weaves this way, will a merge still behave the same
 
24
# way if it's done in a different order?  That's a pretty desirable
 
25
# property.
 
26
 
 
27
# TODO: Nothing here so far assumes the lines are really \n newlines,
 
28
# rather than being split up in some other way.  We could accommodate
 
29
# binaries, perhaps by naively splitting on \n or perhaps using
 
30
# something like a rolling checksum.
 
31
 
 
32
# TODO: End marker for each version so we can stop reading?
 
33
 
 
34
# TODO: Check that no insertion occurs inside a deletion that was
 
35
# active in the version of the insertion.
 
36
 
 
37
# TODO: In addition to the SHA-1 check, perhaps have some code that
 
38
# checks structural constraints of the weave: ie that insertions are
 
39
# properly nested, that there is no text outside of an insertion, that
 
40
# insertions or deletions are not repeated, etc.
 
41
 
 
42
# TODO: Parallel-extract that passes back each line along with a
 
43
# description of which revisions include it.  Nice for checking all
 
44
# shas or calculating stats in parallel.
 
45
 
 
46
# TODO: Using a single _extract routine and then processing the output
 
47
# is probably inefficient.  It's simple enough that we can afford to
 
48
# have slight specializations for different ways its used: annotate,
 
49
# basis for add, get, etc.
 
50
 
 
51
# TODO: Probably the API should work only in names to hide the integer
 
52
# indexes from the user.
 
53
 
 
54
# TODO: Is there any potential performance win by having an add()
 
55
# variant that is passed a pre-cooked version of the single basis
 
56
# version?
 
57
 
 
58
# TODO: Reweave can possibly be made faster by remembering diffs
 
59
# where the basis and destination are unchanged.
 
60
 
 
61
# FIXME: Sometimes we will be given a parents list for a revision
 
62
# that includes some redundant parents (i.e. already a parent of
 
63
# something in the list.)  We should eliminate them.  This can
 
64
# be done fairly efficiently because the sequence numbers constrain
 
65
# the possible relationships.
 
66
 
 
67
# FIXME: the conflict markers should be *7* characters
 
68
 
 
69
from copy import copy
 
70
import os
 
71
import patiencediff
 
72
 
 
73
from ..lazy_import import lazy_import
 
74
lazy_import(globals(), """
 
75
from breezy import tsort
 
76
""")
 
77
from .. import (
 
78
    errors,
 
79
    osutils,
 
80
    )
 
81
from ..errors import (
 
82
    RevisionAlreadyPresent,
 
83
    RevisionNotPresent,
 
84
    UnavailableRepresentation,
 
85
    )
 
86
from ..osutils import dirname, sha, sha_strings, split_lines
 
87
from ..revision import NULL_REVISION
 
88
from ..sixish import (
 
89
    BytesIO,
 
90
    )
 
91
from ..trace import mutter
 
92
from .versionedfile import (
 
93
    AbsentContentFactory,
 
94
    adapter_registry,
 
95
    ContentFactory,
 
96
    sort_groupcompress,
 
97
    VersionedFile,
 
98
    )
 
99
from .weavefile import _read_weave_v5, write_weave_v5
 
100
 
 
101
 
 
102
class WeaveError(errors.BzrError):
 
103
 
 
104
    _fmt = "Error in processing weave: %(msg)s"
 
105
 
 
106
    def __init__(self, msg=None):
 
107
        errors.BzrError.__init__(self)
 
108
        self.msg = msg
 
109
 
 
110
 
 
111
class WeaveRevisionAlreadyPresent(WeaveError):
 
112
 
 
113
    _fmt = "Revision {%(revision_id)s} already present in %(weave)s"
 
114
 
 
115
    def __init__(self, revision_id, weave):
 
116
 
 
117
        WeaveError.__init__(self)
 
118
        self.revision_id = revision_id
 
119
        self.weave = weave
 
120
 
 
121
 
 
122
class WeaveRevisionNotPresent(WeaveError):
 
123
 
 
124
    _fmt = "Revision {%(revision_id)s} not present in %(weave)s"
 
125
 
 
126
    def __init__(self, revision_id, weave):
 
127
        WeaveError.__init__(self)
 
128
        self.revision_id = revision_id
 
129
        self.weave = weave
 
130
 
 
131
 
 
132
class WeaveFormatError(WeaveError):
 
133
 
 
134
    _fmt = "Weave invariant violated: %(what)s"
 
135
 
 
136
    def __init__(self, what):
 
137
        WeaveError.__init__(self)
 
138
        self.what = what
 
139
 
 
140
 
 
141
class WeaveParentMismatch(WeaveError):
 
142
 
 
143
    _fmt = "Parents are mismatched between two revisions. %(msg)s"
 
144
 
 
145
 
 
146
class WeaveInvalidChecksum(WeaveError):
 
147
 
 
148
    _fmt = "Text did not match its checksum: %(msg)s"
 
149
 
 
150
 
 
151
class WeaveTextDiffers(WeaveError):
 
152
 
 
153
    _fmt = ("Weaves differ on text content. Revision:"
 
154
            " {%(revision_id)s}, %(weave_a)s, %(weave_b)s")
 
155
 
 
156
    def __init__(self, revision_id, weave_a, weave_b):
 
157
        WeaveError.__init__(self)
 
158
        self.revision_id = revision_id
 
159
        self.weave_a = weave_a
 
160
        self.weave_b = weave_b
 
161
 
 
162
 
 
163
class WeaveContentFactory(ContentFactory):
 
164
    """Content factory for streaming from weaves.
 
165
 
 
166
    :seealso ContentFactory:
 
167
    """
 
168
 
 
169
    def __init__(self, version, weave):
 
170
        """Create a WeaveContentFactory for version from weave."""
 
171
        ContentFactory.__init__(self)
 
172
        self.sha1 = weave.get_sha1s([version])[version]
 
173
        self.key = (version,)
 
174
        parents = weave.get_parent_map([version])[version]
 
175
        self.parents = tuple((parent,) for parent in parents)
 
176
        self.storage_kind = 'fulltext'
 
177
        self._weave = weave
 
178
 
 
179
    def get_bytes_as(self, storage_kind):
 
180
        if storage_kind == 'fulltext':
 
181
            return self._weave.get_text(self.key[-1])
 
182
        elif storage_kind in ('chunked', 'lines'):
 
183
            return self._weave.get_lines(self.key[-1])
 
184
        else:
 
185
            raise UnavailableRepresentation(self.key, storage_kind, 'fulltext')
 
186
 
 
187
    def iter_bytes_as(self, storage_kind):
 
188
        if storage_kind in ('chunked', 'lines'):
 
189
            return iter(self._weave.get_lines(self.key[-1]))
 
190
        else:
 
191
            raise UnavailableRepresentation(self.key, storage_kind, 'fulltext')
 
192
 
 
193
 
 
194
class Weave(VersionedFile):
 
195
    """weave - versioned text file storage.
 
196
 
 
197
    A Weave manages versions of line-based text files, keeping track
 
198
    of the originating version for each line.
 
199
 
 
200
    To clients the "lines" of the file are represented as a list of strings.
 
201
    These strings  will typically have terminal newline characters, but
 
202
    this is not required.  In particular files commonly do not have a newline
 
203
    at the end of the file.
 
204
 
 
205
    Texts can be identified in either of two ways:
 
206
 
 
207
    * a nonnegative index number.
 
208
 
 
209
    * a version-id string.
 
210
 
 
211
    Typically the index number will be valid only inside this weave and
 
212
    the version-id is used to reference it in the larger world.
 
213
 
 
214
    The weave is represented as a list mixing edit instructions and
 
215
    literal text.  Each entry in _weave can be either a string (or
 
216
    unicode), or a tuple.  If a string, it means that the given line
 
217
    should be output in the currently active revisions.
 
218
 
 
219
    If a tuple, it gives a processing instruction saying in which
 
220
    revisions the enclosed lines are active.  The tuple has the form
 
221
    (instruction, version).
 
222
 
 
223
    The instruction can be '{' or '}' for an insertion block, and '['
 
224
    and ']' for a deletion block respectively.  The version is the
 
225
    integer version index.  There is no replace operator, only deletes
 
226
    and inserts.  For '}', the end of an insertion, there is no
 
227
    version parameter because it always closes the most recently
 
228
    opened insertion.
 
229
 
 
230
    Constraints/notes:
 
231
 
 
232
    * A later version can delete lines that were introduced by any
 
233
      number of ancestor versions; this implies that deletion
 
234
      instructions can span insertion blocks without regard to the
 
235
      insertion block's nesting.
 
236
 
 
237
    * Similarly, deletions need not be properly nested with regard to
 
238
      each other, because they might have been generated by
 
239
      independent revisions.
 
240
 
 
241
    * Insertions are always made by inserting a new bracketed block
 
242
      into a single point in the previous weave.  This implies they
 
243
      can nest but not overlap, and the nesting must always have later
 
244
      insertions on the inside.
 
245
 
 
246
    * It doesn't seem very useful to have an active insertion
 
247
      inside an inactive insertion, but it might happen.
 
248
 
 
249
    * Therefore, all instructions are always"considered"; that
 
250
      is passed onto and off the stack.  An outer inactive block
 
251
      doesn't disable an inner block.
 
252
 
 
253
    * Lines are enabled if the most recent enclosing insertion is
 
254
      active and none of the enclosing deletions are active.
 
255
 
 
256
    * There is no point having a deletion directly inside its own
 
257
      insertion; you might as well just not write it.  And there
 
258
      should be no way to get an earlier version deleting a later
 
259
      version.
 
260
 
 
261
    _weave
 
262
        Text of the weave; list of control instruction tuples and strings.
 
263
 
 
264
    _parents
 
265
        List of parents, indexed by version number.
 
266
        It is only necessary to store the minimal set of parents for
 
267
        each version; the parent's parents are implied.
 
268
 
 
269
    _sha1s
 
270
        List of hex SHA-1 of each version.
 
271
 
 
272
    _names
 
273
        List of symbolic names for each version.  Each should be unique.
 
274
 
 
275
    _name_map
 
276
        For each name, the version number.
 
277
 
 
278
    _weave_name
 
279
        Descriptive name of this weave; typically the filename if known.
 
280
        Set by read_weave.
 
281
    """
 
282
 
 
283
    __slots__ = ['_weave', '_parents', '_sha1s', '_names', '_name_map',
 
284
                 '_weave_name', '_matcher', '_allow_reserved']
 
285
 
 
286
    def __init__(self, weave_name=None, access_mode='w', matcher=None,
 
287
                 get_scope=None, allow_reserved=False):
 
288
        """Create a weave.
 
289
 
 
290
        :param get_scope: A callable that returns an opaque object to be used
 
291
            for detecting when this weave goes out of scope (should stop
 
292
            answering requests or allowing mutation).
 
293
        """
 
294
        super(Weave, self).__init__()
 
295
        self._weave = []
 
296
        self._parents = []
 
297
        self._sha1s = []
 
298
        self._names = []
 
299
        self._name_map = {}
 
300
        self._weave_name = weave_name
 
301
        if matcher is None:
 
302
            self._matcher = patiencediff.PatienceSequenceMatcher
 
303
        else:
 
304
            self._matcher = matcher
 
305
        if get_scope is None:
 
306
            def get_scope():
 
307
                return None
 
308
        self._get_scope = get_scope
 
309
        self._scope = get_scope()
 
310
        self._access_mode = access_mode
 
311
        self._allow_reserved = allow_reserved
 
312
 
 
313
    def __repr__(self):
 
314
        return "Weave(%r)" % self._weave_name
 
315
 
 
316
    def _check_write_ok(self):
 
317
        """Is the versioned file marked as 'finished' ? Raise if it is."""
 
318
        if self._get_scope() != self._scope:
 
319
            raise errors.OutSideTransaction()
 
320
        if self._access_mode != 'w':
 
321
            raise errors.ReadOnlyObjectDirtiedError(self)
 
322
 
 
323
    def copy(self):
 
324
        """Return a deep copy of self.
 
325
 
 
326
        The copy can be modified without affecting the original weave."""
 
327
        other = Weave()
 
328
        other._weave = self._weave[:]
 
329
        other._parents = self._parents[:]
 
330
        other._sha1s = self._sha1s[:]
 
331
        other._names = self._names[:]
 
332
        other._name_map = self._name_map.copy()
 
333
        other._weave_name = self._weave_name
 
334
        return other
 
335
 
 
336
    def __eq__(self, other):
 
337
        if not isinstance(other, Weave):
 
338
            return False
 
339
        return self._parents == other._parents \
 
340
            and self._weave == other._weave \
 
341
            and self._sha1s == other._sha1s
 
342
 
 
343
    def __ne__(self, other):
 
344
        return not self.__eq__(other)
 
345
 
 
346
    def _idx_to_name(self, version):
 
347
        return self._names[version]
 
348
 
 
349
    def _lookup(self, name):
 
350
        """Convert symbolic version name to index."""
 
351
        if not self._allow_reserved:
 
352
            self.check_not_reserved_id(name)
 
353
        try:
 
354
            return self._name_map[name]
 
355
        except KeyError:
 
356
            raise RevisionNotPresent(name, self._weave_name)
 
357
 
 
358
    def versions(self):
 
359
        """See VersionedFile.versions."""
 
360
        return self._names[:]
 
361
 
 
362
    def has_version(self, version_id):
 
363
        """See VersionedFile.has_version."""
 
364
        return (version_id in self._name_map)
 
365
 
 
366
    __contains__ = has_version
 
367
 
 
368
    def get_record_stream(self, versions, ordering, include_delta_closure):
 
369
        """Get a stream of records for versions.
 
370
 
 
371
        :param versions: The versions to include. Each version is a tuple
 
372
            (version,).
 
373
        :param ordering: Either 'unordered' or 'topological'. A topologically
 
374
            sorted stream has compression parents strictly before their
 
375
            children.
 
376
        :param include_delta_closure: If True then the closure across any
 
377
            compression parents will be included (in the opaque data).
 
378
        :return: An iterator of ContentFactory objects, each of which is only
 
379
            valid until the iterator is advanced.
 
380
        """
 
381
        versions = [version[-1] for version in versions]
 
382
        if ordering == 'topological':
 
383
            parents = self.get_parent_map(versions)
 
384
            new_versions = tsort.topo_sort(parents)
 
385
            new_versions.extend(set(versions).difference(set(parents)))
 
386
            versions = new_versions
 
387
        elif ordering == 'groupcompress':
 
388
            parents = self.get_parent_map(versions)
 
389
            new_versions = sort_groupcompress(parents)
 
390
            new_versions.extend(set(versions).difference(set(parents)))
 
391
            versions = new_versions
 
392
        for version in versions:
 
393
            if version in self:
 
394
                yield WeaveContentFactory(version, self)
 
395
            else:
 
396
                yield AbsentContentFactory((version,))
 
397
 
 
398
    def get_parent_map(self, version_ids):
 
399
        """See VersionedFile.get_parent_map."""
 
400
        result = {}
 
401
        for version_id in version_ids:
 
402
            if version_id == NULL_REVISION:
 
403
                parents = ()
 
404
            else:
 
405
                try:
 
406
                    parents = tuple(
 
407
                        map(self._idx_to_name,
 
408
                            self._parents[self._lookup(version_id)]))
 
409
                except RevisionNotPresent:
 
410
                    continue
 
411
            result[version_id] = parents
 
412
        return result
 
413
 
 
414
    def get_parents_with_ghosts(self, version_id):
 
415
        raise NotImplementedError(self.get_parents_with_ghosts)
 
416
 
 
417
    def insert_record_stream(self, stream):
 
418
        """Insert a record stream into this versioned file.
 
419
 
 
420
        :param stream: A stream of records to insert.
 
421
        :return: None
 
422
        :seealso VersionedFile.get_record_stream:
 
423
        """
 
424
        adapters = {}
 
425
        for record in stream:
 
426
            # Raise an error when a record is missing.
 
427
            if record.storage_kind == 'absent':
 
428
                raise RevisionNotPresent([record.key[0]], self)
 
429
            # adapt to non-tuple interface
 
430
            parents = [parent[0] for parent in record.parents]
 
431
            if record.storage_kind in ('fulltext', 'chunked', 'lines'):
 
432
                self.add_lines(
 
433
                    record.key[0], parents,
 
434
                    record.get_bytes_as('lines'))
 
435
            else:
 
436
                adapter_key = record.storage_kind, 'lines'
 
437
                try:
 
438
                    adapter = adapters[adapter_key]
 
439
                except KeyError:
 
440
                    adapter_factory = adapter_registry.get(adapter_key)
 
441
                    adapter = adapter_factory(self)
 
442
                    adapters[adapter_key] = adapter
 
443
                lines = adapter.get_bytes(record, 'lines')
 
444
                try:
 
445
                    self.add_lines(record.key[0], parents, lines)
 
446
                except RevisionAlreadyPresent:
 
447
                    pass
 
448
 
 
449
    def _check_repeated_add(self, name, parents, text, sha1):
 
450
        """Check that a duplicated add is OK.
 
451
 
 
452
        If it is, return the (old) index; otherwise raise an exception.
 
453
        """
 
454
        idx = self._lookup(name)
 
455
        if sorted(self._parents[idx]) != sorted(parents) \
 
456
                or sha1 != self._sha1s[idx]:
 
457
            raise RevisionAlreadyPresent(name, self._weave_name)
 
458
        return idx
 
459
 
 
460
    def _add_lines(self, version_id, parents, lines, parent_texts,
 
461
                   left_matching_blocks, nostore_sha, random_id,
 
462
                   check_content):
 
463
        """See VersionedFile.add_lines."""
 
464
        idx = self._add(version_id, lines, list(map(self._lookup, parents)),
 
465
                        nostore_sha=nostore_sha)
 
466
        return sha_strings(lines), sum(map(len, lines)), idx
 
467
 
 
468
    def _add(self, version_id, lines, parents, sha1=None, nostore_sha=None):
 
469
        """Add a single text on top of the weave.
 
470
 
 
471
        Returns the index number of the newly added version.
 
472
 
 
473
        version_id
 
474
            Symbolic name for this version.
 
475
            (Typically the revision-id of the revision that added it.)
 
476
            If None, a name will be allocated based on the hash. (sha1:SHAHASH)
 
477
 
 
478
        parents
 
479
            List or set of direct parent version numbers.
 
480
 
 
481
        lines
 
482
            Sequence of lines to be added in the new version.
 
483
 
 
484
        :param nostore_sha: See VersionedFile.add_lines.
 
485
        """
 
486
        self._check_lines_not_unicode(lines)
 
487
        self._check_lines_are_lines(lines)
 
488
        if not sha1:
 
489
            sha1 = sha_strings(lines)
 
490
        if sha1 == nostore_sha:
 
491
            raise errors.ExistingContent
 
492
        if version_id is None:
 
493
            version_id = b"sha1:" + sha1
 
494
        if version_id in self._name_map:
 
495
            return self._check_repeated_add(version_id, parents, lines, sha1)
 
496
 
 
497
        self._check_versions(parents)
 
498
        new_version = len(self._parents)
 
499
 
 
500
        # if we abort after here the (in-memory) weave will be corrupt because
 
501
        # only some fields are updated
 
502
        # XXX: FIXME implement a succeed-or-fail of the rest of this routine.
 
503
        #      - Robert Collins 20060226
 
504
        self._parents.append(parents[:])
 
505
        self._sha1s.append(sha1)
 
506
        self._names.append(version_id)
 
507
        self._name_map[version_id] = new_version
 
508
 
 
509
        if not parents:
 
510
            # special case; adding with no parents revision; can do
 
511
            # this more quickly by just appending unconditionally.
 
512
            # even more specially, if we're adding an empty text we
 
513
            # need do nothing at all.
 
514
            if lines:
 
515
                self._weave.append((b'{', new_version))
 
516
                self._weave.extend(lines)
 
517
                self._weave.append((b'}', None))
 
518
            return new_version
 
519
 
 
520
        if len(parents) == 1:
 
521
            pv = list(parents)[0]
 
522
            if sha1 == self._sha1s[pv]:
 
523
                # special case: same as the single parent
 
524
                return new_version
 
525
 
 
526
        ancestors = self._inclusions(parents)
 
527
 
 
528
        l = self._weave
 
529
 
 
530
        # basis a list of (origin, lineno, line)
 
531
        basis_lineno = []
 
532
        basis_lines = []
 
533
        for origin, lineno, line in self._extract(ancestors):
 
534
            basis_lineno.append(lineno)
 
535
            basis_lines.append(line)
 
536
 
 
537
        # another small special case: a merge, producing the same text
 
538
        # as auto-merge
 
539
        if lines == basis_lines:
 
540
            return new_version
 
541
 
 
542
        # add a sentinel, because we can also match against the final line
 
543
        basis_lineno.append(len(self._weave))
 
544
 
 
545
        # XXX: which line of the weave should we really consider
 
546
        # matches the end of the file?  the current code says it's the
 
547
        # last line of the weave?
 
548
 
 
549
        # print 'basis_lines:', basis_lines
 
550
        # print 'new_lines:  ', lines
 
551
 
 
552
        s = self._matcher(None, basis_lines, lines)
 
553
 
 
554
        # offset gives the number of lines that have been inserted
 
555
        # into the weave up to the current point; if the original edit
 
556
        # instruction says to change line A then we actually change (A+offset)
 
557
        offset = 0
 
558
 
 
559
        for tag, i1, i2, j1, j2 in s.get_opcodes():
 
560
            # i1,i2 are given in offsets within basis_lines; we need to map
 
561
            # them back to offsets within the entire weave print 'raw match',
 
562
            # tag, i1, i2, j1, j2
 
563
            if tag == 'equal':
 
564
                continue
 
565
            i1 = basis_lineno[i1]
 
566
            i2 = basis_lineno[i2]
 
567
            # the deletion and insertion are handled separately.
 
568
            # first delete the region.
 
569
            if i1 != i2:
 
570
                self._weave.insert(i1 + offset, (b'[', new_version))
 
571
                self._weave.insert(i2 + offset + 1, (b']', new_version))
 
572
                offset += 2
 
573
 
 
574
            if j1 != j2:
 
575
                # there may have been a deletion spanning up to
 
576
                # i2; we want to insert after this region to make sure
 
577
                # we don't destroy ourselves
 
578
                i = i2 + offset
 
579
                self._weave[i:i] = ([(b'{', new_version)] +
 
580
                                    lines[j1:j2] +
 
581
                                    [(b'}', None)])
 
582
                offset += 2 + (j2 - j1)
 
583
        return new_version
 
584
 
 
585
    def _inclusions(self, versions):
 
586
        """Return set of all ancestors of given version(s)."""
 
587
        if not len(versions):
 
588
            return []
 
589
        i = set(versions)
 
590
        for v in range(max(versions), 0, -1):
 
591
            if v in i:
 
592
                # include all its parents
 
593
                i.update(self._parents[v])
 
594
        return i
 
595
 
 
596
    def get_ancestry(self, version_ids, topo_sorted=True):
 
597
        """See VersionedFile.get_ancestry."""
 
598
        if isinstance(version_ids, bytes):
 
599
            version_ids = [version_ids]
 
600
        i = self._inclusions([self._lookup(v) for v in version_ids])
 
601
        return [self._idx_to_name(v) for v in i]
 
602
 
 
603
    def _check_versions(self, indexes):
 
604
        """Check everything in the sequence of indexes is valid"""
 
605
        for i in indexes:
 
606
            try:
 
607
                self._parents[i]
 
608
            except IndexError:
 
609
                raise IndexError("invalid version number %r" % i)
 
610
 
 
611
    def _compatible_parents(self, my_parents, other_parents):
 
612
        """During join check that other_parents are joinable with my_parents.
 
613
 
 
614
        Joinable is defined as 'is a subset of' - supersets may require
 
615
        regeneration of diffs, but subsets do not.
 
616
        """
 
617
        return len(other_parents.difference(my_parents)) == 0
 
618
 
 
619
    def annotate(self, version_id):
 
620
        """Return a list of (version-id, line) tuples for version_id.
 
621
 
 
622
        The index indicates when the line originated in the weave."""
 
623
        incls = [self._lookup(version_id)]
 
624
        return [(self._idx_to_name(origin), text) for origin, lineno, text in
 
625
                self._extract(incls)]
 
626
 
 
627
    def iter_lines_added_or_present_in_versions(self, version_ids=None,
 
628
                                                pb=None):
 
629
        """See VersionedFile.iter_lines_added_or_present_in_versions()."""
 
630
        if version_ids is None:
 
631
            version_ids = self.versions()
 
632
        version_ids = set(version_ids)
 
633
        for lineno, inserted, deletes, line in self._walk_internal(
 
634
                version_ids):
 
635
            if inserted not in version_ids:
 
636
                continue
 
637
            if not line.endswith(b'\n'):
 
638
                yield line + b'\n', inserted
 
639
            else:
 
640
                yield line, inserted
 
641
 
 
642
    def _walk_internal(self, version_ids=None):
 
643
        """Helper method for weave actions."""
 
644
 
 
645
        istack = []
 
646
        dset = set()
 
647
 
 
648
        lineno = 0         # line of weave, 0-based
 
649
 
 
650
        for l in self._weave:
 
651
            if l.__class__ == tuple:
 
652
                c, v = l
 
653
                if c == b'{':
 
654
                    istack.append(self._names[v])
 
655
                elif c == b'}':
 
656
                    istack.pop()
 
657
                elif c == b'[':
 
658
                    dset.add(self._names[v])
 
659
                elif c == b']':
 
660
                    dset.remove(self._names[v])
 
661
                else:
 
662
                    raise WeaveFormatError('unexpected instruction %r' % v)
 
663
            else:
 
664
                yield lineno, istack[-1], frozenset(dset), l
 
665
            lineno += 1
 
666
 
 
667
        if istack:
 
668
            raise WeaveFormatError("unclosed insertion blocks "
 
669
                                   "at end of weave: %s" % istack)
 
670
        if dset:
 
671
            raise WeaveFormatError(
 
672
                "unclosed deletion blocks at end of weave: %s" % dset)
 
673
 
 
674
    def plan_merge(self, ver_a, ver_b):
 
675
        """Return pseudo-annotation indicating how the two versions merge.
 
676
 
 
677
        This is computed between versions a and b and their common
 
678
        base.
 
679
 
 
680
        Weave lines present in none of them are skipped entirely.
 
681
        """
 
682
        inc_a = set(self.get_ancestry([ver_a]))
 
683
        inc_b = set(self.get_ancestry([ver_b]))
 
684
        inc_c = inc_a & inc_b
 
685
 
 
686
        for lineno, insert, deleteset, line in self._walk_internal(
 
687
                [ver_a, ver_b]):
 
688
            if deleteset & inc_c:
 
689
                # killed in parent; can't be in either a or b
 
690
                # not relevant to our work
 
691
                yield 'killed-base', line
 
692
            elif insert in inc_c:
 
693
                # was inserted in base
 
694
                killed_a = bool(deleteset & inc_a)
 
695
                killed_b = bool(deleteset & inc_b)
 
696
                if killed_a and killed_b:
 
697
                    yield 'killed-both', line
 
698
                elif killed_a:
 
699
                    yield 'killed-a', line
 
700
                elif killed_b:
 
701
                    yield 'killed-b', line
 
702
                else:
 
703
                    yield 'unchanged', line
 
704
            elif insert in inc_a:
 
705
                if deleteset & inc_a:
 
706
                    yield 'ghost-a', line
 
707
                else:
 
708
                    # new in A; not in B
 
709
                    yield 'new-a', line
 
710
            elif insert in inc_b:
 
711
                if deleteset & inc_b:
 
712
                    yield 'ghost-b', line
 
713
                else:
 
714
                    yield 'new-b', line
 
715
            else:
 
716
                # not in either revision
 
717
                yield 'irrelevant', line
 
718
 
 
719
    def _extract(self, versions):
 
720
        """Yield annotation of lines in included set.
 
721
 
 
722
        Yields a sequence of tuples (origin, lineno, text), where
 
723
        origin is the origin version, lineno the index in the weave,
 
724
        and text the text of the line.
 
725
 
 
726
        The set typically but not necessarily corresponds to a version.
 
727
        """
 
728
        for i in versions:
 
729
            if not isinstance(i, int):
 
730
                raise ValueError(i)
 
731
 
 
732
        included = self._inclusions(versions)
 
733
 
 
734
        istack = []
 
735
        iset = set()
 
736
        dset = set()
 
737
 
 
738
        lineno = 0         # line of weave, 0-based
 
739
 
 
740
        isactive = None
 
741
 
 
742
        result = []
 
743
 
 
744
        # wow.
 
745
        #  449       0   4474.6820   2356.5590   breezy.weave:556(_extract)
 
746
        #  +285282   0   1676.8040   1676.8040   +<isinstance>
 
747
        # 1.6 seconds in 'isinstance'.
 
748
        # changing the first isinstance:
 
749
        #  449       0   2814.2660   1577.1760   breezy.weave:556(_extract)
 
750
        #  +140414   0    762.8050    762.8050   +<isinstance>
 
751
        # note that the inline time actually dropped (less function calls)
 
752
        # and total processing time was halved.
 
753
        # we're still spending ~1/4 of the method in isinstance though.
 
754
        # so lets hard code the acceptable string classes we expect:
 
755
        #  449       0   1202.9420    786.2930   breezy.weave:556(_extract)
 
756
        # +71352     0    377.5560    377.5560   +<method 'append' of 'list'
 
757
        #                                          objects>
 
758
        # yay, down to ~1/4 the initial extract time, and our inline time
 
759
        # has shrunk again, with isinstance no longer dominating.
 
760
        # tweaking the stack inclusion test to use a set gives:
 
761
        #  449       0   1122.8030    713.0080   breezy.weave:556(_extract)
 
762
        # +71352     0    354.9980    354.9980   +<method 'append' of 'list'
 
763
        #                                          objects>
 
764
        # - a 5% win, or possibly just noise. However with large istacks that
 
765
        # 'in' test could dominate, so I'm leaving this change in place - when
 
766
        # its fast enough to consider profiling big datasets we can review.
 
767
 
 
768
        for l in self._weave:
 
769
            if l.__class__ == tuple:
 
770
                c, v = l
 
771
                isactive = None
 
772
                if c == b'{':
 
773
                    istack.append(v)
 
774
                    iset.add(v)
 
775
                elif c == b'}':
 
776
                    iset.remove(istack.pop())
 
777
                elif c == b'[':
 
778
                    if v in included:
 
779
                        dset.add(v)
 
780
                elif c == b']':
 
781
                    if v in included:
 
782
                        dset.remove(v)
 
783
                else:
 
784
                    raise AssertionError()
 
785
            else:
 
786
                if isactive is None:
 
787
                    isactive = (not dset) and istack and (
 
788
                        istack[-1] in included)
 
789
                if isactive:
 
790
                    result.append((istack[-1], lineno, l))
 
791
            lineno += 1
 
792
        if istack:
 
793
            raise WeaveFormatError("unclosed insertion blocks "
 
794
                                   "at end of weave: %s" % istack)
 
795
        if dset:
 
796
            raise WeaveFormatError(
 
797
                "unclosed deletion blocks at end of weave: %s" % dset)
 
798
        return result
 
799
 
 
800
    def _maybe_lookup(self, name_or_index):
 
801
        """Convert possible symbolic name to index, or pass through indexes.
 
802
 
 
803
        NOT FOR PUBLIC USE.
 
804
        """
 
805
        # GZ 2017-04-01: This used to check for long as well, but I don't think
 
806
        # there are python implementations with sys.maxsize > sys.maxint
 
807
        if isinstance(name_or_index, int):
 
808
            return name_or_index
 
809
        else:
 
810
            return self._lookup(name_or_index)
 
811
 
 
812
    def get_lines(self, version_id):
 
813
        """See VersionedFile.get_lines()."""
 
814
        int_index = self._maybe_lookup(version_id)
 
815
        result = [line for (origin, lineno, line)
 
816
                  in self._extract([int_index])]
 
817
        expected_sha1 = self._sha1s[int_index]
 
818
        measured_sha1 = sha_strings(result)
 
819
        if measured_sha1 != expected_sha1:
 
820
            raise WeaveInvalidChecksum(
 
821
                'file %s, revision %s, expected: %s, measured %s'
 
822
                % (self._weave_name, version_id,
 
823
                   expected_sha1, measured_sha1))
 
824
        return result
 
825
 
 
826
    def get_sha1s(self, version_ids):
 
827
        """See VersionedFile.get_sha1s()."""
 
828
        result = {}
 
829
        for v in version_ids:
 
830
            result[v] = self._sha1s[self._lookup(v)]
 
831
        return result
 
832
 
 
833
    def num_versions(self):
 
834
        """How many versions are in this weave?"""
 
835
        return len(self._parents)
 
836
 
 
837
    __len__ = num_versions
 
838
 
 
839
    def check(self, progress_bar=None):
 
840
        # TODO evaluate performance hit of using string sets in this routine.
 
841
        # TODO: check no circular inclusions
 
842
        # TODO: create a nested progress bar
 
843
        for version in range(self.num_versions()):
 
844
            inclusions = list(self._parents[version])
 
845
            if inclusions:
 
846
                inclusions.sort()
 
847
                if inclusions[-1] >= version:
 
848
                    raise WeaveFormatError(
 
849
                        "invalid included version %d for index %d"
 
850
                        % (inclusions[-1], version))
 
851
 
 
852
        # try extracting all versions; parallel extraction is used
 
853
        nv = self.num_versions()
 
854
        sha1s = {}
 
855
        texts = {}
 
856
        inclusions = {}
 
857
        for i in range(nv):
 
858
            # For creating the ancestry, IntSet is much faster (3.7s vs 0.17s)
 
859
            # The problem is that set membership is much more expensive
 
860
            name = self._idx_to_name(i)
 
861
            sha1s[name] = sha()
 
862
            texts[name] = []
 
863
            new_inc = {name}
 
864
            for p in self._parents[i]:
 
865
                new_inc.update(inclusions[self._idx_to_name(p)])
 
866
 
 
867
            if set(new_inc) != set(self.get_ancestry(name)):
 
868
                raise AssertionError(
 
869
                    'failed %s != %s'
 
870
                    % (set(new_inc), set(self.get_ancestry(name))))
 
871
            inclusions[name] = new_inc
 
872
 
 
873
        nlines = len(self._weave)
 
874
 
 
875
        update_text = 'checking weave'
 
876
        if self._weave_name:
 
877
            short_name = os.path.basename(self._weave_name)
 
878
            update_text = 'checking %s' % (short_name,)
 
879
            update_text = update_text[:25]
 
880
 
 
881
        for lineno, insert, deleteset, line in self._walk_internal():
 
882
            if progress_bar:
 
883
                progress_bar.update(update_text, lineno, nlines)
 
884
 
 
885
            for name, name_inclusions in inclusions.items():
 
886
                # The active inclusion must be an ancestor,
 
887
                # and no ancestors must have deleted this line,
 
888
                # because we don't support resurrection.
 
889
                if ((insert in name_inclusions) and
 
890
                        not (deleteset & name_inclusions)):
 
891
                    sha1s[name].update(line)
 
892
 
 
893
        for i in range(nv):
 
894
            version = self._idx_to_name(i)
 
895
            hd = sha1s[version].hexdigest().encode()
 
896
            expected = self._sha1s[i]
 
897
            if hd != expected:
 
898
                raise WeaveInvalidChecksum(
 
899
                    "mismatched sha1 for version %s: "
 
900
                    "got %s, expected %s"
 
901
                    % (version, hd, expected))
 
902
 
 
903
        # TODO: check insertions are properly nested, that there are
 
904
        # no lines outside of insertion blocks, that deletions are
 
905
        # properly paired, etc.
 
906
 
 
907
    def _imported_parents(self, other, other_idx):
 
908
        """Return list of parents in self corresponding to indexes in other."""
 
909
        new_parents = []
 
910
        for parent_idx in other._parents[other_idx]:
 
911
            parent_name = other._names[parent_idx]
 
912
            if parent_name not in self._name_map:
 
913
                # should not be possible
 
914
                raise WeaveError("missing parent {%s} of {%s} in %r"
 
915
                                 % (parent_name, other._name_map[other_idx], self))
 
916
            new_parents.append(self._name_map[parent_name])
 
917
        return new_parents
 
918
 
 
919
    def _check_version_consistent(self, other, other_idx, name):
 
920
        """Check if a version in consistent in this and other.
 
921
 
 
922
        To be consistent it must have:
 
923
 
 
924
         * the same text
 
925
         * the same direct parents (by name, not index, and disregarding
 
926
           order)
 
927
 
 
928
        If present & correct return True;
 
929
        if not present in self return False;
 
930
        if inconsistent raise error."""
 
931
        this_idx = self._name_map.get(name, -1)
 
932
        if this_idx != -1:
 
933
            if self._sha1s[this_idx] != other._sha1s[other_idx]:
 
934
                raise WeaveTextDiffers(name, self, other)
 
935
            self_parents = self._parents[this_idx]
 
936
            other_parents = other._parents[other_idx]
 
937
            n1 = {self._names[i] for i in self_parents}
 
938
            n2 = {other._names[i] for i in other_parents}
 
939
            if not self._compatible_parents(n1, n2):
 
940
                raise WeaveParentMismatch(
 
941
                    "inconsistent parents "
 
942
                    "for version {%s}: %s vs %s" % (name, n1, n2))
 
943
            else:
 
944
                return True         # ok!
 
945
        else:
 
946
            return False
 
947
 
 
948
    def _reweave(self, other, pb, msg):
 
949
        """Reweave self with other - internal helper for join().
 
950
 
 
951
        :param other: The other weave to merge
 
952
        :param pb: An optional progress bar, indicating how far done we are
 
953
        :param msg: An optional message for the progress
 
954
        """
 
955
        new_weave = _reweave(self, other, pb=pb, msg=msg)
 
956
        self._copy_weave_content(new_weave)
 
957
 
 
958
    def _copy_weave_content(self, otherweave):
 
959
        """adsorb the content from otherweave."""
 
960
        for attr in self.__slots__:
 
961
            if attr != '_weave_name':
 
962
                setattr(self, attr, copy(getattr(otherweave, attr)))
 
963
 
 
964
 
 
965
class WeaveFile(Weave):
 
966
    """A WeaveFile represents a Weave on disk and writes on change."""
 
967
 
 
968
    WEAVE_SUFFIX = '.weave'
 
969
 
 
970
    def __init__(self, name, transport, filemode=None, create=False,
 
971
                 access_mode='w', get_scope=None):
 
972
        """Create a WeaveFile.
 
973
 
 
974
        :param create: If not True, only open an existing knit.
 
975
        """
 
976
        super(WeaveFile, self).__init__(name, access_mode, get_scope=get_scope,
 
977
                                        allow_reserved=False)
 
978
        self._transport = transport
 
979
        self._filemode = filemode
 
980
        try:
 
981
            f = self._transport.get(name + WeaveFile.WEAVE_SUFFIX)
 
982
            _read_weave_v5(BytesIO(f.read()), self)
 
983
        except errors.NoSuchFile:
 
984
            if not create:
 
985
                raise
 
986
            # new file, save it
 
987
            self._save()
 
988
 
 
989
    def _add_lines(self, version_id, parents, lines, parent_texts,
 
990
                   left_matching_blocks, nostore_sha, random_id,
 
991
                   check_content):
 
992
        """Add a version and save the weave."""
 
993
        self.check_not_reserved_id(version_id)
 
994
        result = super(WeaveFile, self)._add_lines(
 
995
            version_id, parents, lines, parent_texts, left_matching_blocks,
 
996
            nostore_sha, random_id, check_content)
 
997
        self._save()
 
998
        return result
 
999
 
 
1000
    def copy_to(self, name, transport):
 
1001
        """See VersionedFile.copy_to()."""
 
1002
        # as we are all in memory always, just serialise to the new place.
 
1003
        sio = BytesIO()
 
1004
        write_weave_v5(self, sio)
 
1005
        sio.seek(0)
 
1006
        transport.put_file(name + WeaveFile.WEAVE_SUFFIX, sio, self._filemode)
 
1007
 
 
1008
    def _save(self):
 
1009
        """Save the weave."""
 
1010
        self._check_write_ok()
 
1011
        sio = BytesIO()
 
1012
        write_weave_v5(self, sio)
 
1013
        sio.seek(0)
 
1014
        bytes = sio.getvalue()
 
1015
        path = self._weave_name + WeaveFile.WEAVE_SUFFIX
 
1016
        try:
 
1017
            self._transport.put_bytes(path, bytes, self._filemode)
 
1018
        except errors.NoSuchFile:
 
1019
            self._transport.mkdir(dirname(path))
 
1020
            self._transport.put_bytes(path, bytes, self._filemode)
 
1021
 
 
1022
    @staticmethod
 
1023
    def get_suffixes():
 
1024
        """See VersionedFile.get_suffixes()."""
 
1025
        return [WeaveFile.WEAVE_SUFFIX]
 
1026
 
 
1027
    def insert_record_stream(self, stream):
 
1028
        super(WeaveFile, self).insert_record_stream(stream)
 
1029
        self._save()
 
1030
 
 
1031
 
 
1032
def _reweave(wa, wb, pb=None, msg=None):
 
1033
    """Combine two weaves and return the result.
 
1034
 
 
1035
    This works even if a revision R has different parents in
 
1036
    wa and wb.  In the resulting weave all the parents are given.
 
1037
 
 
1038
    This is done by just building up a new weave, maintaining ordering
 
1039
    of the versions in the two inputs.  More efficient approaches
 
1040
    might be possible but it should only be necessary to do
 
1041
    this operation rarely, when a new previously ghost version is
 
1042
    inserted.
 
1043
 
 
1044
    :param pb: An optional progress bar, indicating how far done we are
 
1045
    :param msg: An optional message for the progress
 
1046
    """
 
1047
    wr = Weave()
 
1048
    # first determine combined parents of all versions
 
1049
    # map from version name -> all parent names
 
1050
    combined_parents = _reweave_parent_graphs(wa, wb)
 
1051
    mutter("combined parents: %r", combined_parents)
 
1052
    order = tsort.topo_sort(combined_parents.items())
 
1053
    mutter("order to reweave: %r", order)
 
1054
 
 
1055
    if pb and not msg:
 
1056
        msg = 'reweave'
 
1057
 
 
1058
    for idx, name in enumerate(order):
 
1059
        if pb:
 
1060
            pb.update(msg, idx, len(order))
 
1061
        if name in wa._name_map:
 
1062
            lines = wa.get_lines(name)
 
1063
            if name in wb._name_map:
 
1064
                lines_b = wb.get_lines(name)
 
1065
                if lines != lines_b:
 
1066
                    mutter('Weaves differ on content. rev_id {%s}', name)
 
1067
                    mutter('weaves: %s, %s', wa._weave_name, wb._weave_name)
 
1068
                    import difflib
 
1069
                    lines = list(difflib.unified_diff(lines, lines_b,
 
1070
                                                      wa._weave_name, wb._weave_name))
 
1071
                    mutter('lines:\n%s', ''.join(lines))
 
1072
                    raise WeaveTextDiffers(name, wa, wb)
 
1073
        else:
 
1074
            lines = wb.get_lines(name)
 
1075
        wr._add(name, lines, [wr._lookup(i) for i in combined_parents[name]])
 
1076
    return wr
 
1077
 
 
1078
 
 
1079
def _reweave_parent_graphs(wa, wb):
 
1080
    """Return combined parent ancestry for two weaves.
 
1081
 
 
1082
    Returned as a list of (version_name, set(parent_names))"""
 
1083
    combined = {}
 
1084
    for weave in [wa, wb]:
 
1085
        for idx, name in enumerate(weave._names):
 
1086
            p = combined.setdefault(name, set())
 
1087
            p.update(map(weave._idx_to_name, weave._parents[idx]))
 
1088
    return combined