~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/weave.py

  • Committer: Martin Pool
  • Date: 2005-07-11 05:45:11 UTC
  • Revision ID: mbp@sourcefrog.net-20050711054511-aab2162e0f02dc64
- small optimization for weave extract

- show progressbar during weave check

Show diffs side-by-side

added added

removed removed

Lines of Context:
45
45
 
46
46
# TODO: Separate out some code to read and write weaves.
47
47
 
48
 
# TODO: End marker for each version?
 
48
# TODO: End marker for each version so we can stop reading?
49
49
 
50
50
# TODO: Check that no insertion occurs inside a deletion that was
51
51
# active in the version of the insertion.
52
52
 
 
53
# TODO: Perhaps a special slower check() method that verifies more
 
54
# nesting constraints and the MD5 of each version?
 
55
 
 
56
 
53
57
 
54
58
try:
55
59
    set
135
139
      version.
136
140
 
137
141
    _l
138
 
        Text of the weave. 
 
142
        Text of the weave.
139
143
 
140
144
    _v
141
 
        List of versions, indexed by index number.
 
145
        List of parents, indexed by version number.
 
146
        It is only necessary to store the minimal set of parents for
 
147
        each version; the parent's parents are implied.
142
148
 
143
 
        For each version we store the set (included_versions), which
144
 
        lists the previous versions also considered active; the
145
 
        versions included in those versions are included transitively.
146
 
        So new versions created from nothing list []; most versions
147
 
        have a single entry; some have more.
 
149
    _sha1s
 
150
        List of hex SHA-1 of each version, or None if not recorded.
148
151
    """
149
152
    def __init__(self):
150
153
        self._l = []
151
154
        self._v = []
152
 
 
 
155
        self._sha1s = []
153
156
 
154
157
 
155
158
    def __eq__(self, other):
169
172
        Returns the index number of the newly added version.
170
173
 
171
174
        parents
172
 
            List or set of parent version numbers.  This must normally include
173
 
            the parents and the parent's parents, or wierd things might happen.
174
 
 
 
175
            List or set of direct parent version numbers.
 
176
            
175
177
        text
176
178
            Sequence of lines to be added in the new version."""
177
 
        self._check_versions(parents)
178
 
        self._check_lines(text)
179
 
 
 
179
        ## self._check_versions(parents)
 
180
        ## self._check_lines(text)
180
181
        idx = len(self._v)
181
182
 
 
183
        import sha
 
184
        s = sha.new()
 
185
        for l in text:
 
186
            s.update(l)
 
187
        sha1 = s.hexdigest()
 
188
        del s
 
189
 
182
190
        if parents:
183
 
            delta = self._delta(self.inclusions(parents), text)
 
191
            ancestors = self.inclusions(parents)
 
192
            delta = self._delta(ancestors, text)
184
193
 
185
194
            # offset gives the number of lines that have been inserted
186
195
            # into the weave up to the current point; if the original edit instruction
210
219
                                   + [('}', idx)]
211
220
                    offset += 2 + len(newlines)
212
221
 
213
 
            # TODO: Could eliminate any parents that are implied by
214
 
            # the others
215
 
                    
216
222
            self._addversion(parents)
217
223
        else:
218
224
            # special case; adding with no parents revision; can do this
222
228
            self._l.append(('}', idx))
223
229
 
224
230
            self._addversion(None)
 
231
 
 
232
        self._sha1s.append(sha1)
225
233
            
226
234
        return idx
227
235
 
228
236
 
229
237
    def inclusions(self, versions):
230
 
        """Expand out everything included by versions."""
 
238
        """Return set of all ancestors of given version(s)."""
231
239
        i = set(versions)
232
 
        for v in versions:
233
 
            i.update(self._v[v])
234
 
        return i
 
240
        v = max(versions)
 
241
        try:
 
242
            while v >= 0:
 
243
                if v in i:
 
244
                    # include all its parents
 
245
                    i.update(self._v[v])
 
246
                v -= 1
 
247
            return i
 
248
        except IndexError:
 
249
            raise ValueError("version %d not present in weave" % v)
 
250
 
 
251
 
 
252
    def minimal_parents(self, version):
 
253
        """Find the minimal set of parents for the version."""
 
254
        included = self._v[version]
 
255
        if not included:
 
256
            return []
 
257
        
 
258
        li = list(included)
 
