~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/weave.py

  • Committer: Martin Pool
  • Date: 2005-09-22 06:19:33 UTC
  • Revision ID: mbp@sourcefrog.net-20050922061933-4b71d0f1e205b153
- keep track of number of checked revisions

Show diffs side-by-side

added added

removed removed

Lines of Context:
45
45
 
46
46
# with delta folded in and mutation of the list, 36.13s
47
47
 
48
 
# with all this and simplification of add code, 33s 
 
48
# with all this and simplification of add code, 33s
 
49
 
 
50
 
 
51
 
49
52
 
50
53
 
51
54
# TODO: Perhaps have copy method for Weave instances?
59
62
# binaries, perhaps by naively splitting on \n or perhaps using
60
63
# something like a rolling checksum.
61
64
 
62
 
# TODO: Track version names as well as indexes. 
63
 
 
64
65
# TODO: End marker for each version so we can stop reading?
65
66
 
66
67
# TODO: Check that no insertion occurs inside a deletion that was
75
76
# description of which revisions include it.  Nice for checking all
76
77
# shas in parallel.
77
78
 
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
# TODO: Is there any potential performance win by having an add()
 
88
# variant that is passed a pre-cooked version of the single basis
 
89
# version?
 
90
 
 
91
 
 
92
 
 
93
import sha
 
94
from difflib import SequenceMatcher
 
95
 
 
96
from cStringIO import StringIO
 
97
 
 
98
from bzrlib.osutils import sha_strings
79
99
 
80
100
 
81
101
class WeaveError(Exception):
118
138
    The instruction can be '{' or '}' for an insertion block, and '['
119
139
    and ']' for a deletion block respectively.  The version is the
120
140
    integer version index.  There is no replace operator, only deletes
121
 
    and inserts.
 
141
    and inserts.  For '}', the end of an insertion, there is no
 
142
    version parameter because it always closes the most recently
 
143
    opened insertion.
122
144
 
123
145
    Constraints/notes:
124
146
 
160
182
        each version; the parent's parents are implied.
161
183
 
162
184
    _sha1s
163
 
        List of hex SHA-1 of each version, or None if not recorded.
 
185
        List of hex SHA-1 of each version.
 
186
 
 
187
    _names
 
188
        List of symbolic names for each version.  Each should be unique.
 
189
 
 
190
    _name_map
 
191
        For each name, the version number.
 
192
 
 
193
    _weave_name
 
194
        Descriptive name of this weave; typically the filename if known.
 
195
        Set by read_weave.
164
196
    """
165
197
 
166
 
    __slots__ = ['_weave', '_parents', '_sha1s']
 
198
    __slots__ = ['_weave', '_parents', '_sha1s', '_names', '_name_map',
 
199
                 '_weave_name']
167
200
    
168
 
    def __init__(self):
 
201
    def __init__(self, weave_name=None):
169
202
        self._weave = []
170
203
        self._parents = []
171
204
        self._sha1s = []
 
205
        self._names = []
 
206
        self._name_map = {}
 
207
        self._weave_name = weave_name
172
208
 
173
209
 
174
210
    def __eq__(self, other):
175
211
        if not isinstance(other, Weave):
176
212
            return False
177
213
        return self._parents == other._parents \
178
 
               and self._weave == other._weave
 
214
               and self._weave == other._weave \
 
215
               and self._sha1s == other._sha1s 
 
216
 
179
217
    
180
 
 
181
218
    def __ne__(self, other):
182
219
        return not self.__eq__(other)
183
220
 
184
 
        
185
 
    def add(self, parents, text):
 
221
 
 
222
    def maybe_lookup(self, name_or_index):
 
223
        """Convert possible symbolic name to index, or pass through indexes."""
 
224
        if isinstance(name_or_index, (int, long)):
 
225
            return name_or_index
 
226
        else:
 
227
            return self.lookup(name_or_index)
 
228
 
 
229
        
 
230
    def lookup(self, name):
 
231
        """Convert symbolic version name to index."""
 
232
        try:
 
233
            return self._name_map[name]
 
234
        except KeyError:
 
235
            raise WeaveError("name %r not present in weave %r" %
 
236
                             (name, self._weave_name))
 
237
 
 
238
 
 
239
    def idx_to_name(self, version):
 
240
        return self._names[version]
 
241
 
 
242
 
 
243
    def _check_repeated_add(self, name, parents, text, sha1):
 
244
        """Check that a duplicated add is OK.
 
245
 
 
246
        If it is, return the (old) index; otherwise raise an exception.
 
247
        """
 
248
        idx = self.lookup(name)
 
249
        if sorted(self._parents[idx]) != sorted(parents):
 
250
            raise WeaveError("name \"%s\" already present in weave "
 
251
                             "with different parents" % name)
 
252
        if sha1 != self._sha1s[idx]:
 
253
            raise WeaveError("name \"%s\" already present in weave "
 
254
                             "with different text" % name)            
 
255
        return idx
 
256
        
 
257
 
 
258
        
 
259
    def add(self, name, parents, text, sha1=None):
186
260
        """Add a single text on top of the weave.
