/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/weave.py

More work on roundtrip push support.

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
 
# TODO: Perhaps have copy method for Weave instances?
25
 
 
26
 
# XXX: If we do weaves this way, will a merge still behave the same
27
 
# way if it's done in a different order?  That's a pretty desirable
28
 
# property.
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
 
 
33
 
# TODO: Nothing here so far assumes the lines are really \n newlines,
34
 
# rather than being split up in some other way.  We could accomodate
35
 
# binaries, perhaps by naively splitting on \n or perhaps using
36
 
# something like a rolling checksum.
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
 
 
48
 
# TODO: End marker for each version so we can stop reading?
49
 
 
50
 
# TODO: Check that no insertion occurs inside a deletion that was
51
 
# active in the version of the insertion.
52
 
 
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
66
 
 
67
 
 
68
 
class WeaveError(Exception):
69
 
    """Exception in processing weave"""
70
 
 
71
 
 
72
 
class WeaveFormatError(WeaveError):
73
 
    """Weave invariant violated"""
74
 
    
75
 
 
76
 
class Weave(object):
77
 
    """weave - versioned text file storage.
78
 
    
79
 
    A Weave manages versions of line-based text files, keeping track
80
 
    of the originating version for each line.
81
 
 
82
 
    To clients the "lines" of the file are represented as a list of strings.
83
 
    These strings  will typically have terminal newline characters, but
84
 
    this is not required.  In particular files commonly do not have a newline
85
 
    at the end of the file.
86
 
 
87
 
    Texts can be identified in either of two ways:
88
 
 
89
 
    * a nonnegative index number.
90
 
 
91
 
    * a version-id string.
92
 
 
93
 
    Typically the index number will be valid only inside this weave and
94
 
    the version-id is used to reference it in the larger world.
95
 
 
96
 
    The weave is represented as a list mixing edit instructions and
97
 
    literal text.  Each entry in _l can be either a string (or
98
 
    unicode), or a tuple.  If a string, it means that the given line
99
 
    should be output in the currently active revisions.
100
 
 
101
 
    If a tuple, it gives a processing instruction saying in which
102
 
    revisions the enclosed lines are active.  The tuple has the form
103
 
    (instruction, version).
104
 
 
105
 
    The instruction can be '{' or '}' for an insertion block, and '['
106
 
    and ']' for a deletion block respectively.  The version is the
107
 
    integer version index.  There is no replace operator, only deletes
108
 
    and inserts.
109
 
 
110
 
    Constraints/notes:
111
 
 
112
 
    * A later version can delete lines that were introduced by any
113
 
      number of ancestor versions; this implies that deletion
114
 
      instructions can span insertion blocks without regard to the
115
 
      insertion block's nesting.
116
 
 
117
 
    * Similarly, deletions need not be properly nested with regard to
118
 
      each other, because they might have been generated by
119
 
      independent revisions.
120
 
 
121
 
    * Insertions are always made by inserting a new bracketed block
122
 
      into a single point in the previous weave.  This implies they
123
 
      can nest but not overlap, and the nesting must always have later
124
 
      insertions on the inside.
125
 
 
126
 
    * It doesn't seem very useful to have an active insertion
127
 
      inside an inactive insertion, but it might happen.
128
 
      
129
 
    * Therefore, all instructions are always"considered"; that
130
 
      is passed onto and off the stack.  An outer inactive block
131
 
      doesn't disable an inner block.
132
 
 
133
 
    * Lines are enabled if the most recent enclosing insertion is
134
 
      active and none of the enclosing deletions are active.
135
 
 
136
 
    * There is no point having a deletion directly inside its own
137
 
      insertion; you might as well just not write it.  And there
138
 
      should be no way to get an earlier version deleting a later
139
 
      version.
140
 
 
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.
152
 
 
153
 
    _sha1s
154
 
        List of hex SHA-1 of each version, or None if not recorded.
155
 
    """
156
 
    def __init__(self):
157
 
        self._l = []
158
 
        self._v = []
159
 
        self._sha1s = []
160
 
 
161
 
 
162
 
    def __eq__(self, other):
163
 
        if not isinstance(other, Weave):
164
 
            return False
165
 
        return self._v == other._v \
166
 
               and self._l == other._l
167
 
    
168
 
 
169
 
    def __ne__(self, other):
