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