~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to weave.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-09 04:51:05 UTC
  • Revision ID: mbp@sourcefrog.net-20050309045105-d02cd410a115da2c
import all docs from arch

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?
49
 
 
50
 
# TODO: Check that no insertion occurs inside a deletion that was
51
 
# active in the version of the insertion.
52
 
 
53
 
 
54
 
try:
55
 
    set
56
 
    frozenset
57
 
except NameError:
58
 
    from sets import Set, ImmutableSet
59
 
    set = Set
60
 
    frozenset = ImmutableSet
61
 
    del Set, ImmutableSet
62
 
 
63
 
 
64
 
class WeaveError(Exception):
65
 
    """Exception in processing weave"""
66
 
 
67
 
 
68
 
class WeaveFormatError(WeaveError):
69
 
    """Weave invariant violated"""
70
 
    
71
 
 
72
 
class Weave(object):
73
 
    """weave - versioned text file storage.
74
 
    
75
 
    A Weave manages versions of line-based text files, keeping track
76
 
    of the originating version for each line.
77
 
 
78
 
    To clients the "lines" of the file are represented as a list of strings.
79
 
    These strings  will typically have terminal newline characters, but
80
 
    this is not required.  In particular files commonly do not have a newline
81
 
    at the end of the file.
82
 
 
83
 
    Texts can be identified in either of two ways:
84
 
 
85
 
    * a nonnegative index number.
86
 
 
87
 
    * a version-id string.
88
 
 
89
 
    Typically the index number will be valid only inside this weave and
90
 
    the version-id is used to reference it in the larger world.
91
 
 
92
 
    The weave is represented as a list mixing edit instructions and
93
 
    literal text.  Each entry in _l can be either a string (or
94
 
    unicode), or a tuple.  If a string, it means that the given line
95
 
    should be output in the currently active revisions.
96
 
 
97
 
    If a tuple, it gives a processing instruction saying in which
98
 
    revisions the enclosed lines are active.  The tuple has the form
99
 
    (instruction, version).
100
 
 
101
 
    The instruction can be '{' or '}' for an insertion block, and '['
102
 
    and ']' for a deletion block respectively.  The version is the
103
 
    integer version index.  There is no replace operator, only deletes
104
 
    and inserts.
105
 
 
106
 
    Constraints/notes:
107
 
 
108
 
    * A later version can delete lines that were introduced by any
109
 
      number of ancestor versions; this implies that deletion
110
 
      instructions can span insertion blocks without regard to the
111
 
      insertion block's nesting.
112
 
 
113
 
    * Similarly, deletions need not be properly nested with regard to
114
 
      each other, because they might have been generated by
115
 
      independent revisions.
116
 
 
117
 
    * Insertions are always made by inserting a new bracketed block
118
 
      into a single point in the previous weave.  This implies they
119
 
      can nest but not overlap, and the nesting must always have later
120
 
      insertions on the inside.
121
 
 
122
 
    * It doesn't seem very useful to have an active insertion
123
 
      inside an inactive insertion, but it might happen.
124
 
      
125
 
    * Therefore, all instructions are always"considered"; that
126
 
      is passed onto and off the stack.  An outer inactive block
127
 
      doesn't disable an inner block.
128
 
 
129
 
    * Lines are enabled if the most recent enclosing insertion is
130
 
      active and none of the enclosing deletions are active.
131
 
 
132
 
    * There is no point having a deletion directly inside its own
133
 
      insertion; you might as well just not write it.  And there
134
 
      should be no way to get an earlier version deleting a later
135
 
      version.
136
 
 
137
 
    _l
138
 
        Text of the weave. 
139
 
 
140
 
    _v
141
 
        List of versions, indexed by index number.
142
 
 
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.
148
 
    """
149
 
    def __init__(self):
150
 
        self._l = []
151
 
        self._v = []
152
 
 
153
 
 
154
 
 
155
 
    def __eq__(self, other):
156
 
        if not isinstance(other, Weave):
157
 
            return False
158
 
        return self._v == other._v \
159
 
               and self._l == other._l
160
 
    
161
 
 
162
 
    def __ne__(self, other):
163
 
        return not self.__eq__(other)
164
 
 
165
 
        
166
 
    def add(self, parents, text):
167
 
        """Add a single text on top of the weave.
168
 
  
169
 
        Returns the index number of the newly added version.
170
 
 
171
 
        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
 
        text
176
 
            Sequence of lines to be added in the new version."""
177
 
        self._check_versions(parents)
178
 
        self._check_lines(text)
179
 
 
180
 
        idx = len(self._v)
181
 
 
182
 
        if parents:
