/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/versionedfile.py

  • Committer: Robert Collins
  • Date: 2006-03-02 01:26:22 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060302012622-6d1d0b92fe94d9be
Checkpointing: created InterObject to factor out common inter object worker code, added InterVersionedFile and tests to allow making join work between any versionedfile.

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
from copy import deepcopy
 
28
from unittest import TestSuite
 
29
 
 
30
 
 
31
from bzrlib.inter import InterObject
 
32
from bzrlib.symbol_versioning import *
 
33
 
 
34
 
 
35
class VersionedFile(object):
 
36
    """Versioned text file storage.
 
37
    
 
38
    A versioned file manages versions of line-based text files,
 
39
    keeping track of the originating version for each line.
 
40
 
 
41
    To clients the "lines" of the file are represented as a list of
 
42
    strings. These strings will typically have terminal newline
 
43
    characters, but this is not required.  In particular files commonly
 
44
    do not have a newline at the end of the file.
 
45
 
 
46
    Texts are identified by a version-id string.
 
47
    """
 
48
 
 
49
    @deprecated_method(zero_eight)
 
50
    def names(self):
 
51
        """Return a list of all the versions in this versioned file.
 
52
 
 
53
        Please use versionedfile.versions() now.
 
54
        """
 
55
        return self.versions()
 
56
 
 
57
    def versions(self):
 
58
        """Return a unsorted list of versions."""
 
59
        raise NotImplementedError(self.versions)
 
60
 
 
61
    def has_version(self, version_id):
 
62
        """Returns whether version is present."""
 
63
        raise NotImplementedError(self.has_version)
 
64
 
 
65
    def add_lines(self, version_id, parents, lines):
 
66
        """Add a single text on top of the versioned file.
 
67
 
 
68
        Must raise RevisionAlreadyPresent if the new version is
 
69
        already present in file history.
 
70
 
 
71
        Must raise RevisionNotPresent if any of the given parents are
 
72
        not present in file history."""
 
73
        raise NotImplementedError(self.add_lines)
 
74
 
 
75
    def clear_cache(self):
 
76
        """Remove any data cached in the versioned file object."""
 
77
 
 
78
    def clone_text(self, new_version_id, old_version_id, parents):
 
79
        """Add an identical text to old_version_id as new_version_id.
 
80
 
 
81
        Must raise RevisionNotPresent if the old version or any of the
 
82
        parents are not present in file history.
 
83
 
 
84
        Must raise RevisionAlreadyPresent if the new version is
 
85
        already present in file history."""
 
86
        raise NotImplementedError(self.clone_text)
 
87
 
 
88
    def get_text(self, version_id):
 
89
        """Return version contents as a text string.
 
90
 
 
91
        Raises RevisionNotPresent if version is not present in
 
92
        file history.
 
93
        """
 
94
        return ''.join(self.get_lines(version_id))
 
95
    get_string = get_text
 
96
 
 
97
    def get_lines(self, version_id):
 
98
        """Return version contents as a sequence of lines.
 
99
 
 
100
        Raises RevisionNotPresent if version is not present in
 
101
        file history.
 
102
        """
 
103
        raise NotImplementedError(self.get_lines)
 
104
 
 
105
    def get_ancestry(self, version_ids):
 
106
        """Return a list of all ancestors of given version(s). This
 
107
        will not include the null revision.
 
108
 
 
109
        Must raise RevisionNotPresent if any of the given versions are
 
110
        not present in file history."""
 
111
        if isinstance(version_ids, basestring):
 
112
            version_ids = [version_ids]
 
113
        raise NotImplementedError(self.get_ancestry)
 
114
        
 
115
    def get_graph(self, version_id):
 
116
        """Return a graph.
 
117
 
 
118
        Must raise RevisionNotPresent if version is not present in
 
119
        file history."""
 
120
        raise NotImplementedError(self.get_graph)
 
121
 
 
122
    @deprecated_method(zero_eight)
 
123
    def parent_names(self, version):
 
124
        """Return version names for parents of a version.
 
125
        
 
126
        See get_parents for the current api.
 
127
        """
 
128
        return self.get_parents(version)
 
129
 
 
130
    def get_parents(self, version_id):
 