259
        li.sort(reverse=True)
 
260
 
 
261
        mininc = []
 
262
        gotit = set()
 
263
 
 
264
        for pv in li:
 
265
            if pv not in gotit:
 
266
                mininc.append(pv)
 
267
                gotit.update(self.inclusions(pv))
 
268
 
 
269
        assert mininc[0] >= 0
 
270
        assert mininc[-1] < version
 
271
        return mininc
235
272
 
236
273
 
237
274
    def _addversion(self, parents):
238
275
        if parents:
239
 
            self._v.append(frozenset(parents))
 
276
            self._v.append(parents)
240
277
        else:
241
278
            self._v.append(frozenset())
242
279
 
247
284
 
248
285
        for l in text:
249
286
            if not isinstance(l, basestring):
250
 
                raise ValueError("text line should be a string or unicode, not %s" % type(l))
 
287
                raise ValueError("text line should be a string or unicode, not %s"
 
288
                                 % type(l))
251
289
        
252
290
 
253
291
 
268
306
        """Yield list of (index-id, line) pairs for the specified version.
269
307
 
270
308
        The index indicates when the line originated in the weave."""
271
 
        included = self.inclusions([version])
272
 
        for origin, lineno, text in self._extract(included):
 
309
        for origin, lineno, text in self._extract([version]):
273
310
            yield origin, text
274
311
 
275
312
 
276
 
    def _extract(self, included):
 
313
    def _extract(self, versions):
277
314
        """Yield annotation of lines in included set.
278
315
 
279
316
        Yields a sequence of tuples (origin, lineno, text), where
282
319
 
283
320
        The set typically but not necessarily corresponds to a version.
284
321
        """
285
 
        istack = []          # versions for which an insertion block is current
286
 
 
287
 
        dset = set()         # versions for which a deletion block is current
288
 
 
289
 
        isactive = False
 
322
        included = self.inclusions(versions)
 
323
 
 
324
        istack = []
 
325
        dset = set()
290
326
 
291
327
        lineno = 0         # line of weave, 0-based
292
328
 
293
 
        # TODO: Probably only need to put included revisions in the istack
294
 
 
295
 
        # TODO: Could split this into two functions, one that updates
296
 
        # the stack and the other that processes the results -- but
297
 
        # I'm not sure it's really needed.
 
329
        isactive = None
298
330
 
299
331
        WFE = WeaveFormatError
300
 
        
 
332
 
301
333
        for l in self._l:
302
334
            if isinstance(l, tuple):
303
335
                c, v = l
 
336
                isactive = None
304
337
                if c == '{':
305
 
                    if istack and (istack[-1] >= v):
306
 
                        raise WFE("improperly nested insertions %d>=%d on line %d" 
307
 
                                  % (istack[-1], v, lineno))
 
338
                    assert v not in istack
308
339
                    istack.append(v)
309
340
                elif c == '}':
310
 
                    try:
311
 
                        oldv = istack.pop()
312
 
                    except IndexError:
313
 
                        raise WFE("unmatched close of insertion %d on line %d"
314
 
                                  % (v, lineno))
315
 
                    if oldv != v:
316
 
                        raise WFE("mismatched close of insertion %d!=%d on line %d"
317
 
                                  % (oldv, v, lineno))
 
341
                    oldv = istack.pop()
 
342
                    assert oldv == v
318
343
                elif c == '[':
319
 
                    # block deleted in v
320
 
                    if v in dset:
321
 
                        raise WFE("repeated deletion marker for version %d on line %d"
322
 
                                  % (v, lineno))
323
 
                    if istack:
324
 
                        if istack[-1] == v:
325
 
                            raise WFE("version %d deletes own text on line %d"
326
 
                                      % (v, lineno))
 
344
                    if v in included:
 
345
                        assert v not in dset
327
346
                        dset.add(v)
328
 
                elif c == ']':
329
 
                    if v in dset:
 
347
                else:
 
348
                    assert c == ']'
 
349
                    if v in included:
 
350
                        assert v in dset
330
351
                        dset.remove(v)
331
 
                    else:
332
 
                        raise WFE("unmatched close of deletion %d on line %d"
333
 
                                  % (v, lineno))
334
 
                else:
335
 
                    raise WFE("invalid processing instruction %r on line %d"
336
 
                              % (l, lineno))
