~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/weave.py

  • Committer: Robert Collins
  • Date: 2006-03-01 03:26:23 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060301032623-9d3c073e102f2239
Move WeaveStore down into bzrlib.store.versioned.weave.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python
 
2
 
 
3
# Copyright (C) 2005 Canonical Ltd
 
4
 
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 2 of the License, or
 
8
# (at your option) any later version.
 
9
 
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
 
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
 
 
19
# Author: Martin Pool <mbp@canonical.com>
 
20
 
 
21
 
 
22
"""Weave - storage of related text file versions"""
 
23
 
 
24
 
 
25
# XXX: If we do weaves this way, will a merge still behave the same
 
26
# way if it's done in a different order?  That's a pretty desirable
 
27
# property.
 
28
 
 
29
# TODO: Nothing here so far assumes the lines are really \n newlines,
 
30
# rather than being split up in some other way.  We could accomodate
 
31
# binaries, perhaps by naively splitting on \n or perhaps using
 
32
# something like a rolling checksum.
 
33
 
 
34
# TODO: End marker for each version so we can stop reading?
 
35
 
 
36
# TODO: Check that no insertion occurs inside a deletion that was
 
37
# active in the version of the insertion.
 
38
 
 
39
# TODO: In addition to the SHA-1 check, perhaps have some code that
 
40
# checks structural constraints of the weave: ie that insertions are
 
41
# properly nested, that there is no text outside of an insertion, that
 
42
# insertions or deletions are not repeated, etc.
 
43
 
 
44
# TODO: Parallel-extract that passes back each line along with a
 
45
# description of which revisions include it.  Nice for checking all
 
46
# shas or calculating stats in parallel.
 
47
 
 
48
# TODO: Using a single _extract routine and then processing the output
 
49
# is probably inefficient.  It's simple enough that we can afford to
 
50
# have slight specializations for different ways its used: annotate,
 
51
# basis for add, get, etc.
 
52
 
 
53
# TODO: Probably the API should work only in names to hide the integer
 
54
# indexes from the user.
 
55
 
 
56
# TODO: Is there any potential performance win by having an add()
 
57
# variant that is passed a pre-cooked version of the single basis
 
58
# version?
 
59
 
 
60
# TODO: Reweave can possibly be made faster by remembering diffs
 
61
# where the basis and destination are unchanged.
 
62
 
 
63
# FIXME: Sometimes we will be given a parents list for a revision
 
64
# that includes some redundant parents (i.e. already a parent of 
 
65
# something in the list.)  We should eliminate them.  This can 
 
66
# be done fairly efficiently because the sequence numbers constrain
 
67
# the possible relationships.
 
68
 
 
69
 
 
70
import os
 
71
import sha
 
72
import time
 
73
from difflib import SequenceMatcher
 
74
 
 
75
from bzrlib.trace import mutter
 
76
from bzrlib.errors import (WeaveError, WeaveFormatError, WeaveParentMismatch,
 
77
        RevisionAlreadyPresent,
 
78
        RevisionNotPresent,
 
79
        WeaveRevisionAlreadyPresent,
 
80
        WeaveRevisionNotPresent,
 
81
        )
 
82
import bzrlib.errors as errors
 
83
from bzrlib.osutils import sha_strings
 
84
from bzrlib.symbol_versioning import *
 
85
from bzrlib.tsort import topo_sort
 
86
from bzrlib.versionedfile import VersionedFile
 
87
 
 
88
 
 
89
class Weave(VersionedFile):
 
90
    """weave - versioned text file storage.
 
91
    
 
92
    A Weave manages versions of line-based text files, keeping track
 
93
    of the originating version for each line.
 
94
 
 
95
    To clients the "lines" of the file are represented as a list of strings.
 
96
    These strings  will typically have terminal newline characters, but
 
97
    this is not required.  In particular files commonly do not have a newline
 
98
    at the end of the file.
 
99
 
 
100
    Texts can be identified in either of two ways:
 
101
 
 
102
    * a nonnegative index number.
 
103
 
 
104
    * a version-id string.
 
105
 
 
106
    Typically the index number will be valid only inside this weave and
 
107
    the version-id is used to reference it in the larger world.
 
108
 
 
109
    The weave is represented as a list mixing edit instructions and
 
110
    literal text.  Each entry in _weave can be either a string (or
 
111
    unicode), or a tuple.  If a string, it means that the given line
 
112
    should be output in the currently active revisions.
 
113
 
 
114
    If a tuple, it gives a processing instruction saying in which
 
115
    revisions the enclosed lines are active.  The tuple has the form
 
116
    (instruction, version).
 
117
 
 
118
    The instruction can be '{' or '}' for an insertion block, and '['
 
119
    and ']' for a deletion block respectively.  The version is the
 
120
    integer version index.  There is no replace operator, only deletes
 
121
    and inserts.  For '}', the end of an insertion, there is no
 
122
    version parameter because it always closes the most recently
 
123
    opened insertion.
 
124
 
 
125
    Constraints/notes:
 
126
 
 
127
    * A later version can delete lines that were introduced by any
 
128
      number of ancestor versions; this implies that deletion
 
129
      instructions can span insertion blocks without regard to the
 
130
      insertion block's nesting.
 
131
 
 
132
    * Similarly, deletions need not be properly nested with regard to
 
133
      each other, because they might have been generated by
 
134
      independent revisions.
 
135
 
 
136
    * Insertions are always made by inserting a new bracketed block
 
137
      into a single point in the previous weave.  This implies they
 
138
      can nest but not overlap, and the nesting must always have later
 
139
      insertions on the inside.
 
140
 
 
141
    * It doesn't seem very useful to have an active insertion
 
142
      inside an inactive insertion, but it might happen.
 
143
      
 
144
    * Therefore, all instructions are always"considered"; that
 
145
      is passed onto and off the stack.  An outer inactive block
 
146
      doesn't disable an inner block.
 
147
 
 
148
    * Lines are enabled if the most recent enclosing insertion is
 
149
      active and none of the enclosing deletions are active.
 
150
 
 
151
    * There is no point having a deletion directly inside its own
 
152
      insertion; you might as well just not write it.  And there
 
153
      should be no way to get an earlier version deleting a later
 
154
      version.
 
155
 
 
156
    _weave
 
157
        Text of the weave; list of control instruction tuples and strings.
 
158
 
 
159
    _parents
 
160
        List of parents, indexed by version number.
 
161
        It is only necessary to store the minimal set of parents for
 
162
        each version; the parent's parents are implied.
 
163
 
 
164
    _sha1s
 
165
        List of hex SHA-1 of each version.
 
166
 
 
167
    _names
 
168
        List of symbolic names for each version.  Each should be unique.
 
169
 
 
170
    _name_map
 
171
        For each name, the version number.
 
172
 
 
173
    _weave_name
 
174
        Descriptive name of this weave; typically the filename if known.
 
175
        Set by read_weave.
 
176
    """
 
