1
# Copyright (C) 2005 by Canonical Ltd
 
 
4
#   Johan Rydberg <jrydberg@gnu.org>
 
 
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.
 
 
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.
 
 
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
 
 
20
# Remaing to do is to figure out if get_graph should return a simple
 
 
21
# map, or a graph object of some kind.
 
 
24
"""Versioned text file storage api."""
 
 
27
from copy import deepcopy
 
 
28
from unittest import TestSuite
 
 
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
 
 
38
class VersionedFile(object):
 
 
39
    """Versioned text file storage.
 
 
41
    A versioned file manages versions of line-based text files,
 
 
42
    keeping track of the originating version for each line.
 
 
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.
 
 
49
    Texts are identified by a version-id string.
 
 
52
    def copy_to(self, name, transport):
 
 
53
        """Copy this versioned file to name on transport."""
 
 
54
        raise NotImplementedError(self.copy_to)
 
 
56
    @deprecated_method(zero_eight)
 
 
58
        """Return a list of all the versions in this versioned file.
 
 
60
        Please use versionedfile.versions() now.
 
 
62
        return self.versions()
 
 
65
        """Return a unsorted list of versions."""
 
 
66
        raise NotImplementedError(self.versions)
 
 
68
    def has_version(self, version_id):
 
 
69
        """Returns whether version is present."""
 
 
70
        raise NotImplementedError(self.has_version)
 
 
72
    def add_lines(self, version_id, parents, lines):
 
 
73
        """Add a single text on top of the versioned file.
 
 
75
        Must raise RevisionAlreadyPresent if the new version is
 
 
76
        already present in file history.
 
 
78
        Must raise RevisionNotPresent if any of the given parents are
 
 
79
        not present in file history."""
 
 
80
        raise NotImplementedError(self.add_lines)
 
 
82
    def check(self, progress_bar=None):
 
 
83
        """Check the versioned file for integrity."""
 
 
84
        raise NotImplementedError(self.check)
 
 
86
    def clear_cache(self):
 
 
87
        """Remove any data cached in the versioned file object."""
 
 
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.
 
 
92
        Must raise RevisionNotPresent if the old version or any of the
 
 
93
        parents are not present in file history.
 
 
95
        Must raise RevisionAlreadyPresent if the new version is
 
 
96
        already present in file history."""
 
 
97
        raise NotImplementedError(self.clone_text)
 
 
99
    def create_empty(self, name, transport, mode=None):
 
 
100
        """Create a new versioned file of this exact type.
 
 
102
        :param name: the file name
 
 
103
        :param transport: the transport
 
 
104
        :param mode: optional file mode.
 
 
106
        raise NotImplementedError(self.create_empty)
 
 
108
    def get_suffixes(self):
 
 
109
        """Return the file suffixes associated with this versioned file."""
 
 
110
        raise NotImplementedError(self.get_suffixes)
 
 
112
    def get_text(self, version_id):
 
 
113
        """Return version contents as a text string.
 
 
115
        Raises RevisionNotPresent if version is not present in
 
 
118
        return ''.join(self.get_lines(version_id))
 
 
119
    get_string = get_text
 
 
121
    def get_lines(self, version_id):
 
 
122
        """Return version contents as a sequence of lines.
 
 
124
        Raises RevisionNotPresent if version is not present in
 
 
127
        raise NotImplementedError(self.get_lines)
 
 
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.
 
 
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)
 
 
140
        """Return a graph for the entire versioned file."""
 
 
142
        for version in self.versions():
 
 
143
            result[version] = self.get_parents(version)
 
 
146
    @deprecated_method(zero_eight)
 
 
147
    def parent_names(self, version):
 
 
148
        """Return version names for parents of a version.
 
 
150
        See get_parents for the current api.
 
 
152
        return self.get_parents(version)
 
 
154
    def get_parents(self, version_id):
 
 
155
        """Return version names for parents of a version.
 
 
157
        Must raise RevisionNotPresent if version is not present in
 
 
160
        raise NotImplementedError(self.get_parents)
 
 
