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 *
35
class VersionedFile(object):
36
"""Versioned text file storage.
38
A versioned file manages versions of line-based text files,
39
keeping track of the originating version for each line.
41
To clients the "lines" of the file are represented as a list of
42
strings. These strings will typically have terminal newline
43
characters, but this is not required. In particular files commonly
44
do not have a newline at the end of the file.
46
Texts are identified by a version-id string.
49
@deprecated_method(zero_eight)
51
"""Return a list of all the versions in this versioned file.
53
Please use versionedfile.versions() now.
55
return self.versions()
58
"""Return a unsorted list of versions."""
59
raise NotImplementedError(self.versions)
61
def has_version(self, version_id):
62
"""Returns whether version is present."""
63
raise NotImplementedError(self.has_version)
65
def add_lines(self, version_id, parents, lines):
66
"""Add a single text on top of the versioned file.
68
Must raise RevisionAlreadyPresent if the new version is
69
already present in file history.
71
Must raise RevisionNotPresent if any of the given parents are
72
not present in file history."""
73
raise NotImplementedError(self.add_lines)
75
def clear_cache(self):
76
"""Remove any data cached in the versioned file object."""
78
def clone_text(self, new_version_id, old_version_id, parents):
79
"""Add an identical text to old_version_id as new_version_id.
81
Must raise RevisionNotPresent if the old version or any of the
82
parents are not present in file history.
84
Must raise RevisionAlreadyPresent if the new version is
85
already present in file history."""
86
raise NotImplementedError(self.clone_text)
88
def get_text(self, version_id):
89
"""Return version contents as a text string.
91
Raises RevisionNotPresent if version is not present in
94
return ''.join(self.get_lines(version_id))
97
def get_lines(self, version_id):
98
"""Return version contents as a sequence of lines.
100
Raises RevisionNotPresent if version is not present in
103
raise NotImplementedError(self.get_lines)
105
def get_ancestry(self, version_ids):
106
"""Return a list of all ancestors of given version(s). This
107
will not include the null revision.
109
Must raise RevisionNotPresent if any of the given versions are
110
not present in file history."""
111
if isinstance(version_ids, basestring):
112
version_ids = [version_ids]
113
raise NotImplementedError(self.get_ancestry)
115
def get_graph(self, version_id):
118
Must raise RevisionNotPresent if version is not present in
120
raise NotImplementedError(self.get_graph)
122
@deprecated_method(zero_eight)
123
def parent_names(self, version):
124
"""Return version names for parents of a version.
126
See get_parents for the current api.
128
return self.get_parents(version)
130
def get_parents(self, version_id):
131
"""Return version names for parents of a version.
133
Must raise RevisionNotPresent if version is not present in
136
raise NotImplementedError(self.get_parents)
138
def annotate_iter(self, version_id):
139
"""Yield list of (version-id, line) pairs for the specified
142
Must raise RevisionNotPresent if any of the given versions are
143
not present in file history.
145
raise NotImplementedError(self.annotate_iter)
147
def annotate(self, version_id):
148
return list(self.annotate_iter(version_id))
150
def join(self, other, pb=None, msg=None, version_ids=None):
151
"""Integrate versions from other into this versioned file.
153
If version_ids is None all versions from other should be
154
incorporated into this versioned file.
156
Must raise RevisionNotPresent if any of the specified versions
157
are not present in the other files history."""
158
raise NotImplementedError(self.join)
160
def walk(self, version_ids=None):
161
"""Walk the versioned file as a weave-like structure, for
162
versions relative to version_ids. Yields sequence of (lineno,
163
insert, deletes, text) for each relevant line.
165
Must raise RevisionNotPresent if any of the specified versions
166
are not present in the file history.
168
:param version_ids: the version_ids to walk with respect to. If not
169
supplied the entire weave-like structure is walked.
171
raise NotImplementedError(self.walk)
173
@deprecated_method(zero_eight)
174
def iter_names(self):
175
"""Walk the names list."""
176
return iter(self.versions())
178
def plan_merge(self, ver_a, ver_b):
179
"""Return pseudo-annotation indicating how the two versions merge.
181
This is computed between versions a and b and their common
184
Weave lines present in none of them are skipped entirely.
186
inc_a = set(self.inclusions([ver_a]))
187
inc_b = set(self.inclusions([ver_b]))
188
inc_c = inc_a & inc_b
190
for lineno, insert, deleteset, line in self.walk():
191
if deleteset & inc_c:
192
# killed in parent; can't be in either a or b
193
# not relevant to our work
194
yield 'killed-base', line
195
elif insert in inc_c:
196
# was inserted in base
197
killed_a = bool(deleteset & inc_a)
198
killed_b = bool(deleteset & inc_b)
199
if killed_a and killed_b:
200
yield 'killed-both', line
202
yield 'killed-a', line
204
yield 'killed-b', line
206
yield 'unchanged', line
207
elif insert in inc_a:
208
if deleteset & inc_a:
209
yield 'ghost-a', line
213
elif insert in inc_b:
214
if deleteset & inc_b:
215
yield 'ghost-b', line
219
# not in either revision
220
yield 'irrelevant', line
222
yield 'unchanged', '' # terminator
224
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
228
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
229
# conflicted regions), rather than just inserting the markers.
231
# TODO: Show some version information (e.g. author, date) on
232
# conflicted regions.
233
for state, line in plan:
234
if state == 'unchanged' or state == 'killed-both':
235
# resync and flush queued conflicts changes if any
236
if not lines_a and not lines_b:
238
elif ch_a and not ch_b:
240
for l in lines_a: yield l
241
elif ch_b and not ch_a:
242
for l in lines_b: yield l
243
elif lines_a == lines_b:
244
for l in lines_a: yield l
247
for l in lines_a: yield l
249
for l in lines_b: yield l
256
if state == 'unchanged':
259
elif state == 'killed-a':
262
elif state == 'killed-b':
265
elif state == 'new-a':
268
elif state == 'new-b':
272
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
277
class InterVersionedFile(InterObject):
278
"""This class represents operations taking place between two versionedfiles..
280
Its instances have methods like join, and contain
281
references to the source and target versionedfiles these operations can be
284
Often we will provide convenience methods on 'versionedfile' which carry out
285
operations with another versionedfile - they will always forward to
286
InterVersionedFile.get(other).method_name(parameters).
290
"""The available optimised InterVersionedFile types."""
293
class InterVersionedFileTestProviderAdapter(object):
294
"""A tool to generate a suite testing multiple inter versioned-file classes.
296
This is done by copying the test once for each interversionedfile provider
297
and injecting the transport_server, transport_readonly_server,
298
versionedfile_factory and versionedfile_factory_to classes into each copy.
299
Each copy is also given a new id() to make it easy to identify.
302
def __init__(self, transport_server, transport_readonly_server, formats):
303
self._transport_server = transport_server
304
self._transport_readonly_server = transport_readonly_server
305
self._formats = formats
307
def adapt(self, test):
309
for (interversionedfile_class,
310
versionedfile_factory,
311
versionedfile_factory_to) in self._formats:
312
new_test = deepcopy(test)
313
new_test.transport_server = self._transport_server
314
new_test.transport_readonly_server = self._transport_readonly_server
315
new_test.interversionedfile_class = interversionedfile_class
316
new_test.versionedfile_factory = versionedfile_factory
317
new_test.versionedfile_factory_to = versionedfile_factory_to
318
def make_new_test_id():
319
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
320
return lambda: new_id
321
new_test.id = make_new_test_id()
322
result.addTest(new_test)
326
def default_test_list():
327
"""Generate the default list of interversionedfile permutations to test."""
328
from bzrlib.weave import WeaveFile
329
from bzrlib.knit import AnnotatedKnitFactory
331
# test the fallback InterVersionedFile from weave to annotated knits
332
result.append((InterVersionedFile,
334
AnnotatedKnitFactory))
335
for optimiser in InterVersionedFile._optimisers:
336
result.append((optimiser,
337
optimiser._matching_file_factory,
338
optimiser._matching_file_factory
340
# if there are specific combinations we want to use, we can add them