~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to weave.py

  • Committer: Martin Pool
  • Date: 2005-07-01 02:36:27 UTC
  • mto: This revision was merged to the branch mainline in revision 852.
  • Revision ID: mbp@sourcefrog.net-20050701023627-d8422b67a4c1d6d1
Show profile when converting inventory too.

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
 
22
22
"""Weave - storage of related text file versions"""
23
23
 
24
 
# before intset (r923) 2000 versions in 41.5s
25
 
# with intset (r926) 2000 versions in 93s !!!
26
 
# better to just use plain sets.
27
 
 
28
 
# making _extract build and return a list, rather than being a generator
29
 
# takes 37.94s
30
 
 
31
 
# with python -O, r923 does 2000 versions in 36.87s
32
 
 
33
 
# with optimizations to avoid mutating lists - 35.75!  I guess copying
34
 
# all the elements every time costs more than the small manipulations.
35
 
# a surprisingly small change.
36
 
 
37
 
# r931, which avoids using a generator for extract, does 36.98s
38
 
 
39
 
# with memoized inclusions, takes 41.49s; not very good
40
 
 
41
 
# with slots, takes 37.35s; without takes 39.16, a bit surprising
42
 
 
43
 
# with the delta calculation mixed in with the add method, rather than
44
 
# separated, takes 36.78s
45
 
 
46
 
# with delta folded in and mutation of the list, 36.13s
47
 
 
48
 
# with all this and simplification of add code, 33s
49
 
 
50
 
 
51
 
 
52
 
 
53
 
 
54
24
# TODO: Perhaps have copy method for Weave instances?
55
25
 
56
26
# XXX: If we do weaves this way, will a merge still behave the same
57
27
# way if it's done in a different order?  That's a pretty desirable
58
28
# property.
59
29
 
 
30
# TODO: How to write these to disk?  One option is cPickle, which
 
31
# would be fast but less friendly to C, and perhaps not portable.  Another is
 
32
 
60
33
# TODO: Nothing here so far assumes the lines are really \n newlines,
61
34
# rather than being split up in some other way.  We could accomodate
62
35
# binaries, perhaps by naively splitting on \n or perhaps using
63
36
# something like a rolling checksum.
64
37
 
 
38
# TODO: Perhaps track SHA-1 in the header for protection?  This would
 
39
# be redundant with it being stored in the inventory, but perhaps
 
40
# usefully so?
 
41
 
 
42
# TODO: Track version names as well as indexes. 
 
43
 
 
44
# TODO: Probably do transitive expansion when specifying parents?
 
45
 
 
46
# TODO: Separate out some code to read and write weaves.
 
47
 
65
48
# TODO: End marker for each version so we can stop reading?
66
49
 
67
50
# TODO: Check that no insertion occurs inside a deletion that was
68
51
# active in the version of the insertion.
69
52
 
70
 
# TODO: In addition to the SHA-1 check, perhaps have some code that
71
 
# checks structural constraints of the weave: ie that insertions are
72
 
# properly nested, that there is no text outside of an insertion, that
73
 
# insertions or deletions are not repeated, etc.
74
 
 
75
 
# TODO: Parallel-extract that passes back each line along with a
76
 
# description of which revisions include it.  Nice for checking all
77
 
# shas in parallel.
78
 
 
79
 
# TODO: Using a single _extract routine and then processing the output
80
 
# is probably inefficient.  It's simple enough that we can afford to
81
 
# have slight specializations for different ways its used: annotate,
82
 
# basis for add, get, etc.
83
 
 
84
 
# TODO: Perhaps the API should work only in names to hide the integer
85
 
# indexes from the user?
86
 
 
87
 
 
88
 
 
89
 
import sha
90
 
 
91
 
from cStringIO import StringIO
92
 
 
93
 
from bzrlib.osutils import sha_strings
 
53
# TODO: Perhaps a special slower check() method that verifies more
 
54
# nesting constraints and the MD5 of each version?
 
55
 
 
56
 
 
57
 
 
58
try:
 
59
    set
 
60
    frozenset
 
61
except NameError:
 
62
    from sets import Set, ImmutableSet
 
63
    set = Set
 
64
    frozenset = ImmutableSet
 
65
    del Set, ImmutableSet
94
66
 
95
67
 
96
68
class WeaveError(Exception):
116
88
 
117
89
    * a nonnegative index number.
118
90
 
119
 
    * a version-id string. (not implemented yet)
 
91
    * a version-id string.
120
92
 
121
93
    Typically the index number will be valid only inside this weave and
122
94
    the version-id is used to reference it in the larger world.
123
95
 
124
96
    The weave is represented as a list mixing edit instructions and
