~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

Merge in knit repository use of knits - still not a stable format, but can be experimented with.

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