337
352
            else:
338
353
                assert isinstance(l, basestring)
339
 
                if not istack:
340
 
                    raise WFE("literal at top level on line %d"
341
 
                              % lineno)
342
 
                isactive = (istack[-1] in included) \
343
 
                           and not included.intersection(dset)
 
354
                if isactive is None:
 
355
                    isactive = (not dset) and istack and (istack[-1] in included)
344
356
                if isactive:
345
 
                    origin = istack[-1]
346
 
                    yield origin, lineno, l
 
357
                    yield istack[-1], lineno, l
347
358
            lineno += 1
348
359
 
349
360
        if istack:
356
367
 
357
368
    def get_iter(self, version):
358
369
        """Yield lines for the specified version."""
359
 
        for origin, lineno, line in self._extract(self.inclusions([version])):
 
370
        for origin, lineno, line in self._extract([version]):
360
371
            yield line
361
372
 
362
373
 
364
375
        return list(self.get_iter(index))
365
376
 
366
377
 
367
 
    def merge_iter(self, included):
 
378
    def mash_iter(self, included):
368
379
        """Return composed version of multiple included versions."""
369
380
        included = frozenset(included)
370
381
        for origin, lineno, text in self._extract(included):
379
390
        pprint(self._v, to_file)
380
391
 
381
392
 
382
 
    def check(self):
383
 
        for vers_info in self._v:
384
 
            included = set()
385
 
            for vi in vers_info[0]:
386
 
                if vi < 0 or vi >= index:
 
393
 
 
394
    def numversions(self):
 
395
        l = len(self._v)
 
396
        assert l == len(self._sha1s)
 
397
        return l
 
398
 
 
399
 
 
400
    def check(self, progress_bar=None):
 
401
        # check no circular inclusions
 
402
        for version in range(self.numversions()):
 
403
            inclusions = list(self._v[version])
 
404
            if inclusions:
 
405
                inclusions.sort()
 
406
                if inclusions[-1] >= version:
387
407
                    raise WeaveFormatError("invalid included version %d for index %d"
388
 
                                               % (vi, index))
389
 
                if vi in included:
390
 
                    raise WeaveFormatError("repeated included version %d for index %d"
391
 
                                               % (vi, index))
392
 
                included.add(vi)
 
408
                                           % (inclusions[-1], version))
 
409
 
 
410
        # try extracting all versions; this is a bit slow and parallel
 
411
        # extraction could be used
 
412
        import sha
 
413
        nv = self.numversions()
 
414
        for version in range(nv):
 
415
            if progress_bar:
 
416
                progress_bar.update('checking text', version, nv)
 
417
            s = sha.new()
 
418
            for l in self.get_iter(version):
 
419
                s.update(l)
 
420
            hd = s.hexdigest()
 
421
            expected = self._sha1s[version]
 
422
            if hd != expected:
 
423
                raise WeaveError("mismatched sha1 for version %d; "
 
424
                                 "got %s, expected %s"
 
425
                                 % (version, hd, expected))
 
426
 
 
427
        # TODO: check insertions are properly nested, that there are
 
428
        # no lines outside of insertion blocks, that deletions are
 
429
        # properly paired, etc.
 
430
 
 
431
 
 
432
 
 
433
    def merge(self, merge_versions):
 
434
        """Automerge and mark conflicts between versions.
 
435
 
 
436
        This returns a sequence, each entry describing alternatives
 
437
        for a chunk of the file.  Each of the alternatives is given as
 
438
        a list of lines.
 
439
 
 
440
        If there is a chunk of the file where there's no diagreement,
 
441
        only one alternative is given.
 
442
        """
 
443
 
 
444
        # approach: find the included versions common to all the
 
445
        # merged versions
 
446
        raise NotImplementedError()
393
447
 
394
448
 
395
449
 
412
466
        If line1=line2, this is a pure insert; if newlines=[] this is a
413
467
        pure delete.  (Similar to difflib.)