125
 
    literal text.  Each entry in _weave can be either a string (or
 
97
    literal text.  Each entry in _l can be either a string (or
126
98
    unicode), or a tuple.  If a string, it means that the given line
127
99
    should be output in the currently active revisions.
128
100
 
133
105
    The instruction can be '{' or '}' for an insertion block, and '['
134
106
    and ']' for a deletion block respectively.  The version is the
135
107
    integer version index.  There is no replace operator, only deletes
136
 
    and inserts.  For '}', the end of an insertion, there is no
137
 
    version parameter because it always closes the most recently
138
 
    opened insertion.
 
108
    and inserts.
139
109
 
140
110
    Constraints/notes:
141
111
 
168
138
      should be no way to get an earlier version deleting a later
169
139
      version.
170
140
 
171
 
    _weave
172
 
        Text of the weave; list of control instruction tuples and strings.
173
 
 
174
 
    _parents
175
 
        List of parents, indexed by version number.
176
 
        It is only necessary to store the minimal set of parents for
177
 
        each version; the parent's parents are implied.
178
 
 
179
 
    _sha1s
180
 
        List of hex SHA-1 of each version.
181
 
 
182
 
    _names
183
 
        List of symbolic names for each version.  Each should be unique.
184
 
 
185
 
    _name_map
186
 
        For each name, the version number.
187
 
 
188
 
    _weave_name
189
 
        Descriptive name of this weave; typically the filename if known.
190
 
        Set by read_weave.
 
141
    _l
 
142
        Text of the weave. 
 
143
 
 
144
    _v
 
145
        List of versions, indexed by index number.
 
146
 
 
147
        For each version we store the set (included_versions), which
 
148
        lists the previous versions also considered active; the
 
149
        versions included in those versions are included transitively.
 
150
        So new versions created from nothing list []; most versions
 
151
        have a single entry; some have more.
191
152
    """
 
153
    def __init__(self):
 
154
        self._l = []
 
155
        self._v = []
192
156
 
193
 
    __slots__ = ['_weave', '_parents', '_sha1s', '_names', '_name_map',
194
 
                 '_weave_name']
195
 
    
196
 
    def __init__(self, weave_name=None):
197
 
        self._weave = []
198
 
        self._parents = []
199
 
        self._sha1s = []
200
 
        self._names = []
201
 
        self._name_map = {}
202
 
        self._weave_name = weave_name
203
157
 
204
158
 
205
159
    def __eq__(self, other):
206
160
        if not isinstance(other, Weave):
207
161
            return False
208
 
        return self._parents == other._parents \
209
 
               and self._weave == other._weave \
210
 
               and self._sha1s == other._sha1s 
 
162
        return self._v == other._v \
 
163
               and self._l == other._l
 
164
    
211
165
 
212
 
    
213
166
    def __ne__(self, other):
214
167
        return not self.__eq__(other)
215
168
 
216
 
 
217
 
    def maybe_lookup(self, name_or_index):
218
 
        """Convert possible symbolic name to index, or pass through indexes."""
219
 
        if isinstance(name_or_index, (int, long)):
220
 
            return name_or_index
221
 
        else:
222
 
            return self.lookup(name_or_index)
223
 
 
224
 
        
225
 
    def lookup(self, name):
226
 
        """Convert symbolic version name to index."""
227
 
        try:
228
 
            return self._name_map[name]
229
 
        except KeyError:
230
 
            raise WeaveError("name %r not present in weave %r" %
231
 
                             (name, self._weave_name))
232
 
 
233
 
 
234
 
    def idx_to_name(self, version):
235
 
        return self._names[version]
236
 
 
237
 
 
238
 
    def _check_repeated_add(self, name, parents, text):
239
 
        """Check that a duplicated add is OK.
240
 
 
241
 
        If it is, return the (old) index; otherwise raise an exception.
242
 
        """
243
 
        idx = self.lookup(name)
244
 
        if sorted(self._parents[idx]) != sorted(parents):
245
 
            raise WeaveError("name \"%s\" already present in weave "
246
 
                             "with different parents" % name)
247
 
        new_sha1 = sha_strings(text)
248
 
        if new_sha1 != self._sha1s[idx]:
249
 
            raise WeaveError("name \"%s\" already present in weave "
250
 
                             "with different text" % name)            
251
 
        return idx
252
 
        
253
 
 
254
 
        
255
 
    def add(self, name, parents, text):
 
169
        
 
170
    def add(self, parents, text):
256
171
        """Add a single text on top of the weave.
257
172
  
258
173
        Returns the index number of the newly added version.
259
174
 
260
 
        name
261
 
            Symbolic name for this version.
262
 
            (Typically the revision-id of the revision that added it.)
263
 
 
264
175
        parents
265
 
            List or set of direct parent version numbers.
266
 
            
 
176
            List or set of parent version numbers.  This must normally include
 
177
            the parents and the parent's parents, or wierd things might happen.
 
178
 
267
179
        text
268
180
            Sequence of lines to be added in the new version."""
269
 
 
270
 
        assert isinstance(name, basestring)
271
 
        if name in self._name_map:
272
 
            return self._check_repeated_add(name, parents, text)
273
 
 
274
 
        parents = map(self.maybe_lookup, parents)
275
 
        self._check_versions(parents)
 
181
        ## self._check_versions(parents)
276
182
        ## self._check_lines(text)
277
 
        new_version = len(self._parents)
278
 
 
279
 
        sha1 = sha_strings(text)
280
 
 
281
 
        # if we abort after here the (in-memory) weave will be corrupt because only
282
 
        # some fields are updated
283
 
        self._parents.append(parents[:])
284
 
        self._sha1s.append(sha1)
285
 
        self._names.append(name)
286
 
        self._name_map[name] = new_version
287
 
 
288
 
            
289
 
        if not parents:
290
 
            # special case; adding with no parents revision; can do
291
 
            # this more quickly by just appending unconditionally.
292
 
            # even more specially, if we're adding an empty text we
293
 
            # need do nothing at all.
294
 
            if text:
295
 
                self._weave.append(('{', new_version))
296
 
                self._weave.extend(text)
297
 
                self._weave.append(('}', None))
298
 
        
299
 
            return new_version
300
 
 
301
 
        if len(parents) == 1:
302
 
            pv = list(parents)[0]
303
 
            if sha1 == self._sha1s[pv]:
304
 
                # special case: same as the single parent
305
 
                return new_version
306
 
            
307
 
 
308
 
        ancestors = self.inclusions(parents)
309
 
 
310
 
        l = self._weave
311
 
 
312
 
        # basis a list of (origin, lineno, line)
313
 
        basis_lineno = []
314
 
        basis_lines = []
315
 
        for origin, lineno, line in self._extract(ancestors):
316
 
            basis_lineno.append(lineno)
317
 
            basis_lines.append(line)
318
 
 
319
 
        # another small special case: a merge, producing the same text
320
 
        # as auto-merge
321
 
        if text == basis_lines:
322
 
            return new_version            
323
 
 
324
 
        # add a sentinal, because we can also match against the final line
325
 
        basis_lineno.append(len(self._weave))
326
 
 
327
 
        # XXX: which line of the weave should we really consider
328
 
        # matches the end of the file?  the current code says it's the
329
 
        # last line of the weave?
330
 
 
331
 
        #print 'basis_lines:', basis_lines
332
 
        #print 'new_lines:  ', lines
333
 
 
334
 
        from difflib import SequenceMatcher
335
 
        s = SequenceMatcher(None, basis_lines, text)
336
 
 
337
 
        # offset gives the number of lines that have been inserted
338
 
        # into the weave up to the current point; if the original edit instruction
339
 
        # says to change line A then we actually change (A+offset)
340
 
        offset = 0
341
 
 
342
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
343
 
            # i1,i2 are given in offsets within basis_lines; we need to map them
344
 
            # back to offsets within the entire weave
345
 
            #print 'raw match', tag, i1, i2, j1, j2
346
 
            if tag == 'equal':
347
 
                continue
348
 
 
349
 
            i1 = basis_lineno[i1]
350
 
            i2 = basis_lineno[i2]
351
 
 
352
 
            assert 0 <= j1 <= j2 <= len(text)
353
 
 
354
 
            #print tag, i1, i2, j1, j2
355
 
 
356
 
            # the deletion and insertion are handled separately.
357
 
            # first delete the region.
358
 
            if i1 != i2:
359
 
                self._weave.insert(i1+offset, ('[', new_version))
360
 
                self._weave.insert(i2+offset+1, (']', new_version))
361
 
                offset += 2
362
 
 
363
 
            if j1 != j2:
364
 
                # there may have been a deletion spanning up to
365
 
                # i2; we want to insert after this region to make sure
366
 
                # we don't destroy ourselves
367
 
                i = i2 + offset
368
 
                self._weave[i:i] = ([('{', new_version)] 
369
 
                                    + text[j1:j2] 
370
 
                                    + [('}', None)])
371
 
                offset += 2 + (j2 - j1)
372
 
 
373
 
        return new_version
 
183
 
 
184
        idx = len(self._v)
 
185
 
 
186
        if parents:
 
187
            delta = self._delta(self.inclusions(parents), text)
 
188
 
 
189
            # offset gives the number of lines that have been inserted
 
190
            # into the weave up to the current point; if the original edit instruction
 
191
            # says to change line A then we actually change (A+offset)
 
192
            offset = 0
 
193
 
 
194
            for i1, i2, newlines in delta:
 
195
                assert 0 <= i1
 
196
                assert i1 <= i2
 
197
                assert i2 <= len(self._l)
 
198
 
 
199
                # the deletion and insertion are handled separately.
 
200
                # first delete the region.
 
201
                if i1 != i2:
 
202
                    self._l.insert(i1+offset, ('[', idx))
 
203
                    self._l.insert(i2+offset+1, (']', idx))
 
204
                    offset += 2
 
205
                    # is this OK???
 
206
 
 
207
                if newlines:
 
208
                    # there may have been a deletion spanning up to
 
209
                    # i2; we want to insert after this region to make sure
 
210
                    # we don't destroy ourselves
 
211
                    i = i2 + offset
 
212
                    self._l[i:i] = [('{', idx)] \
 
213
                                   + newlines \
 
214
                                   + [('}', idx)]
 
215
                    offset += 2 + len(newlines)
 
216
 
 
217
            self._addversion(parents)
 
218
        else:
 
219
            # special case; adding with no parents revision; can do this
 
220
            # more quickly by just appending unconditionally
 
221
            self._l.append(('{', idx))
 
222
            self._l += text
 
223
            self._l.append(('}', idx))
 
224
 
 
225
            self._addversion(None)
 
226
            
 
227
        return idx
374
228
 
375
229
 
376
230
    def inclusions(self, versions):
377
 
        """Return set of all ancestors of given version(s)."""
 
231
        """Expand out everything included by versions."""
378
232
        i = set(versions)
379
 
        v = max(versions)
380
 
        try:
381
 
            while v >= 0:
382
 
                if v in i:
383
 
                    # include all its parents
384
 
                    i.update(self._parents[v])
385
 
                v -= 1
386
 
            return i
387
 
        except IndexError:
388
 
            raise ValueError("version %d not present in weave" % v)
389
 
 
390
 
 
391
 
    def parents(self, version):
392
 
        return self._parents[version]
393
 
 
394
 
 
395
 
    def minimal_parents(self, version):
396
 
        """Find the minimal set of parents for the version."""
397
 
        included = self._parents[version]
398
 
        if not included:
399
 
            return []
400
 
        
401
 
        li = list(included)
402
 
        li.sort(reverse=True)
403
 
 
404
 
        mininc = []
405
 
        gotit = set()
406
 
 
407
 
        for pv in li:
408
 
            if pv not in gotit:
409
 
                mininc.append(pv)
410
 
                gotit.update(self.inclusions(pv))
411
 
 
412
 
        assert mininc[0] >= 0
413
 
        assert mininc[-1] < version
414
 
        return mininc
415
 
 
 
233
        for v in versions:
 
234
            i.update(self._v[v])
 
235
        return i
 
236
 
 
237
 
 
238
    def _addversion(self, parents):
 
239
        if parents:
 
240
            self._v.append(frozenset(parents))
 
241
        else:
 
242
            self._v.append(frozenset())
416
243
 
417
244
 
418
245
    def _check_lines(self, text):
421
248
 
422
249
        for l in text:
423
250
            if not isinstance(l, basestring):
424
 
                raise ValueError("text line should be a string or unicode, not %s"
425
 
                                 % type(l))
 
251
                raise ValueError("text line should be a string or unicode, not %s" % type(l))
426
252
        
427
253
 
428
254
 
430
256
        """Check everything in the sequence of indexes is valid"""
431
257
        for i in indexes:
432
258
            try:
433
 
                self._parents[i]
 
259
                self._v[i]
434
260
            except IndexError:
435
261
                raise IndexError("invalid version number %r" % i)
436
262
 
437
263
    
438
 
    def annotate(self, name_or_index):
439
 
        return list(self.annotate_iter(name_or_index))
440
 
 
441
 
 
442
 
    def annotate_iter(self, name_or_index):
 
264
    def annotate(self, index):
 
265
        return list(self.annotate_iter(index))
 
266
 
 
267
 
 
268
    def annotate_iter(self, version):
443
269
        """Yield list of (index-id, line) pairs for the specified version.
444
270
 
445
271
        The index indicates when the line originated in the weave."""
446
 
        incls = [self.maybe_lookup(name_or_index)]
447
 
        for origin, lineno, text in self._extract(incls):
 
272
        included = self.inclusions([version])
 
273
        for origin, lineno, text in self._extract(included):
448
274
            yield origin, text
449
275
 
450
276
 
451
 
    def _walk(self):
452
 
        """Walk the weave.
453
 
 
454
 
        Yields sequence of
455
 
        (lineno, insert, deletes, text)
456
 
        for each literal line.
457
 
        """
458
 
        
459
 
        istack = []
460
 
        dset = set()
461
 
 
462
 
        lineno = 0         # line of weave, 0-based
463
 
 
464
 
        for l in self._weave:
465
 
            if isinstance(l, tuple):
466
 
                c, v = l
467
 
                isactive = None
468
 
                if c == '{':
469
 
                    istack.append(v)
470
 
                elif c == '}':
471
 
                    istack.pop()
472
 
                elif c == '[':
473
 
                    assert v not in dset
474
 
                    dset.add(v)
475
 
                elif c == ']':
476
 
                    dset.remove(v)
477
 
                else:
478
 
                    raise WeaveFormatError('unexpected instruction %r'
479
 
                                           % v)
480
 
            else:
481
 
                assert isinstance(l, basestring)
482
 
                assert istack
483
 
                yield lineno, istack[-1], dset, l
484
 
            lineno += 1
485
 
 
486
 
 
487
 
 
488
 
    def _extract(self, versions):
 
277
    def _extract(self, included):
489
278
        """Yield annotation of lines in included set.
490
279
 
491
280
        Yields a sequence of tuples (origin, lineno, text), where
494
283
 
495
284
        The set typically but not necessarily corresponds to a version.
496
285
        """
497
 
        for i in versions:
498
 
            if not isinstance(i, int):
499
 
                raise ValueError(i)
500
 
            
501
 
        included = self.inclusions(versions)
502
 
 
503
 
        istack = []
504
 
        dset = set()
505
 
 
506
 
        lineno = 0         # line of weave, 0-based
 
286
        istack = []          # versions for which an insertion block is current
 
287
 
 
288
        dset = set()         # versions for which a deletion block is current
507
289
 
508
290
        isactive = None
509
291
 
510
 
        result = []
 
292
        lineno = 0         # line of weave, 0-based
 
293
 
 
294
        # TODO: Probably only need to put included revisions in the istack
 
295
 
 
296
        # TODO: Could split this into two functions, one that updates
 
297
        # the stack and the other that processes the results -- but
 
298
        # I'm not sure it's really needed.
 
299
 
 
300
        # TODO: In fact, I think we only need to store the *count* of
 
301
        # active insertions and deletions, and we can maintain that by
 
302
        # just by just counting as we go along.
511
303
 
512
304
        WFE = WeaveFormatError
513
 
 
514
 
        for l in self._weave:
 
305
        
 
306
        for l in self._l:
515
307
            if isinstance(l, tuple):
 
308
                isactive = None         # recalculate
516
309
                c, v = l
517
 
                isactive = None
518
310
                if c == '{':
519
 
                    assert v not in istack
 
311
                    if istack and (istack[-1] >= v):
 
312
                        raise WFE("improperly nested insertions %d>=%d on line %d" 
 
313
                                  % (istack[-1], v, lineno))
520
314
                    istack.append(v)
521
315
                elif c == '}':
522
 
                    istack.pop()
 
316
                    try:
 
317
                        oldv = istack.pop()
 
318
                    except IndexError:
 
319
                        raise WFE("unmatched close of insertion %d on line %d"
 
320
                                  % (v, lineno))
 
321
                    if oldv != v:
 
322
                        raise WFE("mismatched close of insertion %d!=%d on line %d"
 
323
                                  % (oldv, v, lineno))
523
324
                elif c == '[':
524
 
                    if v in included:
525
 
                        assert v not in dset
 
325
                    # block deleted in v
 
326
                    if v in dset:
 
327
                        raise WFE("repeated deletion marker for version %d on line %d"
 
328
                                  % (v, lineno))
 
329
                    if istack:
 
330
                        if istack[-1] == v:
 
331
                            raise WFE("version %d deletes own text on line %d"
 
332
                                      % (v, lineno))
526
333
                        dset.add(v)
527
 
                else:
528
 
                    assert c == ']'
529
 
                    if v in included:
530
 
                        assert v in dset
 
334
                elif c == ']':
 
335
                    if v in dset:
531
336
                        dset.remove(v)
 
337
                    else:
 
338
                        raise WFE("unmatched close of deletion %d on line %d"
 
339
                                  % (v, lineno))
 
340
                else:
 
341
                    raise WFE("invalid processing instruction %r on line %d"
 
342
                              % (l, lineno))
532
343
            else:
533
344
                assert isinstance(l, basestring)
534
 
                if isactive is None:
535
 
                    isactive = (not dset) and istack and (istack[-1] in included)
 
345
                if not istack:
 
346
                    raise WFE("literal at top level on line %d"
 
347
                              % lineno)
 
348
                if isactive == None:
 
349
                    isactive = (istack[-1] in included) \
 
350
                               and not included.intersection(dset)
536
351
                if isactive:
537
 
                    result.append((istack[-1], lineno, l))
 
352
                    origin = istack[-1]
 
353
                    yield origin, lineno, l
538
354
            lineno += 1
539
355
 
540
356
        if istack:
544
360
            raise WFE("unclosed deletion blocks at end of weave",
545
361
                                   dset)
546
362
 
547
 
        return result
548
 
    
549
 
 
550
 
 
551
 
    def get_iter(self, name_or_index):
 
363
 
 
364
    def get_iter(self, version):
552
365
        """Yield lines for the specified version."""
553
 
        incls = [self.maybe_lookup(name_or_index)]
554
 
        for origin, lineno, line in self._extract(incls):
 
366
        for origin, lineno, line in self._extract(self.inclusions([version])):
555
367
            yield line
556
368
 
557
369
 
558
 
    def get_text(self, version):
559
 
        assert isinstance(version, int)
560
 
        s = StringIO()
561
 
        s.writelines(self.get_iter(version))
562
 
        return s.getvalue()
563
 
 
564
 
 
565
 
    def get(self, name_or_index):
566
 
        return list(self.get_iter(name_or_index))
567
 
 
568
 
 
569
 
    def mash_iter(self, included):
 
370
    def get(self, index):
 
371
        return list(self.get_iter(index))
 
372
 
 
373
 
 
374
    def merge_iter(self, included):
570
375
        """Return composed version of multiple included versions."""
 
376
        included = frozenset(included)
571
377
        for origin, lineno, text in self._extract(included):
572
378
            yield text
573
379
 
574
380
 
575
381
    def dump(self, to_file):
576
382
        from pprint import pprint
577
 
        print >>to_file, "Weave._weave = ",
578
 
        pprint(self._weave, to_file)
579
 
        print >>to_file, "Weave._parents = ",
580
 
        pprint(self._parents, to_file)
581
 
 
582
 
 
583
 
 
584
 
    def numversions(self):
585
 
        l = len(self._parents)
586
 
        assert l == len(self._sha1s)
587
 
        return l
588
 
 
589
 
 
590
 
    def __len__(self):
591
 
        return self.numversions()
592
 
 
593
 
 
594
 
    def check(self, progress_bar=None):
595
 
        # check no circular inclusions
596
 
        for version in range(self.numversions()):
597
 
            inclusions = list(self._parents[version])
598
 
            if inclusions:
599
 
                inclusions.sort()
600
 
                if inclusions[-1] >= version:
 
383
        print >>to_file, "Weave._l = ",
 
384
        pprint(self._l, to_file)
 
385
        print >>to_file, "Weave._v = ",
 
386
        pprint(self._v, to_file)
 
387
 
 
388
 
 
389
    def check(self):
 
390
        for vers_info in self._v:
 
391
            included = set()
 
392
            for vi in vers_info[0]:
 
393
                if vi < 0 or vi >= index:
601
394
                    raise WeaveFormatError("invalid included version %d for index %d"
602
 
                                           % (inclusions[-1], version))
603
 
 
604
 
        # try extracting all versions; this is a bit slow and parallel
605
 
        # extraction could be used
606
 
        nv = self.numversions()
607
 
        for version in range(nv):
608
 
            if progress_bar:
609
 
                progress_bar.update('checking text', version, nv)
610
 
            s = sha.new()
611
 
            for l in self.get_iter(version):
612
 
                s.update(l)
613
 
            hd = s.hexdigest()
614
 
            expected = self._sha1s[version]
615
 
            if hd != expected:
616
 
                raise WeaveError("mismatched sha1 for version %d; "
617
 
                                 "got %s, expected %s"
618
 
                                 % (version, hd, expected))
619
 
 
620
 
        # TODO: check insertions are properly nested, that there are
621
 
        # no lines outside of insertion blocks, that deletions are
622
 
        # properly paired, etc.
623
 
 
624
 
 
625
 
 
626
 
    def merge(self, merge_versions):
627
 
        """Automerge and mark conflicts between versions.
628
 
 
629
 
        This returns a sequence, each entry describing alternatives
630
 
        for a chunk of the file.  Each of the alternatives is given as
631
 
        a list of lines.
632
 
 
633
 
        If there is a chunk of the file where there's no diagreement,
634
 
        only one alternative is given.
635
 
        """
636
 
 
637
 
        # approach: find the included versions common to all the
638
 
        # merged versions
639
 
        raise NotImplementedError()
 
395
                                               % (vi, index))
 
396
                if vi in included:
 
397
                    raise WeaveFormatError("repeated included version %d for index %d"
 
398
                                               % (vi, index))
 
399
                included.add(vi)
640
400
 
641
401
 
642
402
 
659
419
        If line1=line2, this is a pure insert; if newlines=[] this is a
660
420
        pure delete.  (Similar to difflib.)
661
421
        """
662
 
 
663
 
 
664
 
            
665
 
    def plan_merge(self, ver_a, ver_b):
666
 
        """Return pseudo-annotation indicating how the two versions merge.
667
 
 
668
 
        This is computed between versions a and b and their common
669
 
        base.
670
 
 
671
 
        Weave lines present in none of them are skipped entirely.
672
 
        """
673
 
        inc_a = self.inclusions([ver_a])
674
 
        inc_b = self.inclusions([ver_b])
675
 
        inc_c = inc_a & inc_b
676
 
 
677
 
        for lineno, insert, deleteset, line in self._walk():
678
 
            if deleteset & inc_c:
679
 
                # killed in parent; can't be in either a or b
680
 
                # not relevant to our work
681
 
                yield 'killed-base', line
682
 
            elif insert in inc_c:
683
 
                # was inserted in base
684
 
                killed_a = bool(deleteset & inc_a)
685
 
                killed_b = bool(deleteset & inc_b)
686
 
                if killed_a and killed_b:
687
 
                    yield 'killed-both', line
688
 
                elif killed_a:
689
 
                    yield 'killed-a', line
690
 
                elif killed_b:
691
 
                    yield 'killed-b', line
692
 
                else:
693
 
                    yield 'unchanged', line
694
 
            elif insert in inc_a:
695
 
                if deleteset & inc_a:
696
 
                    yield 'ghost-a', line
697
 
                else:
698
 
                    # new in A; not in B
699
 
                    yield 'new-a', line
700
 
            elif insert in inc_b:
701
 
                if deleteset & inc_b:
702
 
                    yield 'ghost-b', line
703
 
                else:
704
 
                    yield 'new-b', line
705
 
            else:
706
 
                # not in either revision
707
 
                yield 'irrelevant', line
708
 
 
709
 
        yield 'unchanged', ''           # terminator
710
 
 
711
 
 
712
 
 
713
 
    def weave_merge(self, plan):
714
 
        lines_a = []
715
 
        lines_b = []
716
 
        ch_a = ch_b = False
717
 
 
718
 
        for state, line in plan:
719
 
            if state == 'unchanged' or state == 'killed-both':
720
 
                # resync and flush queued conflicts changes if any
721
 
                if not lines_a and not lines_b:
722
 
                    pass
723
 
                elif ch_a and not ch_b:
724
 
                    # one-sided change:                    
725
 
                    for l in lines_a: yield l
726
 
                elif ch_b and not ch_a:
727
 
                    for l in lines_b: yield l
728
 
                elif lines_a == lines_b:
729
 
                    for l in lines_a: yield l
730
 
                else:
731
 
                    yield '<<<<\n'
732
 
                    for l in lines_a: yield l
733
 
                    yield '====\n'
734
 
                    for l in lines_b: yield l
735
 
                    yield '>>>>\n'
736
 
 
737
 
                del lines_a[:]
738
 
                del lines_b[:]
739
 
                ch_a = ch_b = False
740
 
                
741
 
            if state == 'unchanged':
742
 
                if line:
743
 
                    yield line
744
 
            elif state == 'killed-a':
745
 
                ch_a = True
746
 
                lines_b.append(line)
747
 
            elif state == 'killed-b':
748
 
                ch_b = True
749
 
                lines_a.append(line)
750
 
            elif state == 'new-a':
751
 
                ch_a = True
752
 
                lines_a.append(line)
753
 
            elif state == 'new-b':
754
 
                ch_b = True
755
 
                lines_b.append(line)
756
 
            else:
757
 
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
758
 
                                 'killed-both'), \
