/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 07:43:46 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060302074346-f6ea05e3f19f6b8b
Change WeaveStore into VersionedFileStore and make its versoined file class parameterisable.

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