~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: Robert Collins
  • Date: 2006-02-22 10:35:05 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060222103505-bddb211d353f2543
Merge in a variation of the versionedfile api from versioned-file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 by Canonical Ltd
 
2
#
 
3
# Authors:
 
4
#   Johan Rydberg <jrydberg@gnu.org>
 
5
#
 
6
# This program is free software; you can redistribute it and/or modify
 
7
# it under the terms of the GNU General Public License as published by
 
8
# the Free Software Foundation; either version 2 of the License, or
 
9
# (at your option) any later version.
 
10
 
 
11
# This program is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
# GNU General Public License for more details.
 
15
 
 
16
# You should have received a copy of the GNU General Public License
 
17
# along with this program; if not, write to the Free Software
 
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
19
 
 
20
# Remaing to do is to figure out if get_graph should return a simple
 
21
# map, or a graph object of some kind.
 
22
 
 
23
 
 
24
"""Versioned text file storage api."""
 
25
 
 
26
 
 
27
class VersionedFile(object):
 
28
    """Versioned text file storage.
 
29
    
 
30
    A versioned file manages versions of line-based text files,
 
31
    keeping track of the originating version for each line.
 
32
 
 
33
    To clients the "lines" of the file are represented as a list of
 
34
    strings. These strings will typically have terminal newline
 
35
    characters, but this is not required.  In particular files commonly
 
36
    do not have a newline at the end of the file.
 
37
 
 
38
    Texts are identified by a version-id string.
 
39
    """
 
40
 
 
41
    def versions(self):
 
42
        """Return a unsorted list of versions."""
 
43
        raise NotImplementedError(self.versions)
 
44
 
 
45
    def has_version(self, version_id):
 
46
        """Returns whether version is present."""
 
47
        raise NotImplementedError(self.has_version)
 
48
 
 
49
    def add_lines(self, version_id, parents, lines):
 
50
        """Add a single text on top of the versioned file.
 
51
 
 
52
        Must raise RevisionAlreadyPresent if the new version is
 
53
        already present in file history.
 
54
 
 
55
        Must raise RevisionNotPresent if any of the given parents are
 
56
        not present in file history."""
 
57
        raise NotImplementedError(self.add_text)
 
58
 
 
59
    def clone_text(self, new_version_id, old_version_id, parents,
 
60
                   transaction):
 
61
        """Add an identical text to old_version_id as new_version_id.
 
62
 
 
63
        Must raise RevisionNotPresent if the old version or any of the
 
64
        parents are not present in file history.
 
65
 
 
66
        Must raise RevisionAlreadyPresent if the new version is
 
67
        already present in file history."""
 
68
        raise NotImplementedError(self.clone_text)
 
69
 
 
70
    def get_text(self, version_id):
 
71
        """Return version contents as a text string.
 
72
 
 
73
        Raises RevisionNotPresent if version is not present in
 
74
        file history.
 
75
        """
 
76
        return ''.join(self.get_lines(version_id))
 
77
    get_string = get_text
 
78
 
 
79
    def get_lines(self, version_id):
 
80
        """Return version contents as a sequence of lines.
 
81
 
 
82
        Raises RevisionNotPresent if version is not present in
 
83
        file history.
 
84
        """
 
85
        raise NotImplementedError(self.get_lines)
 
86
 
 
87
    def get_ancestry(self, version_ids):
 
88
        """Return a list of all ancestors of given version(s). This
 
89
        will not include the null revision.
 
90
 
 
91
        Must raise RevisionNotPresent if any of the given versions are
 
92
        not present in file history."""
 
93
        if isinstance(version_ids, basestring):
 
94
            version_ids = [version_ids]
 
95
        raise NotImplementedError(self.get_ancestry)
 
96
        
 
97
    def get_graph(self, version_id):
 
98
        """Return a graph.
 
99
 
 
100
        Must raise RevisionNotPresent if version is not present in
 
101
        file history."""
 
102
        raise NotImplementedError(self.get_graph)
 
103
 
 
104
    def get_parents(self, version_id):
 
105
        """Return version names for parents of a version.
 
106
 
 
107
        Must raise RevisionNotPresent if version is not present in
 
108
        file history.
 
109
        """
 
110
        raise NotImplementedError(self.get_parents)
 
111
 
 
112
    def annotate_iter(self, version_id):
 
