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
import bzrlib.errors as errors
32
from bzrlib.inter import InterObject
33
from bzrlib.symbol_versioning import *
34
from bzrlib.transport.memory import MemoryTransport
35
from bzrlib.tsort import topo_sort
39
class VersionedFile(object):
40
"""Versioned text file storage.
42
A versioned file manages versions of line-based text files,
43
keeping track of the originating version for each line.
45
To clients the "lines" of the file are represented as a list of
46
strings. These strings will typically have terminal newline
47
characters, but this is not required. In particular files commonly
48
do not have a newline at the end of the file.
50
Texts are identified by a version-id string.
53
def __init__(self, access_mode):
55
self._access_mode = access_mode
57
def copy_to(self, name, transport):
58
"""Copy this versioned file to name on transport."""
59
raise NotImplementedError(self.copy_to)
61
@deprecated_method(zero_eight)
63
"""Return a list of all the versions in this versioned file.
65
Please use versionedfile.versions() now.
67
return self.versions()
70
"""Return a unsorted list of versions."""
71
raise NotImplementedError(self.versions)
73
def has_ghost(self, version_id):
74
"""Returns whether version is present as a ghost."""
75
raise NotImplementedError(self.has_ghost)
77
def has_version(self, version_id):
78
"""Returns whether version is present."""
79
raise NotImplementedError(self.has_version)
81
def add_lines(self, version_id, parents, lines):
82
"""Add a single text on top of the versioned file.
84
Must raise RevisionAlreadyPresent if the new version is
85
already present in file history.
87
Must raise RevisionNotPresent if any of the given parents are
88
not present in file history.
90
self._check_write_ok()
91
return self._add_lines(version_id, parents, lines)
93
def _add_lines(self, version_id, parents, lines):
94
"""Helper to do the class specific add_lines."""
95
raise NotImplementedError(self.add_lines)
97
def add_lines_with_ghosts(self, version_id, parents, lines):
98
"""Add lines to the versioned file, allowing ghosts to be present."""
99
self._check_write_ok()
100
return self._add_lines_with_ghosts(version_id, parents, lines)
102
def _add_lines_with_ghosts(self, version_id, parents, lines):
103
"""Helper to do class specific add_lines_with_ghosts."""
104
raise NotImplementedError(self.add_lines_with_ghosts)
106
def check(self, progress_bar=None):
107
"""Check the versioned file for integrity."""
108
raise NotImplementedError(self.check)
110
def _check_write_ok(self):
111
"""Is the versioned file marked as 'finished' ? Raise if it is."""
113
raise errors.OutSideTransaction()
114
if self._access_mode != 'w':
115
raise errors.ReadOnlyObjectDirtiedError(self)
117
def clear_cache(self):
118
"""Remove any data cached in the versioned file object."""
120
def clone_text(self, new_version_id, old_version_id, parents):
121
"""Add an identical text to old_version_id as new_version_id.
123
Must raise RevisionNotPresent if the old version or any of the
124
parents are not present in file history.
126
Must raise RevisionAlreadyPresent if the new version is
127
already present in file history."""
128
self._check_write_ok()
129
return self._clone_text(new_version_id, old_version_id, parents)
131
def _clone_text(self, new_version_id, old_version_id, parents):
132
"""Helper function to do the _clone_text work."""
133
raise NotImplementedError(self.clone_text)
135
def create_empty(self, name, transport, mode=None):
136
"""Create a new versioned file of this exact type.
138
:param name: the file name
139
:param transport: the transport
140
:param mode: optional file mode.
142
raise NotImplementedError(self.create_empty)
144
def fix_parents(self, version, new_parents):
145
"""Fix the parents list for version.
147
This is done by appending a new version to the index
148
with identical data except for the parents list.
149
the parents list must be a superset of the current
152
self._check_write_ok()
153
return self._fix_parents(version, new_parents)
155
def _fix_parents(self, version, new_parents):
156
"""Helper for fix_parents."""
157
raise NotImplementedError(self.fix_parents)
159
def get_suffixes(self):
160
"""Return the file suffixes associated with this versioned file."""
161
raise NotImplementedError(self.get_suffixes)
163
def get_text(self, version_id):
164
"""Return version contents as a text string.
166
Raises RevisionNotPresent if version is not present in
169
return ''.join(self.get_lines(version_id))
170
get_string = get_text
172
def get_lines(self, version_id):
173
"""Return version contents as a sequence of lines.
175
Raises RevisionNotPresent if version is not present in
178
raise NotImplementedError(self.get_lines)
180
def get_ancestry(self, version_ids):
181
"""Return a list of all ancestors of given version(s). This
182
will not include the null revision.
184
Must raise RevisionNotPresent if any of the given versions are
185
not present in file history."""
186
if isinstance(version_ids, basestring):
187
version_ids = [version_ids]
188
raise NotImplementedError(self.get_ancestry)
190
def get_ancestry_with_ghosts(self, version_ids):
191
"""Return a list of all ancestors of given version(s). This
192
will not include the null revision.
194
Must raise RevisionNotPresent if any of the given versions are
195
not present in file history.
197
Ghosts that are known about will be included in ancestry list,
198
but are not explicitly marked.
200
raise NotImplementedError(self.get_ancestry_with_ghosts)
203
"""Return a graph for the entire versioned file.
205
Ghosts are not listed or referenced in the graph.
208
for version in self.versions():
209
result[version] = self.get_parents(version)
212
def get_graph_with_ghosts(self):
213
"""Return a graph for the entire versioned file.
215
Ghosts are referenced in parents list but are not
218
raise NotImplementedError(self.get_graph_with_ghosts)
220
@deprecated_method(zero_eight)
221
def parent_names(self, version):
222
"""Return version names for parents of a version.
224
See get_parents for the current api.
226
return self.get_parents(version)
228
def get_parents(self, version_id):
229
"""Return version names for parents of a version.
231
Must raise RevisionNotPresent if version is not present in
234
raise NotImplementedError(self.get_parents)
236
def get_parents_with_ghosts(self, version_id):
237
"""Return version names for parents of version_id.
239
Will raise RevisionNotPresent if version_id is not present
242
Ghosts that are known about will be included in the parent list,
243
but are not explicitly marked.
245
raise NotImplementedError(self.get_parents_with_ghosts)
247
def annotate_iter(self, version_id):
248
"""Yield list of (version-id, line) pairs for the specified
251
Must raise RevisionNotPresent if any of the given versions are
252
not present in file history.
254
raise NotImplementedError(self.annotate_iter)
256
def annotate(self, version_id):
257
return list(self.annotate_iter(version_id))
259
def join(self, other, pb=None, msg=None, version_ids=None,
260
ignore_missing=False):
261
"""Integrate versions from other into this versioned file.
263
If version_ids is None all versions from other should be
264
incorporated into this versioned file.
266
Must raise RevisionNotPresent if any of the specified versions
267
are not present in the other files history unless ignore_missing
268
is supplied when they are silently skipped.
270
self._check_write_ok()
271
return InterVersionedFile.get(other, self).join(
277
def iter_lines_added_or_present_in_versions(self, version_ids=None):
278
"""Iterate over the lines in the versioned file from version_ids.
280
This may return lines from other versions, and does not return the
281
specific version marker at this point. The api may be changed
282
during development to include the version that the versioned file
283
thinks is relevant, but given that such hints are just guesses,
284
its better not to have it if we dont need it.
286
NOTES: Lines are normalised: they will all have \n terminators.
287
Lines are returned in arbitrary order.
289
raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
291
def transaction_finished(self):
292
"""The transaction that this file was opened in has finished.
294
This records self.finished = True and should cause all mutating
299
@deprecated_method(zero_eight)
300
def walk(self, version_ids=None):
301
"""Walk the versioned file as a weave-like structure, for
302
versions relative to version_ids. Yields sequence of (lineno,
303
insert, deletes, text) for each relevant line.
305
Must raise RevisionNotPresent if any of the specified versions
306
are not present in the file history.
308
:param version_ids: the version_ids to walk with respect to. If not
309
supplied the entire weave-like structure is walked.
311
walk is deprecated in favour of iter_lines_added_or_present_in_versions
313
raise NotImplementedError(self.walk)
315
@deprecated_method(zero_eight)
316
def iter_names(self):
317
"""Walk the names list."""
318
return iter(self.versions())
320
def plan_merge(self, ver_a, ver_b):
321
"""Return pseudo-annotation indicating how the two versions merge.
323
This is computed between versions a and b and their common
326
Weave lines present in none of them are skipped entirely.
328
inc_a = set(self.get_ancestry([ver_a]))
329
inc_b = set(self.get_ancestry([ver_b]))
330
inc_c = inc_a & inc_b
332
for lineno, insert, deleteset, line in self.walk([ver_a, ver_b]):
333
if deleteset & inc_c:
334
# killed in parent; can't be in either a or b
335
# not relevant to our work
336
yield 'killed-base', line
337
elif insert in inc_c:
338
# was inserted in base
339
killed_a = bool(deleteset & inc_a)
340
killed_b = bool(deleteset & inc_b)
341
if killed_a and killed_b:
342
yield 'killed-both', line
344
yield 'killed-a', line
346
yield 'killed-b', line
348
yield 'unchanged', line
349
elif insert in inc_a:
350
if deleteset & inc_a:
351
yield 'ghost-a', line
355
elif insert in inc_b:
356
if deleteset & inc_b:
357
yield 'ghost-b', line
361
# not in either revision
362
yield 'irrelevant', line
364
yield 'unchanged', '' # terminator
366
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
370
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
371
# conflicted regions), rather than just inserting the markers.
373
# TODO: Show some version information (e.g. author, date) on
374
# conflicted regions.
375
for state, line in plan:
376
if state == 'unchanged' or state == 'killed-both':
377
# resync and flush queued conflicts changes if any
378
if not lines_a and not lines_b:
380
elif ch_a and not ch_b:
382
for l in lines_a: yield l
383
elif ch_b and not ch_a:
384
for l in lines_b: yield l
385
elif lines_a == lines_b:
386
for l in lines_a: yield l
389
for l in lines_a: yield l
391
for l in lines_b: yield l
398
if state == 'unchanged':
401
elif state == 'killed-a':
404
elif state == 'killed-b':
407
elif state == 'new-a':
410
elif state == 'new-b':
414
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
419
class InterVersionedFile(InterObject):
420
"""This class represents operations taking place between two versionedfiles..
422
Its instances have methods like join, and contain
423
references to the source and target versionedfiles these operations can be
426
Often we will provide convenience methods on 'versionedfile' which carry out
427
operations with another versionedfile - they will always forward to
428
InterVersionedFile.get(other).method_name(parameters).
432
"""The available optimised InterVersionedFile types."""
434
def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
435
"""Integrate versions from self.source into self.target.
437
If version_ids is None all versions from source should be
438
incorporated into this versioned file.
440
Must raise RevisionNotPresent if any of the specified versions
441
are not present in the other files history unless ignore_missing is
442
supplied when they are silently skipped.
445
# - if the target is empty, just add all the versions from
446
# source to target, otherwise:
447
# - make a temporary versioned file of type target
448
# - insert the source content into it one at a time
450
if not self.target.versions():
453
# Make a new target-format versioned file.
454
temp_source = self.target.create_empty("temp", MemoryTransport())
456
graph = self.source.get_graph()
457
order = topo_sort(graph.items())
458
pb = ui.ui_factory.nested_progress_bar()
460
for index, version in enumerate(order):
461
pb.update('Converting versioned data', index, len(order))
462
target.add_lines(version,
463
self.source.get_parents(version),
464
self.source.get_lines(version))
466
# this should hit the native code path for target
467
if target is not self.target:
468
return self.target.join(temp_source,
477
class InterVersionedFileTestProviderAdapter(object):
478
"""A tool to generate a suite testing multiple inter versioned-file classes.
480
This is done by copying the test once for each interversionedfile provider
481
and injecting the transport_server, transport_readonly_server,
482
versionedfile_factory and versionedfile_factory_to classes into each copy.
483
Each copy is also given a new id() to make it easy to identify.
486
def __init__(self, transport_server, transport_readonly_server, formats):
487
self._transport_server = transport_server
488
self._transport_readonly_server = transport_readonly_server
489
self._formats = formats
491
def adapt(self, test):
493
for (interversionedfile_class,
494
versionedfile_factory,
495
versionedfile_factory_to) in self._formats:
496
new_test = deepcopy(test)
497
new_test.transport_server = self._transport_server
498
new_test.transport_readonly_server = self._transport_readonly_server
499
new_test.interversionedfile_class = interversionedfile_class
500
new_test.versionedfile_factory = versionedfile_factory
501
new_test.versionedfile_factory_to = versionedfile_factory_to
502
def make_new_test_id():
503
new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
504
return lambda: new_id
505
new_test.id = make_new_test_id()
506
result.addTest(new_test)
510
def default_test_list():
511
"""Generate the default list of interversionedfile permutations to test."""
512
from bzrlib.weave import WeaveFile
513
from bzrlib.knit import KnitVersionedFile
515
# test the fallback InterVersionedFile from weave to annotated knits
516
result.append((InterVersionedFile,
519
for optimiser in InterVersionedFile._optimisers:
520
result.append((optimiser,
521
optimiser._matching_file_factory,
522
optimiser._matching_file_factory
524
# if there are specific combinations we want to use, we can add them