131
        """Return version names for parents of a version.
 
132
 
 
133
        Must raise RevisionNotPresent if version is not present in
 
134
        file history.
 
135
        """
 
136
        raise NotImplementedError(self.get_parents)
 
137
 
 
138
    def annotate_iter(self, version_id):
 
139
        """Yield list of (version-id, line) pairs for the specified
 
140
        version.
 
141
 
 
142
        Must raise RevisionNotPresent if any of the given versions are
 
143
        not present in file history.
 
144
        """
 
145
        raise NotImplementedError(self.annotate_iter)
 
146
 
 
147
    def annotate(self, version_id):
 
148
        return list(self.annotate_iter(version_id))
 
149
 
 
150
    def join(self, other, pb=None, msg=None, version_ids=None):
 
151
        """Integrate versions from other into this versioned file.
 
152
 
 
153
        If version_ids is None all versions from other should be
 
154
        incorporated into this versioned file.
 
155
 
 
156
        Must raise RevisionNotPresent if any of the specified versions
 
157
        are not present in the other files history."""
 
158
        raise NotImplementedError(self.join)
 
159
 
 
160
    def walk(self, version_ids=None):
 
161
        """Walk the versioned file as a weave-like structure, for
 
162
        versions relative to version_ids.  Yields sequence of (lineno,
 
163
        insert, deletes, text) for each relevant line.
 
164
 
 
165
        Must raise RevisionNotPresent if any of the specified versions
 
166
        are not present in the file history.
 
167
 
 
168
        :param version_ids: the version_ids to walk with respect to. If not
 
169
                            supplied the entire weave-like structure is walked.
 
170
        """
 
171
        raise NotImplementedError(self.walk)
 
172
 
 
173
    @deprecated_method(zero_eight)
 
174
    def iter_names(self):
 
175
        """Walk the names list."""
 
176
        return iter(self.versions())
 
177
 
 
178
    def plan_merge(self, ver_a, ver_b):
 
179
        """Return pseudo-annotation indicating how the two versions merge.
 
180
 
 
181
        This is computed between versions a and b and their common
 
182
        base.
 
183
 
 
184
        Weave lines present in none of them are skipped entirely.
 
185
        """
 
186
        inc_a = set(self.inclusions([ver_a]))
 
187
        inc_b = set(self.inclusions([ver_b]))
 
188
        inc_c = inc_a & inc_b
 
189
 
 
190
        for lineno, insert, deleteset, line in self.walk():
 
191
            if deleteset & inc_c:
 
192
                # killed in parent; can't be in either a or b
 
193
                # not relevant to our work
 
194
                yield 'killed-base', line
 
195
            elif insert in inc_c:
 
196
                # was inserted in base
 
197
                killed_a = bool(deleteset & inc_a)
 
198
                killed_b = bool(deleteset & inc_b)
 
199
                if killed_a and killed_b:
 
200
                    yield 'killed-both', line
 
201
                elif killed_a:
 
202
                    yield 'killed-a', line
 
203
                elif killed_b:
 
204
                    yield 'killed-b', line
 
205
                else:
 
206
                    yield 'unchanged', line
 
207
            elif insert in inc_a:
 
208
                if deleteset & inc_a:
 
209
                    yield 'ghost-a', line
 
210
                else:
 
211
                    # new in A; not in B
 
212
                    yield 'new-a', line
 
213
            elif insert in inc_b:
 
214
                if deleteset & inc_b:
 
215
                    yield 'ghost-b', line
 
216
                else:
 
217
                    yield 'new-b', line
 
218
            else:
 
219
                # not in either revision
 
220
                yield 'irrelevant', line
 
221
 
 
222
        yield 'unchanged', ''           # terminator
 
223
 
 
224
    def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
 
225
        lines_a = []
 
226
        lines_b = []
 
227
        ch_a = ch_b = False
 
228
        # TODO: Return a structured form of the conflicts (e.g. 2-tuples for
 
229
        # conflicted regions), rather than just inserting the markers.
 
230
        # 
 
231
        # TODO: Show some version information (e.g. author, date) on 
 
232
        # conflicted regions.
 
233
        for state, line in plan:
 
234
            if state == 'unchanged' or state == 'killed-both':
 
235
                # resync and flush queued conflicts changes if any
 
236
                if not lines_a and not lines_b:
 
237
                    pass
 
