/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

Partially fix pull.

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
 
            i.update(self._v[v])
246
 
        return i
247
 
 
248
 
 
249
 
    def _addversion(self, parents):
250
 
        if parents:
251
 
            self._v.append(frozenset(parents))
252
 
        else:
253
 
            self._v.append(frozenset())
254
 
 
255
 
 
256
 
    def _check_lines(self, text):
257
 
        if not isinstance(text, list):
258
 
            raise ValueError("text should be a list, not %s" % type(text))
259
 
 
260
 
        for l in text:
261
 
            if not isinstance(l, basestring):
262
 
                raise ValueError("text line should be a string or unicode, not %s" % type(l))
263
 
        
264
 
 
265
 
 
266
 
    def _check_versions(self, indexes):
267
 
        """Check everything in the sequence of indexes is valid"""
268
 
        for i in indexes:
269
 
            try:
270
 
                self._v[i]
271
 
            except IndexError:
272
 
                raise IndexError("invalid version number %r" % i)
273
 
 
274
 
    
275
 
    def annotate(self, index):
276
 
        return list(self.annotate_iter(index))
277
 
 
278
 
 
279
 
    def annotate_iter(self, version):
280
 
        """Yield list of (index-id, line) pairs for the specified version.
281
 
 
282
 
        The index indicates when the line originated in the weave."""
283
 
        included = self.inclusions([version])
284
 
        for origin, lineno, text in self._extract(included):
285
 
            yield origin, text
286
 
 
287
 
 
288
 
    def _extract(self, included):
289
 
        """Yield annotation of lines in included set.
290
 
 
291
 
        Yields a sequence of tuples (origin, lineno, text), where
292
 
        origin is the origin version, lineno the index in the weave,
293
 
        and text the text of the line.
294
 
 
295
 
        The set typically but not necessarily corresponds to a version.
296
 
        """
297
 
        istack = []          # versions for which an insertion block is current
298
 
 
299
 
        dset = set()         # versions for which a deletion block is current
300
 
 
301
 
        isactive = None
302
 
 
303
 
        lineno = 0         # line of weave, 0-based
304
 
 
305
 
        # TODO: Probably only need to put included revisions in the istack
306
 
 
307
 
        # TODO: Could split this into two functions, one that updates
308
 
        # the stack and the other that processes the results -- but
309
 
        # I'm not sure it's really needed.
310
 
 
311
 
        # TODO: In fact, I think we only need to store the *count* of
312
 
        # active insertions and deletions, and we can maintain that by
313
 
        # just by just counting as we go along.
314
 
 
315
 
        WFE = WeaveFormatError
316
 
 
317
 
        for l in self._l:
318
 
            if isinstance(l, tuple):
319
 
                isactive = None         # recalculate
320
 
                c, v = l
321
 
                if c == '{':
322
 
                    if istack and (istack[-1] >= v):
323
 
                        raise WFE("improperly nested insertions %d>=%d on line %d" 
324
 
                                  % (istack[-1], v, lineno))
325
 
                    istack.append(v)
326
 
                elif c == '}':
327
 
                    try:
328
 
                        oldv = istack.pop()
329
 
                    except IndexError:
330
 
                        raise WFE("unmatched close of insertion %d on line %d"
331
 
                                  % (v, lineno))
332
 
                    if oldv != v:
333
 
                        raise WFE("mismatched close of insertion %d!=%d on line %d"
334
 
                                  % (oldv, v, lineno))
335
 
                elif c == '[':
336
 
                    # block deleted in v
337
 
                    if v in dset:
338
 
                        raise WFE("repeated deletion marker for version %d on line %d"
339
 
                                  % (v, lineno))
340
 
                    if istack:
341
 
                        if istack[-1] == v:
342
 
                            raise WFE("version %d deletes own text on line %d"
343
 
                                      % (v, lineno))
344
 
                        # XXX
345
 
                        dset.add(v)
346
 
                elif c == ']':
347
 
                    if v in dset:
348
 
                        dset.remove(v)
349
 
                    else:
350
 
                        raise WFE("unmatched close of deletion %d on line %d"
351
 
                                  % (v, lineno))
352
 
                else:
353
 
                    raise WFE("invalid processing instruction %r on line %d"
354
 
                              % (l, lineno))
355
 
            else:
356
 
                assert isinstance(l, basestring)
357
 
                if not istack:
