/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-06 12:11:25 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060306121125-4f05992d44e3bda8
Convert Knit repositories to use knits.

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