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_text)
59
def clone_text(self, new_version_id, old_version_id, parents,
61
"""Add an identical text to old_version_id as new_version_id.
63
Must raise RevisionNotPresent if the old version or any of the
64
parents are not present in file history.
66
Must raise RevisionAlreadyPresent if the new version is
67
already present in file history."""
68
raise NotImplementedError(self.clone_text)
70
def get_text(self, version_id):
71
"""Return version contents as a text string.
73
Raises RevisionNotPresent if version is not present in
76
return ''.join(self.get_lines(version_id))
79
def get_lines(self, version_id):
80
"""Return version contents as a sequence of lines.
82
Raises RevisionNotPresent if version is not present in
85
raise NotImplementedError(self.get_lines)
87
def get_ancestry(self, version_ids):
88
"""Return a list of all ancestors of given version(s). This
89
will not include the null revision.
91
Must raise RevisionNotPresent if any of the given versions are
92
not present in file history."""
93
if isinstance(version_ids, basestring):
94
version_ids = [version_ids]
95
raise NotImplementedError(self.get_ancestry)
97
def get_graph(self, version_id):
100
Must raise RevisionNotPresent if version is not present in
102
raise NotImplementedError(self.get_graph)
104
def get_parents(self, version_id):
105
"""Return version names for parents of a version.
107
Must raise RevisionNotPresent if version is not present in
110
raise NotImplementedError(self.get_parents)
112
def annotate_iter(self, version_id):
113
"""Yield list of (version-id, line) pairs for the specified
116
Must raise RevisionNotPresent if any of the given versions are
117
not present in file history.
119
raise NotImplementedError(self.annotate_iter)
121
def annotate(self, version_id):
122
return list(self.annotate_iter(version_id))
124
def join(self, other, version_ids, transaction, pb=None):
125
"""Integrate versions from other into this versioned file.
127
If version_ids is None all versions from other should be
128
incorporated into this versioned file.
130
Must raise RevisionNotPresent if any of the specified versions
131
are not present in the other files history."""
132
raise NotImplementedError(self.join)
134
def walk(self, version_ids=None):
135
"""Walk the versioned file as a weave-like structure, for
136
versions relative to version_ids. Yields sequence of (lineno,
137
insert, deletes, text) for each relevant line.
139
Must raise RevisionNotPresent if any of the specified versions
140
are not present in the file history.
142
:param version_ids: the version_ids to walk with respect to. If not
143
supplied the entire weave-like structure is walked.
145
raise NotImplementedError(self.walk)
147
def plan_merge(self, ver_a, ver_b):
148
"""Return pseudo-annotation indicating how the two versions merge.
150
This is computed between versions a and b and their common
153
Weave lines present in none of them are skipped entirely.
155
inc_a = set(self.inclusions([ver_a]))
156
inc_b = set(self.inclusions([ver_b]))
157
inc_c = inc_a & inc_b
159
for lineno, insert, deleteset, line in self.walk():
160
if deleteset & inc_c:
161
# killed in parent; can't be in either a or b
162
# not relevant to our work
163
yield 'killed-base', line
164
elif insert in inc_c:
165
# was inserted in base
166
killed_a = bool(deleteset & inc_a)
167
killed_b = bool(deleteset & inc_b)
168
if killed_a and killed_b:
169
yield 'killed-both', line
171
yield 'killed-a', line
173
yield 'killed-b', line
175
yield 'unchanged', line
176
elif insert in inc_a:
177
if deleteset & inc_a:
178
yield 'ghost-a', line
182
elif insert in inc_b:
183
if deleteset & inc_b:
184
yield 'ghost-b', line
188
# not in either revision
189
yield 'irrelevant', line
191
yield 'unchanged', '' # terminator
193
def weave_merge(self, plan):
197
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
198
# conflicted regions), rather than just inserting the markers.
200
# TODO: Show some version information (e.g. author, date) on
201
# conflicted regions.
202
for state, line in plan:
203
if state == 'unchanged' or state == 'killed-both':
204
# resync and flush queued conflicts changes if any
205
if not lines_a and not lines_b:
207
elif ch_a and not ch_b:
209
for l in lines_a: yield l
210
elif ch_b and not ch_a:
211
for l in lines_b: yield l
212
elif lines_a == lines_b:
213
for l in lines_a: yield l
216
for l in lines_a: yield l
218
for l in lines_b: yield l
225
if state == 'unchanged':
228
elif state == 'killed-a':
231
elif state == 'killed-b':
234
elif state == 'new-a':
237
elif state == 'new-b':
241
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
246
def plan_merge(file, version_a, version_b):
247
"""Return pseudo-annotation indicating how the two versions merge.
249
This is computed between versions a and b and their common
252
Weave lines present in none of them are skipped entirely.
254
inc_a = set(file.get_ancestry([version_a]))
255
inc_b = set(file.get_ancestry([version_b]))
256
inc_c = inc_a & inc_b
258
for lineno, insert, deleteset, line in file.walk([version_a, version_b]):
259
if deleteset & inc_c:
260
# killed in parent; can't be in either a or b
261
# not relevant to our work
262
yield 'killed-base', line
263
elif insert in inc_c:
264
# was inserted in base
265
killed_a = bool(deleteset & inc_a)
266
killed_b = bool(deleteset & inc_b)
267
if killed_a and killed_b:
268
yield 'killed-both', line
270
yield 'killed-a', line
272
yield 'killed-b', line
274
yield 'unchanged', line
275
elif insert in inc_a:
276
if deleteset & inc_a:
277
yield 'ghost-a', line
281
elif insert in inc_b:
282
if deleteset & inc_b:
283
yield 'ghost-b', line
287
# not in either revision
288
yield 'irrelevant', line
290
yield 'unchanged', '' # terminator
293
def weave_merge(plan):
294
"""Yield merged sequence of lines based on merge plan."""
300
for state, line in plan:
301
if state == 'unchanged' or state == 'killed-both':
302
# resync and flush queued conflicts changes if any
303
if not lines_a and not lines_b:
305
elif ch_a and not ch_b:
307
for l in lines_a: yield l
308
elif ch_b and not ch_a:
309
for l in lines_b: yield l
310
elif lines_a == lines_b:
311
for l in lines_a: yield l
314
for l in lines_a: yield l
316
for l in lines_b: yield l
323
if state == 'unchanged':
326
elif state == 'killed-a':
329
elif state == 'killed-b':
332
elif state == 'new-a':
335
elif state == 'new-b':
339
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
340
'killed-both'), state