177
 
 
178
    __slots__ = ['_weave', '_parents', '_sha1s', '_names', '_name_map',
 
179
                 '_weave_name']
 
180
    
 
181
    def __init__(self, weave_name=None):
 
182
        self._weave = []
 
183
        self._parents = []
 
184
        self._sha1s = []
 
185
        self._names = []
 
186
        self._name_map = {}
 
187
        self._weave_name = weave_name
 
188
 
 
189
    def __repr__(self):
 
190
        return "Weave(%r)" % self._weave_name
 
191
 
 
192
 
 
193
    def copy(self):
 
194
        """Return a deep copy of self.
 
195
        
 
196
        The copy can be modified without affecting the original weave."""
 
197
        other = Weave()
 
198
        other._weave = self._weave[:]
 
199
        other._parents = self._parents[:]
 
200
        other._sha1s = self._sha1s[:]
 
201
        other._names = self._names[:]
 
202
        other._name_map = self._name_map.copy()
 
203
        other._weave_name = self._weave_name
 
204
        return other
 
205
 
 
206
    def __eq__(self, other):
 
207
        if not isinstance(other, Weave):
 
208
            return False
 
209
        return self._parents == other._parents \
 
210
               and self._weave == other._weave \
 
211
               and self._sha1s == other._sha1s 
 
212
 
 
213
    
 
214
    def __ne__(self, other):
 
215
        return not self.__eq__(other)
 
216
 
 
217
    @deprecated_method(zero_eight)
 
218
    def idx_to_name(self, index):
 
219
        """Old public interface, the public interface is all names now."""
 
220
        return index
 
221
 
 
222
    def _idx_to_name(self, version):
 
223
        return self._names[version]
 
224
 
 
225
    @deprecated_method(zero_eight)
 
226
    def lookup(self, name):
 
227
        """Backwards compatability thunk:
 
228
 
 
229
        Return name, as name is valid in the api now, and spew deprecation
 
230
        warnings everywhere.
 
231
        """
 
232
        return name
 
233
 
 
234
    def _lookup(self, name):
 
235
        """Convert symbolic version name to index."""
 
236
        try:
 
237
            return self._name_map[name]
 
238
        except KeyError:
 
239
            raise RevisionNotPresent(name, self._weave_name)
 
240
 
 
241
    @deprecated_method(zero_eight)
 
242
    def iter_names(self):
 
243
        """Deprecated convenience function, please see VersionedFile.names()."""
 
244
        return iter(self.names())
 
245
 
 
246
    @deprecated_method(zero_eight)
 
247
    def names(self):
 
248
        """See Weave.versions for the current api."""
 
249
        return self.versions()
 
250
 
 
251
    def versions(self):
 
252
        """See VersionedFile.versions."""
 
253
        return self._names[:]
 
254
 
 
255
    def has_version(self, version_id):
 
256
        """See VersionedFile.has_version."""
 
257
        return self._name_map.has_key(version_id)
 
258
 
 
259
    __contains__ = has_version
 
260
 
 
261
    @deprecated_method(zero_eight)
 
262
    def parent_names(self, version):
 
263
        """Return version names for parents of a version.
 
264
        
 
265
        See get_parents for the current api.
 
266
        """
 
267
        return self.get_parents(version)
 
268
 
 
269
    def get_parents(self, version_id):
 
270
        """See VersionedFile.get_parent."""
 
271
        return map(self._idx_to_name, self._parents[self._lookup(version_id)])
 
272
 
 
273
    def _check_repeated_add(self, name, parents, text, sha1):
 
274
        """Check that a duplicated add is OK.
 
275
 
 
276
        If it is, return the (old) index; otherwise raise an exception.
 
277
        """
 
278
        idx = self._lookup(name)
 
279
        if sorted(self._parents[idx]) != sorted(parents) \
 
280
            or sha1 != self._sha1s[idx]:
 
281
            raise RevisionAlreadyPresent(name, self._weave_name)
 
282
        return idx
 
283
 
 
284
    @deprecated_method(zero_eight)
 
285
    def add_identical(self, old_rev_id, new_rev_id, parents):
 
286
        """Please use Weave.clone_text now."""
 
287
        return self.clone_text(new_rev_id, old_rev_id, parents)
 
288
 
 
289
    def add_lines(self, version_id, parents, lines):
 
290
        """See VersionedFile.add_lines."""
 
291
        return self._add(version_id, lines, map(self._lookup, parents))
 
292
 
 
293
    @deprecated_method(zero_eight)
 
294
    def add(self, name, parents, text, sha1=None):
 
295
        """See VersionedFile.add_lines for the non deprecated api."""
 