238
                elif ch_a and not ch_b:
 
239
                    # one-sided change:                    
 
240
                    for l in lines_a: yield l
 
241
                elif ch_b and not ch_a:
 
242
                    for l in lines_b: yield l
 
243
                elif lines_a == lines_b:
 
244
                    for l in lines_a: yield l
 
245
                else:
 
246
                    yield a_marker
 
247
                    for l in lines_a: yield l
 
248
                    yield '=======\n'
 
249
                    for l in lines_b: yield l
 
250
                    yield b_marker
 
251
 
 
252
                del lines_a[:]
 
253
                del lines_b[:]
 
254
                ch_a = ch_b = False
 
255
                
 
256
            if state == 'unchanged':
 
257
                if line:
 
258
                    yield line
 
259
            elif state == 'killed-a':
 
260
                ch_a = True
 
261
                lines_b.append(line)
 
262
            elif state == 'killed-b':
 
263
                ch_b = True
 
264
                lines_a.append(line)
 
265
            elif state == 'new-a':
 
266
                ch_a = True
 
267
                lines_a.append(line)
 
268
            elif state == 'new-b':
 
269
                ch_b = True
 
270
                lines_b.append(line)
 
271
            else:
 
272
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
273
                                 'killed-both'), \
 
274
                       state
 
275
 
 
276
 
 
277
class InterVersionedFile(InterObject):
 
278
    """This class represents operations taking place between two versionedfiles..
 
279
 
 
280
    Its instances have methods like join, and contain
 
281
    references to the source and target versionedfiles these operations can be 
 
282
    carried out on.
 
283
 
 
284
    Often we will provide convenience methods on 'versionedfile' which carry out
 
285
    operations with another versionedfile - they will always forward to
 
286
    InterVersionedFile.get(other).method_name(parameters).
 
287
    """
 
288
 
 
289
    _optimisers = set()
 
290
    """The available optimised InterVersionedFile types."""
 
291
 
 
292
 
 
293
class InterVersionedFileTestProviderAdapter(object):
 
294
    """A tool to generate a suite testing multiple inter versioned-file classes.
 
295
 
 
296
    This is done by copying the test once for each interversionedfile provider
 
297
    and injecting the transport_server, transport_readonly_server,
 
298
    versionedfile_factory and versionedfile_factory_to classes into each copy.
 
299
    Each copy is also given a new id() to make it easy to identify.
 
300
    """
 
301
 
 
302
    def __init__(self, transport_server, transport_readonly_server, formats):
 
303
        self._transport_server = transport_server
 
304
        self._transport_readonly_server = transport_readonly_server
 
305
        self._formats = formats
 
306
    
 
307
    def adapt(self, test):
 
308
        result = TestSuite()
 
309
        for (interversionedfile_class,
 
310
             versionedfile_factory,
 
311
             versionedfile_factory_to) in self._formats:
 
312
            new_test = deepcopy(test)
 
313
            new_test.transport_server = self._transport_server
 
314
            new_test.transport_readonly_server = self._transport_readonly_server
 
315
            new_test.interversionedfile_class = interversionedfile_class
 
316
            new_test.versionedfile_factory = versionedfile_factory
 
317
            new_test.versionedfile_factory_to = versionedfile_factory_to
 
318
            def make_new_test_id():
 
319
                new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
 
320
                return lambda: new_id
 
321
            new_test.id = make_new_test_id()
 
322
            result.addTest(new_test)
 
323
        return result
 
324
 
 
325
    @staticmethod
 
326
    def default_test_list():
 
327
        """Generate the default list of interversionedfile permutations to test."""
 
328
        from bzrlib.weave import WeaveFile
 
329
        from bzrlib.knit import AnnotatedKnitFactory
 
330
        result = []
 
331
        # test the fallback InterVersionedFile from weave to annotated knits
 
332
        result.append((InterVersionedFile, 
 
333
                       WeaveFile,
 
334
                       AnnotatedKnitFactory))
 
335
        for optimiser in InterVersionedFile._optimisers:
 
336
            result.append((optimiser,
 
337
                           optimiser._matching_file_factory,
 
338
                           optimiser._matching_file_factory
 
339
                           ))
 
340
        # if there are specific combinations we want to use, we can add them 
 
341
        # here.
 
342
        return result