187
261
  
188
262
        Returns the index number of the newly added version.
189
263
 
 
264
        name
 
265
            Symbolic name for this version.
 
266
            (Typically the revision-id of the revision that added it.)
 
267
 
190
268
        parents
191
269
            List or set of direct parent version numbers.
192
270
            
193
271
        text
194
 
            Sequence of lines to be added in the new version."""
195
 
 
 
272
            Sequence of lines to be added in the new version.
 
273
 
 
274
        sha -- SHA-1 of the file, if known.  This is trusted to be
 
275
            correct if supplied.
 
276
        """
 
277
 
 
278
        assert isinstance(name, basestring)
 
279
        if sha1 is None:
 
280
            sha1 = sha_strings(text)
 
281
        if name in self._name_map:
 
282
            return self._check_repeated_add(name, parents, text, sha1)
 
283
 
 
284
        parents = map(self.maybe_lookup, parents)
196
285
        self._check_versions(parents)
197
286
        ## self._check_lines(text)
198
287
        new_version = len(self._parents)
199
288
 
200
 
        import sha
201
 
        s = sha.new()
202
 
        map(s.update, text)
203
 
        sha1 = s.hexdigest()
204
 
        del s
205
289
 
206
 
        # if we abort after here the weave will be corrupt
207
 
        self._parents.append(frozenset(parents))
 
290
        # if we abort after here the (in-memory) weave will be corrupt because only
 
291
        # some fields are updated
 
292
        self._parents.append(parents[:])
208
293
        self._sha1s.append(sha1)
 
294
        self._names.append(name)
 
295
        self._name_map[name] = new_version
209
296
 
210
297
            
211
298
        if not parents:
216
303
            if text:
217
304
                self._weave.append(('{', new_version))
218
305
                self._weave.extend(text)
219
 
                self._weave.append(('}', new_version))
 
306
                self._weave.append(('}', None))
220
307
        
221
308
            return new_version
222
309
 
238
325
            basis_lineno.append(lineno)
239
326
            basis_lines.append(line)
240
327
 
241
 
        # another small special case: a merge, producing the same text as auto-merge
 
328
        # another small special case: a merge, producing the same text
 
329
        # as auto-merge
242
330
        if text == basis_lines:
243
331
            return new_version            
244
332
 
252
340
        #print 'basis_lines:', basis_lines
253
341
        #print 'new_lines:  ', lines
254
342
 
255
 
        from difflib import SequenceMatcher
256
343
        s = SequenceMatcher(None, basis_lines, text)
257
344
 
258
345
        # offset gives the number of lines that have been inserted
287
374
                # we don't destroy ourselves
288
375
                i = i2 + offset
289
376
                self._weave[i:i] = ([('{', new_version)] 
290
 
                                + text[j1:j2] 
291
 
                                + [('}', new_version)])
 
377
                                    + text[j1:j2] 
 
378
                                    + [('}', None)])
292
379
                offset += 2 + (j2 - j1)
293
380
 
294
381
        return new_version
297
384
    def inclusions(self, versions):
298
385
        """Return set of all ancestors of given version(s)."""
299
386
        i = set(versions)
300
 
        v = max(versions)
301
 
        try:
302
 
            while v >= 0:
303
 
                if v in i:
304
 
                    # include all its parents
305
 
                    i.update(self._parents[v])
306
 
                v -= 1
307
 
            return i
308
 
        except IndexError:
309
 
            raise ValueError("version %d not present in weave" % v)
 
387
        for v in xrange(max(versions), 0, -1):
 
388
            if v in i:
 
389
                # include all its parents
 
390
                i.update(self._parents[v])
 
391
        return i
 
392
        ## except IndexError:
 
393
        ##     raise ValueError("version %d not present in weave" % v)
 
394
 
 
395
 
 
396
    def parents(self, version):
 
397
        return self._parents[version]
310
398
 
311
399
 
312
400
    def minimal_parents(self, version):
352
440
                raise IndexError("invalid version number %r" % i)
353
441
 
354
442
    
355
 
    def annotate(self, index):
356
 
        return list(self.annotate_iter(index))
357
 
 
358
 
 
359
 
    def annotate_iter(self, version):
 
443
    def annotate(self, name_or_index):
 
444
        return list(self.annotate_iter(name_or_index))
 
445
 
 
446
 
 
447
    def annotate_iter(self, name_or_index):
360
448
        """Yield list of (index-id, line) pairs for the specified version.
361
449
 
362
450
        The index indicates when the line originated in the weave."""
363
 
        for origin, lineno, text in self._extract([version]):
 