296
        return self._add(name, text, map(self._maybe_lookup, parents), sha1)
 
297
 
 
298
    def _add(self, version_id, lines, parents, sha1=None):
 
299
        """Add a single text on top of the weave.
 
300
  
 
301
        Returns the index number of the newly added version.
 
302
 
 
303
        version_id
 
304
            Symbolic name for this version.
 
305
            (Typically the revision-id of the revision that added it.)
 
306
 
 
307
        parents
 
308
            List or set of direct parent version numbers.
 
309
            
 
310
        lines
 
311
            Sequence of lines to be added in the new version.
 
312
        """
 
313
 
 
314
        assert isinstance(version_id, basestring)
 
315
        if not sha1:
 
316
            sha1 = sha_strings(lines)
 
317
        if version_id in self._name_map:
 
318
            return self._check_repeated_add(version_id, parents, lines, sha1)
 
319
 
 
320
        self._check_versions(parents)
 
321
        ## self._check_lines(lines)
 
322
        new_version = len(self._parents)
 
323
 
 
324
        # if we abort after here the (in-memory) weave will be corrupt because only
 
325
        # some fields are updated
 
326
        self._parents.append(parents[:])
 
327
        self._sha1s.append(sha1)
 
328
        self._names.append(version_id)
 
329
        self._name_map[version_id] = new_version
 
330
 
 
331
            
 
332
        if not parents:
 
333
            # special case; adding with no parents revision; can do
 
334
            # this more quickly by just appending unconditionally.
 
335
            # even more specially, if we're adding an empty text we
 
336
            # need do nothing at all.
 
337
            if lines:
 
338
                self._weave.append(('{', new_version))
 
339
                self._weave.extend(lines)
 
340
                self._weave.append(('}', None))
 
341
            return new_version
 
342
 
 
343
        if len(parents) == 1:
 
344
            pv = list(parents)[0]
 
345
            if sha1 == self._sha1s[pv]:
 
346
                # special case: same as the single parent
 
347
                return new_version
 
348
            
 
349
 
 
350
        ancestors = self._inclusions(parents)
 
351
 
 
352
        l = self._weave
 
353
 
 
354
        # basis a list of (origin, lineno, line)
 
355
        basis_lineno = []
 
356
        basis_lines = []
 
357
        for origin, lineno, line in self._extract(ancestors):
 
358
            basis_lineno.append(lineno)
 
359
            basis_lines.append(line)
 
360
 
 
361
        # another small special case: a merge, producing the same text
 
362
        # as auto-merge
 
363
        if lines == basis_lines:
 
364
            return new_version            
 
365
 
 
366
        # add a sentinal, because we can also match against the final line
 
367
        basis_lineno.append(len(self._weave))
 
368
 
 
369
        # XXX: which line of the weave should we really consider
 
370
        # matches the end of the file?  the current code says it's the
 
371
        # last line of the weave?
 
372
 
 
373
        #print 'basis_lines:', basis_lines
 
374
        #print 'new_lines:  ', lines
 
375
 
 
376
        s = SequenceMatcher(None, basis_lines, lines)
 
377
 
 
378
        # offset gives the number of lines that have been inserted
 
379
        # into the weave up to the current point; if the original edit instruction
 
380
        # says to change line A then we actually change (A+offset)
 
381
        offset = 0
 
382
 
 
383
        for tag, i1, i2, j1, j2 in s.get_opcodes():
 
384
            # i1,i2 are given in offsets within basis_lines; we need to map them
 
385
            # back to offsets within the entire weave
 
386
            #print 'raw match', tag, i1, i2, j1, j2
 
387
            if tag == 'equal':
 
388
                continue
 
389
 
 
390
            i1 = basis_lineno[i1]
 
391
            i2 = basis_lineno[i2]
 
392
 
 
393
            assert 0 <= j1 <= j2 <= len(lines)
 
394
 
 
395
            #print tag, i1, i2, j1, j2
 
396
 
 
397
            # the deletion and insertion are handled separately.
 
398
            # first delete the region.
 
399
            if i1 != i2:
 
400
                self._weave.insert(i1+offset, ('[', new_version))
 
401
                self._weave.insert(i2+offset+1, (']', new_version))
 
402
                offset += 2
 
403
 
 
404
            if j1 != j2:
 
405
                # there may have been a deletion spanning up to
 
406
                # i2; we want to insert after this region to make sure
 
407
                # we don't destroy ourselves
 
408
                i = i2 + offset
 
409
                self._weave[i:i] = ([('{', new_version)] 
 
410
                                    + lines[j1:j2] 
 
411
                                    + [('}', None)])
 
412
                offset += 2 + (j2 - j1)
 
413
        return new_version
 
414
 
 
415
    def clone_text(self, new_version_id, old_version_id, parents):
 
416
        """See VersionedFile.clone_text."""
 
417
        old_lines = self.get_text(old_version_id)
 
418
        self.add_lines(new_version_id, parents, old_lines)
 
419
 
 
420
    def _inclusions(self, versions):
 
421
        """Return set of all ancestors of given version(s)."""
 
422
        i = set(versions)
 
423
        for v in xrange(max(versions), 0, -1):
 
424
            if v in i:
 
425
                # include all its parents
 
426
                i.update(self._parents[v])
 
427
        return i
 
428
        ## except IndexError:
 
429
        ##     raise ValueError("version %d not present in weave" % v)
 
430
 
 
431
    @deprecated_method(zero_eight)
 
432
    def inclusions(self, version_ids):
 
433
        """Deprecated - see VersionedFile.get_ancestry for the replacement."""
 
434
        if not version_ids:
 
435
            return []
 
436
        if isinstance(version_ids[0], int):
 
437
            return [self._idx_to_name(v) for v in self._inclusions(version_ids)]
 