759
 
                       state
760
 
 
761
 
                
762
 
 
763
 
 
764
 
 
765
 
 
766
 
 
767
 
def weave_toc(w):
768
 
    """Show the weave's table-of-contents"""
769
 
    print '%6s %50s %10s %10s' % ('ver', 'name', 'sha1', 'parents')
770
 
    for i in (6, 50, 10, 10):
771
 
        print '-' * i,
772
 
    print
773
 
    for i in range(w.numversions()):
774
 
        sha1 = w._sha1s[i]
775
 
        name = w._names[i]
776
 
        parent_str = ' '.join(map(str, w._parents[i]))
777
 
        print '%6d %-50.50s %10.10s %s' % (i, name, sha1, parent_str)
778
 
 
779
 
 
780
 
 
781
 
def weave_stats(weave_file):
782
 
    from bzrlib.progress import ProgressBar
783
 
    from bzrlib.weavefile import read_weave
784
 
 
785
 
    pb = ProgressBar()
786
 
 
787
 
    wf = file(weave_file, 'rb')
788
 
    w = read_weave(wf)
789
 
    # FIXME: doesn't work on pipes
790
 
    weave_size = wf.tell()
791
 
 
792
 
    total = 0
793
 
    vers = len(w)
794
 
    for i in range(vers):