414
468
        """
415
 
 
416
 
        self._check_versions(included)
417
 
 
418
 
        ##from pprint import pprint
419
 
 
420
 
        # first get basis for comparison
421
 
        # basis holds (lineno, origin, line)
422
 
        basis = []
423
 
 
424
 
        ##print 'my lines:'
425
 
        ##pprint(self._l)
426
 
 
427
469
        # basis a list of (origin, lineno, line)
428
 
        basis = list(self._extract(included))
429
 
 
430
 
        # now make a parallel list with only the text, to pass to the differ
431
 
        basis_lines = [line for (origin, lineno, line) in basis]
 
470
        basis_lineno = []
 
471
        basis_lines = []
 
472
        for origin, lineno, line in self._extract(included):
 
473
            basis_lineno.append(lineno)
 
474
            basis_lines.append(line)
432
475
 
433
476
        # add a sentinal, because we can also match against the final line
434
 
        basis.append((None, len(self._l), None))
 
477
        basis_lineno.append(len(self._l))
435
478
 
436
479
        # XXX: which line of the weave should we really consider
437
480
        # matches the end of the file?  the current code says it's the
440
483
        from difflib import SequenceMatcher
441
484
        s = SequenceMatcher(None, basis_lines, lines)
442
485
 
443
 
        ##print 'basis sequence:'
444
 
        ##pprint(basis)
445
 
 
446
486
        # TODO: Perhaps return line numbers from composed weave as well?
447
487
 
448
488
        for tag, i1, i2, j1, j2 in s.get_opcodes():
453
493
 
454
494
            # i1,i2 are given in offsets within basis_lines; we need to map them
455
495
            # back to offsets within the entire weave
456
 
            real_i1 = basis[i1][1]
457
 
            real_i2 = basis[i2][1]
 
496
            real_i1 = basis_lineno[i1]
 
497
            real_i2 = basis_lineno[i2]
458
498
 
459
499
            assert 0 <= j1
460
500
            assert j1 <= j2
464
504
 
465
505
 
466
506
 
 
507
def weave_info(filename, out):
 
508
    """Show some text information about the weave."""
 
509
    from weavefile import read_weave
 
510
    wf = file(filename, 'rb')
 
511
    w = read_weave(wf)
 
512
    # FIXME: doesn't work on pipes
 
513
    weave_size = wf.tell()
 
514
    print >>out, "weave file size %d bytes" % weave_size
 
515
    print >>out, "weave contains %d versions" % len(w._v)
 
516
 
 
517
    total = 0
 
518
    print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents')
 
519
    for i in (6, 6, 8, 40, 20):
 
520
        print '-' * i,
 
521
    print
 
522
    for i in range(len(w._v)):
 
523
        text = w.get(i)
 
524
        lines = len(text)
 
525
        bytes = sum((len(a) for a in text))
 
526
        sha1 = w._sha1s[i]
 
527
        print '%6d %6d %8d %40s' % (i, lines, bytes, sha1),
 
528
        for pv in w._v[i]:
 
529
            print pv,
 
530
        print
 
531
        total += bytes
 
532
 
 
533
    print >>out, "versions total %d bytes" % total
 
534
    print >>out, "compression ratio %.3f" % (float(total)/float(weave_size))
 
535
 
 
536
 
 
537
def usage():
 
538
    print """bzr weave tool
 
539
 
 
540
Experimental tool for weave algorithm.
 
541
 
 
542
usage:
 
543
    weave init WEAVEFILE
 
544
        Create an empty weave file
 
545
    weave get WEAVEFILE VERSION
 
546
        Write out specified version.
 
547
    weave check WEAVEFILE
 
548
        Check consistency of all versions.
 
549
    weave info WEAVEFILE
 
550
        Display table of contents.
 
551
    weave add WEAVEFILE [BASE...] < NEWTEXT
 
552
        Add NEWTEXT, with specified parent versions.
 
553
    weave annotate WEAVEFILE VERSION
 
554
        Display origin of each line.
 
555
    weave mash WEAVEFILE VERSION...
 
556
        Display composite of all selected versions.
 
557
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
 
558
        Auto-merge two versions and display conflicts.
 
559
 
 
560
example:
 
561
 
 
562
    % weave init foo.weave
 
563
    % vi foo.txt
 
564
    % weave add foo.weave < foo.txt
 
565
    added version 0
 
566
 
 
567
    (create updated version)
 
568
    % vi foo.txt
 
569
    % weave get foo.weave 0 | diff -u - foo.txt
 
570
    % weave add foo.weave 0 < foo.txt
 
571
    added version 1
 
572
 
 
573
    % weave get foo.weave 0 > foo.txt       (create forked version)
 
574
    % vi foo.txt
 