438
        else:
 
439
            return self.get_ancestry(version_ids)
 
440
 
 
441
    def get_ancestry(self, version_ids):
 
442
        """See VersionedFile.get_ancestry."""
 
443
        if isinstance(version_ids, basestring):
 
444
            version_ids = [version_ids]
 
445
        i = self._inclusions([self._lookup(v) for v in version_ids])
 
446
        return [self._idx_to_name(v) for v in i]
 
447
 
 
448
    def _check_lines(self, text):
 
449
        if not isinstance(text, list):
 
450
            raise ValueError("text should be a list, not %s" % type(text))
 
451
 
 
452
        for l in text:
 
453
            if not isinstance(l, basestring):
 
454
                raise ValueError("text line should be a string or unicode, not %s"
 
455
                                 % type(l))
 
456
        
 
457
 
 
458
 
 
459
    def _check_versions(self, indexes):
 
460
        """Check everything in the sequence of indexes is valid"""
 
461
        for i in indexes:
 
462
            try:
 
463
                self._parents[i]
 
464
            except IndexError:
 
465
                raise IndexError("invalid version number %r" % i)
 
466
 
 
467
    def annotate(self, version_id):
 
468
        if isinstance(version_id, int):
 
469
            warn('Weave.annotate(int) is deprecated. Please use version names'
 
470
                 ' in all circumstances as of 0.8',
 
471
                 DeprecationWarning,
 
472
                 stacklevel=2
 
473
                 )
 
474
            result = []
 
475
            for origin, lineno, text in self._extract([version_id]):
 
476
                result.append((origin, text))
 
477
            return result
 
478
        else:
 
479
            return super(Weave, self).annotate(version_id)
 
480
    
 
481
    def annotate_iter(self, version_id):
 
482
        """Yield list of (version-id, line) pairs for the specified version.
 
483
 
 
484
        The index indicates when the line originated in the weave."""
 
485
        incls = [self._lookup(version_id)]
 
486
        for origin, lineno, text in self._extract(incls):
 
487
            yield self._idx_to_name(origin), text
 
488
 
 
489
    @deprecated_method(zero_eight)
 
490
    def _walk(self):
 
491
        """_walk has become walk, a supported api."""
 
492
        return self.walk()
 
493
 
 
494
    def walk(self, version_ids=None):
 
495
        """See VersionedFile.walk."""
 
496
        
 
497
        istack = []
 
498
        dset = set()
 
499
 
 
500
        lineno = 0         # line of weave, 0-based
 
501
 
 
502
        for l in self._weave:
 
503
            if isinstance(l, tuple):
 
504
                c, v = l
 
505
                isactive = None
 
506
                if c == '{':
 
507
                    istack.append(self._idx_to_name(v))
 
508
                elif c == '}':
 
509
                    istack.pop()
 
510
                elif c == '[':
 
511
                    assert self._idx_to_name(v) not in dset
 
512
                    dset.add(self._idx_to_name(v))
 
513
                elif c == ']':
 
514
                    dset.remove(self._idx_to_name(v))
 
515
                else:
 
516
                    raise WeaveFormatError('unexpected instruction %r' % v)
 
517
            else:
 
518
                assert isinstance(l, basestring)
 
519
                assert istack
 
520
                yield lineno, istack[-1], dset.copy(), l
 
521
            lineno += 1
 
522
 
 
523
        if istack:
 
524
            raise WeaveFormatError("unclosed insertion blocks "
 
525
                    "at end of weave: %s" % istack)
 
526
        if dset:
 
527
            raise WeaveFormatError("unclosed deletion blocks at end of weave: %s"
 
528
                                   % dset)
 
529
 
 
530
    def _extract(self, versions):
 
531
        """Yield annotation of lines in included set.
 
532
 
 
533
        Yields a sequence of tuples (origin, lineno, text), where
 
534
        origin is the origin version, lineno the index in the weave,
 
535
        and text the text of the line.
 
536
 
 
537
        The set typically but not necessarily corresponds to a version.
 
538
        """
 
539
        for i in versions:
 
540
            if not isinstance(i, int):
 
541
                raise ValueError(i)
 
542
            
 
543
        included = self._inclusions(versions)
 
544
 
 
545
        istack = []
 
546
        dset = set()
 
547
 
 
548
        lineno = 0         # line of weave, 0-based
 
549
 
 
550
        isactive = None
 
551
 
 
552
        result = []
 
553
 
 
554
        WFE = WeaveFormatError
 
555
 
 
556
        for l in self._weave:
 
557
            if isinstance(l, tuple):
 
558
                c, v = l
 
559
                isactive = None
 
560
                if c == '{':
 
561
                    assert v not in istack
 
562
                    istack.append(v)
 
563
                elif c == '}':
 
564
                    istack.pop()
 
565
                elif c == '[':
 
566
                    if v in included:
 
567
                        assert v not in dset
 
568
                        dset.add(v)
 
569
                else:
 
570
                    assert c == ']'
 
571
                    if v in included:
 
572
                        assert v in dset
 
573
                        dset.remove(v)
 
574
            else:
 
575
                assert isinstance(l, basestring)
 
576
                if isactive is None:
 
577
                    isactive = (not dset) and istack and (istack[-1] in included)
 
578
                if isactive:
 
579
                    result.append((istack[-1], lineno, l))
 
580
            lineno += 1
 
581
        if istack:
 
582
            raise WeaveFormatError("unclosed insertion blocks "
 
583
                    "at end of weave: %s" % istack)
 
584
        if dset:
 
585
            raise WeaveFormatError("unclosed deletion blocks at end of weave: %s"
 
586
                                   % dset)
 
587
        return result
 
588
 
 
589
    @deprecated_method(zero_eight)
 
590
    def get_iter(self, name_or_index):
 