795
 
        pb.update('checking sizes', i, vers)
796
 
        for line in w.get_iter(i):
797
 
            total += len(line)
798
 
 
799
 
    pb.clear()
800
 
 
801
 
    print 'versions          %9d' % vers
802
 
    print 'weave file        %9d bytes' % weave_size
803
 
    print 'total contents    %9d bytes' % total
804
 
    print 'compression ratio %9.2fx' % (float(total) / float(weave_size))
805
 
    if vers:
806
 
        avg = total/vers
807
 
        print 'average size      %9d bytes' % avg
808
 
        print 'relative size     %9.2fx' % (float(weave_size) / float(avg))
809
 
 
810
 
 
811
 
def usage():
812
 
    print """bzr weave tool
813
 
 
814
 
Experimental tool for weave algorithm.
815
 
 
816
 
usage:
817
 
    weave init WEAVEFILE
818
 
        Create an empty weave file
819
 
    weave get WEAVEFILE VERSION
820
 
        Write out specified version.
821
 
    weave check WEAVEFILE
822
 
        Check consistency of all versions.
823
 
    weave toc WEAVEFILE
824
 
        Display table of contents.
825
 
    weave add WEAVEFILE NAME [BASE...] < NEWTEXT
826
 
        Add NEWTEXT, with specified parent versions.
827
 
    weave annotate WEAVEFILE VERSION
828
 
        Display origin of each line.
829
 
    weave mash WEAVEFILE VERSION...
830
 
        Display composite of all selected versions.
831
 
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
832
 
        Auto-merge two versions and display conflicts.
833
 
 
834
 
example:
835
 
 
836
 
    % weave init foo.weave
837
 
    % vi foo.txt
838
 
    % weave add foo.weave ver0 < foo.txt
839
 
    added version 0
840
 
 
841
 
    (create updated version)
842
 
    % vi foo.txt
843
 
    % weave get foo.weave 0 | diff -u - foo.txt
844
 
    % weave add foo.weave ver1 0 < foo.txt
845
 
    added version 1
846
 
 
847
 
    % weave get foo.weave 0 > foo.txt       (create forked version)
848
 
    % vi foo.txt
849
 
    % weave add foo.weave ver2 0 < foo.txt
850
 
    added version 2
851
 
 
852
 
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
853
 
    % vi foo.txt                            (resolve conflicts)
854
 
    % weave add foo.weave merged 1 2 < foo.txt     (commit merged version)     
855
 
    
856
 
"""
857
 
    
 