170
 
        return not self.__eq__(other)
171
 
 
172
 
        
173
 
    def add(self, parents, text):
174
 
        """Add a single text on top of the weave.
175
 
  
176
 
        Returns the index number of the newly added version.
177
 
 
178
 
        parents
179
 
            List or set of parent version numbers.  This must normally include
180
 
            the parents and the parent's parents, or wierd things might happen.
181
 
 
182
 
        text
183
 
            Sequence of lines to be added in the new version."""
184
 
        ## self._check_versions(parents)
185
 
        ## self._check_lines(text)
186
 
        idx = len(self._v)
187
 
 
188
 
        import sha
189
 
        s = sha.new()
190
 
        for l in text:
191
 
            s.update(l)
192
 
        sha1 = s.hexdigest()
193
 
        del s
194
 
 
195
 
        if parents:
196
 
            delta = self._delta(self.inclusions(parents), text)
197
 
 
198
 
            # offset gives the number of lines that have been inserted
199
 
            # into the weave up to the current point; if the original edit instruction
200
 
            # says to change line A then we actually change (A+offset)
201
 
            offset = 0
202
 
 
203
 
            for i1, i2, newlines in delta:
204
 
                assert 0 <= i1
205
 
                assert i1 <= i2
206
 
                assert i2 <= len(self._l)
207
 
 
208
 
                # the deletion and insertion are handled separately.
209
 
                # first delete the region.
210
 
                if i1 != i2:
211
 
                    self._l.insert(i1+offset, ('[', idx))
212
 
                    self._l.insert(i2+offset+1, (']', idx))
213
 
                    offset += 2
214
 
                    # is this OK???
215
 
 
216
 
                if newlines:
217
 
                    # there may have been a deletion spanning up to
218
 
                    # i2; we want to insert after this region to make sure
219
 
                    # we don't destroy ourselves
220
 
                    i = i2 + offset
221
 
                    self._l[i:i] = [('{', idx)] \
222
 
                                   + newlines \
223
 
                                   + [('}', idx)]
224
 
                    offset += 2 + len(newlines)
225
 
 
226
 
            self._addversion(parents)
227
 
        else:
228
 
            # special case; adding with no parents revision; can do this
229
 
            # more quickly by just appending unconditionally
230
 
            self._l.append(('{', idx))
231
 
            self._l += text
232
 
            self._l.append(('}', idx))
233
 
 
234
 
            self._addversion(None)
235
 
 
236
 
        self._sha1s.append(sha1)
237
 
            
238
 
        return idx
239
 
 
240
 
 
241
 
    def inclusions(self, versions):
242
 
        """Expand out everything included by versions."""
243
 
        i = set(versions)
244
 
        for v in versions:
245
 
            try:
246
 
                i.update(self._v[v])
247
 
            except IndexError:
248
 
                raise ValueError("version %d not present in weave" % v)
249
 
        return i
250
 
 
251
 
 
252
 
    def _addversion(self, parents):
253
 
        if parents:
254
 
            self._v.append(frozenset(parents))
255
 
        else:
256
 
            self._v.append(frozenset())
257
 
 
258
 
 
259
 
    def _check_lines(self, text):
260
 
        if not isinstance(text, list):
261
 
            raise ValueError("text should be a list, not %s" % type(text))
262
 
 
263
 
        for l in text:
264
 
            if not isinstance(l, basestring):
265
 
                raise ValueError("text line should be a string or unicode, not %s"
266
 
                                 % type(l))
267
 
        
268
 
 
269
 
 
270
 
    def _check_versions(self, indexes):
271
 
        """Check everything in the sequence of indexes is valid"""
272
 
        for i in indexes:
273
 
            try:
274
 
                self._v[i]
275
 
            except IndexError:
276
 
                raise IndexError("invalid version number %r" % i)
277
 
 
278
 
    
279
 
    def annotate(self, index):
280
 
        return list(self.annotate_iter(index))
281
 
 
282
 
 
283
 
    def annotate_iter(self, version):
284
 
        """Yield list of (index-id, line) pairs for the specified version.
285
 
 
286
 
        The index indicates when the line originated in the weave."""
287
 
        included = self.inclusions([version])
288
 
        for origin, lineno, text in self._extract(included):
289
 
            yield origin, text
290
 
 
291
 
 
292
 
    def _extract(self, included):
