/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

Add versionedfile.fix_parents api for correcting data post hoc.

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