422
        # basis a list of (origin, lineno, line)
 
423
        basis_lineno = []
 
424
        basis_lines = []
 
425
        for origin, lineno, line in self._extract(included):
 
426
            basis_lineno.append(lineno)
 
427
            basis_lines.append(line)
 
428
 
 
429
        # add a sentinal, because we can also match against the final line
 
430
        basis_lineno.append(len(self._l))
 
431
 
 
432
        # XXX: which line of the weave should we really consider
 
433
        # matches the end of the file?  the current code says it's the
 
434
        # last line of the weave?
 
435
 
 
436
        from difflib import SequenceMatcher
 
437
        s = SequenceMatcher(None, basis_lines, lines)
 
438
 
 
439
        # TODO: Perhaps return line numbers from composed weave as well?
 
440
 
 
441
        for tag, i1, i2, j1, j2 in s.get_opcodes():
 
442
            ##print tag, i1, i2, j1, j2
 
443
 
 
444
            if tag == 'equal':
 
445
                continue
 
446
 
 
447
            # i1,i2 are given in offsets within basis_lines; we need to map them
 
448
            # back to offsets within the entire weave
 
449
            real_i1 = basis_lineno[i1]
 
450
            real_i2 = basis_lineno[i2]
 
451
 
 
452
            assert 0 <= j1
 