162
    def annotate_iter(self, version_id):
 
 
163
        """Yield list of (version-id, line) pairs for the specified
 
 
166
        Must raise RevisionNotPresent if any of the given versions are
 
 
167
        not present in file history.
 
 
169
        raise NotImplementedError(self.annotate_iter)
 
 
171
    def annotate(self, version_id):
 
 
172
        return list(self.annotate_iter(version_id))
 
 
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.
 
 
178
        If version_ids is None all versions from other should be
 
 
179
        incorporated into this versioned file.
 
 
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.
 
 
185
        return InterVersionedFile.get(other, self).join(
 
 
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.
 
 
196
        Must raise RevisionNotPresent if any of the specified versions
 
 
197
        are not present in the file history.
 
 
199
        :param version_ids: the version_ids to walk with respect to. If not
 
 
200
                            supplied the entire weave-like structure is walked.
 
 
202
        raise NotImplementedError(self.walk)
 
 
204
    @deprecated_method(zero_eight)
 
 
205
    def iter_names(self):
 
 
206
        """Walk the names list."""
 
 
207
        return iter(self.versions())
 
 
209
    def plan_merge(self, ver_a, ver_b):
 
 
210
        """Return pseudo-annotation indicating how the two versions merge.
 
 
212
        This is computed between versions a and b and their common
 
 
215
        Weave lines present in none of them are skipped entirely.
 
 
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
 
 
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
 
 
233
                    yield 'killed-a', line
 
 
235
                    yield 'killed-b', line
 
 
237
                    yield 'unchanged', line
 
 
238
            elif insert in inc_a:
 
 
239
                if deleteset & inc_a:
 
 
240
                    yield 'ghost-a', line
 
 
244
            elif insert in inc_b:
 
 
245
                if deleteset & inc_b:
 
 
246
                    yield 'ghost-b', line
 
 
250
                # not in either revision
 
 
251
                yield 'irrelevant', line
 
 
253
        yield 'unchanged', ''           # terminator
 
 
255
    def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
 
 
259
        # TODO: Return a structured form of the conflicts (e.g. 2-tuples for
 
 
260
        # conflicted regions), rather than just inserting the markers.
 
 
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:
 
 
269
                elif ch_a and not ch_b:
 
 
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
 
 
278
                    for l in lines_a: yield l
 
 
280
                    for l in lines_b: yield l
 
 
287
            if state == 'unchanged':
 
 
290
            elif state == 'killed-a':
 
 
293
            elif state == 'killed-b':
 
 
296
            elif state == 'new-a':
 
 
299
            elif state == 'new-b':
 
 
303
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
 
308
class InterVersionedFile(InterObject):
 
 
309
    """This class represents operations taking place between two versionedfiles..
 
 
311
    Its instances have methods like join, and contain
 
 
312
    references to the source and target versionedfiles these operations can be 
 
 
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).
 
 
321
    """The available optimised InterVersionedFile types."""
 
 
323
    def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
 
 
324
        """Integrate versions from self.source into self.target.
 
 
326
        If version_ids is None all versions from source should be
 
 
327
        incorporated into this versioned file.
 
 
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.
 
 
334
        # - make a temporary versioned file of type target
 
 
335
        # - insert the source content into it one at a time
 
 
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()
 
 
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))
 
 
349
            # this should hit the native code path for target
 
 
350
            return self.target.join(temp_source,
 
 
359
class InterVersionedFileTestProviderAdapter(object):
 
 
360
    """A tool to generate a suite testing multiple inter versioned-file classes.
 
 
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.
 
 
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
 
 
373
    def adapt(self, test):
 
 
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)
 
 
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
 
 
397
        # test the fallback InterVersionedFile from weave to annotated knits
 
 
398
        result.append((InterVersionedFile, 
 
 
401
        for optimiser in InterVersionedFile._optimisers:
 
 
402
            result.append((optimiser,
 
 
403
                           optimiser._matching_file_factory,
 
 
404
                           optimiser._matching_file_factory
 
 
406
        # if there are specific combinations we want to use, we can add them