293
 
        """Yield annotation of lines in included set.
294
 
 
295
 
        Yields a sequence of tuples (origin, lineno, text), where
296
 
        origin is the origin version, lineno the index in the weave,
297
 
        and text the text of the line.
298
 
 
299
 
        The set typically but not necessarily corresponds to a version.
300
 
        """
301
 
 
302
 
        istack = []
303
 
        dset = set()
304
 
 
305
 
        lineno = 0         # line of weave, 0-based
306
 
        isactive = False
307
 
 
308
 
        WFE = WeaveFormatError
309
 
 
310
 
        for l in self._l:
311
 
            if isinstance(l, tuple):
312
 
                c, v = l
313
 
                if v in included:       # only active blocks are interesting
314
 
                    if c == '{':
315
 
                        assert v not in istack
316
 
                        istack.append(v)
317
 
                        isactive = not dset
318
 
                    elif c == '}':
319
 
                        oldv = istack.pop()
320
 
                        assert oldv == v
321
 
                        isactive = istack and not dset
322
 
                    elif c == '[':
323
 
                        assert v not in dset
324
 
                        dset.add(v)
325
 
                        isactive = False
326
 
                    else:
327
 
                        assert c == ']'
328
 
                        assert v in dset
329
 
                        dset.remove(v)
330
 
                        isactive = istack and not dset
331
 
            else:
332
 
                assert isinstance(l, basestring)
333
 
                if isactive:
334
 
                    yield origin, lineno, l
335
 
            lineno += 1
336
 
 
337
 
        if istack:
338
 
            raise WFE("unclosed insertion blocks at end of weave",
339
 
                                   istack)
340
 
        if dset:
341
 
            raise WFE("unclosed deletion blocks at end of weave",
342
 
                                   dset)
343
 
 
344
 
 
345
 
    def get_iter(self, version):
346
 
        """Yield lines for the specified version."""
347
 
        for origin, lineno, line in self._extract(self.inclusions([version])):
348
 
            yield line
349
 
 
350
 
 
351
 
    def get(self, index):
352
 
        return list(self.get_iter(index))
353
 
 
354
 
 
355
 
    def mash_iter(self, included):
356
 
        """Return composed version of multiple included versions."""
357
 
        included = frozenset(included)
358
 
        for origin, lineno, text in self._extract(included):
359
 
            yield text
360
 
 
361
 
 
362
 
    def dump(self, to_file):
363
 
        from pprint import pprint
364
 
        print >>to_file, "Weave._l = ",
365
 
        pprint(self._l, to_file)
366
 
        print >>to_file, "Weave._v = ",
367
 
        pprint(self._v, to_file)
368
 
 
369
 
 
370
 
 
371
 
    def numversions(self):
372
 
        l = len(self._v)
373
 
        assert l == len(self._sha1s)
374
 
        return l
375
 
 
376
 
 
377
 
    def check(self):
378
 
        # check no circular inclusions
379
 
        for version in range(self.numversions()):
380
 
            inclusions = list(self._v[version])
381
 
            if inclusions:
382
 
                inclusions.sort()
383
 
                if inclusions[-1] >= version:
384
 
                    raise WeaveFormatError("invalid included version %d for index %d"
385
 
                                           % (inclusions[-1], version))
386
 
 
387
 
        # try extracting all versions; this is a bit slow and parallel
388
 
        # extraction could be used
389
 
        import sha
390
 
        for version in range(self.numversions()):
391
 
            s = sha.new()
392
 
            for l in self.get_iter(version):
393
 
                s.update(l)
394
 
            hd = s.hexdigest()
395
 
            expected = self._sha1s[version]
396
 
            if hd != expected:
397
 
                raise WeaveError("mismatched sha1 for version %d; "
398
 
                                 "got %s, expected %s"
399
 
                                 % (version, hd, expected))
400
 
 
401
 
        # TODO: check insertions are properly nested, that there are
402
 
        # no lines outside of insertion blocks, that deletions are
403
 
        # properly paired, etc.
404
 
 
405
 
 
406
 
 
407
 
    def merge(self, merge_versions):
408
 
        """Automerge and mark conflicts between versions.
409
 
 
410
 
        This returns a sequence, each entry describing alternatives
411
 
        for a chunk of the file.  Each of the alternatives is given as
412
 
        a list of lines.
413
 
 
414
 
        If there is a chunk of the file where there's no diagreement,
415
 
        only one alternative is given.
416
 
        """