358
 
                    raise WFE("literal at top level on line %d"
359
 
                              % lineno)
360
 
                if isactive == None:
361
 
                    isactive = (istack[-1] in included) \
362
 
                               and not included.intersection(dset)
363
 
                if isactive:
364
 
                    origin = istack[-1]
365
 
                    yield origin, lineno, l
366
 
            lineno += 1
367
 
 
368
 
        if istack:
369
 
            raise WFE("unclosed insertion blocks at end of weave",
370
 
                                   istack)
371
 
        if dset:
372
 
            raise WFE("unclosed deletion blocks at end of weave",
373
 
                                   dset)
374
 
 
375
 
 
376
 
    def get_iter(self, version):
377
 
        """Yield lines for the specified version."""
378
 
        for origin, lineno, line in self._extract(self.inclusions([version])):
379
 
            yield line
380
 
 
381
 
 
382
 
    def get(self, index):
383
 
        return list(self.get_iter(index))
384
 
 
385
 
 
386
 
    def mash_iter(self, included):
387
 
        """Return composed version of multiple included versions."""
388
 
        included = frozenset(included)
389
 
        for origin, lineno, text in self._extract(included):
390
 
            yield text
391
 
 
392
 
 
393
 
    def dump(self, to_file):
394
 
        from pprint import pprint
395
 
        print >>to_file, "Weave._l = ",
396
 
        pprint(self._l, to_file)
397
 
        print >>to_file, "Weave._v = ",
398
 
        pprint(self._v, to_file)
399
 
 
400
 
 
401
 
 
402
 
    def numversions(self):
403
 
        l = len(self._v)
404
 
        assert l == len(self._sha1s)
405
 
        return l
406
 
 
407
 
 
408
 
    def check(self):
409
 
        # check no circular inclusions
410
 
        for version in range(self.numversions()):
411
 
            inclusions = list(self._v[version])
412
 
            if inclusions:
413
 
                inclusions.sort()
414
 
                if inclusions[-1] >= version:
415
 
                    raise WeaveFormatError("invalid included version %d for index %d"
416
 
                                           % (inclusions[-1], version))
417
 
 
418
 
        # try extracting all versions; this is a bit slow and parallel
419
 
        # extraction could be used
420
 
        import sha
421
 
        for version in range(self.numversions()):
422
 
            s = sha.new()
423
 
            for l in self.get_iter(version):
424
 
                s.update(l)
425
 
            hd = s.hexdigest()
426
 
            expected = self._sha1s[version]
427
 
            if hd != expected:
428
 
                raise WeaveError("mismatched sha1 for version %d; "
429
 
                                 "got %s, expected %s"
430
 
                                 % (version, hd, expected))
431
 
 
432
 
 
433
 
 
434
 
    def merge(self, merge_versions):
435
 
        """Automerge and mark conflicts between versions.
436
 
 
437
 
        This returns a sequence, each entry describing alternatives
438
 
        for a chunk of the file.  Each of the alternatives is given as
439
 
        a list of lines.
440
 
 
441
 
        If there is a chunk of the file where there's no diagreement,
442
 
        only one alternative is given.
443
 
        """
444
 
 
445
 
        # approach: find the included versions common to all the
446
 
        # merged versions
447
 
        raise NotImplementedError()
448
 
 
449
 
 
450
 
 
451
 
    def _delta(self, included, lines):
452
 
        """Return changes from basis to new revision.
453
 
 
454
 
        The old text for comparison is the union of included revisions.
455
 
 
456
 
        This is used in inserting a new text.
457
 
 
458
 
        Delta is returned as a sequence of
459
 
        (weave1, weave2, newlines).
460
 
 
461
 
        This indicates that weave1:weave2 of the old weave should be
462
 
        replaced by the sequence of lines in newlines.  Note that
463
 
        these line numbers are positions in the total weave and don't
464
 
        correspond to the lines in any extracted version, or even the
465
 
        extracted union of included versions.
466
 
 
467
 
        If line1=line2, this is a pure insert; if newlines=[] this is a
468
 
        pure delete.  (Similar to difflib.)
469
 
        """
470
 
        # basis a list of (origin, lineno, line)
471
 
        basis_lineno = []
472
 
        basis_lines = []
473
 
        for origin, lineno, line in self._extract(included):
474
 
            basis_lineno.append(lineno)
475
 
            basis_lines.append(line)
