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
class VersionedFile(object):
28
"""Versioned text file storage.
30
A versioned file manages versions of line-based text files,
31
keeping track of the originating version for each line.
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.
38
Texts are identified by a version-id string.
42
"""Return a unsorted list of versions."""
43
raise NotImplementedError(self.versions)
45
def has_version(self, version_id):
46
"""Returns whether version is present."""
47
raise NotImplementedError(self.has_version)
49
def add_lines(self, version_id, parents, lines):
50
"""Add a single text on top of the versioned file.
52
Must raise RevisionAlreadyPresent if the new version is
53
already present in file history.
55
Must raise RevisionNotPresent if any of the given parents are
56
not present in file history."""
57
raise NotImplementedError(self.add_lines)
59
def clone_text(self, new_version_id, old_version_id, parents):
60
"""Add an identical text to old_version_id as new_version_id.
62
Must raise RevisionNotPresent if the old version or any of the
63
parents are not present in file history.
65
Must raise RevisionAlreadyPresent if the new version is
66
already present in file history."""
67
raise NotImplementedError(self.clone_text)
69
def get_text(self, version_id):
70
"""Return version contents as a text string.
72
Raises RevisionNotPresent if version is not present in
75
return ''.join(self.get_lines(version_id))
78
def get_lines(self, version_id):
79
"""Return version contents as a sequence of lines.
81
Raises RevisionNotPresent if version is not present in
84
raise NotImplementedError(self.get_lines)
86
def get_ancestry(self, version_ids):
87
"""Return a list of all ancestors of given version(s). This
88
will not include the null revision.
90
Must raise RevisionNotPresent if any of the given versions are
91
not present in file history."""
92
if isinstance(version_ids, basestring):
93
version_ids = [version_ids]
94
raise NotImplementedError(self.get_ancestry)
96
def get_graph(self, version_id):
99
Must raise RevisionNotPresent if version is not present in
101
raise NotImplementedError(self.get_graph)
103
def get_parents(self, version_id):
104
"""Return version names for parents of a version.
106
Must raise RevisionNotPresent if version is not present in
109
raise NotImplementedError(self.get_parents)
111
def annotate_iter(self, version_id):
112
"""Yield list of (version-id, line) pairs for the specified
115
Must raise RevisionNotPresent if any of the given versions are
116
not present in file history.
118
raise NotImplementedError(self.annotate_iter)
120
def annotate(self, version_id):
121
return list(self.annotate_iter(version_id))
123
def join(self, other, pb=None, msg=None, version_ids=None):
124
"""Integrate versions from other into this versioned file.
126
If version_ids is None all versions from other should be
127
incorporated into this versioned file.
129
Must raise RevisionNotPresent if any of the specified versions
130
are not present in the other files history."""
131
raise NotImplementedError(self.join)
133
def walk(self, version_ids=None):
134
"""Walk the versioned file as a weave-like structure, for
135
versions relative to version_ids. Yields sequence of (lineno,
136
insert, deletes, text) for each relevant line.
138
Must raise RevisionNotPresent if any of the specified versions
139
are not present in the file history.
141
:param version_ids: the version_ids to walk with respect to. If not
142
supplied the entire weave-like structure is walked.
144
raise NotImplementedError(self.walk)
146
def plan_merge(self, ver_a, ver_b):
147
"""Return pseudo-annotation indicating how the two versions merge.
149
This is computed between versions a and b and their common
152
Weave lines present in none of them are skipped entirely.
154
inc_a = set(self.inclusions([ver_a]))
155
inc_b = set(self.inclusions([ver_b]))
156
inc_c = inc_a & inc_b
158
for lineno, insert, deleteset, line in self.walk():
159
if deleteset & inc_c:
160
# killed in parent; can't be in either a or b
161
# not relevant to our work
162
yield 'killed-base', line
163
elif insert in inc_c:
164
# was inserted in base
165
killed_a = bool(deleteset & inc_a)
166
killed_b = bool(deleteset & inc_b)
167
if killed_a and killed_b:
168
yield 'killed-both', line
170
yield 'killed-a', line
172
yield 'killed-b', line
174
yield 'unchanged', line
175
elif insert in inc_a:
176
if deleteset & inc_a:
177
yield 'ghost-a', line
181
elif insert in inc_b:
182
if deleteset & inc_b:
183
yield 'ghost-b', line
187
# not in either revision
188
yield 'irrelevant', line
190
yield 'unchanged', '' # terminator
192
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
196
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
197
# conflicted regions), rather than just inserting the markers.
199
# TODO: Show some version information (e.g. author, date) on
200
# conflicted regions.
201
for state, line in plan:
202
if state == 'unchanged' or state == 'killed-both':
203
# resync and flush queued conflicts changes if any
204
if not lines_a and not lines_b:
206
elif ch_a and not ch_b:
208
for l in lines_a: yield l
209
elif ch_b and not ch_a:
210
for l in lines_b: yield l
211
elif lines_a == lines_b:
212
for l in lines_a: yield l
215
for l in lines_a: yield l
217
for l in lines_b: yield l
224
if state == 'unchanged':
227
elif state == 'killed-a':
230
elif state == 'killed-b':
233
elif state == 'new-a':
236
elif state == 'new-b':
240
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',