~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to knit.py

  • Committer: mbp at sourcefrog
  • Date: 2005-04-01 04:59:19 UTC
  • Revision ID: mbp@sourcefrog.net-20050401045918-5c135d1ac180d5e0
few more test cases

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
 
# GNU GPL v2
6
 
 
7
 
# Author: Martin Pool <mbp@canonical.com>
8
 
 
9
 
 
10
 
"""knit - a weave-like structure"""
11
 
 
12
 
 
13
 
class VerInfo(object):
14
 
    included = frozenset()
15
 
    def __init__(self, included=None):
16
 
        if included:
17
 
            self.included = frozenset(included)
18
 
 
19
 
    def __repr__(self):
20
 
        s = self.__class__.__name__ + '('
21
 
        if self.included:
22
 
            s += 'included=%r' % (list(self.included))
23
 
        s += ')'
24
 
        return s
25
 
 
26
 
 
27
 
class Knit(object):
28
 
    """knit - versioned text file storage.
29
 
    
30
 
    A Knit manages versions of line-based text files, keeping track of the
31
 
    originating version for each line.
32
 
 
33
 
    Texts can be identified in either of two ways:
34
 
 
35
 
    * a nonnegative index number.
36
 
 
37
 
    * a version-id string.
38
 
 
39
 
    Typically the index number will be valid only inside this knit and
40
 
    the version-id is used to reference it in the larger world.
41
 
 
42
 
    _l
43
 
        List of edit instructions.
44
 
 
45
 
        Each line is stored as a tuple of (index-id, text).  The line
46
 
        is present in the version equal to index-id.
47
 
 
48
 
    _v
49
 
        List of versions, indexed by index number.
50
 
 
51
 
        For each version we store the tuple (included_versions), which
52
 
        lists the previous versions also considered active.
53
 
    """
54
 
    def __init__(self):
55
 
        self._l = []
56
 
        self._v = []
57
 
 
58
 
        
59
 
    def add(self, parents, text):
60
 
        """Add a single text on top of the weave.
61
 
 
62
 
        Returns the index number of the newly added version.
63
 
 
64
 
        parents
65
 
            List or set of parent version numbers.
66
 
 
67
 
        text
68
 
            Sequence of lines to be added in the new version."""
69
 
        if not isinstance(text, list):
70
 
            raise ValueError("text should be a list, not %s" % type(text))
71
 
 
72
 
        self._check_versions(parents)
73
 
 
74
 
        idx = len(self._v)
75
 
 
76
 
        if parents:
77
 
            parents = frozenset(parents)
78
 
            delta = self._delta(parents, text)
79
 
 
80
 
            for i1, i2, newlines in delta:
81
 
                # TODO: handle lines being offset as we insert stuff
82
 
                if i1 != i2:
83
 
                    raise NotImplementedError("can't handle replacing weave lines %d-%d yet"
84
 
                                              % (i1, i2))
85
 
 
86
 
                # a pure insertion
87
 
                to_insert = []
88
 
                for line in newlines:
89
 
                    to_insert.append((idx, line))
90
 
                
91
 
                self._l[i1:i1] = to_insert
92
 
 
93
 
            self._v.append(VerInfo(parents))
94
 
        else:
95
 
            # special case; adding with no parents revision; can do this
96
 
            # more quickly by just appending unconditionally
97
 
            for line in text:
98
 
                self._l.append((idx, line))
99
 
 
100
 
            self._v.append(VerInfo())
101
 
            
102
 
        return idx
103
 
 
104
 
 
105
 
    def _check_versions(self, indexes):
106
 
        """Check everything in the sequence of indexes is valid"""
107
 
        for i in indexes:
108
 
            try:
109
 
                self._v[i]
110
 
            except IndexError:
111
 
                raise IndexError("invalid version number %r" % i)
112
 
 
113
 
    
114
 
    def annotate(self, index):
115
 
        return list(self.annotate_iter(index))
116
 
 
117
 
 
118
 
    def annotate_iter(self, index):
119
 
        """Yield list of (index-id, line) pairs for the specified version.
120
 
 
121
 
        The index indicates when the line originated in the weave."""
122
 
        try:
123
 
            vi = self._v[index]
124
 
        except IndexError:
125
 
            raise IndexError('version index %d out of range' % index)
126
 
        included = set(vi.included)
127
 
        included.add(index)
128
 
        return iter(self._extract(included))
129
 
 
130
 
 
131
 
    def _extract(self, included):
132
 
        """Yield annotation of lines in included set.
133
 
 
134
 
        The set typically but not necessarily corresponds to a version.
135
 
        """
136
 
        for origin, line in self._l:
137
 
            if origin in included:
138
 
                yield origin, line
139
 
        
140
 
 
141
 
 
142
 
    def getiter(self, index):
143
 
        """Yield lines for the specified version."""
144
 
        for origin, line in self.annotate_iter(index):
145
 
            yield line
146
 
 
147
 
 
148
 
    def get(self, index):
149
 
        return list(self.getiter(index))
150
 
 
151
 
 
152
 
    def dump(self, to_file):
153
 
        from pprint import pprint
