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 check(self, progress_bar=None):
82
"""Check the versioned file for integrity."""
83
raise NotImplementedError(self.check)
85
def clear_cache(self):
86
"""Remove any data cached in the versioned file object."""
88
def clone_text(self, new_version_id, old_version_id, parents):
89
"""Add an identical text to old_version_id as new_version_id.
91
Must raise RevisionNotPresent if the old version or any of the
92
parents are not present in file history.
94
Must raise RevisionAlreadyPresent if the new version is
95
already present in file history."""
96
raise NotImplementedError(self.clone_text)
98
def create_empty(self, name, transport, mode=None):
99
"""Create a new versioned file of this exact type.
101
:param name: the file name
102
:param transport: the transport
103
:param mode: optional file mode.
105
raise NotImplementedError(self.create_empty)
107
def get_suffixes(self):
108
"""Return the file suffixes associated with this versioned file."""
109
raise NotImplementedError(self.get_suffixes)
111
def get_text(self, version_id):
112
"""Return version contents as a text string.
114
Raises RevisionNotPresent if version is not present in
117
return ''.join(self.get_lines(version_id))
118
get_string = get_text
120
def get_lines(self, version_id):
121
"""Return version contents as a sequence of lines.
123
Raises RevisionNotPresent if version is not present in
126
raise NotImplementedError(self.get_lines)
128
def get_ancestry(self, version_ids):
129
"""Return a list of all ancestors of given version(s). This
130
will not include the null revision.
132
Must raise RevisionNotPresent if any of the given versions are
133
not present in file history."""
134
if isinstance(version_ids, basestring):
135
version_ids = [version_ids]
136
raise NotImplementedError(self.get_ancestry)
139
"""Return a graph for the entire versioned file."""
141
for version in self.versions():
142
result[version] = self.get_parents(version)
145
@deprecated_method(zero_eight)
146
def parent_names(self, version):
147
"""Return version names for parents of a version.
149
See get_parents for the current api.
151
return self.get_parents(version)
153
def get_parents(self, version_id):
154
"""Return version names for parents of a version.
156
Must raise RevisionNotPresent if version is not present in
159
raise NotImplementedError(self.get_parents)
161
def annotate_iter(self, version_id):
162
"""Yield list of (version-id, line) pairs for the specified
165
Must raise RevisionNotPresent if any of the given versions are
166
not present in file history.
168
raise NotImplementedError(self.annotate_iter)
170
def annotate(self, version_id):
171
return list(self.annotate_iter(version_id))
173
def join(self, other, pb=None, msg=None, version_ids=None,
174
ignore_missing=False):
175
"""Integrate versions from other into this versioned file.
177
If version_ids is None all versions from other should be
178
incorporated into this versioned file.
180
Must raise RevisionNotPresent if any of the specified versions
181
are not present in the other files history unless ignore_missing
182
is supplied when they are silently skipped.
184
return InterVersionedFile.get(other, self).join(
190
def walk(self, version_ids=None):
191
"""Walk the versioned file as a weave-like structure, for
192
versions relative to version_ids. Yields sequence of (lineno,
193
insert, deletes, text) for each relevant line.
195
Must raise RevisionNotPresent if any of the specified versions
196
are not present in the file history.
198
:param version_ids: the version_ids to walk with respect to. If not
199
supplied the entire weave-like structure is walked.
201
raise NotImplementedError(self.walk)
203
@deprecated_method(zero_eight)
204
def iter_names(self):
205
"""Walk the names list."""
206
return iter(self.versions())
208
def plan_merge(self, ver_a, ver_b):
209
"""Return pseudo-annotation indicating how the two versions merge.
211
This is computed between versions a and b and their common
214
Weave lines present in none of them are skipped entirely.
216
inc_a = set(self.inclusions([ver_a]))
217
inc_b = set(self.inclusions([ver_b]))
218
inc_c = inc_a & inc_b
220
for lineno, insert, deleteset, line in self.walk():
221
if deleteset & inc_c:
222
# killed in parent; can't be in either a or b
223
# not relevant to our work
224
yield 'killed-base', line
225
elif insert in inc_c:
226
# was inserted in base
227
killed_a = bool(deleteset & inc_a)
228
killed_b = bool(deleteset & inc_b)
229
if killed_a and killed_b:
230
yield 'killed-both', line
232
yield 'killed-a', line
234
yield 'killed-b', line
236
yield 'unchanged', line
237
elif insert in inc_a:
238
if deleteset & inc_a:
239
yield 'ghost-a', line
243
elif insert in inc_b:
244
if deleteset & inc_b:
245
yield 'ghost-b', line
249
# not in either revision
250
yield 'irrelevant', line
252
yield 'unchanged', '' # terminator
254
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
258
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
259
# conflicted regions), rather than just inserting the markers.
261
# TODO: Show some version information (e.g. author, date) on
262
# conflicted regions.
263
for state, line in plan:
264
if state == 'unchanged' or state == 'killed-both':
265
# resync and flush queued conflicts changes if any
266
if not lines_a and not lines_b:
268
elif ch_a and not ch_b:
270
for l in lines_a: yield l
271
elif ch_b and not ch_a:
272
for l in lines_b: yield l
273
elif lines_a == lines_b:
274
for l in lines_a: yield l
277
for l in lines_a: yield l
279
for l in lines_b: yield l
286
if state == 'unchanged':
289
elif state == 'killed-a':
292
elif state == 'killed-b':
295
elif state == 'new-a':
298
elif state == 'new-b':
302
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
307
class InterVersionedFile(InterObject):
308
"""This class represents operations taking place between two versionedfiles..
310
Its instances have methods like join, and contain
311
references to the source and target versionedfiles these operations can be
314
Often we will provide convenience methods on 'versionedfile' which carry out
315
operations with another versionedfile - they will always forward to
316
InterVersionedFile.get(other).method_name(parameters).
320
"""The available optimised InterVersionedFile types."""
322
def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
323
"""Integrate versions from self.source into self.target.
325
If version_ids is None all versions from source should be
326
incorporated into this versioned file.
328
Must raise RevisionNotPresent if any of the specified versions
329
are not present in the other files history unless ignore_missing is
330
supplied when they are silently skipped.
333
# - make a temporary versioned file of type target
334
# - insert the source content into it one at a time
336
# Make a new target-format versioned file.
337
temp_source = self.target.create_empty("temp", MemoryTransport())
338
graph = self.source.get_graph()
339
order = topo_sort(graph.items())
340
for version in order:
341
temp_source.add_lines(version,
342
self.source.get_parents(version),
343
self.source.get_lines(version))
345
# this should hit the native code path for target
346
return self.target.join(temp_source,
353
class InterVersionedFileTestProviderAdapter(object):
354
"""A tool to generate a suite testing multiple inter versioned-file classes.
356
This is done by copying the test once for each interversionedfile provider
357
and injecting the transport_server, transport_readonly_server,
358
versionedfile_factory and versionedfile_factory_to classes into each copy.
359
Each copy is also given a new id() to make it easy to identify.
362
def __init__(self, transport_server, transport_readonly_server, formats):
363
self._transport_server = transport_server
364
self._transport_readonly_server = transport_readonly_server
365
self._formats = formats
367
def adapt(self, test):
369
for (interversionedfile_class,
370
versionedfile_factory,
371
versionedfile_factory_to) in self._formats:
372
new_test = deepcopy(test)
373
new_test.transport_server = self._transport_server
374
new_test.transport_readonly_server = self._transport_readonly_server
375
new_test.interversionedfile_class = interversionedfile_class
376
new_test.versionedfile_factory = versionedfile_factory
377
new_test.versionedfile_factory_to = versionedfile_factory_to
378
def make_new_test_id():
379
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
380
return lambda: new_id
381
new_test.id = make_new_test_id()
382
result.addTest(new_test)
386
def default_test_list():
387
"""Generate the default list of interversionedfile permutations to test."""
388
from bzrlib.weave import WeaveFile
389
from bzrlib.knit import KnitVersionedFile
391
# test the fallback InterVersionedFile from weave to annotated knits
392
result.append((InterVersionedFile,
395
for optimiser in InterVersionedFile._optimisers:
396
result.append((optimiser,
397
optimiser._matching_file_factory,
398
optimiser._matching_file_factory
400
# if there are specific combinations we want to use, we can add them