1
# Copyright (C) 2005, 2006 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
"""Versioned text file storage api."""
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
33
from bzrlib.transport.memory import MemoryTransport
36
from cStringIO import StringIO
38
from bzrlib.inter import InterObject
39
from bzrlib.symbol_versioning import *
40
from bzrlib.textmerge import TextMerge
43
class VersionedFile(object):
44
"""Versioned text file storage.
46
A versioned file manages versions of line-based text files,
47
keeping track of the originating version for each line.
49
To clients the "lines" of the file are represented as a list of
50
strings. These strings will typically have terminal newline
51
characters, but this is not required. In particular files commonly
52
do not have a newline at the end of the file.
54
Texts are identified by a version-id string.
57
def __init__(self, access_mode):
59
self._access_mode = access_mode
62
def check_not_reserved_id(version_id):
63
revision.check_not_reserved_id(version_id)
65
def copy_to(self, name, transport):
66
"""Copy this versioned file to name on transport."""
67
raise NotImplementedError(self.copy_to)
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, parent_texts=None,
82
left_matching_blocks=None, nostore_sha=None, random_id=False,
84
"""Add a single text on top of the versioned file.
86
Must raise RevisionAlreadyPresent if the new version is
87
already present in file history.
89
Must raise RevisionNotPresent if any of the given parents are
90
not present in file history.
92
:param lines: A list of lines. Each line must be a bytestring. And all
93
of them except the last must be terminated with \n and contain no
94
other \n's. The last line may either contain no \n's or a single
95
terminated \n. If the lines list does meet this constraint the add
96
routine may error or may succeed - but you will be unable to read
97
the data back accurately. (Checking the lines have been split
98
correctly is expensive and extremely unlikely to catch bugs so it
99
is not done at runtime unless check_content is True.)
100
:param parent_texts: An optional dictionary containing the opaque
101
representations of some or all of the parents of version_id to
102
allow delta optimisations. VERY IMPORTANT: the texts must be those
103
returned by add_lines or data corruption can be caused.
104
:param left_matching_blocks: a hint about which areas are common
105
between the text and its left-hand-parent. The format is
106
the SequenceMatcher.get_matching_blocks format.
107
:param nostore_sha: Raise ExistingContent and do not add the lines to
108
the versioned file if the digest of the lines matches this.
109
:param random_id: If True a random id has been selected rather than
110
an id determined by some deterministic process such as a converter
111
from a foreign VCS. When True the backend may choose not to check
112
for uniqueness of the resulting key within the versioned file, so
113
this should only be done when the result is expected to be unique
115
:param check_content: If True, the lines supplied are verified to be
116
bytestrings that are correctly formed lines.
117
:return: The text sha1, the number of bytes in the text, and an opaque
118
representation of the inserted version which can be provided
119
back to future add_lines calls in the parent_texts dictionary.
121
self._check_write_ok()
122
return self._add_lines(version_id, parents, lines, parent_texts,
123
left_matching_blocks, nostore_sha, random_id, check_content)
125
def _add_lines(self, version_id, parents, lines, parent_texts,
126
left_matching_blocks, nostore_sha, random_id, check_content):
127
"""Helper to do the class specific add_lines."""
128
raise NotImplementedError(self.add_lines)
130
def add_lines_with_ghosts(self, version_id, parents, lines,
131
parent_texts=None, nostore_sha=None, random_id=False,
132
check_content=True, left_matching_blocks=None):
133
"""Add lines to the versioned file, allowing ghosts to be present.
135
This takes the same parameters as add_lines and returns the same.
137
self._check_write_ok()
138
return self._add_lines_with_ghosts(version_id, parents, lines,
139
parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
141
def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
142
nostore_sha, random_id, check_content, left_matching_blocks):
143
"""Helper to do class specific add_lines_with_ghosts."""
144
raise NotImplementedError(self.add_lines_with_ghosts)
146
def check(self, progress_bar=None):
147
"""Check the versioned file for integrity."""
148
raise NotImplementedError(self.check)
150
def _check_lines_not_unicode(self, lines):
151
"""Check that lines being added to a versioned file are not unicode."""
153
if line.__class__ is not str:
154
raise errors.BzrBadParameterUnicode("lines")
156
def _check_lines_are_lines(self, lines):
157
"""Check that the lines really are full lines without inline EOL."""
159
if '\n' in line[:-1]:
160
raise errors.BzrBadParameterContainsNewline("lines")
162
def _check_write_ok(self):
163
"""Is the versioned file marked as 'finished' ? Raise if it is."""
165
raise errors.OutSideTransaction()
166
if self._access_mode != 'w':
167
raise errors.ReadOnlyObjectDirtiedError(self)
169
def enable_cache(self):
170
"""Tell this versioned file that it should cache any data it reads.
172
This is advisory, implementations do not have to support caching.
176
def clear_cache(self):
177
"""Remove any data cached in the versioned file object.
179
This only needs to be supported if caches are supported
183
def clone_text(self, new_version_id, old_version_id, parents):
184
"""Add an identical text to old_version_id as new_version_id.
186
Must raise RevisionNotPresent if the old version or any of the
187
parents are not present in file history.
189
Must raise RevisionAlreadyPresent if the new version is
190
already present in file history."""
191
self._check_write_ok()
192
return self._clone_text(new_version_id, old_version_id, parents)
194
def _clone_text(self, new_version_id, old_version_id, parents):
195
"""Helper function to do the _clone_text work."""
196
raise NotImplementedError(self.clone_text)
198
def create_empty(self, name, transport, mode=None):
199
"""Create a new versioned file of this exact type.
201
:param name: the file name
202
:param transport: the transport
203
:param mode: optional file mode.
205
raise NotImplementedError(self.create_empty)
207
def get_format_signature(self):
208
"""Get a text description of the data encoding in this file.
212
raise NotImplementedError(self.get_format_signature)
214
def make_mpdiffs(self, version_ids):
215
"""Create multiparent diffs for specified versions."""
216
knit_versions = set()
217
knit_versions.update(version_ids)
218
parent_map = self.get_parent_map(version_ids)
219
for version_id in version_ids:
221
knit_versions.update(parent_map[version_id])
223
raise RevisionNotPresent(version_id, self)
224
# We need to filter out ghosts, because we can't diff against them.
225
knit_versions = set(self.get_parent_map(knit_versions).keys())
226
lines = dict(zip(knit_versions,
227
self._get_lf_split_line_list(knit_versions)))
229
for version_id in version_ids:
230
target = lines[version_id]
232
parents = [lines[p] for p in parent_map[version_id] if p in
235
raise RevisionNotPresent(version_id, self)
237
left_parent_blocks = self._extract_blocks(version_id,
240
left_parent_blocks = None
241
diffs.append(multiparent.MultiParent.from_lines(target, parents,
245
def _extract_blocks(self, version_id, source, target):
248
def add_mpdiffs(self, records):
249
"""Add mpdiffs to this VersionedFile.
251
Records should be iterables of version, parents, expected_sha1,
252
mpdiff. mpdiff should be a MultiParent instance.
254
# Does this need to call self._check_write_ok()? (IanC 20070919)
256
mpvf = multiparent.MultiMemoryVersionedFile()
258
for version, parent_ids, expected_sha1, mpdiff in records:
259
versions.append(version)
260
mpvf.add_diff(mpdiff, version, parent_ids)
261
needed_parents = set()
262
for version, parent_ids, expected_sha1, mpdiff in records:
263
needed_parents.update(p for p in parent_ids
264
if not mpvf.has_version(p))
265
present_parents = set(self.get_parent_map(needed_parents).keys())
266
for parent_id, lines in zip(present_parents,
267
self._get_lf_split_line_list(present_parents)):
268
mpvf.add_version(lines, parent_id, [])
269
for (version, parent_ids, expected_sha1, mpdiff), lines in\
270
zip(records, mpvf.get_line_list(versions)):
271
if len(parent_ids) == 1:
272
left_matching_blocks = list(mpdiff.get_matching_blocks(0,
273
mpvf.get_diff(parent_ids[0]).num_lines()))
275
left_matching_blocks = None
277
_, _, version_text = self.add_lines_with_ghosts(version,
278
parent_ids, lines, vf_parents,
279
left_matching_blocks=left_matching_blocks)
280
except NotImplementedError:
281
# The vf can't handle ghosts, so add lines normally, which will
282
# (reasonably) fail if there are ghosts in the data.
283
_, _, version_text = self.add_lines(version,
284
parent_ids, lines, vf_parents,
285
left_matching_blocks=left_matching_blocks)
286
vf_parents[version] = version_text
287
for (version, parent_ids, expected_sha1, mpdiff), sha1 in\
288
zip(records, self.get_sha1s(versions)):
289
if expected_sha1 != sha1:
290
raise errors.VersionedFileInvalidChecksum(version)
292
def get_sha1(self, version_id):
293
"""Get the stored sha1 sum for the given revision.
295
:param version_id: The name of the version to lookup
297
raise NotImplementedError(self.get_sha1)
299
def get_sha1s(self, version_ids):
300
"""Get the stored sha1 sums for the given revisions.
302
:param version_ids: The names of the versions to lookup
303
:return: a list of sha1s in order according to the version_ids
305
raise NotImplementedError(self.get_sha1s)
307
def get_suffixes(self):
308
"""Return the file suffixes associated with this versioned file."""
309
raise NotImplementedError(self.get_suffixes)
311
def get_text(self, version_id):
312
"""Return version contents as a text string.
314
Raises RevisionNotPresent if version is not present in
317
return ''.join(self.get_lines(version_id))
318
get_string = get_text
320
def get_texts(self, version_ids):
321
"""Return the texts of listed versions as a list of strings.
323
Raises RevisionNotPresent if version is not present in
326
return [''.join(self.get_lines(v)) for v in version_ids]
328
def get_lines(self, version_id):
329
"""Return version contents as a sequence of lines.
331
Raises RevisionNotPresent if version is not present in
334
raise NotImplementedError(self.get_lines)
336
def _get_lf_split_line_list(self, version_ids):
337
return [StringIO(t).readlines() for t in self.get_texts(version_ids)]
339
def get_ancestry(self, version_ids, topo_sorted=True):
340
"""Return a list of all ancestors of given version(s). This
341
will not include the null revision.
343
This list will not be topologically sorted if topo_sorted=False is
346
Must raise RevisionNotPresent if any of the given versions are
347
not present in file history."""
348
if isinstance(version_ids, basestring):
349
version_ids = [version_ids]
350
raise NotImplementedError(self.get_ancestry)
352
def get_ancestry_with_ghosts(self, version_ids):
353
"""Return a list of all ancestors of given version(s). This
354
will not include the null revision.
356
Must raise RevisionNotPresent if any of the given versions are
357
not present in file history.
359
Ghosts that are known about will be included in ancestry list,
360
but are not explicitly marked.
362
raise NotImplementedError(self.get_ancestry_with_ghosts)
364
def get_graph(self, version_ids=None):
365
"""Return a graph from the versioned file.
367
Ghosts are not listed or referenced in the graph.
368
:param version_ids: Versions to select.
369
None means retrieve all versions.
371
if version_ids is None:
372
return dict(self.iter_parents(self.versions()))
374
pending = set(version_ids)
376
this_iteration = pending
378
for version, parents in self.iter_parents(this_iteration):
379
result[version] = parents
380
for parent in parents:
386
def get_graph_with_ghosts(self):
387
"""Return a graph for the entire versioned file.
389
Ghosts are referenced in parents list but are not
392
raise NotImplementedError(self.get_graph_with_ghosts)
394
def get_parent_map(self, version_ids):
395
"""Get a map of the parents of version_ids.
397
:param version_ids: The version ids to look up parents for.
398
:return: A mapping from version id to parents.
400
raise NotImplementedError(self.get_parent_map)
402
@deprecated_method(one_four)
403
def get_parents(self, version_id):
404
"""Return version names for parents of a version.
406
Must raise RevisionNotPresent if version is not present in
410
all = self.get_parent_map([version_id])[version_id]
412
raise errors.RevisionNotPresent(version_id, self)
414
parent_parents = self.get_parent_map(all)
415
for version_id in all:
416
if version_id in parent_parents:
417
result.append(version_id)
420
def get_parents_with_ghosts(self, version_id):
421
"""Return version names for parents of version_id.
423
Will raise RevisionNotPresent if version_id is not present
426
Ghosts that are known about will be included in the parent list,
427
but are not explicitly marked.
430
return list(self.get_parent_map([version_id])[version_id])
432
raise errors.RevisionNotPresent(version_id, self)
434
def annotate_iter(self, version_id):
435
"""Yield list of (version-id, line) pairs for the specified
438
Must raise RevisionNotPresent if the given version is
439
not present in file history.
441
raise NotImplementedError(self.annotate_iter)
443
def annotate(self, version_id):
444
return list(self.annotate_iter(version_id))
446
def join(self, other, pb=None, msg=None, version_ids=None,
447
ignore_missing=False):
448
"""Integrate versions from other into this versioned file.
450
If version_ids is None all versions from other should be
451
incorporated into this versioned file.
453
Must raise RevisionNotPresent if any of the specified versions
454
are not present in the other file's history unless ignore_missing
455
is supplied in which case they are silently skipped.
457
self._check_write_ok()
458
return InterVersionedFile.get(other, self).join(
464
def iter_lines_added_or_present_in_versions(self, version_ids=None,
466
"""Iterate over the lines in the versioned file from version_ids.
468
This may return lines from other versions. Each item the returned
469
iterator yields is a tuple of a line and a text version that that line
470
is present in (not introduced in).
472
Ordering of results is in whatever order is most suitable for the
473
underlying storage format.
475
If a progress bar is supplied, it may be used to indicate progress.
476
The caller is responsible for cleaning up progress bars (because this
479
NOTES: Lines are normalised: they will all have \n terminators.
480
Lines are returned in arbitrary order.
482
:return: An iterator over (line, version_id).
484
raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
486
def iter_parents(self, version_ids):
487
"""Iterate through the parents for many version ids.
489
:param version_ids: An iterable yielding version_ids.
490
:return: An iterator that yields (version_id, parents). Requested
491
version_ids not present in the versioned file are simply skipped.
492
The order is undefined, allowing for different optimisations in
493
the underlying implementation.
495
return self.get_parent_map(version_ids).iteritems()
497
def transaction_finished(self):
498
"""The transaction that this file was opened in has finished.
500
This records self.finished = True and should cause all mutating
505
def plan_merge(self, ver_a, ver_b):
506
"""Return pseudo-annotation indicating how the two versions merge.
508
This is computed between versions a and b and their common
511
Weave lines present in none of them are skipped entirely.
514
killed-base Dead in base revision
515
killed-both Killed in each revision
518
unchanged Alive in both a and b (possibly created in both)
521
ghost-a Killed in a, unborn in b
522
ghost-b Killed in b, unborn in a
523
irrelevant Not in either revision
525
raise NotImplementedError(VersionedFile.plan_merge)
527
def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
528
b_marker=TextMerge.B_MARKER):
529
return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
532
class _PlanMergeVersionedFile(object):
533
"""A VersionedFile for uncommitted and committed texts.
535
It is intended to allow merges to be planned with working tree texts.
536
It implements only the small part of the VersionedFile interface used by
537
PlanMerge. It falls back to multiple versionedfiles for data not stored in
538
_PlanMergeVersionedFile itself.
541
def __init__(self, file_id, fallback_versionedfiles=None):
544
:param file_id: Used when raising exceptions.
545
:param fallback_versionedfiles: If supplied, the set of fallbacks to
546
use. Otherwise, _PlanMergeVersionedFile.fallback_versionedfiles
547
can be appended to later.
549
self._file_id = file_id
550
if fallback_versionedfiles is None:
551
self.fallback_versionedfiles = []
553
self.fallback_versionedfiles = fallback_versionedfiles
557
def plan_merge(self, ver_a, ver_b, base=None):
558
"""See VersionedFile.plan_merge"""
559
from bzrlib.merge import _PlanMerge
561
return _PlanMerge(ver_a, ver_b, self).plan_merge()
562
old_plan = list(_PlanMerge(ver_a, base, self).plan_merge())
563
new_plan = list(_PlanMerge(ver_a, ver_b, self).plan_merge())
564
return _PlanMerge._subtract_plans(old_plan, new_plan)
566
def plan_lca_merge(self, ver_a, ver_b, base=None):
567
from bzrlib.merge import _PlanLCAMerge
568
graph = self._get_graph()
569
new_plan = _PlanLCAMerge(ver_a, ver_b, self, graph).plan_merge()
572
old_plan = _PlanLCAMerge(ver_a, base, self, graph).plan_merge()
573
return _PlanLCAMerge._subtract_plans(list(old_plan), list(new_plan))
575
def add_lines(self, version_id, parents, lines):
576
"""See VersionedFile.add_lines
578
Lines are added locally, not fallback versionedfiles. Also, ghosts are
579
permitted. Only reserved ids are permitted.
581
if not revision.is_reserved_id(version_id):
582
raise ValueError('Only reserved ids may be used')
584
raise ValueError('Parents may not be None')
586
raise ValueError('Lines may not be None')
587
self._parents[version_id] = tuple(parents)
588
self._lines[version_id] = lines
590
def get_lines(self, version_id):
591
"""See VersionedFile.get_ancestry"""
592
lines = self._lines.get(version_id)
593
if lines is not None:
595
for versionedfile in self.fallback_versionedfiles:
597
return versionedfile.get_lines(version_id)
598
except errors.RevisionNotPresent:
601
raise errors.RevisionNotPresent(version_id, self._file_id)
603
def get_ancestry(self, version_id, topo_sorted=False):
604
"""See VersionedFile.get_ancestry.
606
Note that this implementation assumes that if a VersionedFile can
607
answer get_ancestry at all, it can give an authoritative answer. In
608
fact, ghosts can invalidate this assumption. But it's good enough
609
99% of the time, and far cheaper/simpler.
611
Also note that the results of this version are never topologically
612
sorted, and are a set.
615
raise ValueError('This implementation does not provide sorting')
616
parents = self._parents.get(version_id)
618
for vf in self.fallback_versionedfiles:
620
return vf.get_ancestry(version_id, topo_sorted=False)
621
except errors.RevisionNotPresent:
624
raise errors.RevisionNotPresent(version_id, self._file_id)
625
ancestry = set([version_id])
626
for parent in parents:
627
ancestry.update(self.get_ancestry(parent, topo_sorted=False))
630
def get_parent_map(self, version_ids):
631
"""See VersionedFile.get_parent_map"""
633
pending = set(version_ids)
634
for key in version_ids:
636
result[key] = self._parents[key]
639
pending = pending - set(result.keys())
640
for versionedfile in self.fallback_versionedfiles:
641
parents = versionedfile.get_parent_map(pending)
642
result.update(parents)
643
pending = pending - set(parents.keys())
648
def _get_graph(self):
649
from bzrlib.graph import (
652
_StackedParentsProvider,
654
from bzrlib.repofmt.knitrepo import _KnitParentsProvider
655
parent_providers = [DictParentsProvider(self._parents)]
656
for vf in self.fallback_versionedfiles:
657
parent_providers.append(_KnitParentsProvider(vf))
658
return Graph(_StackedParentsProvider(parent_providers))
661
class PlanWeaveMerge(TextMerge):
662
"""Weave merge that takes a plan as its input.
664
This exists so that VersionedFile.plan_merge is implementable.
665
Most callers will want to use WeaveMerge instead.
668
def __init__(self, plan, a_marker=TextMerge.A_MARKER,
669
b_marker=TextMerge.B_MARKER):
670
TextMerge.__init__(self, a_marker, b_marker)
673
def _merge_struct(self):
678
def outstanding_struct():
679
if not lines_a and not lines_b:
681
elif ch_a and not ch_b:
684
elif ch_b and not ch_a:
686
elif lines_a == lines_b:
689
yield (lines_a, lines_b)
691
# We previously considered either 'unchanged' or 'killed-both' lines
692
# to be possible places to resynchronize. However, assuming agreement
693
# on killed-both lines may be too aggressive. -- mbp 20060324
694
for state, line in self.plan:
695
if state == 'unchanged':
696
# resync and flush queued conflicts changes if any
697
for struct in outstanding_struct():
703
if state == 'unchanged':
706
elif state == 'killed-a':
709
elif state == 'killed-b':
712
elif state == 'new-a':
715
elif state == 'new-b':
718
elif state == 'conflicted-a':
721
elif state == 'conflicted-b':
725
assert state in ('irrelevant', 'ghost-a', 'ghost-b',
726
'killed-base', 'killed-both'), state
727
for struct in outstanding_struct():
731
class WeaveMerge(PlanWeaveMerge):
732
"""Weave merge that takes a VersionedFile and two versions as its input."""
734
def __init__(self, versionedfile, ver_a, ver_b,
735
a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
736
plan = versionedfile.plan_merge(ver_a, ver_b)
737
PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
740
class InterVersionedFile(InterObject):
741
"""This class represents operations taking place between two VersionedFiles.
743
Its instances have methods like join, and contain
744
references to the source and target versionedfiles these operations can be
747
Often we will provide convenience methods on 'versionedfile' which carry out
748
operations with another versionedfile - they will always forward to
749
InterVersionedFile.get(other).method_name(parameters).
753
"""The available optimised InterVersionedFile types."""
755
def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
756
"""Integrate versions from self.source into self.target.
758
If version_ids is None all versions from source should be
759
incorporated into this versioned file.
761
Must raise RevisionNotPresent if any of the specified versions
762
are not present in the other file's history unless ignore_missing is
763
supplied in which case they are silently skipped.
766
# - if the target is empty, just add all the versions from
767
# source to target, otherwise:
768
# - make a temporary versioned file of type target
769
# - insert the source content into it one at a time
771
if not self.target.versions():
774
# Make a new target-format versioned file.
775
temp_source = self.target.create_empty("temp", MemoryTransport())
777
version_ids = self._get_source_version_ids(version_ids, ignore_missing)
778
graph = self.source.get_graph(version_ids)
779
order = tsort.topo_sort(graph.items())
780
pb = ui.ui_factory.nested_progress_bar()
783
# TODO for incremental cross-format work:
784
# make a versioned file with the following content:
785
# all revisions we have been asked to join
786
# all their ancestors that are *not* in target already.
787
# the immediate parents of the above two sets, with
788
# empty parent lists - these versions are in target already
789
# and the incorrect version data will be ignored.
790
# TODO: for all ancestors that are present in target already,
791
# check them for consistent data, this requires moving sha1 from
793
# TODO: remove parent texts when they are not relevant any more for
794
# memory pressure reduction. RBC 20060313
795
# pb.update('Converting versioned data', 0, len(order))
797
parent_map = self.source.get_parent_map(order)
798
for index, version in enumerate(order):
799
pb.update('Converting versioned data', index, total)
800
_, _, parent_text = target.add_lines(version,
802
self.source.get_lines(version),
803
parent_texts=parent_texts)
804
parent_texts[version] = parent_text
806
# this should hit the native code path for target
807
if target is not self.target:
808
return self.target.join(temp_source,
818
def _get_source_version_ids(self, version_ids, ignore_missing):
819
"""Determine the version ids to be used from self.source.
821
:param version_ids: The caller-supplied version ids to check. (None
822
for all). If None is in version_ids, it is stripped.
823
:param ignore_missing: if True, remove missing ids from the version
824
list. If False, raise RevisionNotPresent on
825
a missing version id.
826
:return: A set of version ids.
828
if version_ids is None:
829
# None cannot be in source.versions
830
return set(self.source.versions())
833
return set(self.source.versions()).intersection(set(version_ids))
835
new_version_ids = set()
836
for version in version_ids:
839
if not self.source.has_version(version):
840
raise errors.RevisionNotPresent(version, str(self.source))
842
new_version_ids.add(version)
843
return new_version_ids