7
7
# it under the terms of the GNU General Public License as published by
8
8
# the Free Software Foundation; either version 2 of the License, or
9
9
# (at your option) any later version.
11
11
# This program is distributed in the hope that it will be useful,
12
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
14
# GNU General Public License for more details.
16
16
# You should have received a copy of the GNU General Public License
17
17
# along with this program; if not, write to the Free Software
18
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
20
"""Versioned text file storage api."""
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
27
24
from copy import deepcopy
28
from unittest import TestSuite
31
import bzrlib.errors as errors
34
from bzrlib.transport.memory import MemoryTransport
32
37
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
38
from bzrlib.textmerge import TextMerge
39
from bzrlib.symbol_versioning import (deprecated_function,
39
45
class VersionedFile(object):
54
60
self.finished = False
55
61
self._access_mode = access_mode
64
def check_not_reserved_id(version_id):
65
revision.check_not_reserved_id(version_id)
57
67
def copy_to(self, name, transport):
58
68
"""Copy this versioned file to name on transport."""
59
69
raise NotImplementedError(self.copy_to)
61
71
@deprecated_method(zero_eight)
63
73
"""Return a list of all the versions in this versioned file.
154
170
"""Check the versioned file for integrity."""
155
171
raise NotImplementedError(self.check)
173
def _check_lines_not_unicode(self, lines):
174
"""Check that lines being added to a versioned file are not unicode."""
176
if line.__class__ is not str:
177
raise errors.BzrBadParameterUnicode("lines")
179
def _check_lines_are_lines(self, lines):
180
"""Check that the lines really are full lines without inline EOL."""
182
if '\n' in line[:-1]:
183
raise errors.BzrBadParameterContainsNewline("lines")
157
185
def _check_write_ok(self):
158
186
"""Is the versioned file marked as 'finished' ? Raise if it is."""
159
187
if self.finished:
161
189
if self._access_mode != 'w':
162
190
raise errors.ReadOnlyObjectDirtiedError(self)
192
def enable_cache(self):
193
"""Tell this versioned file that it should cache any data it reads.
195
This is advisory, implementations do not have to support caching.
164
199
def clear_cache(self):
165
"""Remove any data cached in the versioned file object."""
200
"""Remove any data cached in the versioned file object.
202
This only needs to be supported if caches are supported
167
206
def clone_text(self, new_version_id, old_version_id, parents):
168
207
"""Add an identical text to old_version_id as new_version_id.
196
237
the parents list must be a superset of the current
240
version_id = osutils.safe_revision_id(version_id)
241
new_parents = [osutils.safe_revision_id(p) for p in new_parents]
199
242
self._check_write_ok()
200
return self._fix_parents(version, new_parents)
243
return self._fix_parents(version_id, new_parents)
202
def _fix_parents(self, version, new_parents):
245
def _fix_parents(self, version_id, new_parents):
203
246
"""Helper for fix_parents."""
204
247
raise NotImplementedError(self.fix_parents)
219
262
version_id is the version_id created by that delta.
222
for version in versions:
223
result[version] = self.get_delta(version)
265
for version_id in version_ids:
266
result[version_id] = self.get_delta(version_id)
269
def get_sha1(self, version_id):
270
"""Get the stored sha1 sum for the given revision.
272
:param name: The name of the version to lookup
274
raise NotImplementedError(self.get_sha1)
226
276
def get_suffixes(self):
227
277
"""Return the file suffixes associated with this versioned file."""
228
278
raise NotImplementedError(self.get_suffixes)
267
325
raise NotImplementedError(self.get_ancestry_with_ghosts)
270
"""Return a graph for the entire versioned file.
327
def get_graph(self, version_ids=None):
328
"""Return a graph from the versioned file.
272
330
Ghosts are not listed or referenced in the graph.
331
:param version_ids: Versions to select.
332
None means retrieve all versions.
275
for version in self.versions():
276
result[version] = self.get_parents(version)
335
if version_ids is None:
336
for version in self.versions():
337
result[version] = self.get_parents(version)
339
pending = set(osutils.safe_revision_id(v) for v in version_ids)
341
version = pending.pop()
342
if version in result:
344
parents = self.get_parents(version)
345
for parent in parents:
349
result[version] = parents
279
352
def get_graph_with_ghosts(self):
353
def iter_lines_added_or_present_in_versions(self, version_ids=None):
426
def iter_lines_added_or_present_in_versions(self, version_ids=None,
354
428
"""Iterate over the lines in the versioned file from version_ids.
356
430
This may return lines from other versions, and does not return the
357
431
specific version marker at this point. The api may be changed
358
432
during development to include the version that the versioned file
359
433
thinks is relevant, but given that such hints are just guesses,
360
its better not to have it if we dont need it.
434
its better not to have it if we don't need it.
436
If a progress bar is supplied, it may be used to indicate progress.
437
The caller is responsible for cleaning up progress bars (because this
362
440
NOTES: Lines are normalised: they will all have \n terminators.
363
441
Lines are returned in arbitrary order.
402
480
Weave lines present in none of them are skipped entirely.
483
killed-base Dead in base revision
484
killed-both Killed in each revision
487
unchanged Alive in both a and b (possibly created in both)
490
ghost-a Killed in a, unborn in b
491
ghost-b Killed in b, unborn in a
492
irrelevant Not in either revision
404
inc_a = set(self.get_ancestry([ver_a]))
405
inc_b = set(self.get_ancestry([ver_b]))
406
inc_c = inc_a & inc_b
408
for lineno, insert, deleteset, line in self.walk([ver_a, ver_b]):
409
if deleteset & inc_c:
410
# killed in parent; can't be in either a or b
411
# not relevant to our work
412
yield 'killed-base', line
413
elif insert in inc_c:
414
# was inserted in base
415
killed_a = bool(deleteset & inc_a)
416
killed_b = bool(deleteset & inc_b)
417
if killed_a and killed_b:
418
yield 'killed-both', line
420
yield 'killed-a', line
422
yield 'killed-b', line
424
yield 'unchanged', line
425
elif insert in inc_a:
426
if deleteset & inc_a:
427
yield 'ghost-a', line
431
elif insert in inc_b:
432
if deleteset & inc_b:
433
yield 'ghost-b', line
437
# not in either revision
438
yield 'irrelevant', line
440
yield 'unchanged', '' # terminator
442
def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
494
raise NotImplementedError(VersionedFile.plan_merge)
496
def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
497
b_marker=TextMerge.B_MARKER):
498
return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
501
class PlanWeaveMerge(TextMerge):
502
"""Weave merge that takes a plan as its input.
504
This exists so that VersionedFile.plan_merge is implementable.
505
Most callers will want to use WeaveMerge instead.
508
def __init__(self, plan, a_marker=TextMerge.A_MARKER,
509
b_marker=TextMerge.B_MARKER):
510
TextMerge.__init__(self, a_marker, b_marker)
513
def _merge_struct(self):
445
516
ch_a = ch_b = False
446
# TODO: Return a structured form of the conflicts (e.g. 2-tuples for
447
# conflicted regions), rather than just inserting the markers.
449
# TODO: Show some version information (e.g. author, date) on
450
# conflicted regions.
518
def outstanding_struct():
519
if not lines_a and not lines_b:
521
elif ch_a and not ch_b:
524
elif ch_b and not ch_a:
526
elif lines_a == lines_b:
529
yield (lines_a, lines_b)
452
531
# We previously considered either 'unchanged' or 'killed-both' lines
453
532
# to be possible places to resynchronize. However, assuming agreement
454
# on killed-both lines may be too agressive. -- mbp 20060324
455
for state, line in plan:
533
# on killed-both lines may be too aggressive. -- mbp 20060324
534
for state, line in self.plan:
456
535
if state == 'unchanged':
457
536
# resync and flush queued conflicts changes if any
458
if not lines_a and not lines_b:
460
elif ch_a and not ch_b:
462
for l in lines_a: yield l
463
elif ch_b and not ch_a:
464
for l in lines_b: yield l
465
elif lines_a == lines_b:
466
for l in lines_a: yield l
469
for l in lines_a: yield l
471
for l in lines_b: yield l
537
for struct in outstanding_struct():
476
541
ch_a = ch_b = False
478
543
if state == 'unchanged':
481
546
elif state == 'killed-a':
483
548
lines_b.append(line)
492
557
lines_b.append(line)
494
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
559
assert state in ('irrelevant', 'ghost-a', 'ghost-b',
560
'killed-base', 'killed-both'), state
561
for struct in outstanding_struct():
565
class WeaveMerge(PlanWeaveMerge):
566
"""Weave merge that takes a VersionedFile and two versions as its input"""
568
def __init__(self, versionedfile, ver_a, ver_b,
569
a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
570
plan = versionedfile.plan_merge(ver_a, ver_b)
571
PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
499
574
class InterVersionedFile(InterObject):
533
608
# Make a new target-format versioned file.
534
609
temp_source = self.target.create_empty("temp", MemoryTransport())
535
610
target = temp_source
536
graph = self.source.get_graph()
537
order = topo_sort(graph.items())
611
version_ids = self._get_source_version_ids(version_ids, ignore_missing)
612
graph = self.source.get_graph(version_ids)
613
order = tsort.topo_sort(graph.items())
538
614
pb = ui.ui_factory.nested_progress_bar()
539
615
parent_texts = {}
657
def _get_source_version_ids(self, version_ids, ignore_missing):
658
"""Determine the version ids to be used from self.source.
660
:param version_ids: The caller-supplied version ids to check. (None
661
for all). If None is in version_ids, it is stripped.
662
:param ignore_missing: if True, remove missing ids from the version
663
list. If False, raise RevisionNotPresent on
664
a missing version id.
665
:return: A set of version ids.
667
if version_ids is None:
668
# None cannot be in source.versions
669
return set(self.source.versions())
671
version_ids = [osutils.safe_revision_id(v) for v in version_ids]
673
return set(self.source.versions()).intersection(set(version_ids))
675
new_version_ids = set()
676
for version in version_ids:
679
if not self.source.has_version(version):
680
raise errors.RevisionNotPresent(version, str(self.source))
682
new_version_ids.add(version)
683
return new_version_ids
582
686
class InterVersionedFileTestProviderAdapter(object):
583
687
"""A tool to generate a suite testing multiple inter versioned-file classes.
585
This is done by copying the test once for each interversionedfile provider
689
This is done by copying the test once for each InterVersionedFile provider
586
690
and injecting the transport_server, transport_readonly_server,
587
691
versionedfile_factory and versionedfile_factory_to classes into each copy.
588
692
Each copy is also given a new id() to make it easy to identify.
617
721
from bzrlib.weave import WeaveFile
618
722
from bzrlib.knit import KnitVersionedFile
620
# test the fallback InterVersionedFile from weave to annotated knits
724
# test the fallback InterVersionedFile from annotated knits to weave
621
725
result.append((InterVersionedFile,
624
728
for optimiser in InterVersionedFile._optimisers:
625
729
result.append((optimiser,
626
optimiser._matching_file_factory,
627
optimiser._matching_file_factory
730
optimiser._matching_file_from_factory,
731
optimiser._matching_file_to_factory
629
733
# if there are specific combinations we want to use, we can add them