591
        """Deprecated, please do not use. Lookups are not not needed."""
 
592
        return self._get_iter(self._maybe_lookup(name_or_index))
 
593
 
 
594
    @deprecated_method(zero_eight)
 
595
    def maybe_lookup(self, name_or_index):
 
596
        """Deprecated, please do not use. Lookups are not not needed."""
 
597
        return self._maybe_lookup(name_or_index)
 
598
 
 
599
    def _maybe_lookup(self, name_or_index):
 
600
        """Convert possible symbolic name to index, or pass through indexes.
 
601
        
 
602
        NOT FOR PUBLIC USE.
 
603
        """
 
604
        if isinstance(name_or_index, (int, long)):
 
605
            return name_or_index
 
606
        else:
 
607
            return self._lookup(name_or_index)
 
608
 
 
609
    def _get_iter(self, version_id):
 
610
        """Yield lines for the specified version."""
 
611
        incls = [self._maybe_lookup(version_id)]
 
612
        if len(incls) == 1:
 
613
            index = incls[0]
 
614
            cur_sha = sha.new()
 
615
        else:
 
616
            # We don't have sha1 sums for multiple entries
 
617
            cur_sha = None
 
618
        for origin, lineno, line in self._extract(incls):
 
619
            if cur_sha:
 
620
                cur_sha.update(line)
 
621
            yield line
 
622
        if cur_sha:
 
623
            expected_sha1 = self._sha1s[index]
 
624
            measured_sha1 = cur_sha.hexdigest() 
 
625
            if measured_sha1 != expected_sha1:
 
626
                raise errors.WeaveInvalidChecksum(
 
627
                        'file %s, revision %s, expected: %s, measured %s' 
 
628
                        % (self._weave_name, self._names[index],
 
629
                           expected_sha1, measured_sha1))
 
630
 
 
631
    @deprecated_method(zero_eight)
 
632
    def get(self, version_id):
 
633
        """Please use either Weave.get_text or Weave.get_lines as desired."""
 
634
        return self.get_lines(version_id)
 
635
 
 
636
    def get_lines(self, version_id):
 
637
        """See VersionedFile.get_lines()."""
 
638
        return list(self._get_iter(version_id))
 
639
 
 
640
    def get_sha1(self, name):
 
641
        """Get the stored sha1 sum for the given revision.
 
642
        
 
643
        :param name: The name of the version to lookup
 
644
        """
 
645
        return self._sha1s[self._lookup(name)]
 
646
 
 
647
    def numversions(self):
 
648
        l = len(self._parents)
 
649
        assert l == len(self._sha1s)
 
650
        return l
 
651
 
 
652
    __len__ = numversions
 
653
 
 
654
    def check(self, progress_bar=None):
 
655
        # TODO evaluate performance hit of using string sets in this routine.
 
656
        # check no circular inclusions
 
657
        for version in range(self.numversions()):
 
658
            inclusions = list(self._parents[version])
 
659
            if inclusions:
 
660
                inclusions.sort()
 
661
                if inclusions[-1] >= version:
 
662
                    raise WeaveFormatError("invalid included version %d for index %d"
 
663
                                           % (inclusions[-1], version))
 
664
 
 
665
        # try extracting all versions; parallel extraction is used
 
666
        nv = self.numversions()
 
667
        sha1s = {}
 
668
        texts = {}
 
669
        inclusions = {}
 
670
        for i in range(nv):
 
671
            # For creating the ancestry, IntSet is much faster (3.7s vs 0.17s)
 
672
            # The problem is that set membership is much more expensive
 
673
            name = self._idx_to_name(i)
 
674
            sha1s[name] = sha.new()
 
675
            texts[name] = []
 
676
            new_inc = set([name])
 
677
            for p in self._parents[i]:
 
678
                new_inc.update(inclusions[self._idx_to_name(p)])
 
679
 
 
680
            assert set(new_inc) == set(self.get_ancestry(name)), \
 
681
                'failed %s != %s' % (set(new_inc), set(self.get_ancestry(name)))
 
682
            inclusions[name] = new_inc
 
683
 
 
684
        nlines = len(self._weave)
 
685
 
 
686
        update_text = 'checking weave'
 
687
        if self._weave_name:
 
688
            short_name = os.path.basename(self._weave_name)
 
689
            update_text = 'checking %s' % (short_name,)
 
690
            update_text = update_text[:25]
 
691
 
 
692
        for lineno, insert, deleteset, line in self.walk():
 
693
            if progress_bar:
 
694
                progress_bar.update(update_text, lineno, nlines)
 
695
 
 
696
            for name, name_inclusions in inclusions.items():
 
697
                # The active inclusion must be an ancestor,
 
698
                # and no ancestors must have deleted this line,
 
699
                # because we don't support resurrection.
 
700
                if (insert in name_inclusions) and not (deleteset & name_inclusions):
 
701
                    sha1s[name].update(line)
 
702
 
 
703
        for i in range(nv):
 
704
            version = self._idx_to_name(i)
 
705
            hd = sha1s[version].hexdigest()
 
706
            expected = self._sha1s[i]
 
707
            if hd != expected:
 
708
                raise errors.WeaveInvalidChecksum(
 
709
                        "mismatched sha1 for version %s: "
 
710
                        "got %s, expected %s"
 
711
                        % (version, hd, expected))
 
712
 
 
713
        # TODO: check insertions are properly nested, that there are
 
714
        # no lines outside of insertion blocks, that deletions are
 
715
        # properly paired, etc.
 
716
 
 
717
 
 
718
    def join(self, other, pb=None, msg=None, version_ids=None):
 
719
        import sys, time
 