417
 
 
418
 
        # approach: find the included versions common to all the
419
 
        # merged versions
420
 
        raise NotImplementedError()
421
 
 
422
 
 
423
 
 
424
 
    def _delta(self, included, lines):
425
 
        """Return changes from basis to new revision.
426
 
 
427
 
        The old text for comparison is the union of included revisions.
428
 
 
429
 
        This is used in inserting a new text.
430
 
 
431
 
        Delta is returned as a sequence of
432
 
        (weave1, weave2, newlines).
433
 
 
434
 
        This indicates that weave1:weave2 of the old weave should be
435
 
        replaced by the sequence of lines in newlines.  Note that
436
 
        these line numbers are positions in the total weave and don't
437
 
        correspond to the lines in any extracted version, or even the
438
 
        extracted union of included versions.
439
 
 
440
 
        If line1=line2, this is a pure insert; if newlines=[] this is a
441
 
        pure delete.  (Similar to difflib.)
442
 
        """
443
 
        # basis a list of (origin, lineno, line)
444
 
        basis_lineno = []
445
 
        basis_lines = []
446
 
        for origin, lineno, line in self._extract(included):
447
 
            basis_lineno.append(lineno)
448
 
            basis_lines.append(line)
449
 
 
450
 
        # add a sentinal, because we can also match against the final line
451
 
        basis_lineno.append(len(self._l))
452
 
 
453
 
        # XXX: which line of the weave should we really consider
454
 
        # matches the end of the file?  the current code says it's the
455
 
        # last line of the weave?
456
 
 
457
 
        from difflib import SequenceMatcher
458
 
        s = SequenceMatcher(None, basis_lines, lines)
459
 
 
460
 
        # TODO: Perhaps return line numbers from composed weave as well?
461
 
 
462
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
463
 
            ##print tag, i1, i2, j1, j2
464
 
 
465
 
            if tag == 'equal':
466
 
                continue
467
 
 
468
 
            # i1,i2 are given in offsets within basis_lines; we need to map them
469
 
            # back to offsets within the entire weave
470
 
            real_i1 = basis_lineno[i1]
471
 
            real_i2 = basis_lineno[i2]
472
 
 
473
 
            assert 0 <= j1
474
 
            assert j1 <= j2
475
 
            assert j2 <= len(lines)
476
 
 
477
 
            yield real_i1, real_i2, lines[j1:j2]
478
 
 
479
 
 
480
 
 
481
 
def weave_info(filename, out):
482
 
    """Show some text information about the weave."""
483
 
    from weavefile import read_weave
484
 
    wf = file(filename, 'rb')
485
 
    w = read_weave(wf)
486
 
    # FIXME: doesn't work on pipes
487
 
    weave_size = wf.tell()
488
 
    print >>out, "weave file size %d bytes" % weave_size
489
 
    print >>out, "weave contains %d versions" % len(w._v)
490
 
 
491
 
    total = 0
492
 
    print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents')
493
 
    for i in (6, 6, 8, 40, 20):
494
 
        print '-' * i,
495
 
    print
496
 
    for i in range(len(w._v)):
497
 
        text = w.get(i)
498
 
        lines = len(text)
499
 
        bytes = sum((len(a) for a in text))
500
 
        sha1 = w._sha1s[i]
501
 
        print '%6d %6d %8d %40s' % (i, lines, bytes, sha1),
502
 
        print ', '.join(map(str, w._v[i]))
503
 
        total += bytes
504
 
 
505
 
    print >>out, "versions total %d bytes" % total
506
 
    print >>out, "compression ratio %.3f" % (float(total)/float(weave_size))
507
 
 
508
 
 
509
 
