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 *
33
from bzrlib.transport.memory import MemoryTransport
34
from bzrlib.tsort import topo_sort
37
class VersionedFile(object):
38
"""Versioned text file storage.
40
A versioned file manages versions of line-based text files,
41
keeping track of the originating version for each line.
43
To clients the "lines" of the file are represented as a list of
44
strings. These strings will typically have terminal newline
45
characters, but this is not required. In particular files commonly
46
do not have a newline at the end of the file.
48
Texts are identified by a version-id string.
51
def copy_to(self, name, transport):
52
"""Copy this versioned file to name on transport."""
53
raise NotImplementedError(self.copy_to)
55
@deprecated_method(zero_eight)
57
"""Return a list of all the versions in this versioned file.
59
Please use versionedfile.versions() now.
61
return self.versions()
64
"""Return a unsorted list of versions."""
65
raise NotImplementedError(self.versions)
67
def has_version(self, version_id):
68
"""Returns whether version is present."""
69
raise NotImplementedError(self.has_version)
71
def add_lines(self, version_id, parents, lines):
72
"""Add a single text on top of the versioned file.
74
Must raise RevisionAlreadyPresent if the new version is
75
already present in file history.
77
Must raise RevisionNotPresent if any of the given parents are
78
not present in file history."""
79
raise NotImplementedError(self.add_lines)
81
def clear_cache(self):
82
"""Remove any data cached in the versioned file object."""
84
def clone_text(self, new_version_id, old_version_id, parents):
85
"""Add an identical text to old_version_id as new_version_id.
87
Must raise RevisionNotPresent if the old version or any of the
88
parents are not present in file history.
90
Must raise RevisionAlreadyPresent if the new version is
91
already present in file history."""
92
raise NotImplementedError(self.clone_text)
94
def create_empty(self, name, transport, mode=None):
95
"""Create a new versioned file of this exact type.
97
:param name: the file name
98
:param transport: the transport
99
:param mode: optional file mode.
101
raise NotImplementedError(self.create_empty)
103
def get_suffixes(self):
104
"""Return the file suffixes associated with this versioned file."""
105
raise NotImplementedError(self.get_suffixes)
107
def get_text(self, version_id):
108
"""Return version contents as a text string.
110
Raises RevisionNotPresent if version is not present in
113
return ''.join(self.get_lines(version_id))
114
get_string = get_text
116
def get_lines(self, version_id):
117
"""Return version contents as a sequence of lines.
119
Raises RevisionNotPresent if version is not present in
122
raise NotImplementedError(self.get_lines)
124
def get_ancestry(self, version_ids):
125
"""Return a list of all ancestors of given version(s). This
126
will not include the null revision.
128
Must raise RevisionNotPresent if any of the given versions are
129
not present in file history."""
130
if isinstance(version_ids, basestring):
131
version_ids = [version_ids]
132
raise NotImplementedError(self.get_ancestry)
135
"""Return a graph for the entire versioned file."""
137
for version in self.versions():
138
result[version] = self.get_parents(version)
141
@deprecated_method(zero_eight)
142
def parent_names(self, version):
143
"""Return version names for parents of a version.
145
See get_parents for the current api.
147
return self.get_parents(version)
149
def get_parents(self, version_id):
150
"""Return version names for parents of a version.
152
Must raise RevisionNotPresent if version is not present in
155
raise NotImplementedError(self.get_parents)
157
def annotate_iter(self, version_id):
158
"""Yield list of (version-id, line) pairs for the specified
161
Must raise RevisionNotPresent if any of the given versions are
162
not present in file history.
164
raise NotImplementedError(self.annotate_iter)
166
def annotate(self, version_id):
167
return list(self.annotate_iter(version_id))
169
def join(self, other, pb=None, msg=None, version_ids=None):
170
"""Integrate versions from other into this versioned file.
172
If version_ids is None all versions from other should be
173
incorporated into this versioned file.
175
Must raise RevisionNotPresent if any of the specified versions
176
are not present in the other files history."""
177
return InterVersionedFile.get(other, self).join(pb, msg, version_ids)
179
def walk(self, version_ids=None):
180
"""Walk the versioned file as a weave-like structure, for
181
versions relative to version_ids. Yields sequence of (lineno,
182
insert, deletes, text) for each relevant line.
184
Must raise RevisionNotPresent if any of the specified versions
185
are not present in the file history.
187
:param version_ids: the version_ids to walk with respect to. If not
188
supplied the entire weave-like structure is walked.
190
raise NotImplementedError(self.walk)
192
@deprecated_method(zero_eight)
193
def iter_names(self):
194
"""Walk the names list."""
195
return iter(self.versions())
197
def plan_merge(self, ver_a, ver_b):
198
"""Return pseudo-annotation indicating how the two versions merge.
200
This is computed between versions a and b and their common
203
Weave lines present in none of them are skipped entirely.
205
inc_a = set(self.inclusions([ver_a]))
206
inc_b = set(self.inclusions([ver_b]))
207
inc_c = inc_a & inc_b
209
for lineno, insert, deleteset, line in self.walk():
210
if deleteset & inc_c:
211
# killed in parent; can't be in either a or b
212
# not relevant to our work
213
yield 'killed-base', line
214
elif insert in inc_c:
215
# was inserted in base
216
killed_a = bool(deleteset & inc_a)
217
killed_b = bool(deleteset & inc_b)
218
if killed_a and killed_b:
219
yield 'killed-both', line
221
yield 'killed-a', line
223
yield 'killed-b', line
225
yield 'unchanged', line
226
elif insert in inc_a:
227
if deleteset & inc_a:
228
yield 'ghost-a', line
232
elif insert in inc_b:
233
if deleteset & inc_b:
234
yield 'ghost-b', line
238
# not in either revision
239
yield 'irrelevant', line
241
yield 'unchanged', '' # terminator
243
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
247
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
248
# conflicted regions), rather than just inserting the markers.
250
# TODO: Show some version information (e.g. author, date) on
251
# conflicted regions.
252
for state, line in plan:
253
if state == 'unchanged' or state == 'killed-both':
254
# resync and flush queued conflicts changes if any
255
if not lines_a and not lines_b:
257
elif ch_a and not ch_b:
259
for l in lines_a: yield l
260
elif ch_b and not ch_a:
261
for l in lines_b: yield l
262
elif lines_a == lines_b:
263
for l in lines_a: yield l
266
for l in lines_a: yield l
268
for l in lines_b: yield l
275
if state == 'unchanged':
278
elif state == 'killed-a':
281
elif state == 'killed-b':
284
elif state == 'new-a':
287
elif state == 'new-b':
291
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
296
class InterVersionedFile(InterObject):
297
"""This class represents operations taking place between two versionedfiles..
299
Its instances have methods like join, and contain
300
references to the source and target versionedfiles these operations can be
303
Often we will provide convenience methods on 'versionedfile' which carry out
304
operations with another versionedfile - they will always forward to
305
InterVersionedFile.get(other).method_name(parameters).
309
"""The available optimised InterVersionedFile types."""
311
def join(self, pb=None, msg=None, version_ids=None):
312
"""Integrate versions from self.source into self.target.
314
If version_ids is None all versions from source should be
315
incorporated into this versioned file.
317
Must raise RevisionNotPresent if any of the specified versions
318
are not present in the other files history.
321
# - make a temporary versioned file of type target
322
# - insert the source content into it one at a time
324
# Make a new target-format versioned file.
325
temp_source = self.target.create_empty("temp", MemoryTransport())
326
graph = self.source.get_graph()
327
order = topo_sort(graph.items())
328
for version in order:
329
temp_source.add_lines(version,
330
self.source.get_parents(version),
331
self.source.get_lines(version))
333
# this should hit the native code path for target
334
return self.target.join(temp_source, pb, msg, version_ids)
337
class InterVersionedFileTestProviderAdapter(object):
338
"""A tool to generate a suite testing multiple inter versioned-file classes.
340
This is done by copying the test once for each interversionedfile provider
341
and injecting the transport_server, transport_readonly_server,
342
versionedfile_factory and versionedfile_factory_to classes into each copy.
343
Each copy is also given a new id() to make it easy to identify.
346
def __init__(self, transport_server, transport_readonly_server, formats):
347
self._transport_server = transport_server
348
self._transport_readonly_server = transport_readonly_server
349
self._formats = formats
351
def adapt(self, test):
353
for (interversionedfile_class,
354
versionedfile_factory,
355
versionedfile_factory_to) in self._formats:
356
new_test = deepcopy(test)
357
new_test.transport_server = self._transport_server
358
new_test.transport_readonly_server = self._transport_readonly_server
359
new_test.interversionedfile_class = interversionedfile_class
360
new_test.versionedfile_factory = versionedfile_factory
361
new_test.versionedfile_factory_to = versionedfile_factory_to
362
def make_new_test_id():
363
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
364
return lambda: new_id
365
new_test.id = make_new_test_id()
366
result.addTest(new_test)
370
def default_test_list():
371
"""Generate the default list of interversionedfile permutations to test."""
372
from bzrlib.weave import WeaveFile
373
from bzrlib.knit import KnitVersionedFile
375
# test the fallback InterVersionedFile from weave to annotated knits
376
result.append((InterVersionedFile,
379
for optimiser in InterVersionedFile._optimisers:
380
result.append((optimiser,
381
optimiser._matching_file_factory,
382
optimiser._matching_file_factory
384
# if there are specific combinations we want to use, we can add them