720
        """Integrate versions from other into this weave.
 
721
 
 
722
        The resulting weave contains all the history of both weaves; 
 
723
        any version you could retrieve from either self or other can be 
 
724
        retrieved from self after this call.
 
725
 
 
726
        It is illegal for the two weaves to contain different values 
 
727
        or different parents for any version.  See also reweave().
 
728
 
 
729
        :param other: The other weave to pull into this one
 
730
        :param pb: An optional progress bar
 
731
        :param msg: An optional message to display for progress
 
732
        """
 
733
        if not other.versions():
 
734
            return          # nothing to update, easy
 
735
 
 
736
        if version_ids:
 
737
            for version_id in version_ids:
 
738
                if not self.has_version(version_id):
 
739
                    raise RevisionNotPresent(version_id, self._weave_name)
 
740
        assert version_ids == None
 
741
 
 
742
        # two loops so that we do not change ourselves before verifying it
 
743
        # will be ok
 
744
        # work through in index order to make sure we get all dependencies
 
745
        names_to_join = []
 
746
        processed = 0
 
747
        for other_idx, name in enumerate(other._names):
 
748
            self._check_version_consistent(other, other_idx, name)
 
749
            sha1 = other._sha1s[other_idx]
 
750
 
 
751
            processed += 1
 
752
 
 
753
            if name in self._name_map:
 
754
                idx = self._lookup(name)
 
755
                n1 = set(map(other._idx_to_name, other._parents[other_idx]))
 
756
                n2 = set(map(self._idx_to_name, self._parents[idx]))
 
757
                if sha1 ==  self._sha1s[idx] and n1 == n2:
 
758
                        continue
 
759
 
 
760
            names_to_join.append((other_idx, name))
 
761
 
 
762
        if pb and not msg:
 
763
            msg = 'weave join'
 
764
 
 
765
        merged = 0
 
766
        time0 = time.time()
 
767
        for other_idx, name in names_to_join:
 
768
            # TODO: If all the parents of the other version are already
 
769
            # present then we can avoid some work by just taking the delta
 
770
            # and adjusting the offsets.
 
771
            new_parents = self._imported_parents(other, other_idx)
 
772
            sha1 = other._sha1s[other_idx]
 
773
 
 
774
            merged += 1
 
775
 
 
776
            if pb:
 
777
                pb.update(msg, merged, len(names_to_join))
 
778
           
 
779
            lines = other.get_lines(other_idx)
 
780
            self._add(name, lines, new_parents, sha1)
 
781
 
 
782
        mutter("merged = %d, processed = %d, file_id=%s; deltat=%d"%(
 
783
                merged, processed, self._weave_name, time.time()-time0))
 
784
 
 
785
    def _imported_parents(self, other, other_idx):
 
786
        """Return list of parents in self corresponding to indexes in other."""
 
787
        new_parents = []
 
788
        for parent_idx in other._parents[other_idx]:
 
789
            parent_name = other._names[parent_idx]
 
790
            if parent_name not in self._names:
 
791
                # should not be possible
 
792
                raise WeaveError("missing parent {%s} of {%s} in %r" 
 
793
                                 % (parent_name, other._name_map[other_idx], self))
 
794
            new_parents.append(self._name_map[parent_name])
 
795
        return new_parents
 
796
 
 
797
    def _check_version_consistent(self, other, other_idx, name):
 
798
        """Check if a version in consistent in this and other.
 
799
 
 
800
        To be consistent it must have:
 
801
 
 
802
         * the same text
 
803
         * the same direct parents (by name, not index, and disregarding
 
804
           order)
 
805
        
 
806
        If present & correct return True;
 
807
        if not present in self return False; 
 
808
        if inconsistent raise error."""
 
809
        this_idx = self._name_map.get(name, -1)
 
810
        if this_idx != -1:
 
811
            if self._sha1s[this_idx] != other._sha1s[other_idx]:
 
812
                raise WeaveError("inconsistent texts for version {%s} "
 
813
                                 "when joining weaves"
 
814
                                 % (name))
 
815
            self_parents = self._parents[this_idx]
 
816
            other_parents = other._parents[other_idx]
 
817
            n1 = set([self._names[i] for i in self_parents])
 
818
            n2 = set([other._names[i] for i in other_parents])
 
819
            if n1 != n2:
 
820
                raise WeaveParentMismatch("inconsistent parents "
 
821
                    "for version {%s}: %s vs %s" % (name, n1, n2))
 
822
            else:
 
823
                return True         # ok!
 
824
        else:
 
825
            return False
 
826
 
 
827
    def reweave(self, other, pb=None, msg=None):
 
828
        """Reweave self with other.
 
829
 
 
830
        :param other: The other weave to merge
 
831
        :param pb: An optional progress bar, indicating how far done we are
 
832
        :param msg: An optional message for the progress
 
833
        """
 
834
        new_weave = reweave(self, other, pb=pb, msg=msg)
 
835
        for attr in self.__slots__:
 
836
            setattr(self, attr, getattr(new_weave, attr))
 
837
 
 
838
 
 
839
def reweave(wa, wb, pb=None, msg=None):
 
840
    """Combine two weaves and return the result.
 
841
 
 
842
    This works even if a revision R has different parents in 
 
843
    wa and wb.  In the resulting weave all the parents are given.
 
844
 
 
845
    This is done by just building up a new weave, maintaining ordering 
 
846
    of the versions in the two inputs.  More efficient approaches
 
847
    might be possible but it should only be necessary to do 
 
848
    this operation rarely, when a new previously ghost version is 
 
849
    inserted.
 
850
 
 
851
    :param pb: An optional progress bar, indicating how far done we are
 
852
    :param msg: An optional message for the progress
 
853
    """
 
854
    wr = Weave()
 
855
    ia = ib = 0
 
856
    queue_a = range(wa.numversions())
 
857
    queue_b = range(wb.numversions())
 
858
    # first determine combined parents of all versions
 
859
    # map from version name -> all parent names
 