154
 
        print >>to_file, "Knit._l = ",
155
 
        pprint(self._l, to_file)
156
 
        print >>to_file, "Knit._v = ",
157
 
        pprint(self._v, to_file)
158
 
 
159
 
 
160
 
    def check(self):
161
 
        for vers_info in self._v:
162
 
            included = set()
163
 
            for vi in vers_info[0]:
164
 
                if vi < 0 or vi >= index:
165
 
                    raise ValueError("invalid included version %d for index %d"
166
 
                                     % (vi, index))
167
 
                if vi in included:
168
 
                    raise ValueError("repeated included version %d for index %d"
169
 
                                     % (vi, index))
170
 
                included.add(vi)
171
 
 
172
 
 
173
 
 
174
 
    def _delta(self, included, lines):
175
 
        """Return changes from basis to new revision.
176
 
 
177
 
        The old text for comparison is the union of included revisions.
178
 
 
179
 
        This is used in inserting a new text.
180
 
 
181
 
        Delta is returned as a sequence of (line1, line2, newlines),
182
 
        indicating that line1 through line2 of the old weave should be
183
 
        replaced by the sequence of lines in newlines.  Note that
184
 
        these line numbers are positions in the total weave and don't
185
 
        correspond to the lines in any extracted version, or even the
186
 
        extracted union of included versions.
187
 
 
188
 
        If line1=line2, this is a pure insert; if newlines=[] this is a
189
 
        pure delete.  (Similar to difflib.)
190
 
        """
191
 
 
192
 
        self._check_versions(included)
193
 
 
194
 
        ##from pprint import pprint
195
 
 
196
 
        # first get basis for comparison
197
 
        # basis holds (lineno, origin, line)
198
 
        basis = []
199
 
 
200
 
        ##print 'my lines:'
201
 
        ##pprint(self._l)
202
 
        
203
 
        lineno = 0
204
 
        for origin, line in self._l:
205
 
            if origin in included:
206
 
                basis.append((lineno, line))
207
 
            lineno += 1
208
 
 
209
 
        assert lineno == len(self._l)
210
 
 
211
 
        # now make a parallel list with only the text, to pass to the differ
212
 
        basis_lines = [line for (lineno, line) in basis]
213
 
 
214
 
        # add a sentinal, because we can also match against the final line
215
 
        basis.append((len(self._l), None))
216
 
 
217
 
        # XXX: which line of the weave should we really consider matches the end of the file?
218
 
        # the current code says it's the last line of the weave?
219
 
 
220
 
        from difflib import SequenceMatcher
221
 
        s = SequenceMatcher(None, basis_lines, lines)
222
 
 
223
 
        ##print 'basis sequence:'
224
 
        ##pprint(basis)
225
 
 
226
 
        for tag, i1, i2, j1, j2 in s.get_opcodes():
227
 
            ##print tag, i1, i2, j1, j2
228
 
 
229
 
            if tag == 'equal':
230
 
                continue
231
 
 
232
 
            # i1,i2 are given in offsets within basis_lines; we need to map them
233
 
            # back to offsets within the entire weave
234
 
            real_i1 = basis[i1][0]
235
 
            real_i2 = basis[i2][0]
236
 
 
237
 
            # find the text identified by j:
238
 
            if j1 == j2:
239
 
                newlines = []
240
 
            else:
241
 
                assert 0 <= j1
242
 
                assert j1 <= j2
243
 
                assert j2 <= len(lines)
244
 
                newlines = lines[j1:j2]
245
 
 
246
 
            yield real_i1, real_i2, newlines
247
 
 
248
 
 
249
 
def update_knit(knit, new_vers, new_lines):
250
 
    """Return a new knit whose text matches new_lines.
251
 
 
252
 
    First of all the knit is diffed against the new lines, considering
253
 
    only the text of the lines from the knit.  This identifies lines
254
 
    unchanged from the knit, plus insertions and deletions.
255
 
 
256
 
    The deletions are marked as deleted.  The insertions are added
257
 
    with their new values.
258
 
 
259
 
    
260
 
    """
261
 
    if not isinstance(new_vers, int):
262
 
        raise TypeError('new version-id must be an int: %r' % new_vers)
263
 
    
264
 
    from difflib import SequenceMatcher
265
 
    knit_lines = knit2text(knit)
266
 
    m = SequenceMatcher(None, knit_lines, new_lines)
267
 
 
268
 
    for block in m.get_matching_blocks():
269
 
        print "a[%d] and b[%d] match for %d elements" % block
270
 
    
271
 
    new_knit = []
272
 
    for tag, i1, i2, j1, j2 in m.get_opcodes():
273
 
        print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" %
274
 
               (tag, i1, i2, knit_lines[i1:i2], j1, j2, new_lines[j1:j2]))
275
 
 
276
 
        if tag == 'equal':
277
 
            new_knit.extend(knit[i1:i2])
278
 
        elif tag == 'delete':
279
 
            for i in range(i1, i2):
280
 
                kl = knit[i]
281
 
                new_knit.append((kl[0], kl[1], False))
282
 
 
283
 
    return new_knit
284
 
        
285