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
38
class VersionedFile(object):
39
"""Versioned text file storage.
41
A versioned file manages versions of line-based text files,
42
keeping track of the originating version for each line.
44
To clients the "lines" of the file are represented as a list of
45
strings. These strings will typically have terminal newline
46
characters, but this is not required. In particular files commonly
47
do not have a newline at the end of the file.
49
Texts are identified by a version-id string.
52
def copy_to(self, name, transport):
53
"""Copy this versioned file to name on transport."""
54
raise NotImplementedError(self.copy_to)
56
@deprecated_method(zero_eight)
58
"""Return a list of all the versions in this versioned file.
60
Please use versionedfile.versions() now.
62
return self.versions()
65
"""Return a unsorted list of versions."""
66
raise NotImplementedError(self.versions)
68
def has_version(self, version_id):
69
"""Returns whether version is present."""
70
raise NotImplementedError(self.has_version)
72
def add_lines(self, version_id, parents, lines):
73
"""Add a single text on top of the versioned file.
75
Must raise RevisionAlreadyPresent if the new version is
76
already present in file history.
78
Must raise RevisionNotPresent if any of the given parents are
79
not present in file history."""
80
raise NotImplementedError(self.add_lines)
82
def check(self, progress_bar=None):
83
"""Check the versioned file for integrity."""
84
raise NotImplementedError(self.check)
86
def clear_cache(self):
87
"""Remove any data cached in the versioned file object."""
89
def clone_text(self, new_version_id, old_version_id, parents):
90
"""Add an identical text to old_version_id as new_version_id.
92
Must raise RevisionNotPresent if the old version or any of the
93
parents are not present in file history.
95
Must raise RevisionAlreadyPresent if the new version is
96
already present in file history."""
97
raise NotImplementedError(self.clone_text)
99
def create_empty(self, name, transport, mode=None):
100
"""Create a new versioned file of this exact type.
102
:param name: the file name
103
:param transport: the transport
104
:param mode: optional file mode.
106
raise NotImplementedError(self.create_empty)
108
def fix_parents(self, version, new_parents):
109
"""Fix the parents list for version.
111
This is done by appending a new version to the index
112
with identical data except for the parents list.
113
the parents list must be a superset of the current
116
raise NotImplementedError(self.fix_parents)
118
def get_suffixes(self):
119
"""Return the file suffixes associated with this versioned file."""
120
raise NotImplementedError(self.get_suffixes)
122
def get_text(self, version_id):
123
"""Return version contents as a text string.
125
Raises RevisionNotPresent if version is not present in
128
return ''.join(self.get_lines(version_id))
129
get_string = get_text
131
def get_lines(self, version_id):
132
"""Return version contents as a sequence of lines.
134
Raises RevisionNotPresent if version is not present in
137
raise NotImplementedError(self.get_lines)
139
def get_ancestry(self, version_ids):
140
"""Return a list of all ancestors of given version(s). This
141
will not include the null revision.
143
Must raise RevisionNotPresent if any of the given versions are
144
not present in file history."""
145
if isinstance(version_ids, basestring):
146
version_ids = [version_ids]
147
raise NotImplementedError(self.get_ancestry)
150
"""Return a graph for the entire versioned file."""
152
for version in self.versions():
153
result[version] = self.get_parents(version)
156
@deprecated_method(zero_eight)
157
def parent_names(self, version):
158
"""Return version names for parents of a version.
160
See get_parents for the current api.
162
return self.get_parents(version)
164
def get_parents(self, version_id):
165
"""Return version names for parents of a version.
167
Must raise RevisionNotPresent if version is not present in
170
raise NotImplementedError(self.get_parents)
172
def annotate_iter(self, version_id):
173
"""Yield list of (version-id, line) pairs for the specified
176
Must raise RevisionNotPresent if any of the given versions are
177
not present in file history.
179
raise NotImplementedError(self.annotate_iter)
181
def annotate(self, version_id):
182
return list(self.annotate_iter(version_id))
184
def join(self, other, pb=None, msg=None, version_ids=None,
185
ignore_missing=False):
186
"""Integrate versions from other into this versioned file.
188
If version_ids is None all versions from other should be
189
incorporated into this versioned file.
191
Must raise RevisionNotPresent if any of the specified versions
192
are not present in the other files history unless ignore_missing
193
is supplied when they are silently skipped.
195
return InterVersionedFile.get(other, self).join(
201
def iter_lines_added_or_present_in_versions(self, version_ids=None):
202
"""Iterate over the lines in the versioned file from version_ids.
204
This may return lines from other versions, and does not return the
205
specific version marker at this point. The api may be changed
206
during development to include the version that the versioned file
207
thinks is relevant, but given that such hints are just guesses,
208
its better not to have it if we dont need it.
210
NOTES: Lines are normalised: they will all have \n terminators.
211
Lines are returned in arbitrary order.
213
raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
215
@deprecated_method(zero_eight)
216
def walk(self, version_ids=None):
217
"""Walk the versioned file as a weave-like structure, for
218
versions relative to version_ids. Yields sequence of (lineno,
219
insert, deletes, text) for each relevant line.
221
Must raise RevisionNotPresent if any of the specified versions
222
are not present in the file history.
224
:param version_ids: the version_ids to walk with respect to. If not
225
supplied the entire weave-like structure is walked.
227
walk is deprecated in favour of iter_lines_added_or_present_in_versions
229
raise NotImplementedError(self.walk)
231
@deprecated_method(zero_eight)
232
def iter_names(self):
233
"""Walk the names list."""
234
return iter(self.versions())
236
def plan_merge(self, ver_a, ver_b):
237
"""Return pseudo-annotation indicating how the two versions merge.
239
This is computed between versions a and b and their common
242
Weave lines present in none of them are skipped entirely.
244
inc_a = set(self.get_ancestry([ver_a]))
245
inc_b = set(self.get_ancestry([ver_b]))
246
inc_c = inc_a & inc_b
248
for lineno, insert, deleteset, line in self.walk([ver_a, ver_b]):
249
if deleteset & inc_c:
250
# killed in parent; can't be in either a or b
251
# not relevant to our work
252
yield 'killed-base', line
253
elif insert in inc_c:
254
# was inserted in base
255
killed_a = bool(deleteset & inc_a)
256
killed_b = bool(deleteset & inc_b)
257
if killed_a and killed_b:
258
yield 'killed-both', line
260
yield 'killed-a', line
262
yield 'killed-b', line
264
yield 'unchanged', line
265
elif insert in inc_a:
266
if deleteset & inc_a:
267
yield 'ghost-a', line
271
elif insert in inc_b:
272
if deleteset & inc_b:
273
yield 'ghost-b', line
277
# not in either revision
278
yield 'irrelevant', line
280
yield 'unchanged', '' # terminator
282
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
286
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
287
# conflicted regions), rather than just inserting the markers.
289
# TODO: Show some version information (e.g. author, date) on
290
# conflicted regions.
291
for state, line in plan:
292
if state == 'unchanged' or state == 'killed-both':
293
# resync and flush queued conflicts changes if any
294
if not lines_a and not lines_b:
296
elif ch_a and not ch_b:
298
for l in lines_a: yield l
299
elif ch_b and not ch_a:
300
for l in lines_b: yield l
301
elif lines_a == lines_b:
302
for l in lines_a: yield l
305
for l in lines_a: yield l
307
for l in lines_b: yield l
314
if state == 'unchanged':
317
elif state == 'killed-a':
320
elif state == 'killed-b':
323
elif state == 'new-a':
326
elif state == 'new-b':
330
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
335
class InterVersionedFile(InterObject):
336
"""This class represents operations taking place between two versionedfiles..
338
Its instances have methods like join, and contain
339
references to the source and target versionedfiles these operations can be
342
Often we will provide convenience methods on 'versionedfile' which carry out
343
operations with another versionedfile - they will always forward to
344
InterVersionedFile.get(other).method_name(parameters).
348
"""The available optimised InterVersionedFile types."""
350
def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
351
"""Integrate versions from self.source into self.target.
353
If version_ids is None all versions from source should be
354
incorporated into this versioned file.
356
Must raise RevisionNotPresent if any of the specified versions
357
are not present in the other files history unless ignore_missing is
358
supplied when they are silently skipped.
361
# - make a temporary versioned file of type target
362
# - insert the source content into it one at a time
364
# Make a new target-format versioned file.
365
temp_source = self.target.create_empty("temp", MemoryTransport())
366
graph = self.source.get_graph()
367
order = topo_sort(graph.items())
368
pb = ui.ui_factory.nested_progress_bar()
370
for index, version in enumerate(order):
371
pb.update('Converting versioned data', index, len(order))
372
temp_source.add_lines(version,
373
self.source.get_parents(version),
374
self.source.get_lines(version))
376
# this should hit the native code path for target
377
return self.target.join(temp_source,
386
class InterVersionedFileTestProviderAdapter(object):
387
"""A tool to generate a suite testing multiple inter versioned-file classes.
389
This is done by copying the test once for each interversionedfile provider
390
and injecting the transport_server, transport_readonly_server,
391
versionedfile_factory and versionedfile_factory_to classes into each copy.
392
Each copy is also given a new id() to make it easy to identify.
395
def __init__(self, transport_server, transport_readonly_server, formats):
396
self._transport_server = transport_server
397
self._transport_readonly_server = transport_readonly_server
398
self._formats = formats
400
def adapt(self, test):
402
for (interversionedfile_class,
403
versionedfile_factory,
404
versionedfile_factory_to) in self._formats:
405
new_test = deepcopy(test)
406
new_test.transport_server = self._transport_server
407
new_test.transport_readonly_server = self._transport_readonly_server
408
new_test.interversionedfile_class = interversionedfile_class
409
new_test.versionedfile_factory = versionedfile_factory
410
new_test.versionedfile_factory_to = versionedfile_factory_to
411
def make_new_test_id():
412
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
413
return lambda: new_id
414
new_test.id = make_new_test_id()
415
result.addTest(new_test)
419
def default_test_list():
420
"""Generate the default list of interversionedfile permutations to test."""
421
from bzrlib.weave import WeaveFile
422
from bzrlib.knit import KnitVersionedFile
424
# test the fallback InterVersionedFile from weave to annotated knits
425
result.append((InterVersionedFile,
428
for optimiser in InterVersionedFile._optimisers:
429
result.append((optimiser,
430
optimiser._matching_file_factory,
431
optimiser._matching_file_factory
433
# if there are specific combinations we want to use, we can add them