~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: Robert Collins
  • Date: 2006-03-01 03:26:23 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060301032623-9d3c073e102f2239
Move WeaveStore down into bzrlib.store.versioned.weave.

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
class VersionedFile(object):
 
28
    """Versioned text file storage.
 
29
    
 
30
    A versioned file manages versions of line-based text files,
 
31
    keeping track of the originating version for each line.
 
32
 
 
33
    To clients the "lines" of the file are represented as a list of
 
34
    strings. These strings will typically have terminal newline
 
35
    characters, but this is not required.  In particular files commonly
 
36
    do not have a newline at the end of the file.
 
37
 
 
38
    Texts are identified by a version-id string.
 
39
    """
 
40
 
 
41
    def versions(self):
 
42
        """Return a unsorted list of versions."""
 
43
        raise NotImplementedError(self.versions)
 
44
 
 
45
    def has_version(self, version_id):
 
46
        """Returns whether version is present."""
 
47
        raise NotImplementedError(self.has_version)
 
48
 
 
49
    def add_lines(self, version_id, parents, lines):
 
50
        """Add a single text on top of the versioned file.
 
51
 
 
52
        Must raise RevisionAlreadyPresent if the new version is
 
53
        already present in file history.
 
54
 
 
55
        Must raise RevisionNotPresent if any of the given parents are
 
56
        not present in file history."""
 
57
        raise NotImplementedError(self.add_lines)
 
58
 
 
59
    def clear_cache(self):
 
60
        """Remove any data cached in the versioned file object."""
 
61
 
 
62
    def clone_text(self, new_version_id, old_version_id, parents):
 
63
        """Add an identical text to old_version_id as new_version_id.
 
64
 
 
65
        Must raise RevisionNotPresent if the old version or any of the
 
66
        parents are not present in file history.
 
67
 
 
68
        Must raise RevisionAlreadyPresent if the new version is
 
69
        already present in file history."""
 
70
        raise NotImplementedError(self.clone_text)
 
71
 
 
72
    def get_text(self, version_id):
 
73
        """Return version contents as a text string.
 
74
 
 
75
        Raises RevisionNotPresent if version is not present in
 
76
        file history.
 
77
        """
 
78
        return ''.join(self.get_lines(version_id))
 
79
    get_string = get_text
 
80
 
 
81
    def get_lines(self, version_id):
 
82
        """Return version contents as a sequence of lines.
 
83
 
 
84
        Raises RevisionNotPresent if version is not present in
 
85
        file history.
 
86
        """
 
87
        raise NotImplementedError(self.get_lines)
 
88
 
 
89
    def get_ancestry(self, version_ids):
 
90
        """Return a list of all ancestors of given version(s). This
 
91
        will not include the null revision.
 
92
 
 
93
        Must raise RevisionNotPresent if any of the given versions are
 
94
        not present in file history."""
 
95
        if isinstance(version_ids, basestring):
 
96
            version_ids = [version_ids]
 
97
        raise NotImplementedError(self.get_ancestry)
 
98
        
 
99
    def get_graph(self, version_id):
 
100
        """Return a graph.
 
101
 
 
102
        Must raise RevisionNotPresent if version is not present in
 
103
        file history."""
 
104
        raise NotImplementedError(self.get_graph)
 
105
 
 
106
    def get_parents(self, version_id):
 
107
        """Return version names for parents of a version.
 
108
 
 
109
        Must raise RevisionNotPresent if version is not present in
 
110
        file history.
 
111
        """
 
112
        raise NotImplementedError(self.get_parents)
 
113
 
 
114
    def annotate_iter(self, version_id):
 
115
        """Yield list of (version-id, line) pairs for the specified
 
116
        version.
 
117
 
 
118
        Must raise RevisionNotPresent if any of the given versions are
 
119
        not present in file history.
 
120
        """
 
121
        raise NotImplementedError(self.annotate_iter)
 
122
 
 
123
    def annotate(self, version_id):
 
124
        return list(self.annotate_iter(version_id))
 
125
 
 
126
    def join(self, other, pb=None, msg=None, version_ids=None):
 
127
        """Integrate versions from other into this versioned file.
 
128
 
 
129
        If version_ids is None all versions from other should be
 
130
        incorporated into this versioned file.
 
131
 
 
132
        Must raise RevisionNotPresent if any of the specified versions
 
133
        are not present in the other files history."""
 
