~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to weave.py

  • Committer: Martin Pool
  • Date: 2005-06-30 08:40:59 UTC
  • mto: This revision was merged to the branch mainline in revision 852.
  • Revision ID: mbp@sourcefrog.net-20050630084059-d6eb6cb46972365b
Rename Weave.get_included to inclusions and getiter to get_iter

Refactor annotate() code 

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))