575
    % weave add foo.weave 0 < foo.txt
 
576
    added version 2
 
577
 
 
578
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
 
579
    % vi foo.txt                            (resolve conflicts)
 
580
    % weave add foo.weave 1 2 < foo.txt     (commit merged version)     
 
581
    
 
582
"""
 
583
    
 
584
 
467
585
 
468
586
def main(argv):
469
587
    import sys
470
588
    import os
471
 
    from weavefile import write_weave_v1, read_weave_v1
 
589
    from weavefile import write_weave, read_weave
 
590
    from bzrlib.progress import ProgressBar
 
591
 
 
592
    #import psyco
 
593
    #psyco.full()
 
594
 
472
595
    cmd = argv[1]
473
 
    if cmd == 'add':
474
 
        w = read_weave_v1(file(argv[2], 'rb'))
 
596
 
 
597
    def readit():
 
598
        return read_weave(file(argv[2], 'rb'))
 
599
    
 
600
    if cmd == 'help':
 
601
        usage()
 
602
    elif cmd == 'add':
 
603
        w = readit()
475
604
        # at the moment, based on everything in the file
476
 
        parents = set(range(len(w._v)))
 
605
        parents = map(int, argv[3:])
477
606
        lines = sys.stdin.readlines()
478
607
        ver = w.add(parents, lines)
479
 
        write_weave_v1(w, file(argv[2], 'wb'))
480
 
        print 'added %d' % ver
 
608
        write_weave(w, file(argv[2], 'wb'))
 
609
        print 'added version %d' % ver
481
610
    elif cmd == 'init':
482
611
        fn = argv[2]
483
612
        if os.path.exists(fn):
484
613
            raise IOError("file exists")
485
614
        w = Weave()
486
 
        write_weave_v1(w, file(fn, 'wb'))
487
 
    elif cmd == 'get':
488
 
        w = read_weave_v1(file(argv[2], 'rb'))
489
 
        sys.stdout.writelines(w.getiter(int(argv[3])))
 
615
        write_weave(w, file(fn, 'wb'))
 
616
    elif cmd == 'get': # get one version
 
617
        w = readit()
 
618
        sys.stdout.writelines(w.get_iter(int(argv[3])))
 
619
        
 
620
    elif cmd == 'mash': # get composite
 
621
        w = readit()
 
622
        sys.stdout.writelines(w.mash_iter(map(int, argv[3:])))
 
623
 
490
624
    elif cmd == 'annotate':
491
 
        w = read_weave_v1(file(argv[2], 'rb'))
 
625
        w = readit()
492
626
        # newline is added to all lines regardless; too hard to get
493
627
        # reasonable formatting otherwise
494
628
        lasto = None
499
633
            else:
500
634
                print '%5d | %s' % (origin, text)
501
635
                lasto = origin
 
636
                
 
637
    elif cmd == 'info':
 
638
        weave_info(argv[2], sys.stdout)
 
639
        
 
640
    elif cmd == 'check':
 
641
        w = readit()
 
642
        pb = ProgressBar()
 
643
        w.check(pb)
 
644
        pb.clear()
 
645
 
 
646
    elif cmd == 'inclusions':
 
647
        w = readit()
 
648
        print ' '.join(map(str, w.inclusions([int(argv[3])])))
 
649
 
 
650
    elif cmd == 'parents':
 
651
        w = readit()
 
652
        print ' '.join(map(str, w._v[int(argv[3])]))
 
653
 
 
654
    elif cmd == 'merge':
 
655
        if len(argv) != 5:
 
656
            usage()
 
657
            return 1
 
658
 
 
659
        w = readit()
 
660
        v1, v2 = map(int, argv[3:5])
 
661
 
 
662
        basis = w.inclusions([v1]).intersection(w.inclusions([v2]))
 
663
 
 
664
        base_lines = list(w.mash_iter(basis))
 
665
        a_lines = list(w.get(v1))
 
666
        b_lines = list(w.get(v2))
 
667
 
 
668
        from bzrlib.merge3 import Merge3
 
669
        m3 = Merge3(base_lines, a_lines, b_lines)
 
670
 
 
671
        name_a = 'version %d' % v1
 
672
        name_b = 'version %d' % v2
 
673
        sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b))
502
674
    else:
503
675
        raise ValueError('unknown command %r' % cmd)
504
676