183
 
            delta = self._delta(self.inclusions(parents), text)
184
 
 
185
 
            # offset gives the number of lines that have been inserted
186
 
            # into the weave up to the current point; if the original edit instruction
187
 
            # says to change line A then we actually change (A+offset)
188
 
            offset = 0
189
 
 
190
 
            for i1, i2, newlines in delta:
191
 
                assert 0 <= i1
192
 
                assert i1 <= i2
193
 
                assert i2 <= len(self._l)
194
 
 
195
 
                # the deletion and insertion are handled separately.
196
 
                # first delete the region.
197
 
                if i1 != i2:
198
 
                    self._l.insert(i1+offset, ('[', idx))
199
 
                    self._l.insert(i2+offset+1, (']', idx))
200
 
                    offset += 2
201
 
                    # is this OK???
202
 
 
203
 
                if newlines:
204
 
                    # there may have been a deletion spanning up to
205
 
                    # i2; we want to insert after this region to make sure
206
 
                    # we don't destroy ourselves
207
 
                    i = i2 + offset
208
 
                    self._l[i:i] = [('{', idx)] \
209
 
                                   + newlines \
210
 
                                   + [('}', idx)]
211
 
                    offset += 2 + len(newlines)
212
 
 
213
 
            # TODO: Could eliminate any parents that are implied by
214
 
            # the others
215
 
                    
216
 
            self._addversion(parents)
217
 
        else:
218
 
            # special case; adding with no parents revision; can do this
219
 
            # more quickly by just appending unconditionally
220
 
            self._l.append(('{', idx))
221
 
            self._l += text
222
 
            self._l.append(('}', idx))
223
 
 
224
 
            self._addversion(None)
225
 
            
226
 
        return idx
227
 
 
228
 
 
229
 
    def inclusions(self, versions):
230
 
        """Expand out everything included by versions."""
231
 
        i = set(versions)
232
 
        for v in versions:
233
 
            i.update(self._v[v])
234
 
        return i
235
 
 
236
 
 
237
 
    def _addversion(self, parents):
238
 
        if parents:
239
 
            self._v.append(frozenset(parents))
240
 
        else:
241
 
            self._v.append(frozenset())
242
 
 
243
 
 
244
 
    def _check_lines(self, text):
245
 
        if not isinstance(text, list):
246
 
            raise ValueError("text should be a list, not %s" % type(text))
247
 
 
248
 
        for l in text:
249
 
            if not isinstance(l, basestring):
250
 
                raise ValueError("text line should be a string or unicode, not %s" % type(l))
251
 
        
252
 
 
253
 
 
254
 
    def _check_versions(self, indexes):
255
 
        """Check everything in the sequence of indexes is valid"""
256
 
        for i in indexes:
257
 
            try:
258
 
                self._v[i]
259
 
            except IndexError:
260
 
                raise IndexError("invalid version number %r" % i)
261
 
 
262
 
    
263
 
    def annotate(self, index):
264
 
        return list(self.annotate_iter(index))
265
 
 
266
 
 
267
 
    def annotate_iter(self, version):
268
 
        """Yield list of (index-id, line) pairs for the specified version.
269
 
 
270
 
        The index indicates when the line originated in the weave."""
271
 
        included = self.inclusions([version])
272
 
        for origin, lineno, text in self._extract(included):
273
 
            yield origin, text
274
 
 
275
 
 
276
 
    def _extract(self, included):
277
 
        """Yield annotation of lines in included set.
278
 
 
279
 
        Yields a sequence of tuples (origin, lineno, text), where
280
 
        origin is the origin version, lineno the index in the weave,
281
 
        and text the text of the line.
282
 
 
283
 
        The set typically but not necessarily corresponds to a version.
284
 
        """
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
290
 
 
291
 
        lineno = 0         # line of weave, 0-based
292
 
 
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.
298
 
 
299
 
        WFE = WeaveFormatError
300
 
        
301
 
        for l in self._l:
302
 
            if isinstance(l, tuple):
303
 
                c, v = l
304
 
                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))
308
 
                    istack.append(v)
309
 
                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))
318
 
                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))
327
 
                        dset.add(v)
328
 
                elif c == ']':
329
 
                    if v in dset:
330
 
                        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
 
            else:
338
 
                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)
344
 
                if isactive:
345
 
                    origin = istack[-1]
346
 
                    yield origin, lineno, l
347
 
            lineno += 1
348
 
 
349
 
        if istack:
350
 
            raise WFE("unclosed insertion blocks at end of weave",
351
 
                                   istack)
352
 
        if dset:
353
 
            raise WFE("unclosed deletion blocks at end of weave",
354
 
                                   dset)
