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
import bzrlib.errors as errors
 
 
32
from bzrlib.inter import InterObject
 
 
33
from bzrlib.symbol_versioning import *
 
 
34
from bzrlib.transport.memory import MemoryTransport
 
 
35
from bzrlib.tsort import topo_sort
 
 
39
class VersionedFile(object):
 
 
40
    """Versioned text file storage.
 
 
42
    A versioned file manages versions of line-based text files,
 
 
43
    keeping track of the originating version for each line.
 
 
45
    To clients the "lines" of the file are represented as a list of
 
 
46
    strings. These strings will typically have terminal newline
 
 
47
    characters, but this is not required.  In particular files commonly
 
 
48
    do not have a newline at the end of the file.
 
 
50
    Texts are identified by a version-id string.
 
 
53
    def __init__(self, access_mode):
 
 
55
        self._access_mode = access_mode
 
 
57
    def copy_to(self, name, transport):
 
 
58
        """Copy this versioned file to name on transport."""
 
 
59
        raise NotImplementedError(self.copy_to)
 
 
61
    @deprecated_method(zero_eight)
 
 
63
        """Return a list of all the versions in this versioned file.
 
 
65
        Please use versionedfile.versions() now.
 
 
67
        return self.versions()
 
 
70
        """Return a unsorted list of versions."""
 
 
71
        raise NotImplementedError(self.versions)
 
 
73
    def has_ghost(self, version_id):
 
 
74
        """Returns whether version is present as a ghost."""
 
 
75
        raise NotImplementedError(self.has_ghost)
 
 
77
    def has_version(self, version_id):
 
 
78
        """Returns whether version is present."""
 
 
79
        raise NotImplementedError(self.has_version)
 
 
81
    def add_lines(self, version_id, parents, lines):
 
 
82
        """Add a single text on top of the versioned file.
 
 
84
        Must raise RevisionAlreadyPresent if the new version is
 
 
85
        already present in file history.
 
 
87
        Must raise RevisionNotPresent if any of the given parents are
 
 
88
        not present in file history.
 
 
90
        self._check_write_ok()
 
 
91
        return self._add_lines(version_id, parents, lines)
 
 
93
    def _add_lines(self, version_id, parents, lines):
 
 
94
        """Helper to do the class specific add_lines."""
 
 
95
        raise NotImplementedError(self.add_lines)
 
 
97
    def add_lines_with_ghosts(self, version_id, parents, lines):
 
 
98
        """Add lines to the versioned file, allowing ghosts to be present."""
 
 
99
        self._check_write_ok()
 
 
100
        return self._add_lines_with_ghosts(version_id, parents, lines)
 
 
102
    def _add_lines_with_ghosts(self, version_id, parents, lines):
 
 
103
        """Helper to do class specific add_lines_with_ghosts."""
 
 
104
        raise NotImplementedError(self.add_lines_with_ghosts)
 
 
106
    def check(self, progress_bar=None):
 
 
107
        """Check the versioned file for integrity."""
 
 
108
        raise NotImplementedError(self.check)
 
 
110
    def _check_write_ok(self):
 
 
111
        """Is the versioned file marked as 'finished' ? Raise if it is."""
 
 
113
            raise errors.OutSideTransaction()
 
 
114
        if self._access_mode != 'w':
 
 
115
            raise errors.ReadOnlyObjectDirtiedError(self)
 
 
117
    def clear_cache(self):
 
 
118
        """Remove any data cached in the versioned file object."""
 
 
120
    def clone_text(self, new_version_id, old_version_id, parents):
 
 
121
        """Add an identical text to old_version_id as new_version_id.
 
 
123
        Must raise RevisionNotPresent if the old version or any of the
 
 
124
        parents are not present in file history.
 
 
126
        Must raise RevisionAlreadyPresent if the new version is
 
 
127
        already present in file history."""
 
 
128
        self._check_write_ok()
 
 
129
        return self._clone_text(new_version_id, old_version_id, parents)
 
 
131
    def _clone_text(self, new_version_id, old_version_id, parents):
 
 
132
        """Helper function to do the _clone_text work."""
 
 
133
        raise NotImplementedError(self.clone_text)
 
 
135
    def create_empty(self, name, transport, mode=None):
 
 
136
        """Create a new versioned file of this exact type.
 
 
138
        :param name: the file name
 
 
139
        :param transport: the transport
 
 
