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 get_suffixes(self):
109
"""Return the file suffixes associated with this versioned file."""
110
raise NotImplementedError(self.get_suffixes)
112
def get_text(self, version_id):
113
"""Return version contents as a text string.
115
Raises RevisionNotPresent if version is not present in
118
return ''.join(self.get_lines(version_id))
119
get_string = get_text
121
def get_lines(self, version_id):
122
"""Return version contents as a sequence of lines.
124
Raises RevisionNotPresent if version is not present in
127
raise NotImplementedError(self.get_lines)
129
def get_ancestry(self, version_ids):
130
"""Return a list of all ancestors of given version(s). This
131
will not include the null revision.
133
Must raise RevisionNotPresent if any of the given versions are
134
not present in file history."""
135
if isinstance(version_ids, basestring):
136
version_ids = [version_ids]
137
raise NotImplementedError(self.get_ancestry)
140
"""Return a graph for the entire versioned file."""
142
for version in self.versions():
143
result[version] = self.get_parents(version)
146
@deprecated_method(zero_eight)
147
def parent_names(self, version):
148
"""Return version names for parents of a version.
150
See get_parents for the current api.
152
return self.get_parents(version)
154
def get_parents(self, version_id):
155
"""Return version names for parents of a version.
157
Must raise RevisionNotPresent if version is not present in
160
raise NotImplementedError(self.get_parents)
162
def annotate_iter(self, version_id):
163
"""Yield list of (version-id, line) pairs for the specified
166
Must raise RevisionNotPresent if any of the given versions are
167
not present in file history.
169
raise NotImplementedError(self.annotate_iter)
171
def annotate(self, version_id):
172
return list(self.annotate_iter(version_id))
174
def join(self, other, pb=None, msg=None, version_ids=None,
175
ignore_missing=False):
176
"""Integrate versions from other into this versioned file.
178
If version_ids is None all versions from other should be
179
incorporated into this versioned file.
181
Must raise RevisionNotPresent if any of the specified versions
182
are not present in the other files history unless ignore_missing
183
is supplied when they are silently skipped.
185
return InterVersionedFile.get(other, self).join(
191
def walk(self, version_ids=None):
192
"""Walk the versioned file as a weave-like structure, for
193
versions relative to version_ids. Yields sequence of (lineno,
194
insert, deletes, text) for each relevant line.
196
Must raise RevisionNotPresent if any of the specified versions
197
are not present in the file history.
199
:param version_ids: the version_ids to walk with respect to. If not
200
supplied the entire weave-like structure is walked.
202
raise NotImplementedError(self.walk)
204
@deprecated_method(zero_eight)
205
def iter_names(self):
206
"""Walk the names list."""
207
return iter(self.versions())
209
def plan_merge(self, ver_a, ver_b):
210
"""Return pseudo-annotation indicating how the two versions merge.
212
This is computed between versions a and b and their common
215
Weave lines present in none of them are skipped entirely.
217
inc_a = set(self.get_ancestry([ver_a]))
218
inc_b = set(self.get_ancestry([ver_b]))
219
inc_c = inc_a & inc_b
221
for lineno, insert, deleteset, line in self.walk():
222
if deleteset & inc_c:
223
# killed in parent; can't be in either a or b
224
# not relevant to our work
225
yield 'killed-base', line
226
elif insert in inc_c:
227
# was inserted in base
228
killed_a = bool(deleteset & inc_a)
229
killed_b = bool(deleteset & inc_b)
230
if killed_a and killed_b:
231
yield 'killed-both', line
233
yield 'killed-a', line
235
yield 'killed-b', line
237
yield 'unchanged', line
238
elif insert in inc_a:
239
if deleteset & inc_a:
240
yield 'ghost-a', line
244
elif insert in inc_b:
245
if deleteset & inc_b:
246
yield 'ghost-b', line
250
# not in either revision
251
yield 'irrelevant', line
253
yield 'unchanged', '' # terminator
255
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
259
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
260
# conflicted regions), rather than just inserting the markers.
262
# TODO: Show some version information (e.g. author, date) on
263
# conflicted regions.
264
for state, line in plan:
265
if state == 'unchanged' or state == 'killed-both':
266
# resync and flush queued conflicts changes if any
267
if not lines_a and not lines_b:
269
elif ch_a and not ch_b:
271
for l in lines_a: yield l
272
elif ch_b and not ch_a:
273
for l in lines_b: yield l
274
elif lines_a == lines_b:
275
for l in lines_a: yield l
278
for l in lines_a: yield l
280
for l in lines_b: yield l
287
if state == 'unchanged':
290
elif state == 'killed-a':
293
elif state == 'killed-b':
296
elif state == 'new-a':
299
elif state == 'new-b':
303
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
308
class InterVersionedFile(InterObject):
309
"""This class represents operations taking place between two versionedfiles..
311
Its instances have methods like join, and contain
312
references to the source and target versionedfiles these operations can be
315
Often we will provide convenience methods on 'versionedfile' which carry out
316
operations with another versionedfile - they will always forward to
317
InterVersionedFile.get(other).method_name(parameters).
321
"""The available optimised InterVersionedFile types."""
323
def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
324
"""Integrate versions from self.source into self.target.
326
If version_ids is None all versions from source should be
327
incorporated into this versioned file.
329
Must raise RevisionNotPresent if any of the specified versions
330
are not present in the other files history unless ignore_missing is
331
supplied when they are silently skipped.
334
# - make a temporary versioned file of type target
335
# - insert the source content into it one at a time
337
# Make a new target-format versioned file.
338
temp_source = self.target.create_empty("temp", MemoryTransport())
339
graph = self.source.get_graph()
340
order = topo_sort(graph.items())
341
pb = ui.ui_factory.nested_progress_bar()
343
for index, version in enumerate(order):
344
pb.update('Converting versioned data', index, len(order))
345
temp_source.add_lines(version,
346
self.source.get_parents(version),
347
self.source.get_lines(version))
349
# this should hit the native code path for target
350
return self.target.join(temp_source,
359
class InterVersionedFileTestProviderAdapter(object):
360
"""A tool to generate a suite testing multiple inter versioned-file classes.
362
This is done by copying the test once for each interversionedfile provider
363
and injecting the transport_server, transport_readonly_server,
364
versionedfile_factory and versionedfile_factory_to classes into each copy.
365
Each copy is also given a new id() to make it easy to identify.
368
def __init__(self, transport_server, transport_readonly_server, formats):
369
self._transport_server = transport_server
370
self._transport_readonly_server = transport_readonly_server
371
self._formats = formats
373
def adapt(self, test):
375
for (interversionedfile_class,
376
versionedfile_factory,
377
versionedfile_factory_to) in self._formats:
378
new_test = deepcopy(test)
379
new_test.transport_server = self._transport_server
380
new_test.transport_readonly_server = self._transport_readonly_server
381
new_test.interversionedfile_class = interversionedfile_class
382
new_test.versionedfile_factory = versionedfile_factory
383
new_test.versionedfile_factory_to = versionedfile_factory_to
384
def make_new_test_id():
385
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
386
return lambda: new_id
387
new_test.id = make_new_test_id()
388
result.addTest(new_test)
392
def default_test_list():
393
"""Generate the default list of interversionedfile permutations to test."""
394
from bzrlib.weave import WeaveFile
395
from bzrlib.knit import KnitVersionedFile
397
# test the fallback InterVersionedFile from weave to annotated knits
398
result.append((InterVersionedFile,
401
for optimiser in InterVersionedFile._optimisers:
402
result.append((optimiser,
403
optimiser._matching_file_factory,
404
optimiser._matching_file_factory
406
# if there are specific combinations we want to use, we can add them