113
        """Yield list of (version-id, line) pairs for the specified
 
114
        version.
 
115
 
 
116
        Must raise RevisionNotPresent if any of the given versions are
 
117
        not present in file history.
 
118
        """
 
119
        raise NotImplementedError(self.annotate_iter)
 
120
 
 
121
    def annotate(self, version_id):
 
122
        return list(self.annotate_iter(version_id))
 
123
 
 
124
    def join(self, other, version_ids, transaction, pb=None):
 
125
        """Integrate versions from other into this versioned file.
 
126
 
 
127
        If version_ids is None all versions from other should be
 
128
        incorporated into this versioned file.
 
129
 
 
130
        Must raise RevisionNotPresent if any of the specified versions
 
131
        are not present in the other files history."""
 
132
        raise NotImplementedError(self.join)
 
133
 
 
134
    def walk(self, version_ids=None):
 
135
        """Walk the versioned file as a weave-like structure, for
 
136
        versions relative to version_ids.  Yields sequence of (lineno,
 
137
        insert, deletes, text) for each relevant line.
 
138
 
 
139
        Must raise RevisionNotPresent if any of the specified versions
 
140
        are not present in the file history.
 
141
 
 
142
        :param version_ids: the version_ids to walk with respect to. If not
 
143
                            supplied the entire weave-like structure is walked.
 
144
        """
 
145
        raise NotImplementedError(self.walk)
 
146
 
 
147
    def plan_merge(self, ver_a, ver_b):
 
148
        """Return pseudo-annotation indicating how the two versions merge.
 
149
 
 
150
        This is computed between versions a and b and their common
 
151
        base.
 
152
 
 
153
        Weave lines present in none of them are skipped entirely.
 
154
        """
 
155
        inc_a = set(self.inclusions([ver_a]))
 
156
        inc_b = set(self.inclusions([ver_b]))
 
157
        inc_c = inc_a & inc_b
 
158
 
 
159
        for lineno, insert, deleteset, line in self.walk():
 
160
            if deleteset & inc_c:
 
161
                # killed in parent; can't be in either a or b
 
162
                # not relevant to our work
 
163
                yield 'killed-base', line
 
164
            elif insert in inc_c:
 
165
                # was inserted in base
 
166
                killed_a = bool(deleteset & inc_a)
 
167
                killed_b = bool(deleteset & inc_b)
 
168
                if killed_a and killed_b:
 
169
                    yield 'killed-both', line
 
170
                elif killed_a:
 
171
                    yield 'killed-a', line
 
172
                elif killed_b:
 
173
                    yield 'killed-b', line
 
174
                else:
 
175
                    yield 'unchanged', line
 
176
            elif insert in inc_a:
 
177
                if deleteset & inc_a:
 
178
                    yield 'ghost-a', line
 
179
                else:
 
180
                    # new in A; not in B
 
181
                    yield 'new-a', line
 
182
            elif insert in inc_b:
 
183
                if deleteset & inc_b:
 
184
                    yield 'ghost-b', line
 
185
                else:
 
186
                    yield 'new-b', line
 
187
            else:
 
188
                # not in either revision
 
189
                yield 'irrelevant', line
 
190
 
 
191
        yield 'unchanged', ''           # terminator
 
192
 
 
193
    def weave_merge(self, plan):
 
194
        lines_a = []
 
195
        lines_b = []
 
196
        ch_a = ch_b = False
 
197
        # TODO: Return a structured form of the conflicts (e.g. 2-tuples for
 
198
        # conflicted regions), rather than just inserting the markers.
 
199
        # 
 
200
        # TODO: Show some version information (e.g. author, date) on 
 
201
        # conflicted regions.
 
202
        for state, line in plan:
 
203
            if state == 'unchanged' or state == 'killed-both':
 
204
                # resync and flush queued conflicts changes if any
 
205
                if not lines_a and not lines_b:
 
206
                    pass
 
207
                elif ch_a and not ch_b:
 
208
                    # one-sided change:                    
 
209
                    for l in lines_a: yield l
 
210
                elif ch_b and not ch_a:
 
211
                    for l in lines_b: yield l
 
212
                elif lines_a == lines_b:
 
213
                    for l in lines_a: yield l
 
214
                else:
 
215
                    yield '<<<<<<<\n'
 
216
                    for l in lines_a: yield l
 
217
                    yield '=======\n'
 
218
                    for l in lines_b: yield l
 
219
                    yield '>>>>>>>\n'
 