451
        incls = [self.maybe_lookup(name_or_index)]
 
452
        for origin, lineno, text in self._extract(incls):
364
453
            yield origin, text
365
454
 
366
455
 
384
473
                if c == '{':
385
474
                    istack.append(v)
386
475
                elif c == '}':
387
 
                    oldv = istack.pop()
 
476
                    istack.pop()
388
477
                elif c == '[':
389
478
                    assert v not in dset
390
479
                    dset.add(v)
410
499
 
411
500
        The set typically but not necessarily corresponds to a version.
412
501
        """
 
502
        for i in versions:
 
503
            if not isinstance(i, int):
 
504
                raise ValueError(i)
 
505
            
413
506
        included = self.inclusions(versions)
414
507
 
415
508
        istack = []
431
524
                    assert v not in istack
432
525
                    istack.append(v)
433
526
                elif c == '}':
434
 
                    oldv = istack.pop()
435
 
                    assert oldv == v
 
527
                    istack.pop()
436
528
                elif c == '[':
437
529
                    if v in included:
438
530
                        assert v not in dset
461
553
    
462
554
 
463
555
 
464
 
    def get_iter(self, version):
 
556
    def get_iter(self, name_or_index):
465
557
        """Yield lines for the specified version."""
466
 
        for origin, lineno, line in self._extract([version]):
 
558
        incls = [self.maybe_lookup(name_or_index)]
 
559
        for origin, lineno, line in self._extract(incls):
467
560
            yield line
468
561
 
469
562
 
470
 
    def get(self, index):
471
 
        return list(self.get_iter(index))
 
563
    def get_text(self, version):
 
564
        assert isinstance(version, int)
 
565
        s = StringIO()
 
566
        s.writelines(self.get_iter(version))
 
567
        return s.getvalue()
 
568
 
 
569
 
 
570
    def get(self, name_or_index):
 
571
        return list(self.get_iter(name_or_index))
472
572
 
473
573
 
474
574
    def mash_iter(self, included):
475
575
        """Return composed version of multiple included versions."""
 
576
        included = map(self.maybe_lookup, included)
476
577
        for origin, lineno, text in self._extract(included):
477
578
            yield text
478
579
 
508
609
 
509
610
        # try extracting all versions; this is a bit slow and parallel
510
611
        # extraction could be used
511
 
        import sha
512
612
        nv = self.numversions()
513
613
        for version in range(nv):
514
614
            if progress_bar:
670
770
 
671
771
 
672
772
 
673
 
def weave_info(w):
674
 
    """Show some text information about the weave."""
675
 
    print '%6s %40s %20s' % ('ver', 'sha1', 'parents')
676
 
    for i in (6, 40, 20):
 
773
def weave_toc(w):
 
774
    """Show the weave's table-of-contents"""
 
775
    print '%6s %50s %10s %10s' % ('ver', 'name', 'sha1', 'parents')
 
776
    for i in (6, 50, 10, 10):
677
777
        print '-' * i,
678
778
    print
679
779
    for i in range(w.numversions()):
680
780
        sha1 = w._sha1s[i]
681
 
        print '%6d %40s %s' % (i, sha1, ' '.join(map(str, w._parents[i])))
 
781
        name = w._names[i]
 
782
        parent_str = ' '.join(map(str, w._parents[i]))
 
783
        print '%6d %-50.50s %10.10s %s' % (i, name, sha1, parent_str)
682
784
 
683
785
 
684
786
 
706
808
    print 'weave file        %9d bytes' % weave_size
707
809
    print 'total contents    %9d bytes' % total
708
810
    print 'compression ratio %9.2fx' % (float(total) / float(weave_size))
709
 
 
 
811
    if vers:
 
812
        avg = total/vers
 
813
        print 'average size      %9d bytes' % avg
 
814
        print 'relative size     %9.2fx' % (float(weave_size) / float(avg))
710
815
 
711
816
 
712
817
def usage():
721
826
        Write out specified version.
722
827
    weave check WEAVEFILE
723
828
        Check consistency of all versions.
724
 
    weave info WEAVEFILE
 
829
    weave toc WEAVEFILE
725
830
        Display table of contents.
726
 
    weave add WEAVEFILE [BASE...] < NEWTEXT
 
831
    weave add WEAVEFILE NAME [BASE...] < NEWTEXT
727
832
        Add NEWTEXT, with specified parent versions.
728
833
    weave annotate WEAVEFILE VERSION
729
834
        Display origin of each line.
731
836
        Display composite of all selected versions.
732
837
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
733
838
        Auto-merge two versions and display conflicts.
 
839
    weave diff WEAVEFILE VERSION1 VERSION2 
 
840
        Show differences between two versions.
734
841
 
735
842
example:
736
843
 
737
844
    % weave init foo.weave
738
845
    % vi foo.txt
