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
@deprecated_method(zero_eight)
53
"""Return a list of all the versions in this versioned file.
55
Please use versionedfile.versions() now.
57
return self.versions()
60
"""Return a unsorted list of versions."""
61
raise NotImplementedError(self.versions)
63
def has_version(self, version_id):
64
"""Returns whether version is present."""
65
raise NotImplementedError(self.has_version)
67
def add_lines(self, version_id, parents, lines):
68
"""Add a single text on top of the versioned file.
70
Must raise RevisionAlreadyPresent if the new version is
71
already present in file history.
73
Must raise RevisionNotPresent if any of the given parents are
74
not present in file history."""
75
raise NotImplementedError(self.add_lines)
77
def clear_cache(self):
78
"""Remove any data cached in the versioned file object."""
80
def clone_text(self, new_version_id, old_version_id, parents):
81
"""Add an identical text to old_version_id as new_version_id.
83
Must raise RevisionNotPresent if the old version or any of the
84
parents are not present in file history.
86
Must raise RevisionAlreadyPresent if the new version is
87
already present in file history."""
88
raise NotImplementedError(self.clone_text)
90
def create_empty(self, name, transport, mode=None):
91
"""Create a new versioned file of this exact type.
93
:param name: the file name
94
:param transport: the transport
95
:param mode: optional file mode.
97
raise NotImplementedError(self.create_empty)
99
def get_text(self, version_id):
100
"""Return version contents as a text string.
102
Raises RevisionNotPresent if version is not present in
105
return ''.join(self.get_lines(version_id))
106
get_string = get_text
108
def get_lines(self, version_id):
109
"""Return version contents as a sequence of lines.
111
Raises RevisionNotPresent if version is not present in
114
raise NotImplementedError(self.get_lines)
116
def get_ancestry(self, version_ids):
117
"""Return a list of all ancestors of given version(s). This
118
will not include the null revision.
120
Must raise RevisionNotPresent if any of the given versions are
121
not present in file history."""
122
if isinstance(version_ids, basestring):
123
version_ids = [version_ids]
124
raise NotImplementedError(self.get_ancestry)
127
"""Return a graph for the entire versioned file."""
129
for version in self.versions():
130
result[version] = self.get_parents(version)
133
@deprecated_method(zero_eight)
134
def parent_names(self, version):
135
"""Return version names for parents of a version.
137
See get_parents for the current api.
139
return self.get_parents(version)
141
def get_parents(self, version_id):
142
"""Return version names for parents of a version.
144
Must raise RevisionNotPresent if version is not present in
147
raise NotImplementedError(self.get_parents)
149
def annotate_iter(self, version_id):
150
"""Yield list of (version-id, line) pairs for the specified
153
Must raise RevisionNotPresent if any of the given versions are
154
not present in file history.
156
raise NotImplementedError(self.annotate_iter)
158
def annotate(self, version_id):
159
return list(self.annotate_iter(version_id))
161
def join(self, other, pb=None, msg=None, version_ids=None):
162
"""Integrate versions from other into this versioned file.
164
If version_ids is None all versions from other should be
165
incorporated into this versioned file.
167
Must raise RevisionNotPresent if any of the specified versions
168
are not present in the other files history."""
169
return InterVersionedFile.get(other, self).join(pb, msg, version_ids)
171
def walk(self, version_ids=None):
172
"""Walk the versioned file as a weave-like structure, for
173
versions relative to version_ids. Yields sequence of (lineno,
174
insert, deletes, text) for each relevant line.
176
Must raise RevisionNotPresent if any of the specified versions
177
are not present in the file history.
179
:param version_ids: the version_ids to walk with respect to. If not
180
supplied the entire weave-like structure is walked.
182
raise NotImplementedError(self.walk)
184
@deprecated_method(zero_eight)
185
def iter_names(self):
186
"""Walk the names list."""
187
return iter(self.versions())
189
def plan_merge(self, ver_a, ver_b):
190
"""Return pseudo-annotation indicating how the two versions merge.
192
This is computed between versions a and b and their common
195
Weave lines present in none of them are skipped entirely.
197
inc_a = set(self.inclusions([ver_a]))
198
inc_b = set(self.inclusions([ver_b]))
199
inc_c = inc_a & inc_b
201
for lineno, insert, deleteset, line in self.walk():
202
if deleteset & inc_c:
203
# killed in parent; can't be in either a or b
204
# not relevant to our work
205
yield 'killed-base', line
206
elif insert in inc_c:
207
# was inserted in base
208
killed_a = bool(deleteset & inc_a)
209
killed_b = bool(deleteset & inc_b)
210
if killed_a and killed_b:
211
yield 'killed-both', line
213
yield 'killed-a', line
215
yield 'killed-b', line
217
yield 'unchanged', line
218
elif insert in inc_a:
219
if deleteset & inc_a:
220
yield 'ghost-a', line
224
elif insert in inc_b:
225
if deleteset & inc_b:
226
yield 'ghost-b', line
230
# not in either revision
231
yield 'irrelevant', line
233
yield 'unchanged', '' # terminator
235
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
239
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
240
# conflicted regions), rather than just inserting the markers.
242
# TODO: Show some version information (e.g. author, date) on
243
# conflicted regions.
244
for state, line in plan:
245
if state == 'unchanged' or state == 'killed-both':
246
# resync and flush queued conflicts changes if any
247
if not lines_a and not lines_b:
249
elif ch_a and not ch_b:
251
for l in lines_a: yield l
252
elif ch_b and not ch_a:
253
for l in lines_b: yield l
254
elif lines_a == lines_b:
255
for l in lines_a: yield l
258
for l in lines_a: yield l
260
for l in lines_b: yield l
267
if state == 'unchanged':
270
elif state == 'killed-a':
273
elif state == 'killed-b':
276
elif state == 'new-a':
279
elif state == 'new-b':
283
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
288
class InterVersionedFile(InterObject):
289
"""This class represents operations taking place between two versionedfiles..
291
Its instances have methods like join, and contain
292
references to the source and target versionedfiles these operations can be
295
Often we will provide convenience methods on 'versionedfile' which carry out
296
operations with another versionedfile - they will always forward to
297
InterVersionedFile.get(other).method_name(parameters).
301
"""The available optimised InterVersionedFile types."""
303
def join(self, pb=None, msg=None, version_ids=None):
304
"""Integrate versions from self.source into self.target.
306
If version_ids is None all versions from source should be
307
incorporated into this versioned file.
309
Must raise RevisionNotPresent if any of the specified versions
310
are not present in the other files history.
313
# - make a temporary versioned file of type target
314
# - insert the source content into it one at a time
316
# Make a new target-format versioned file.
317
temp_source = self.target.create_empty("temp", MemoryTransport())
318
graph = self.source.get_graph()
319
order = topo_sort(graph.items())
320
for version in order:
321
temp_source.add_lines(version,
322
self.source.get_parents(version),
323
self.source.get_lines(version))
325
# this should hit the native code path for target
326
return self.target.join(temp_source, pb, msg, version_ids)
329
class InterVersionedFileTestProviderAdapter(object):
330
"""A tool to generate a suite testing multiple inter versioned-file classes.
332
This is done by copying the test once for each interversionedfile provider
333
and injecting the transport_server, transport_readonly_server,
334
versionedfile_factory and versionedfile_factory_to classes into each copy.
335
Each copy is also given a new id() to make it easy to identify.
338
def __init__(self, transport_server, transport_readonly_server, formats):
339
self._transport_server = transport_server
340
self._transport_readonly_server = transport_readonly_server
341
self._formats = formats
343
def adapt(self, test):
345
for (interversionedfile_class,
346
versionedfile_factory,
347
versionedfile_factory_to) in self._formats:
348
new_test = deepcopy(test)
349
new_test.transport_server = self._transport_server
350
new_test.transport_readonly_server = self._transport_readonly_server
351
new_test.interversionedfile_class = interversionedfile_class
352
new_test.versionedfile_factory = versionedfile_factory
353
new_test.versionedfile_factory_to = versionedfile_factory_to
354
def make_new_test_id():
355
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
356
return lambda: new_id
357
new_test.id = make_new_test_id()
358
result.addTest(new_test)
362
def default_test_list():
363
"""Generate the default list of interversionedfile permutations to test."""
364
from bzrlib.weave import WeaveFile
365
from bzrlib.knit import AnnotatedKnitFactory
367
# test the fallback InterVersionedFile from weave to annotated knits
368
result.append((InterVersionedFile,
370
AnnotatedKnitFactory))
371
for optimiser in InterVersionedFile._optimisers:
372
result.append((optimiser,
373
optimiser._matching_file_factory,
374
optimiser._matching_file_factory
376
# if there are specific combinations we want to use, we can add them