220
 
 
221
                del lines_a[:]
 
222
                del lines_b[:]
 
223
                ch_a = ch_b = False
 
224
                
 
225
            if state == 'unchanged':
 
226
                if line:
 
227
                    yield line
 
228
            elif state == 'killed-a':
 
229
                ch_a = True
 
230
                lines_b.append(line)
 
231
            elif state == 'killed-b':
 
232
                ch_b = True
 
233
                lines_a.append(line)
 
234
            elif state == 'new-a':
 
235
                ch_a = True
 
236
                lines_a.append(line)
 
237
            elif state == 'new-b':
 
238
                ch_b = True
 
239
                lines_b.append(line)
 
240
            else:
 
241
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
242
                                 'killed-both'), \
 
243
                       state
 
244
 
 
245
 
 
246
def plan_merge(file, version_a, version_b):
 
247
    """Return pseudo-annotation indicating how the two versions merge.
 
248
    
 
249
    This is computed between versions a and b and their common
 
250
    base.
 
251
 
 
252
    Weave lines present in none of them are skipped entirely.
 
253
    """
 
254
    inc_a = set(file.get_ancestry([version_a]))
 
255
    inc_b = set(file.get_ancestry([version_b]))
 
256
    inc_c = inc_a & inc_b
 
257
 
 
258
    for lineno, insert, deleteset, line in file.walk([version_a, version_b]):
 
259
        if deleteset & inc_c:
 
260
            # killed in parent; can't be in either a or b
 
261
            # not relevant to our work
 
262
            yield 'killed-base', line
 
263
        elif insert in inc_c:
 
264
            # was inserted in base
 
265
            killed_a = bool(deleteset & inc_a)
 
266
            killed_b = bool(deleteset & inc_b)
 
267
            if killed_a and killed_b:
 
268
                yield 'killed-both', line
 
269
            elif killed_a:
 
270
                yield 'killed-a', line
 
271
            elif killed_b:
 
272
                yield 'killed-b', line
 
273
            else:
 
274
                yield 'unchanged', line
 
275
        elif insert in inc_a:
 
276
            if deleteset & inc_a:
 
277
                yield 'ghost-a', line
 
278
            else:
 
279
                # new in A; not in B
 
280
                yield 'new-a', line
 
281
        elif insert in inc_b:
 
282
            if deleteset & inc_b:
 
283
                yield 'ghost-b', line
 
284
            else:
 
285
                yield 'new-b', line
 
286
        else:
 
287
            # not in either revision
 
288
            yield 'irrelevant', line
 
289
 
 
290
    yield 'unchanged', ''           # terminator
 
291
 
 
292
 
 
293
def weave_merge(plan):
 
294
    """Yield merged sequence of lines based on merge plan."""
 
295
 
 
296
    lines_a = []
 
297
    lines_b = []
 
298
    ch_a = ch_b = False
 
299
 
 
300
    for state, line in plan:
 
301
        if state == 'unchanged' or state == 'killed-both':
 
302
            # resync and flush queued conflicts changes if any
 
303
            if not lines_a and not lines_b:
 
304
                pass
 
305
            elif ch_a and not ch_b:
 
306
                # one-sided change:                    
 
307
                for l in lines_a: yield l
 
308
            elif ch_b and not ch_a:
 
309
                for l in lines_b: yield l
 
310
            elif lines_a == lines_b:
 
311
                for l in lines_a: yield l
 
312
            else:
 
313
                yield '<<<<<<<\n'
 
314
                for l in lines_a: yield l
 
315
                yield '=======\n'
 
316
                for l in lines_b: yield l
 
317
                yield '>>>>>>>\n'
 
318
 
 
319
            del lines_a[:]
 
320
            del lines_b[:]
 
321
            ch_a = ch_b = False
 
322
                
 
323
        if state == 'unchanged':
 
324
            if line:
 
325
                yield line
 
326
        elif state == 'killed-a':
 
327
            ch_a = True
 
328
            lines_b.append(line)
 
329
        elif state == 'killed-b':
 
330
            ch_b = True
 
331
            lines_a.append(line)
 
332
        elif state == 'new-a':
 
333
            ch_a = True
 
334
            lines_a.append(line)
 
335
        elif state == 'new-b':
 
336
            ch_b = True
 
337
            lines_b.append(line)
 
338
        else:
 
339
            assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
340
                             'killed-both'), state