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_ghost(self, version_id):
69
"""Returns whether version is present as a ghost."""
70
raise NotImplementedError(self.has_ghost)
72
def has_version(self, version_id):
73
"""Returns whether version is present."""
74
raise NotImplementedError(self.has_version)
76
def add_lines(self, version_id, parents, lines):
77
"""Add a single text on top of the versioned file.
79
Must raise RevisionAlreadyPresent if the new version is
80
already present in file history.
82
Must raise RevisionNotPresent if any of the given parents are
83
not present in file history."""
84
raise NotImplementedError(self.add_lines)
86
def add_lines_with_ghosts(self, version_id, parents, lines):
87
"""Add lines to the versioned file, allowing ghosts to be present."""
88
raise NotImplementedError(self.add_lines_with_ghosts)
90
def check(self, progress_bar=None):
91
"""Check the versioned file for integrity."""
92
raise NotImplementedError(self.check)
94
def clear_cache(self):
95
"""Remove any data cached in the versioned file object."""
97
def clone_text(self, new_version_id, old_version_id, parents):
98
"""Add an identical text to old_version_id as new_version_id.
100
Must raise RevisionNotPresent if the old version or any of the
101
parents are not present in file history.
103
Must raise RevisionAlreadyPresent if the new version is
104
already present in file history."""
105
raise NotImplementedError(self.clone_text)
107
def create_empty(self, name, transport, mode=None):
108
"""Create a new versioned file of this exact type.
110
:param name: the file name
111
:param transport: the transport
112
:param mode: optional file mode.
114
raise NotImplementedError(self.create_empty)
116
def fix_parents(self, version, new_parents):
117
"""Fix the parents list for version.
119
This is done by appending a new version to the index
120
with identical data except for the parents list.
121
the parents list must be a superset of the current
124
raise NotImplementedError(self.fix_parents)
126
def get_suffixes(self):
127
"""Return the file suffixes associated with this versioned file."""
128
raise NotImplementedError(self.get_suffixes)
130
def get_text(self, version_id):
131
"""Return version contents as a text string.
133
Raises RevisionNotPresent if version is not present in
136
return ''.join(self.get_lines(version_id))
137
get_string = get_text
139
def get_lines(self, version_id):
140
"""Return version contents as a sequence of lines.
142
Raises RevisionNotPresent if version is not present in
145
raise NotImplementedError(self.get_lines)
147
def get_ancestry(self, version_ids):
148
"""Return a list of all ancestors of given version(s). This
149
will not include the null revision.
151
Must raise RevisionNotPresent if any of the given versions are
152
not present in file history."""
153
if isinstance(version_ids, basestring):
154
version_ids = [version_ids]
155
raise NotImplementedError(self.get_ancestry)
157
def get_ancestry_with_ghosts(self, version_ids):
158
"""Return a list of all ancestors of given version(s). This
159
will not include the null revision.
161
Must raise RevisionNotPresent if any of the given versions are
162
not present in file history.
164
Ghosts that are known about will be included in ancestry list,
165
but are not explicitly marked.
167
raise NotImplementedError(self.get_ancestry_with_ghosts)
170
"""Return a graph for the entire versioned file.
172
Ghosts are not listed or referenced in the graph.
175
for version in self.versions():
176
result[version] = self.get_parents(version)
179
def get_graph_with_ghosts(self):
180
"""Return a graph for the entire versioned file.
182
Ghosts are referenced in parents list but are not
185
raise NotImplementedError(self.get_graph_with_ghosts)
187
@deprecated_method(zero_eight)
188
def parent_names(self, version):
189
"""Return version names for parents of a version.
191
See get_parents for the current api.
193
return self.get_parents(version)
195
def get_parents(self, version_id):
196
"""Return version names for parents of a version.
198
Must raise RevisionNotPresent if version is not present in
201
raise NotImplementedError(self.get_parents)
203
def get_parents_with_ghosts(self, version_id):
204
"""Return version names for parents of version_id.
206
Will raise RevisionNotPresent if version_id is not present
209
Ghosts that are known about will be included in the parent list,
210
but are not explicitly marked.
212
raise NotImplementedError(self.get_parents_with_ghosts)
214
def annotate_iter(self, version_id):
215
"""Yield list of (version-id, line) pairs for the specified
218
Must raise RevisionNotPresent if any of the given versions are
219
not present in file history.
221
raise NotImplementedError(self.annotate_iter)
223
def annotate(self, version_id):
224
return list(self.annotate_iter(version_id))
226
def join(self, other, pb=None, msg=None, version_ids=None,
227
ignore_missing=False):
228
"""Integrate versions from other into this versioned file.
230
If version_ids is None all versions from other should be
231
incorporated into this versioned file.
233
Must raise RevisionNotPresent if any of the specified versions
234
are not present in the other files history unless ignore_missing
235
is supplied when they are silently skipped.
237
return InterVersionedFile.get(other, self).join(
243
def iter_lines_added_or_present_in_versions(self, version_ids=None):
244
"""Iterate over the lines in the versioned file from version_ids.
246
This may return lines from other versions, and does not return the
247
specific version marker at this point. The api may be changed
248
during development to include the version that the versioned file
249
thinks is relevant, but given that such hints are just guesses,
250
its better not to have it if we dont need it.
252
NOTES: Lines are normalised: they will all have \n terminators.
253
Lines are returned in arbitrary order.
255
raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
257
@deprecated_method(zero_eight)
258
def walk(self, version_ids=None):
259
"""Walk the versioned file as a weave-like structure, for
260
versions relative to version_ids. Yields sequence of (lineno,
261
insert, deletes, text) for each relevant line.
263
Must raise RevisionNotPresent if any of the specified versions
264
are not present in the file history.
266
:param version_ids: the version_ids to walk with respect to. If not
267
supplied the entire weave-like structure is walked.
269
walk is deprecated in favour of iter_lines_added_or_present_in_versions
271
raise NotImplementedError(self.walk)
273
@deprecated_method(zero_eight)
274
def iter_names(self):
275
"""Walk the names list."""
276
return iter(self.versions())
278
def plan_merge(self, ver_a, ver_b):
279
"""Return pseudo-annotation indicating how the two versions merge.
281
This is computed between versions a and b and their common
284
Weave lines present in none of them are skipped entirely.
286
inc_a = set(self.get_ancestry([ver_a]))
287
inc_b = set(self.get_ancestry([ver_b]))
288
inc_c = inc_a & inc_b
290
for lineno, insert, deleteset, line in self.walk([ver_a, ver_b]):
291
if deleteset & inc_c:
292
# killed in parent; can't be in either a or b
293
# not relevant to our work
294
yield 'killed-base', line
295
elif insert in inc_c:
296
# was inserted in base
297
killed_a = bool(deleteset & inc_a)
298
killed_b = bool(deleteset & inc_b)
299
if killed_a and killed_b:
300
yield 'killed-both', line
302
yield 'killed-a', line
304
yield 'killed-b', line
306
yield 'unchanged', line
307
elif insert in inc_a:
308
if deleteset & inc_a:
309
yield 'ghost-a', line
313
elif insert in inc_b:
314
if deleteset & inc_b:
315
yield 'ghost-b', line
319
# not in either revision
320
yield 'irrelevant', line
322
yield 'unchanged', '' # terminator
324
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
328
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
329
# conflicted regions), rather than just inserting the markers.
331
# TODO: Show some version information (e.g. author, date) on
332
# conflicted regions.
333
for state, line in plan:
334
if state == 'unchanged' or state == 'killed-both':
335
# resync and flush queued conflicts changes if any
336
if not lines_a and not lines_b:
338
elif ch_a and not ch_b:
340
for l in lines_a: yield l
341
elif ch_b and not ch_a:
342
for l in lines_b: yield l
343
elif lines_a == lines_b:
344
for l in lines_a: yield l
347
for l in lines_a: yield l
349
for l in lines_b: yield l
356
if state == 'unchanged':
359
elif state == 'killed-a':
362
elif state == 'killed-b':
365
elif state == 'new-a':
368
elif state == 'new-b':
372
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
377
class InterVersionedFile(InterObject):
378
"""This class represents operations taking place between two versionedfiles..
380
Its instances have methods like join, and contain
381
references to the source and target versionedfiles these operations can be
384
Often we will provide convenience methods on 'versionedfile' which carry out
385
operations with another versionedfile - they will always forward to
386
InterVersionedFile.get(other).method_name(parameters).
390
"""The available optimised InterVersionedFile types."""
392
def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
393
"""Integrate versions from self.source into self.target.
395
If version_ids is None all versions from source should be
396
incorporated into this versioned file.
398
Must raise RevisionNotPresent if any of the specified versions
399
are not present in the other files history unless ignore_missing is
400
supplied when they are silently skipped.
403
# - if the target is empty, just add all the versions from
404
# source to target, otherwise:
405
# - make a temporary versioned file of type target
406
# - insert the source content into it one at a time
408
if not self.target.versions():
411
# Make a new target-format versioned file.
412
temp_source = self.target.create_empty("temp", MemoryTransport())
414
graph = self.source.get_graph()
415
order = topo_sort(graph.items())
416
pb = ui.ui_factory.nested_progress_bar()
418
for index, version in enumerate(order):
419
pb.update('Converting versioned data', index, len(order))
420
target.add_lines(version,
421
self.source.get_parents(version),
422
self.source.get_lines(version))
424
# this should hit the native code path for target
425
if target is not self.target:
426
return self.target.join(temp_source,
435
class InterVersionedFileTestProviderAdapter(object):
436
"""A tool to generate a suite testing multiple inter versioned-file classes.
438
This is done by copying the test once for each interversionedfile provider
439
and injecting the transport_server, transport_readonly_server,
440
versionedfile_factory and versionedfile_factory_to classes into each copy.
441
Each copy is also given a new id() to make it easy to identify.
444
def __init__(self, transport_server, transport_readonly_server, formats):
445
self._transport_server = transport_server
446
self._transport_readonly_server = transport_readonly_server
447
self._formats = formats
449
def adapt(self, test):
451
for (interversionedfile_class,
452
versionedfile_factory,
453
versionedfile_factory_to) in self._formats:
454
new_test = deepcopy(test)
455
new_test.transport_server = self._transport_server
456
new_test.transport_readonly_server = self._transport_readonly_server
457
new_test.interversionedfile_class = interversionedfile_class
458
new_test.versionedfile_factory = versionedfile_factory
459
new_test.versionedfile_factory_to = versionedfile_factory_to
460
def make_new_test_id():
461
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
462
return lambda: new_id
463
new_test.id = make_new_test_id()
464
result.addTest(new_test)
468
def default_test_list():
469
"""Generate the default list of interversionedfile permutations to test."""
470
from bzrlib.weave import WeaveFile
471
from bzrlib.knit import KnitVersionedFile
473
# test the fallback InterVersionedFile from weave to annotated knits
474
result.append((InterVersionedFile,
477
for optimiser in InterVersionedFile._optimisers:
478
result.append((optimiser,
479
optimiser._matching_file_factory,
480
optimiser._matching_file_factory
482
# if there are specific combinations we want to use, we can add them