/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-24 00:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 7504.
  • Revision ID: jelmer@jelmer.uk-20200524003950-bbc545r76vc5yajg
Add github action.

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