/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: 2018-05-19 13:16:11 UTC
  • mto: (6968.4.3 git-archive)
  • mto: This revision was merged to the branch mainline in revision 6972.
  • Revision ID: jelmer@jelmer.uk-20180519131611-l9h9ud41j7qg1m03
Move tar/zip to breezy.archive.

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