140
        :param mode: optional file mode.
 
 
142
        raise NotImplementedError(self.create_empty)
 
 
144
    def fix_parents(self, version, new_parents):
 
 
145
        """Fix the parents list for version.
 
 
147
        This is done by appending a new version to the index
 
 
148
        with identical data except for the parents list.
 
 
149
        the parents list must be a superset of the current
 
 
152
        self._check_write_ok()
 
 
153
        return self._fix_parents(version, new_parents)
 
 
155
    def _fix_parents(self, version, new_parents):
 
 
156
        """Helper for fix_parents."""
 
 
157
        raise NotImplementedError(self.fix_parents)
 
 
159
    def get_suffixes(self):
 
 
160
        """Return the file suffixes associated with this versioned file."""
 
 
161
        raise NotImplementedError(self.get_suffixes)
 
 
163
    def get_text(self, version_id):
 
 
164
        """Return version contents as a text string.
 
 
166
        Raises RevisionNotPresent if version is not present in
 
 
169
        return ''.join(self.get_lines(version_id))
 
 
170
    get_string = get_text
 
 
172
    def get_lines(self, version_id):
 
 
173
        """Return version contents as a sequence of lines.
 
 
175
        Raises RevisionNotPresent if version is not present in
 
 
178
        raise NotImplementedError(self.get_lines)
 
 
180
    def get_ancestry(self, version_ids):
 
 
181
        """Return a list of all ancestors of given version(s). This
 
 
182
        will not include the null revision.
 
 
184
        Must raise RevisionNotPresent if any of the given versions are
 
 
185
        not present in file history."""
 
 
186
        if isinstance(version_ids, basestring):
 
 
187
            version_ids = [version_ids]
 
 
188
        raise NotImplementedError(self.get_ancestry)
 
 
190
    def get_ancestry_with_ghosts(self, version_ids):
 
 
191
        """Return a list of all ancestors of given version(s). This
 
 
192
        will not include the null revision.
 
 
194
        Must raise RevisionNotPresent if any of the given versions are
 
 
195
        not present in file history.
 
 
197
        Ghosts that are known about will be included in ancestry list,
 
 
198
        but are not explicitly marked.
 
 
200
        raise NotImplementedError(self.get_ancestry_with_ghosts)
 
 
203
        """Return a graph for the entire versioned file.
 
 
205
        Ghosts are not listed or referenced in the graph.
 
 
208
        for version in self.versions():
 
 
209
            result[version] = self.get_parents(version)
 
 
212
    def get_graph_with_ghosts(self):
 
 
213
        """Return a graph for the entire versioned file.
 
 
215
        Ghosts are referenced in parents list but are not
 
 
218
        raise NotImplementedError(self.get_graph_with_ghosts)
 
 
220
    @deprecated_method(zero_eight)
 
 
221
    def parent_names(self, version):
 
 
222
        """Return version names for parents of a version.
 
 
224
        See get_parents for the current api.
 
 
226
        return self.get_parents(version)
 
 
228
    def get_parents(self, version_id):
 
 
229
        """Return version names for parents of a version.
 
 
231
        Must raise RevisionNotPresent if version is not present in
 
 
234
        raise NotImplementedError(self.get_parents)
 
 
236
    def get_parents_with_ghosts(self, version_id):
 
 
237
        """Return version names for parents of version_id.
 
 
239
        Will raise RevisionNotPresent if version_id is not present
 
 
242
        Ghosts that are known about will be included in the parent list,
 
 
243
        but are not explicitly marked.
 
 
245
        raise NotImplementedError(self.get_parents_with_ghosts)
 
 
247
    def annotate_iter(self, version_id):
 
 
248
        """Yield list of (version-id, line) pairs for the specified
 
 
251
        Must raise RevisionNotPresent if any of the given versions are
 
 
252
        not present in file history.
 
 
254
        raise NotImplementedError(self.annotate_iter)
 
 
256
    def annotate(self, version_id):
 
 
257
        return list(self.annotate_iter(version_id))
 
 
259
    def join(self, other, pb=None, msg=None, version_ids=None,
 
 
260
             ignore_missing=False):
 
 