739
 
    % weave add foo.weave < foo.txt
 
846
    % weave add foo.weave ver0 < foo.txt
740
847
    added version 0
741
848
 
742
849
    (create updated version)
743
850
    % vi foo.txt
744
851
    % weave get foo.weave 0 | diff -u - foo.txt
745
 
    % weave add foo.weave 0 < foo.txt
 
852
    % weave add foo.weave ver1 0 < foo.txt
746
853
    added version 1
747
854
 
748
855
    % weave get foo.weave 0 > foo.txt       (create forked version)
749
856
    % vi foo.txt
750
 
    % weave add foo.weave 0 < foo.txt
 
857
    % weave add foo.weave ver2 0 < foo.txt
751
858
    added version 2
752
859
 
753
860
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
754
861
    % vi foo.txt                            (resolve conflicts)
755
 
    % weave add foo.weave 1 2 < foo.txt     (commit merged version)     
 
862
    % weave add foo.weave merged 1 2 < foo.txt     (commit merged version)     
756
863
    
757
864
"""
758
865
    
761
868
def main(argv):
762
869
    import sys
763
870
    import os
764
 
    from weavefile import write_weave, read_weave
 
871
    from bzrlib.weavefile import write_weave, read_weave
765
872
    from bzrlib.progress import ProgressBar
766
873
 
767
 
    #import psyco
768
 
    #psyco.full()
 
874
    try:
 
875
        import psyco
 
876
        psyco.full()
 
877
    except ImportError:
 
878
        pass
 
879
 
 
880
    if len(argv) < 2:
 
881
        usage()
 
882
        return 0
769
883
 
770
884
    cmd = argv[1]
771
885
 
777
891
    elif cmd == 'add':
778
892
        w = readit()
779
893
        # at the moment, based on everything in the file
780
 
        parents = map(int, argv[3:])
 
894
        name = argv[3]
 
895
        parents = map(int, argv[4:])
781
896
        lines = sys.stdin.readlines()
782
 
        ver = w.add(parents, lines)
 
897
        ver = w.add(name, parents, lines)
783
898
        write_weave(w, file(argv[2], 'wb'))
784
 
        print 'added version %d' % ver
 
899
        print 'added version %r %d' % (name, ver)
785
900
    elif cmd == 'init':
786
901
        fn = argv[2]
787
902
        if os.path.exists(fn):
796
911
        w = readit()
797
912
        sys.stdout.writelines(w.mash_iter(map(int, argv[3:])))
798
913
 
 
914
    elif cmd == 'diff':
 
915
        from difflib import unified_diff
 
916
        w = readit()
 
917
        fn = argv[2]
 
918
        v1, v2 = map(int, argv[3:5])
 
919
        lines1 = w.get(v1)
 
920
        lines2 = w.get(v2)
 
921
        diff_gen = unified_diff(lines1, lines2,
 
922
                                '%s version %d' % (fn, v1),
 
923
                                '%s version %d' % (fn, v2))
 
924
        sys.stdout.writelines(diff_gen)
 
925
            
799
926
    elif cmd == 'annotate':
800
927
        w = readit()
801
928
        # newline is added to all lines regardless; too hard to get
809
936
                print '%5d | %s' % (origin, text)
810
937
                lasto = origin
811
938
                
812
 
    elif cmd == 'info':
813
 
        weave_info(readit())
 
939
    elif cmd == 'toc':
 
940
        weave_toc(readit())
814
941
 
815
942
    elif cmd == 'stats':
816
943
        weave_stats(argv[2])
865
992
        raise ValueError('unknown command %r' % cmd)
866
993
    
867
994
 
 
995
 
 
996
def profile_main(argv): 
 
997
    import tempfile, hotshot, hotshot.stats
 
998
 
 
999
    prof_f = tempfile.NamedTemporaryFile()
 
1000
 
 
1001
    prof = hotshot.Profile(prof_f.name)
 
1002
 
 
1003
    ret = prof.runcall(main, argv)
 
1004
    prof.close()
 
1005
 
 
1006
    stats = hotshot.stats.load(prof_f.name)
 
1007
    #stats.strip_dirs()
 
1008
    stats.sort_stats('cumulative')
 
1009
    ## XXX: Might like to write to stderr or the trace file instead but
 
1010
    ## print_stats seems hardcoded to stdout
 
1011
    stats.print_stats(20)
 
1012
            
 
1013
    return ret
 
1014
 
 
1015
 
868
1016
if __name__ == '__main__':
869
1017
    import sys
870
 
    sys.exit(main(sys.argv))
 
1018
    if '--profile' in sys.argv:
 
1019
        args = sys.argv[:]
 
1020
        args.remove('--profile')
 
1021
        sys.exit(profile_main(args))
 
1022
    else:
 
1023
        sys.exit(main(sys.argv))
 
1024