453
            assert j1 <= j2
 
454
            assert j2 <= len(lines)
 
455
 
 
456
            yield real_i1, real_i2, lines[j1:j2]
 
457
 
 
458
 
858
459
 
859
460
 
860
461
def main(argv):
861
462
    import sys
862
463
    import os
863
 
    from weavefile import write_weave, read_weave
864
 
    from bzrlib.progress import ProgressBar
865
 
 
866
 
    try:
867
 
        import psyco
868
 
        psyco.full()
869
 
    except ImportError:
870
 
        pass
871
 
 
872
 
    if len(argv) < 2:
873
 
        usage()
874
 
        return 0
875
 
 
 
464
    from weavefile import write_weave_v1, read_weave_v1
876
465
    cmd = argv[1]
877
 
 
878
 
    def readit():
879
 
        return read_weave(file(argv[2], 'rb'))
880
 
    
881
 
    if cmd == 'help':
882
 
        usage()
883
 
    elif cmd == 'add':
884
 
        w = readit()
 
466
    if cmd == 'add':
 
467
        w = read_weave_v1(file(argv[2], 'rb'))
885
468
        # at the moment, based on everything in the file
886
 
        name = argv[3]
887
 
        parents = map(int, argv[4:])
 
469
        parents = set(range(len(w._v)))