261
        """Integrate versions from other into this versioned file.
 
 
263
        If version_ids is None all versions from other should be
 
 
264
        incorporated into this versioned file.
 
 
266
        Must raise RevisionNotPresent if any of the specified versions
 
 
267
        are not present in the other files history unless ignore_missing
 
 
268
        is supplied when they are silently skipped.
 
 
270
        self._check_write_ok()
 
 
271
        return InterVersionedFile.get(other, self).join(
 
 
277
    def iter_lines_added_or_present_in_versions(self, version_ids=None):
 
 
278
        """Iterate over the lines in the versioned file from version_ids.
 
 
280
        This may return lines from other versions, and does not return the
 
 
281
        specific version marker at this point. The api may be changed
 
 
282
        during development to include the version that the versioned file
 
 
283
        thinks is relevant, but given that such hints are just guesses,
 
 
284
        its better not to have it if we dont need it.
 
 
286
        NOTES: Lines are normalised: they will all have \n terminators.
 
 
287
               Lines are returned in arbitrary order.
 
 
289
        raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
 
 
291
    def transaction_finished(self):
 
 
292
        """The transaction that this file was opened in has finished.
 
 
294
        This records self.finished = True and should cause all mutating
 
 
299
    @deprecated_method(zero_eight)
 
 
300
    def walk(self, version_ids=None):
 
 
301
        """Walk the versioned file as a weave-like structure, for
 
 
302
        versions relative to version_ids.  Yields sequence of (lineno,
 
 
303
        insert, deletes, text) for each relevant line.
 
 
305
        Must raise RevisionNotPresent if any of the specified versions
 
 
306
        are not present in the file history.
 
 
308
        :param version_ids: the version_ids to walk with respect to. If not
 
 
309
                            supplied the entire weave-like structure is walked.
 
 
311
        walk is deprecated in favour of iter_lines_added_or_present_in_versions
 
 
313
        raise NotImplementedError(self.walk)
 
 
315
    @deprecated_method(zero_eight)
 
 
316
    def iter_names(self):
 
 
317
        """Walk the names list."""
 
 
318
        return iter(self.versions())
 
 
320
    def plan_merge(self, ver_a, ver_b):
 
 
321
        """Return pseudo-annotation indicating how the two versions merge.
 
 
323
        This is computed between versions a and b and their common
 
 
326
        Weave lines present in none of them are skipped entirely.
 
 
328
        inc_a = set(self.get_ancestry([ver_a]))
 
 
329
        inc_b = set(self.get_ancestry([ver_b]))
 
 
330
        inc_c = inc_a & inc_b
 
 
332
        for lineno, insert, deleteset, line in self.walk([ver_a, ver_b]):
 
 
333
            if deleteset & inc_c:
 
 
334
                # killed in parent; can't be in either a or b
 
 
335
                # not relevant to our work
 
 
336
                yield 'killed-base', line
 
 
337
            elif insert in inc_c:
 
 
338
                # was inserted in base
 
 
339
                killed_a = bool(deleteset & inc_a)
 
 
340
                killed_b = bool(deleteset & inc_b)
 
 
341
                if killed_a and killed_b:
 
 
342
                    yield 'killed-both', line
 
 
344
                    yield 'killed-a', line
 
 
346
                    yield 'killed-b', line
 
 
348
                    yield 'unchanged', line
 
 
349
            elif insert in inc_a:
 
 
350
                if deleteset & inc_a:
 
 
351
                    yield 'ghost-a', line
 
 
355
            elif insert in inc_b:
 
 
356
                if deleteset & inc_b:
 
 
357
                    yield 'ghost-b', line
 
 
361
                # not in either revision
 
 
362
                yield 'irrelevant', line
 
 
364
        yield 'unchanged', ''           # terminator
 
 
366
    def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
 
 
370
        # TODO: Return a structured form of the conflicts (e.g. 2-tuples for
 
 
371
        # conflicted regions), rather than just inserting the markers.
 
 
373
        # TODO: Show some version information (e.g. author, date) on 
 
 
374
        # conflicted regions.
 
 
375
        for state, line in plan:
 
 
376
            if state == 'unchanged' or state == 'killed-both':
 
 
377
                # resync and flush queued conflicts changes if any
 
 
378
                if not lines_a and not lines_b:
 
 
380
                elif ch_a and not ch_b:
 
 
382
                    for l in lines_a: yield l
 
 
383
                elif ch_b and not ch_a:
 
 
384
                    for l in lines_b: yield l
 
 
385
                elif lines_a == lines_b:
 
 
386
                    for l in lines_a: yield l
 
 
389
                    for l in lines_a: yield l
 
 
391
                    for l in lines_b: yield l
 
 
398
            if state == 'unchanged':
 
 
401
            elif state == 'killed-a':
 
 
404
            elif state == 'killed-b':
 
 
407
            elif state == 'new-a':
 
 
410
            elif state == 'new-b':
 
 
414
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
 
419
class InterVersionedFile(InterObject):
 
 
420
    """This class represents operations taking place between two versionedfiles..
 
 