def usage():
510
 
    print """bzr weave tool
511
 
 
512
 
Experimental tool for weave algorithm.
513
 
 
514
 
usage:
515
 
    weave init WEAVEFILE
516
 
        Create an empty weave file
517
 
    weave get WEAVEFILE VERSION
518
 
        Write out specified version.
519
 
    weave check WEAVEFILE
520
 
        Check consistency of all versions.
521
 
    weave info WEAVEFILE
522
 
        Display table of contents.
523
 
    weave add WEAVEFILE [BASE...] < NEWTEXT
524
 
        Add NEWTEXT, with specified parent versions.
525
 
    weave annotate WEAVEFILE VERSION
526
 
        Display origin of each line.
527
 
    weave mash WEAVEFILE VERSION...
528
 
        Display composite of all selected versions.
529
 
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
530
 
        Auto-merge two versions and display conflicts.
531
 
 
532
 
example:
533
 
 
534
 
    % weave init foo.weave
535
 
    % vi foo.txt
536
 
    % weave add foo.weave < foo.txt
537
 
    added version 0
538
 
 
539
 
    (create updated version)
540
 
    % vi foo.txt
541
 
    % weave get foo.weave 0 | diff -u - foo.txt
542
 
    % weave add foo.weave 0 < foo.txt
543
 
    added version 1
544
 
 
545
 
    % weave get foo.weave 0 > foo.txt       (create forked version)
546
 
    % vi foo.txt
547
 
    % weave add foo.weave 0 < foo.txt
548
 
    added version 2
549
 
 
550
 
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
551
 
    % vi foo.txt                            (resolve conflicts)
552
 
    % weave add foo.weave 1 2 < foo.txt     (commit merged version)     
553
 
    
554
 
"""
555
 
    
556
 
 
557
 
 
558
 
def main(argv):
559
 
    import sys
560
 
    import os
561
 
    from weavefile import write_weave, read_weave
562
 
    cmd = argv[1]
563
 
 
564
 
    def readit():
565
 
        return read_weave(file(argv[2], 'rb'))
566
 
    
567
 
    if cmd == 'help':
568
 
        usage()
569
 
    elif cmd == 'add':
570
 
        w = readit()
571
 
        # at the moment, based on everything in the file
572
 
        parents = map(int, argv[3:])
573
 
        lines = sys.stdin.readlines()
574
 
        ver = w.add(parents, lines)
575
 
        write_weave(w, file(argv[2], 'wb'))
576
 
        print 'added version %d' % ver
577
 
    elif cmd == 'init':
578
 
        fn = argv[2]
579
 
        if os.path.exists(fn):
580
 
            raise IOError("file exists")
581
 
        w = Weave()
582
 
        write_weave(w, file(fn, 'wb'))
583
 
    elif cmd == 'get': # get one version
584
 
        w = readit()
585
 
        sys.stdout.writelines(w.get_iter(int(argv[3])))
586
 
        
587
 
    elif cmd == 'mash': # get composite
588
 
        w = readit()
589
 
        sys.stdout.writelines(w.mash_iter(map(int, argv[3:])))
590
 
 
591
 
    elif cmd == 'annotate':
592
 
        w = readit()
593
 
        # newline is added to all lines regardless; too hard to get
594
 
        # reasonable formatting otherwise
595
 
        lasto = None
596
 
        for origin, text in w.annotate(int(argv[3])):
597
 
            text = text.rstrip('\r\n')
598
 
            if origin == lasto:
599
 
                print '      | %s' % (text)
600
 
            else:
601
 
                print '%5d | %s' % (origin, text)
602
 
                lasto = origin
603
 
                
604
 
    elif cmd == 'info':
605
 
        weave_info(argv[2], sys.stdout)
606
 
        
607
 
    elif cmd == 'check':
608
 
        w = readit()
609
 
        w.check()
610
 
 
611
 
    elif cmd == 'merge':
612
 
        if len(argv) != 5:
613
 
            usage()
614
 
            return 1
615
 
 
616
 
        w = readit()
617
 
        v1, v2 = map(int, argv[3:5])
618
 
 
619
 
        basis = w.inclusions([v1]).intersection(w.inclusions([v2]))
620
 
 
621
 
        base_lines = list(w.mash_iter(basis))
622
 
        a_lines = list(w.get(v1))
623
 
        b_lines = list(w.get(v2))
624
 
 
625
 
        from bzrlib.merge3 import Merge3
626
 
        m3 = Merge3(base_lines, a_lines, b_lines)
627
 
 
628
 
        name_a = 'version %d' % v1
629
 
        name_b = 'version %d' % v2
630
 
        sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b))
631
 
    else:
632
 
        raise ValueError('unknown command %r' % cmd)
633
 
    
634
 
 
635
 
if __name__ == '__main__':
636
 
    import sys
637
 
    sys.exit(main(sys.argv))