476
 
 
477
 
        # add a sentinal, because we can also match against the final line
478
 
        basis_lineno.append(len(self._l))
479
 
 
480
 
        # XXX: which line of the weave should we really consider
481
 
        # matches the end of the file?  the current code says it's the
482
 
        # last line of the weave?
483
 
 
484
 
        from difflib import SequenceMatcher
485
 
        s = SequenceMatcher(None, basis_lines, lines)
486
 
 
487
 
        # TODO: Perhaps return line numbers from composed weave as well?
488
 
 
489
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
490
 
            ##print tag, i1, i2, j1, j2
491
 
 
492
 
            if tag == 'equal':
493
 
                continue
494
 
 
495
 
            # i1,i2 are given in offsets within basis_lines; we need to map them
496
 
            # back to offsets within the entire weave
497
 
            real_i1 = basis_lineno[i1]
498
 
            real_i2 = basis_lineno[i2]
499
 
 
500
 
            assert 0 <= j1
501
 
            assert j1 <= j2
502
 
            assert j2 <= len(lines)
503
 
 
504
 
            yield real_i1, real_i2, lines[j1:j2]
505
 
 
506
 
 
507
 
 
508
 
def weave_info(filename, out):
509
 
    """Show some text information about the weave."""
510
 
    from weavefile import read_weave
511
 
    wf = file(filename, 'rb')
512
 
    w = read_weave(wf)
513
 
    # FIXME: doesn't work on pipes
514
 
    weave_size = wf.tell()
515
 
    print >>out, "weave file size %d bytes" % weave_size
516
 
    print >>out, "weave contains %d versions" % len(w._v)
517
 
 
518
 
    total = 0
519
 
    print ' %8s %8s %8s %s' % ('version', 'lines', 'bytes', 'sha1')
520
 
    print ' -------- -------- -------- ----------------------------------------'
521
 
    for i in range(len(w._v)):
522
 
        text = w.get(i)
523
 
        lines = len(text)
524
 
        bytes = sum((len(a) for a in text))
525
 
        sha1 = w._sha1s[i]
526
 
        print ' %8d %8d %8d %s' % (i, lines, bytes, sha1)
527
 
        total += bytes
528
 
 
529
 
    print >>out, "versions total %d bytes" % total
530
 
    print >>out, "compression ratio %.3f" % (float(total)/float(weave_size))
531
 
    
532
 
 
533
 
 
534
 
def main(argv):
535
 
    import sys
536
 
    import os
537
 
    from weavefile import write_weave_v1, read_weave
538
 
    cmd = argv[1]
539
 
    if cmd == 'add':
540
 
        w = read_weave(file(argv[2], 'rb'))
541
 
        # at the moment, based on everything in the file
542
 
        parents = set(range(len(w._v)))
543
 
        lines = sys.stdin.readlines()
544
 
        ver = w.add(parents, lines)
545
 
        write_weave_v1(w, file(argv[2], 'wb'))
546
 
        print 'added %d' % ver
547
 
    elif cmd == 'init':
548
 
        fn = argv[2]
549
 
        if os.path.exists(fn):
550
 
            raise IOError("file exists")
551
 
        w = Weave()
552
 
        write_weave_v1(w, file(fn, 'wb'))
553
 
    elif cmd == 'get':
554
 
        w = read_weave(file(argv[2], 'rb'))
555
 
        sys.stdout.writelines(w.get_iter(int(argv[3])))
556
 
    elif cmd == 'annotate':
557
 
        w = read_weave(file(argv[2], 'rb'))
558
 
        # newline is added to all lines regardless; too hard to get
559
 
        # reasonable formatting otherwise
560
 
        lasto = None
561
 
        for origin, text in w.annotate(int(argv[3])):
562
 
            text = text.rstrip('\r\n')
563
 
            if origin == lasto:
564
 
                print '      | %s' % (text)
565
 
            else:
566
 
                print '%5d | %s' % (origin, text)
567
 
                lasto = origin
568
 
    elif cmd == 'info':
569
 
        weave_info(argv[2], sys.stdout)
570
 
    elif cmd == 'check':
571
 
        w = read_weave(file(argv[2], 'rb'))
572
 
        w.check()
573
 
    else:
574
 
        raise ValueError('unknown command %r' % cmd)
575
 
    
576
 
 
577
 
if __name__ == '__main__':
578
 
    import sys
579
 
    sys.exit(main(sys.argv))