422
    Its instances have methods like join, and contain
 
 
423
    references to the source and target versionedfiles these operations can be 
 
 
426
    Often we will provide convenience methods on 'versionedfile' which carry out
 
 
427
    operations with another versionedfile - they will always forward to
 
 
428
    InterVersionedFile.get(other).method_name(parameters).
 
 
432
    """The available optimised InterVersionedFile types."""
 
 
434
    def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
 
 
435
        """Integrate versions from self.source into self.target.
 
 
437
        If version_ids is None all versions from source should be
 
 
438
        incorporated into this versioned file.
 
 
440
        Must raise RevisionNotPresent if any of the specified versions
 
 
441
        are not present in the other files history unless ignore_missing is 
 
 
442
        supplied when they are silently skipped.
 
 
445
        # - if the target is empty, just add all the versions from 
 
 
446
        #   source to target, otherwise:
 
 
447
        # - make a temporary versioned file of type target
 
 
448
        # - insert the source content into it one at a time
 
 
450
        if not self.target.versions():
 
 
453
            # Make a new target-format versioned file. 
 
 
454
            temp_source = self.target.create_empty("temp", MemoryTransport())
 
 
456
        graph = self.source.get_graph()
 
 
457
        order = topo_sort(graph.items())
 
 
458
        pb = ui.ui_factory.nested_progress_bar()
 
 
460
            for index, version in enumerate(order):
 
 
461
                pb.update('Converting versioned data', index, len(order))
 
 
462
                target.add_lines(version,
 
 
463
                                 self.source.get_parents(version),
 
 
464
                                 self.source.get_lines(version))
 
 
466
            # this should hit the native code path for target
 
 
467
            if target is not self.target:
 
 
468
                return self.target.join(temp_source,
 
 
477
class InterVersionedFileTestProviderAdapter(object):
 
 
478
    """A tool to generate a suite testing multiple inter versioned-file classes.
 
 
480
    This is done by copying the test once for each interversionedfile provider
 
 
481
    and injecting the transport_server, transport_readonly_server,
 
 
482
    versionedfile_factory and versionedfile_factory_to classes into each copy.
 
 
483
    Each copy is also given a new id() to make it easy to identify.
 
 
486
    def __init__(self, transport_server, transport_readonly_server, formats):
 
 
487
        self._transport_server = transport_server
 
 
488
        self._transport_readonly_server = transport_readonly_server
 
 
489
        self._formats = formats
 
 
491
    def adapt(self, test):
 
 
493
        for (interversionedfile_class,
 
 
494
             versionedfile_factory,
 
 
495
             versionedfile_factory_to) in self._formats:
 
 
496
            new_test = deepcopy(test)
 
 
497
            new_test.transport_server = self._transport_server
 
 
498
            new_test.transport_readonly_server = self._transport_readonly_server
 
 
499
            new_test.interversionedfile_class = interversionedfile_class
 
 
500
            new_test.versionedfile_factory = versionedfile_factory
 
 
501
            new_test.versionedfile_factory_to = versionedfile_factory_to
 
 
502
            def make_new_test_id():
 
 
503
                new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
 
 
504
                return lambda: new_id
 
 
505
            new_test.id = make_new_test_id()
 
 
506
            result.addTest(new_test)
 
 
510
    def default_test_list():
 
 
511
        """Generate the default list of interversionedfile permutations to test."""
 
 
512
        from bzrlib.weave import WeaveFile
 
 
513
        from bzrlib.knit import KnitVersionedFile
 
 
515
        # test the fallback InterVersionedFile from weave to annotated knits
 
 
516
        result.append((InterVersionedFile, 
 
 
519
        for optimiser in InterVersionedFile._optimisers:
 
 
520
            result.append((optimiser,
 
 
521
                           optimiser._matching_file_factory,
 
 
522
                           optimiser._matching_file_factory
 
 
524
        # if there are specific combinations we want to use, we can add them