355
 
 
356
 
 
357
 
    def get_iter(self, version):
358
 
        """Yield lines for the specified version."""
359
 
        for origin, lineno, line in self._extract(self.inclusions([version])):
360
 
            yield line
361
 
 
362
 
 
363
 
    def get(self, index):
364
 
        return list(self.get_iter(index))
365
 
 
366
 
 
367
 
    def merge_iter(self, included):
368
 
        """Return composed version of multiple included versions."""
369
 
        included = frozenset(included)
370
 
        for origin, lineno, text in self._extract(included):
371
 
            yield text
372
 
 
373
 
 
374
 
    def dump(self, to_file):
375
 
        from pprint import pprint
376
 
        print >>to_file, "Weave._l = ",
377
 
        pprint(self._l, to_file)
378
 
        print >>to_file, "Weave._v = ",
379
 
        pprint(self._v, to_file)
380
 
 
381
 
 
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:
387
 
                    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)
393
 
 
394
 
 
395
 
 
396
 
    def _delta(self, included, lines):
397
 
        """Return changes from basis to new revision.
398
 
 
399
 
        The old text for comparison is the union of included revisions.
400
 
 
401
 
        This is used in inserting a new text.
402
 
 
403
 
        Delta is returned as a sequence of
404
 
        (weave1, weave2, newlines).
405
 
 
406
 
        This indicates that weave1:weave2 of the old weave should be
407
 
        replaced by the sequence of lines in newlines.  Note that
408
 
        these line numbers are positions in the total weave and don't
409
 
        correspond to the lines in any extracted version, or even the
410
 
        extracted union of included versions.
411
 
 
412
 
        If line1=line2, this is a pure insert; if newlines=[] this is a
413
 
        pure delete.  (Similar to difflib.)
414
 
        """
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
 
        # 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]
432
 
 
433
 
        # add a sentinal, because we can also match against the final line
434
 
        basis.append((None, len(self._l), None))
435
 
 
436
 
        # XXX: which line of the weave should we really consider
437
 
        # matches the end of the file?  the current code says it's the
438
 
        # last line of the weave?
439
 
 
440
 
        from difflib import SequenceMatcher
441
 
        s = SequenceMatcher(None, basis_lines, lines)
442
 
 
443
 
        ##print 'basis sequence:'
444
 
        ##pprint(basis)
445
 
 
446
 
        # TODO: Perhaps return line numbers from composed weave as well?
447
 
 
448
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
449
 
            ##print tag, i1, i2, j1, j2
450
 
 
451
 
            if tag == 'equal':
452
 
                continue
453
 
 
454
 
            # i1,i2 are given in offsets within basis_lines; we need to map them
455
 
            # back to offsets within the entire weave
456
 
            real_i1 = basis[i1][1]
457
 
            real_i2 = basis[i2][1]
458
 
 
459
 
            assert 0 <= j1
460
 
            assert j1 <= j2
461
 
            assert j2 <= len(lines)
462
 
 
463
 
            yield real_i1, real_i2, lines[j1:j2]
464
 
 
465
 
 
466
 
 
467
 
 
468
 
def main(argv):
469
 
    import sys
470
 
    import os
471
 
    from weavefile import write_weave_v1, read_weave_v1
472
 
    cmd = argv[1]
473
 
    if cmd == 'add':
474
 
        w = read_weave_v1(file(argv[2], 'rb'))
475
 
        # at the moment, based on everything in the file
476
 
        parents = set(range(len(w._v)))
477
 
        lines = sys.stdin.readlines()
478
 
        ver = w.add(parents, lines)
479
 
        write_weave_v1(w, file(argv[2], 'wb'))
480
 
        print 'added %d' % ver
481
 
    elif cmd == 'init':
482
 
        fn = argv[2]
483
 
        if os.path.exists(fn):
484
 
            raise IOError("file exists")
485
 
        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])))
490
 
    elif cmd == 'annotate':
491
 
        w = read_weave_v1(file(argv[2], 'rb'))
492
 
        # newline is added to all lines regardless; too hard to get
493
 
        # reasonable formatting otherwise
494
 
        lasto = None
495
 
        for origin, text in w.annotate(int(argv[3])):
496
 
            text = text.rstrip('\r\n')
497
 
            if origin == lasto:
498
 
                print '      | %s' % (text)
499
 
            else:
500
 
                print '%5d | %s' % (origin, text)
501
 
                lasto = origin
502
 
    else:
503
 
        raise ValueError('unknown command %r' % cmd)
504
 
    
505
 
 
506
 
if __name__ == '__main__':
507
 
    import sys
508
 
    sys.exit(main(sys.argv))