888
470
        lines = sys.stdin.readlines()
889
 
        ver = w.add(name, parents, lines)
890
 
        write_weave(w, file(argv[2], 'wb'))
891
 
        print 'added version %r %d' % (name, ver)
 
471
        ver = w.add(parents, lines)
 
472
        write_weave_v1(w, file(argv[2], 'wb'))
 
473
        print 'added %d' % ver
892
474
    elif cmd == 'init':
893
475
        fn = argv[2]
894
476
        if os.path.exists(fn):
895
477
            raise IOError("file exists")
896
478
        w = Weave()
897
 
        write_weave(w, file(fn, 'wb'))
898
 
    elif cmd == 'get': # get one version
899
 
        w = readit()
900
 
        sys.stdout.writelines(w.get_iter(int(argv[3])))
901
 
        
902
 
    elif cmd == 'mash': # get composite
903
 
        w = readit()
904
 
        sys.stdout.writelines(w.mash_iter(map(int, argv[3:])))
905
 
 
 
479
        write_weave_v1(w, file(fn, 'wb'))
 
480
    elif cmd == 'get':
 
481
        w = read_weave_v1(file(argv[2], 'rb'))
 
482
        sys.stdout.writelines(w.getiter(int(argv[3])))
906
483
    elif cmd == 'annotate':