860
    combined_parents = _reweave_parent_graphs(wa, wb)
 
861
    mutter("combined parents: %r", combined_parents)
 
862
    order = topo_sort(combined_parents.iteritems())
 
863
    mutter("order to reweave: %r", order)
 
864
 
 
865
    if pb and not msg:
 
866
        msg = 'reweave'
 
867
 
 
868
    for idx, name in enumerate(order):
 
869
        if pb:
 
870
            pb.update(msg, idx, len(order))
 
871
        if name in wa._name_map:
 
872
            lines = wa.get_lines(name)
 
873
            if name in wb._name_map:
 
874
                lines_b = wb.get_lines(name)
 
875
                if lines != lines_b:
 
876
                    mutter('Weaves differ on content. rev_id {%s}', name)
 
877
                    mutter('weaves: %s, %s', wa._weave_name, wb._weave_name)
 
878
                    import difflib
 
879
                    lines = list(difflib.unified_diff(lines, lines_b,
 
880
                            wa._weave_name, wb._weave_name))
 
881
                    mutter('lines:\n%s', ''.join(lines))
 
882
                    raise errors.WeaveTextDiffers(name, wa, wb)
 
883
        else:
 
884
            lines = wb.get_lines(name)
 
885
        wr._add(name, lines, [wr._lookup(i) for i in combined_parents[name]])
 
886
    return wr
 
887
 
 
888
def _reweave_parent_graphs(wa, wb):
 
889
    """Return combined parent ancestry for two weaves.
 
890
    
 
891
    Returned as a list of (version_name, set(parent_names))"""
 
892
    combined = {}
 
893
    for weave in [wa, wb]:
 
894
        for idx, name in enumerate(weave._names):
 
895
            p = combined.setdefault(name, set())
 
896
            p.update(map(weave._idx_to_name, weave._parents[idx]))
 
897
    return combined
 
898
 
 
899
 
 
900
def weave_toc(w):
 
901
    """Show the weave's table-of-contents"""
 
902
    print '%6s %50s %10s %10s' % ('ver', 'name', 'sha1', 'parents')
 
903
    for i in (6, 50, 10, 10):
 
904
        print '-' * i,
 
905
    print
 
906
    for i in range(w.numversions()):
 
907
        sha1 = w._sha1s[i]
 
908
        name = w._names[i]
 
909
        parent_str = ' '.join(map(str, w._parents[i]))
 
910
        print '%6d %-50.50s %10.10s %s' % (i, name, sha1, parent_str)
 
911
 
 
912
 
 
913
 
 
914
def weave_stats(weave_file, pb):
 
915
    from bzrlib.weavefile import read_weave
 
916
 
 
917
    wf = file(weave_file, 'rb')
 
918
    w = read_weave(wf, WeaveVersionedFile)
 
919
    # FIXME: doesn't work on pipes
 
920
    weave_size = wf.tell()
 
921
 
 
922
    total = 0
 
923
    vers = len(w)
 
924
    for i in range(vers):
 
925
        pb.update('checking sizes', i, vers)
 
926
        for origin, lineno, line in w._extract([i]):
 
927
            total += len(line)
 
928
 
 
929
    pb.clear()
 
930
 
 
931
    print 'versions          %9d' % vers
 
932
    print 'weave file        %9d bytes' % weave_size
 
933
    print 'total contents    %9d bytes' % total
 
934
    print 'compression ratio %9.2fx' % (float(total) / float(weave_size))
 
935
    if vers:
 
936
        avg = total/vers
 
937
        print 'average size      %9d bytes' % avg
 
938
        print 'relative size     %9.2fx' % (float(weave_size) / float(avg))
 
939
 
 
940
 
 
941
def usage():
 
942
    print """bzr weave tool
 
943
 
 
944
Experimental tool for weave algorithm.
 
945
 
 
946
usage:
 
947
    weave init WEAVEFILE
 
948
        Create an empty weave file
 
949
    weave get WEAVEFILE VERSION
 
950
        Write out specified version.
 
951
    weave check WEAVEFILE
 
952
        Check consistency of all versions.
 
953
    weave toc WEAVEFILE
 
954
        Display table of contents.
 
955
    weave add WEAVEFILE NAME [BASE...] < NEWTEXT
 
956
        Add NEWTEXT, with specified parent versions.
 
957
    weave annotate WEAVEFILE VERSION
 
958
        Display origin of each line.
 
959
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
 
960
        Auto-merge two versions and display conflicts.
 
961
    weave diff WEAVEFILE VERSION1 VERSION2 
 
962
        Show differences between two versions.
 
963
 
 
964
example:
 
965
 
 
966
    % weave init foo.weave
 
967
    % vi foo.txt
 
968
    % weave add foo.weave ver0 < foo.txt
 
969
    added version 0
 
970
 
 
971
    (create updated version)
 
972
    % vi foo.txt
 
973
    % weave get foo.weave 0 | diff -u - foo.txt
 
974
    % weave add foo.weave ver1 0 < foo.txt
 
975
    added version 1
 
976
 
 
977
    % weave get foo.weave 0 > foo.txt       (create forked version)
 
978
    % vi foo.txt
 
979
    % weave add foo.weave ver2 0 < foo.txt
 
980
    added version 2
 
981
 
 
982
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
 
983
    % vi foo.txt                            (resolve conflicts)
 
984
    % weave add foo.weave merged 1 2 < foo.txt     (commit merged version)     
 
985
    
 
986
"""
 
987
    
 
988
 
 
989
 
 
990
def main(argv):
 
991
    import sys
 
992
    import os
 
993
    try:
 
994
        import bzrlib
 
995
    except ImportError:
 
996
        # in case we're run directly from the subdirectory
 
997
        sys.path.append('..')
 
998
        import bzrlib
 