134
        raise NotImplementedError(self.join)
 
135
 
 
136
    def walk(self, version_ids=None):
 
137
        """Walk the versioned file as a weave-like structure, for
 
138
        versions relative to version_ids.  Yields sequence of (lineno,
 
139
        insert, deletes, text) for each relevant line.
 
140
 
 
141
        Must raise RevisionNotPresent if any of the specified versions
 
142
        are not present in the file history.
 
143
 
 
144
        :param version_ids: the version_ids to walk with respect to. If not
 
145
                            supplied the entire weave-like structure is walked.
 
146
        """
 
147
        raise NotImplementedError(self.walk)
 
148
 
 
149
    def plan_merge(self, ver_a, ver_b):
 
150
        """Return pseudo-annotation indicating how the two versions merge.
 
151
 
 
152
        This is computed between versions a and b and their common
 
153
        base.
 
154
 
 
155
        Weave lines present in none of them are skipped entirely.
 
156
        """
 
157
        inc_a = set(self.inclusions([ver_a]))
 
158
        inc_b = set(self.inclusions([ver_b]))
 
159
        inc_c = inc_a & inc_b
 
160
 
 
161
        for lineno, insert, deleteset, line in self.walk():
 
162
            if deleteset & inc_c:
 
163
                # killed in parent; can't be in either a or b
 
164
                # not relevant to our work
 
165
                yield 'killed-base', line
 
166
            elif insert in inc_c:
 
167
                # was inserted in base
 
168
                killed_a = bool(deleteset & inc_a)
 
169
                killed_b = bool(deleteset & inc_b)
 
170
                if killed_a and killed_b:
 
171
                    yield 'killed-both', line
 
172
                elif killed_a:
 
173
                    yield 'killed-a', line
 
174
                elif killed_b:
 
175
                    yield 'killed-b', line
 
176
                else:
 
177
                    yield 'unchanged', line
 
178
            elif insert in inc_a:
 
179
                if deleteset & inc_a:
 
180
                    yield 'ghost-a', line
 
181
                else:
 
182
                    # new in A; not in B
 
183
                    yield 'new-a', line
 
184
            elif insert in inc_b:
 
185
                if deleteset & inc_b:
 
186
                    yield 'ghost-b', line
 
187
                else:
 
188
                    yield 'new-b', line
 
189
            else:
 
190
                # not in either revision
 
191
                yield 'irrelevant', line
 
192
 
 
193
        yield 'unchanged', ''           # terminator
 
194
 
 
195
    def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
 
196
        lines_a = []
 
197
        lines_b = []
 
198
        ch_a = ch_b = False
 
199
        # TODO: Return a structured form of the conflicts (e.g. 2-tuples for
 
200
        # conflicted regions), rather than just inserting the markers.
 
201
        # 
 
202
        # TODO: Show some version information (e.g. author, date) on 
 
203
        # conflicted regions.
 
204
        for state, line in plan:
 
205
            if state == 'unchanged' or state == 'killed-both':
 
206
                # resync and flush queued conflicts changes if any
 
207
                if not lines_a and not lines_b:
 
208
                    pass
 
209
                elif ch_a and not ch_b:
 
210
                    # one-sided change:                    
 
211
                    for l in lines_a: yield l
 
212
                elif ch_b and not ch_a:
 
213
                    for l in lines_b: yield l
 
214
                elif lines_a == lines_b:
 
215
                    for l in lines_a: yield l
 
216
                else:
 
217
                    yield a_marker
 
218
                    for l in lines_a: yield l
 
219
                    yield '=======\n'
 
220
                    for l in lines_b: yield l
 
221
                    yield b_marker
 
222
 
 
223
                del lines_a[:]
 
224
                del lines_b[:]
 
225
                ch_a = ch_b = False
 
226
                
 
227
            if state == 'unchanged':
 
228
                if line:
 
229
                    yield line
 
230
            elif state == 'killed-a':
 
231
                ch_a = True
 
232
                lines_b.append(line)
 
233
            elif state == 'killed-b':
 
234
                ch_b = True
 
235
                lines_a.append(line)
 
236
            elif state == 'new-a':
 
237
                ch_a = True
 
238
                lines_a.append(line)
 
239
            elif state == 'new-b':
 
240
                ch_b = True
 
241
                lines_b.append(line)
 
242
            else:
 
243
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
244
                                 'killed-both'), \
 
245
                       state