907
 
        w = readit()
 
484
        w = read_weave_v1(file(argv[2], 'rb'))
908
485
        # newline is added to all lines regardless; too hard to get
909
486
        # reasonable formatting otherwise
910
487
        lasto = None
915
492
            else:
916
493
                print '%5d | %s' % (origin, text)
917
494
                lasto = origin
918
 
                
919
 
    elif cmd == 'toc':
920
 
        weave_toc(readit())
921
 
 
922
 
    elif cmd == 'stats':
923
 
        weave_stats(argv[2])
924
 
        
925
 
    elif cmd == 'check':
926
 
        w = readit()
927
 
        pb = ProgressBar()
928
 
        w.check(pb)
929
 
        pb.clear()
930
 
        print '%d versions ok' % w.numversions()
931
 
 
932
 
    elif cmd == 'inclusions':
933
 
        w = readit()
934
 
        print ' '.join(map(str, w.inclusions([int(argv[3])])))
935
 
 
936
 
    elif cmd == 'parents':
937
 
        w = readit()
938
 
        print ' '.join(map(str, w._parents[int(argv[3])]))
939
 
 
940
 
    elif cmd == 'plan-merge':
941
 
        w = readit()
942
 
        for state, line in w.plan_merge(int(argv[3]), int(argv[4])):
943
 
            if line:
944
 
                print '%14s | %s' % (state, line),
945
 
 
946
 
    elif cmd == 'merge':
947
 
        w = readit()
948
 
        p = w.plan_merge(int(argv[3]), int(argv[4]))
949
 
        sys.stdout.writelines(w.weave_merge(p))
950
 
            
951
 
    elif cmd == 'mash-merge':
952
 
        if len(argv) != 5:
953
 
            usage()
954
 
            return 1
955
 
 
956
 
        w = readit()
957
 
        v1, v2 = map(int, argv[3:5])
958
 
 
959
 
        basis = w.inclusions([v1]).intersection(w.inclusions([v2]))
960
 
 
961
 
        base_lines = list(w.mash_iter(basis))
962
 
        a_lines = list(w.get(v1))
963
 
        b_lines = list(w.get(v2))
964
 
 
965
 
        from bzrlib.merge3 import Merge3
966
 
        m3 = Merge3(base_lines, a_lines, b_lines)
967
 
 
968
 
        name_a = 'version %d' % v1
969
 
        name_b = 'version %d' % v2
970
 
        sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b))
971
495
    else:
972
496
        raise ValueError('unknown command %r' % cmd)
973
497
    
974
498
 
975
 
 
976
 
def profile_main(argv): 
977
 
    import tempfile, hotshot, hotshot.stats
978
 
 
979
 
    prof_f = tempfile.NamedTemporaryFile()
980
 
 
981
 
    prof = hotshot.Profile(prof_f.name)
982
 
 
983
 
    ret = prof.runcall(main, argv)
984
 
    prof.close()
985
 
 
986
 
    stats = hotshot.stats.load(prof_f.name)
987
 
    #stats.strip_dirs()
988
 
    stats.sort_stats('cumulative')
989
 
    ## XXX: Might like to write to stderr or the trace file instead but
990
 
    ## print_stats seems hardcoded to stdout
991
 
    stats.print_stats(20)
992
 
            
993
 
    return ret
994
 
 
995
 
 
996
499
if __name__ == '__main__':
997
500
    import sys
998
 
    if '--profile' in sys.argv:
999
 
        args = sys.argv[:]
1000
 
        args.remove('--profile')
1001
 
        sys.exit(profile_main(args))
1002
 
    else:
1003
 
        sys.exit(main(sys.argv))
1004
 
 
 
501
    sys.exit(main(sys.argv))