999
    from bzrlib.weavefile import write_weave, read_weave
 
1000
    from bzrlib.progress import ProgressBar
 
1001
 
 
1002
    try:
 
1003
        import psyco
 
1004
        psyco.full()
 
1005
    except ImportError:
 
1006
        pass
 
1007
 
 
1008
    if len(argv) < 2:
 
1009
        usage()
 
1010
        return 0
 
1011
 
 
1012
    cmd = argv[1]
 
1013
 
 
1014
    def readit():
 
1015
        return read_weave(file(argv[2], 'rb'))
 
1016
    
 
1017
    if cmd == 'help':
 
1018
        usage()
 
1019
    elif cmd == 'add':
 
1020
        w = readit()
 
1021
        # at the moment, based on everything in the file
 
1022
        name = argv[3]
 
1023
        parents = map(int, argv[4:])
 
1024
        lines = sys.stdin.readlines()
 
1025
        ver = w.add(name, parents, lines)
 
1026
        write_weave(w, file(argv[2], 'wb'))
 
1027
        print 'added version %r %d' % (name, ver)
 
1028
    elif cmd == 'init':
 
1029
        fn = argv[2]
 
1030
        if os.path.exists(fn):
 
1031
            raise IOError("file exists")
 
1032
        w = Weave()
 
1033
        write_weave(w, file(fn, 'wb'))
 
1034
    elif cmd == 'get': # get one version
 
1035
        w = readit()
 
1036
        sys.stdout.writelines(w.get_iter(int(argv[3])))
 
1037
        
 
1038
    elif cmd == 'diff':
 
1039
        from difflib import unified_diff
 
1040
        w = readit()
 
1041
        fn = argv[2]
 
1042
        v1, v2 = map(int, argv[3:5])
 
1043
        lines1 = w.get(v1)
 
1044
        lines2 = w.get(v2)
 
1045
        diff_gen = unified_diff(lines1, lines2,
 
1046
                                '%s version %d' % (fn, v1),
 
1047
                                '%s version %d' % (fn, v2))
 
1048
        sys.stdout.writelines(diff_gen)
 
1049
            
 
1050
    elif cmd == 'annotate':
 
1051
        w = readit()
 
1052
        # newline is added to all lines regardless; too hard to get
 
1053
        # reasonable formatting otherwise
 
1054
        lasto = None
 
1055
        for origin, text in w.annotate(int(argv[3])):
 
1056
            text = text.rstrip('\r\n')
 
1057
            if origin == lasto:
 
1058
                print '      | %s' % (text)
 
1059
            else:
 
1060
                print '%5d | %s' % (origin, text)
 
1061
                lasto = origin
 
1062
                
 
1063
    elif cmd == 'toc':
 
1064
        weave_toc(readit())
 
1065
 
 
1066
    elif cmd == 'stats':
 
1067
        weave_stats(argv[2], ProgressBar())
 
1068
        
 
1069
    elif cmd == 'check':
 
1070
        w = readit()
 
1071
        pb = ProgressBar()
 
1072
        w.check(pb)
 
1073
        pb.clear()
 
1074
        print '%d versions ok' % w.numversions()
 
1075
 
 
1076
    elif cmd == 'inclusions':
 
1077
        w = readit()
 
1078
        print ' '.join(map(str, w.inclusions([int(argv[3])])))
 
1079
 
 
1080
    elif cmd == 'parents':
 
1081
        w = readit()
 
1082
        print ' '.join(map(str, w._parents[int(argv[3])]))
 
1083
 
 
1084
    elif cmd == 'plan-merge':
 
1085
        w = readit()
 
1086
        for state, line in w.plan_merge(int(argv[3]), int(argv[4])):
 
1087
            if line:
 
1088
                print '%14s | %s' % (state, line),
 
1089
 
 
1090
    elif cmd == 'merge':
 
1091
        w = readit()
 
1092
        p = w.plan_merge(int(argv[3]), int(argv[4]))
 
1093
        sys.stdout.writelines(w.weave_merge(p))
 
1094
            
 
1095
    else:
 
1096
        raise ValueError('unknown command %r' % cmd)
 
1097
    
 
1098
 
 
1099
 
 
1100
def profile_main(argv): 
 
1101
    import tempfile, hotshot, hotshot.stats
 
1102
 
 
1103
    prof_f = tempfile.NamedTemporaryFile()
 
1104
 
 
1105
    prof = hotshot.Profile(prof_f.name)
 
1106
 
 
1107
    ret = prof.runcall(main, argv)
 
1108
    prof.close()
 
1109
 
 
1110
    stats = hotshot.stats.load(prof_f.name)
 
1111
    #stats.strip_dirs()
 
1112
    stats.sort_stats('cumulative')
 
1113
    ## XXX: Might like to write to stderr or the trace file instead but
 
1114
    ## print_stats seems hardcoded to stdout
 
1115
    stats.print_stats(20)
 
1116
            
 
1117
    return ret
 
1118
 
 
1119
 
 
1120
def lsprofile_main(argv): 
 
1121
    from bzrlib.lsprof import profile
 
1122
    ret,stats = profile(main, argv)
 
1123
    stats.sort()
 
1124
    stats.pprint()
 
1125
    return ret
 
1126
 
 
1127
 
 
1128
if __name__ == '__main__':
 
1129
    import sys
 
1130
    if '--profile' in sys.argv:
 
1131
        args = sys.argv[:]
 
1132
        args.remove('--profile')
 
1133
        sys.exit(profile_main(args))
 
1134
    elif '--lsprof' in sys.argv:
 
1135
        args = sys.argv[:]
 
1136
        args.remove('--lsprof')
 
1137
        sys.exit(lsprofile_main(args))
 
1138
    else:
 
1139
        sys.exit(main(sys.argv))
 
1140