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.graph import Graph
34
from bzrlib.transport.memory import MemoryTransport
37
from cStringIO import StringIO
39
from bzrlib.inter import InterObject
40
from bzrlib.symbol_versioning import *
41
from bzrlib.textmerge import TextMerge
44
class VersionedFile(object):
45
"""Versioned text file storage.
47
A versioned file manages versions of line-based text files,
48
keeping track of the originating version for each line.
50
To clients the "lines" of the file are represented as a list of
51
strings. These strings will typically have terminal newline
52
characters, but this is not required. In particular files commonly
53
do not have a newline at the end of the file.
55
Texts are identified by a version-id string.
58
def __init__(self, access_mode):
60
self._access_mode = access_mode
63
def check_not_reserved_id(version_id):
64
revision.check_not_reserved_id(version_id)
66
def copy_to(self, name, transport):
67
"""Copy this versioned file to name on transport."""
68
raise NotImplementedError(self.copy_to)
71
"""Return a unsorted list of versions."""
72
raise NotImplementedError(self.versions)
74
def has_ghost(self, version_id):
75
"""Returns whether version is present as a ghost."""
76
raise NotImplementedError(self.has_ghost)
78
def has_version(self, version_id):
79
"""Returns whether version is present."""
80
raise NotImplementedError(self.has_version)
82
def add_lines(self, version_id, parents, lines, parent_texts=None,
83
left_matching_blocks=None, nostore_sha=None, random_id=False,
85
"""Add a single text on top of the versioned file.
87
Must raise RevisionAlreadyPresent if the new version is
88
already present in file history.
90
Must raise RevisionNotPresent if any of the given parents are
91
not present in file history.
93
:param lines: A list of lines. Each line must be a bytestring. And all
94
of them except the last must be terminated with \n and contain no
95
other \n's. The last line may either contain no \n's or a single
96
terminated \n. If the lines list does meet this constraint the add
97
routine may error or may succeed - but you will be unable to read
98
the data back accurately. (Checking the lines have been split
99
correctly is expensive and extremely unlikely to catch bugs so it
100
is not done at runtime unless check_content is True.)
101
:param parent_texts: An optional dictionary containing the opaque
102
representations of some or all of the parents of version_id to
103
allow delta optimisations. VERY IMPORTANT: the texts must be those
104
returned by add_lines or data corruption can be caused.
105
:param left_matching_blocks: a hint about which areas are common
106
between the text and its left-hand-parent. The format is
107
the SequenceMatcher.get_matching_blocks format.
108
:param nostore_sha: Raise ExistingContent and do not add the lines to
109
the versioned file if the digest of the lines matches this.
110
:param random_id: If True a random id has been selected rather than
111
an id determined by some deterministic process such as a converter
112
from a foreign VCS. When True the backend may choose not to check
113
for uniqueness of the resulting key within the versioned file, so
114
this should only be done when the result is expected to be unique
116
:param check_content: If True, the lines supplied are verified to be
117
bytestrings that are correctly formed lines.
118
:return: The text sha1, the number of bytes in the text, and an opaque
119
representation of the inserted version which can be provided
120
back to future add_lines calls in the parent_texts dictionary.
122
self._check_write_ok()
123
return self._add_lines(version_id, parents, lines, parent_texts,
124
left_matching_blocks, nostore_sha, random_id, check_content)
126
def _add_lines(self, version_id, parents, lines, parent_texts,
127
left_matching_blocks, nostore_sha, random_id, check_content):
128
"""Helper to do the class specific add_lines."""
129
raise NotImplementedError(self.add_lines)
131
def add_lines_with_ghosts(self, version_id, parents, lines,
132
parent_texts=None, nostore_sha=None, random_id=False,
133
check_content=True, left_matching_blocks=None):
134
"""Add lines to the versioned file, allowing ghosts to be present.
136
This takes the same parameters as add_lines and returns the same.
138
self._check_write_ok()
139
return self._add_lines_with_ghosts(version_id, parents, lines,
140
parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
142
def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
143
nostore_sha, random_id, check_content, left_matching_blocks):
144
"""Helper to do class specific add_lines_with_ghosts."""
145
raise NotImplementedError(self.add_lines_with_ghosts)
147
def check(self, progress_bar=None):
148
"""Check the versioned file for integrity."""
149
raise NotImplementedError(self.check)
151
def _check_lines_not_unicode(self, lines):
152
"""Check that lines being added to a versioned file are not unicode."""
154
if line.__class__ is not str:
155
raise errors.BzrBadParameterUnicode("lines")
157
def _check_lines_are_lines(self, lines):
158
"""Check that the lines really are full lines without inline EOL."""
160
if '\n' in line[:-1]:
161
raise errors.BzrBadParameterContainsNewline("lines")
163
def _check_write_ok(self):
164
"""Is the versioned file marked as 'finished' ? Raise if it is."""
166
raise errors.OutSideTransaction()
167
if self._access_mode != 'w':
168
raise errors.ReadOnlyObjectDirtiedError(self)
170
def enable_cache(self):
171
"""Tell this versioned file that it should cache any data it reads.
173
This is advisory, implementations do not have to support caching.
177
def clear_cache(self):
178
"""Remove any data cached in the versioned file object.
180
This only needs to be supported if caches are supported
184
def clone_text(self, new_version_id, old_version_id, parents):
185
"""Add an identical text to old_version_id as new_version_id.
187
Must raise RevisionNotPresent if the old version or any of the
188
parents are not present in file history.
190
Must raise RevisionAlreadyPresent if the new version is
191
already present in file history."""
192
self._check_write_ok()
193
return self._clone_text(new_version_id, old_version_id, parents)
195
def _clone_text(self, new_version_id, old_version_id, parents):
196
"""Helper function to do the _clone_text work."""
197
raise NotImplementedError(self.clone_text)
199
def create_empty(self, name, transport, mode=None):
200
"""Create a new versioned file of this exact type.
202
:param name: the file name
203
:param transport: the transport
204
:param mode: optional file mode.
206
raise NotImplementedError(self.create_empty)
208
def get_format_signature(self):
209
"""Get a text description of the data encoding in this file.
213
raise NotImplementedError(self.get_format_signature)
215
def make_mpdiffs(self, version_ids):
216
"""Create multiparent diffs for specified versions."""
217
knit_versions = set()
218
knit_versions.update(version_ids)
219
parent_map = self.get_parent_map(version_ids)
220
for version_id in version_ids:
222
knit_versions.update(parent_map[version_id])
224
raise RevisionNotPresent(version_id, self)
225
# We need to filter out ghosts, because we can't diff against them.
226
knit_versions = set(self.get_parent_map(knit_versions).keys())
227
lines = dict(zip(knit_versions,
228
self._get_lf_split_line_list(knit_versions)))
230
for version_id in version_ids:
231
target = lines[version_id]
233
parents = [lines[p] for p in parent_map[version_id] if p in
236
raise RevisionNotPresent(version_id, self)
238
left_parent_blocks = self._extract_blocks(version_id,
241
left_parent_blocks = None
242
diffs.append(multiparent.MultiParent.from_lines(target, parents,
246
def _extract_blocks(self, version_id, source, target):
249
def add_mpdiffs(self, records):
250
"""Add mpdiffs to this VersionedFile.
252
Records should be iterables of version, parents, expected_sha1,
253
mpdiff. mpdiff should be a MultiParent instance.
255
# Does this need to call self._check_write_ok()? (IanC 20070919)
257
mpvf = multiparent.MultiMemoryVersionedFile()
259
for version, parent_ids, expected_sha1, mpdiff in records:
260
versions.append(version)
261
mpvf.add_diff(mpdiff, version, parent_ids)
262
needed_parents = set()
263
for version, parent_ids, expected_sha1, mpdiff in records:
264
needed_parents.update(p for p in parent_ids
265
if not mpvf.has_version(p))
266
present_parents = set(self.get_parent_map(needed_parents).keys())
267
for parent_id, lines in zip(present_parents,
268
self._get_lf_split_line_list(present_parents)):
269
mpvf.add_version(lines, parent_id, [])
270
for (version, parent_ids, expected_sha1, mpdiff), lines in\
271
zip(records, mpvf.get_line_list(versions)):
272
if len(parent_ids) == 1:
273
left_matching_blocks = list(mpdiff.get_matching_blocks(0,
274
mpvf.get_diff(parent_ids[0]).num_lines()))
276
left_matching_blocks = None
278
_, _, version_text = self.add_lines_with_ghosts(version,
279
parent_ids, lines, vf_parents,
280
left_matching_blocks=left_matching_blocks)
281
except NotImplementedError:
282
# The vf can't handle ghosts, so add lines normally, which will
283
# (reasonably) fail if there are ghosts in the data.
284
_, _, version_text = self.add_lines(version,
285
parent_ids, lines, vf_parents,
286
left_matching_blocks=left_matching_blocks)
287
vf_parents[version] = version_text
288
for (version, parent_ids, expected_sha1, mpdiff), sha1 in\
289
zip(records, self.get_sha1s(versions)):
290
if expected_sha1 != sha1:
291
raise errors.VersionedFileInvalidChecksum(version)
293
def get_sha1(self, version_id):
294
"""Get the stored sha1 sum for the given revision.
296
:param version_id: The name of the version to lookup
298
raise NotImplementedError(self.get_sha1)
300
def get_sha1s(self, version_ids):
301
"""Get the stored sha1 sums for the given revisions.
303
:param version_ids: The names of the versions to lookup
304
:return: a list of sha1s in order according to the version_ids
306
raise NotImplementedError(self.get_sha1s)
308
def get_suffixes(self):
309
"""Return the file suffixes associated with this versioned file."""
310
raise NotImplementedError(self.get_suffixes)
312
def get_text(self, version_id):
313
"""Return version contents as a text string.
315
Raises RevisionNotPresent if version is not present in
318
return ''.join(self.get_lines(version_id))
319
get_string = get_text
321
def get_texts(self, version_ids):
322
"""Return the texts of listed versions as a list of strings.
324
Raises RevisionNotPresent if version is not present in
327
return [''.join(self.get_lines(v)) for v in version_ids]
329
def get_lines(self, version_id):
330
"""Return version contents as a sequence of lines.
332
Raises RevisionNotPresent if version is not present in
335
raise NotImplementedError(self.get_lines)
337
def _get_lf_split_line_list(self, version_ids):
338
return [StringIO(t).readlines() for t in self.get_texts(version_ids)]
340
def get_ancestry(self, version_ids, topo_sorted=True):
341
"""Return a list of all ancestors of given version(s). This
342
will not include the null revision.
344
This list will not be topologically sorted if topo_sorted=False is
347
Must raise RevisionNotPresent if any of the given versions are
348
not present in file history."""
349
if isinstance(version_ids, basestring):
350
version_ids = [version_ids]
351
raise NotImplementedError(self.get_ancestry)
353
def get_ancestry_with_ghosts(self, version_ids):
354
"""Return a list of all ancestors of given version(s). This
355
will not include the null revision.
357
Must raise RevisionNotPresent if any of the given versions are
358
not present in file history.
360
Ghosts that are known about will be included in ancestry list,
361
but are not explicitly marked.
363
raise NotImplementedError(self.get_ancestry_with_ghosts)
365
def get_graph(self, version_ids=None):
366
"""Return a graph from the versioned file.
368
Ghosts are not listed or referenced in the graph.
369
:param version_ids: Versions to select.
370
None means retrieve all versions.
372
if version_ids is None:
373
return dict(self.iter_parents(self.versions()))
375
pending = set(version_ids)
377
this_iteration = pending
379
for version, parents in self.iter_parents(this_iteration):
380
result[version] = parents
381
for parent in parents:
387
def get_graph_with_ghosts(self):
388
"""Return a graph for the entire versioned file.
390
Ghosts are referenced in parents list but are not
393
raise NotImplementedError(self.get_graph_with_ghosts)
395
def get_parent_map(self, version_ids):
396
"""Get a map of the parents of version_ids.
398
:param version_ids: The version ids to look up parents for.
399
:return: A mapping from version id to parents.
401
raise NotImplementedError(self.get_parent_map)
403
@deprecated_method(one_four)
404
def get_parents(self, version_id):
405
"""Return version names for parents of a version.
407
Must raise RevisionNotPresent if version is not present in
411
all = self.get_parent_map([version_id])[version_id]
413
raise errors.RevisionNotPresent(version_id, self)
415
parent_parents = self.get_parent_map(all)
416
for version_id in all:
417
if version_id in parent_parents:
418
result.append(version_id)
421
def get_parents_with_ghosts(self, version_id):
422
"""Return version names for parents of version_id.
424
Will raise RevisionNotPresent if version_id is not present
427
Ghosts that are known about will be included in the parent list,
428
but are not explicitly marked.
431
return list(self.get_parent_map([version_id])[version_id])
433
raise errors.RevisionNotPresent(version_id, self)
435
def annotate_iter(self, version_id):
436
"""Yield list of (version-id, line) pairs for the specified
439
Must raise RevisionNotPresent if the given version is
440
not present in file history.
442
raise NotImplementedError(self.annotate_iter)
444
def annotate(self, version_id):
445
return list(self.annotate_iter(version_id))
447
def join(self, other, pb=None, msg=None, version_ids=None,
448
ignore_missing=False):
449
"""Integrate versions from other into this versioned file.
451
If version_ids is None all versions from other should be
452
incorporated into this versioned file.
454
Must raise RevisionNotPresent if any of the specified versions
455
are not present in the other file's history unless ignore_missing
456
is supplied in which case they are silently skipped.
458
self._check_write_ok()
459
return InterVersionedFile.get(other, self).join(
465
def iter_lines_added_or_present_in_versions(self, version_ids=None,
467
"""Iterate over the lines in the versioned file from version_ids.
469
This may return lines from other versions. Each item the returned
470
iterator yields is a tuple of a line and a text version that that line
471
is present in (not introduced in).
473
Ordering of results is in whatever order is most suitable for the
474
underlying storage format.
476
If a progress bar is supplied, it may be used to indicate progress.
477
The caller is responsible for cleaning up progress bars (because this
480
NOTES: Lines are normalised: they will all have \n terminators.
481
Lines are returned in arbitrary order.
483
:return: An iterator over (line, version_id).
485
raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
487
def iter_parents(self, version_ids):
488
"""Iterate through the parents for many version ids.
490
:param version_ids: An iterable yielding version_ids.
491
:return: An iterator that yields (version_id, parents). Requested
492
version_ids not present in the versioned file are simply skipped.
493
The order is undefined, allowing for different optimisations in
494
the underlying implementation.
496
return self.get_parent_map(version_ids).iteritems()
498
def transaction_finished(self):
499
"""The transaction that this file was opened in has finished.
501
This records self.finished = True and should cause all mutating
506
def plan_merge(self, ver_a, ver_b):
507
"""Return pseudo-annotation indicating how the two versions merge.
509
This is computed between versions a and b and their common
512
Weave lines present in none of them are skipped entirely.
515
killed-base Dead in base revision
516
killed-both Killed in each revision
519
unchanged Alive in both a and b (possibly created in both)
522
ghost-a Killed in a, unborn in b
523
ghost-b Killed in b, unborn in a
524
irrelevant Not in either revision
526
raise NotImplementedError(VersionedFile.plan_merge)
528
def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
529
b_marker=TextMerge.B_MARKER):
530
return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
533
class _PlanMergeVersionedFile(object):
534
"""A VersionedFile for uncommitted and committed texts.
536
It is intended to allow merges to be planned with working tree texts.
537
It implements only the small part of the VersionedFile interface used by
538
PlanMerge. It falls back to multiple versionedfiles for data not stored in
539
_PlanMergeVersionedFile itself.
542
def __init__(self, file_id, fallback_versionedfiles=None):
545
:param file_id: Used when raising exceptions.
546
:param fallback_versionedfiles: If supplied, the set of fallbacks to
547
use. Otherwise, _PlanMergeVersionedFile.fallback_versionedfiles
548
can be appended to later.
550
self._file_id = file_id
551
if fallback_versionedfiles is None:
552
self.fallback_versionedfiles = []
554
self.fallback_versionedfiles = fallback_versionedfiles
558
def plan_merge(self, ver_a, ver_b, base=None):
559
"""See VersionedFile.plan_merge"""
560
from bzrlib.merge import _PlanMerge
562
return _PlanMerge(ver_a, ver_b, self).plan_merge()
563
old_plan = list(_PlanMerge(ver_a, base, self).plan_merge())
564
new_plan = list(_PlanMerge(ver_a, ver_b, self).plan_merge())
565
return _PlanMerge._subtract_plans(old_plan, new_plan)
567
def plan_lca_merge(self, ver_a, ver_b, base=None):
568
from bzrlib.merge import _PlanLCAMerge
569
graph = self._get_graph()
570
new_plan = _PlanLCAMerge(ver_a, ver_b, self, graph).plan_merge()
573
old_plan = _PlanLCAMerge(ver_a, base, self, graph).plan_merge()
574
return _PlanLCAMerge._subtract_plans(list(old_plan), list(new_plan))
576
def add_lines(self, version_id, parents, lines):
577
"""See VersionedFile.add_lines
579
Lines are added locally, not fallback versionedfiles. Also, ghosts are
580
permitted. Only reserved ids are permitted.
582
if not revision.is_reserved_id(version_id):
583
raise ValueError('Only reserved ids may be used')
585
raise ValueError('Parents may not be None')
587
raise ValueError('Lines may not be None')
588
self._parents[version_id] = tuple(parents)
589
self._lines[version_id] = lines
591
def get_lines(self, version_id):
592
"""See VersionedFile.get_ancestry"""
593
lines = self._lines.get(version_id)
594
if lines is not None:
596
for versionedfile in self.fallback_versionedfiles:
598
return versionedfile.get_lines(version_id)
599
except errors.RevisionNotPresent:
602
raise errors.RevisionNotPresent(version_id, self._file_id)
604
def get_ancestry(self, version_id, topo_sorted=False):
605
"""See VersionedFile.get_ancestry.
607
Note that this implementation assumes that if a VersionedFile can
608
answer get_ancestry at all, it can give an authoritative answer. In
609
fact, ghosts can invalidate this assumption. But it's good enough
610
99% of the time, and far cheaper/simpler.
612
Also note that the results of this version are never topologically
613
sorted, and are a set.
616
raise ValueError('This implementation does not provide sorting')
617
parents = self._parents.get(version_id)
619
for vf in self.fallback_versionedfiles:
621
return vf.get_ancestry(version_id, topo_sorted=False)
622
except errors.RevisionNotPresent:
625
raise errors.RevisionNotPresent(version_id, self._file_id)
626
ancestry = set([version_id])
627
for parent in parents:
628
ancestry.update(self.get_ancestry(parent, topo_sorted=False))
631
def get_parent_map(self, version_ids):
632
"""See VersionedFile.get_parent_map"""
634
pending = set(version_ids)
635
for key in version_ids:
637
result[key] = self._parents[key]
640
pending = pending - set(result.keys())
641
for versionedfile in self.fallback_versionedfiles:
642
parents = versionedfile.get_parent_map(pending)
643
result.update(parents)
644
pending = pending - set(parents.keys())
649
def _get_graph(self):
650
from bzrlib.graph import (
653
_StackedParentsProvider,
655
from bzrlib.repofmt.knitrepo import _KnitParentsProvider
656
parent_providers = [DictParentsProvider(self._parents)]
657
for vf in self.fallback_versionedfiles:
658
parent_providers.append(_KnitParentsProvider(vf))
659
return Graph(_StackedParentsProvider(parent_providers))
662
class PlanWeaveMerge(TextMerge):
663
"""Weave merge that takes a plan as its input.
665
This exists so that VersionedFile.plan_merge is implementable.
666
Most callers will want to use WeaveMerge instead.
669
def __init__(self, plan, a_marker=TextMerge.A_MARKER,
670
b_marker=TextMerge.B_MARKER):
671
TextMerge.__init__(self, a_marker, b_marker)
674
def _merge_struct(self):
679
def outstanding_struct():
680
if not lines_a and not lines_b:
682
elif ch_a and not ch_b:
685
elif ch_b and not ch_a:
687
elif lines_a == lines_b:
690
yield (lines_a, lines_b)
692
# We previously considered either 'unchanged' or 'killed-both' lines
693
# to be possible places to resynchronize. However, assuming agreement
694
# on killed-both lines may be too aggressive. -- mbp 20060324
695
for state, line in self.plan:
696
if state == 'unchanged':
697
# resync and flush queued conflicts changes if any
698
for struct in outstanding_struct():
704
if state == 'unchanged':
707
elif state == 'killed-a':
710
elif state == 'killed-b':
713
elif state == 'new-a':
716
elif state == 'new-b':
719
elif state == 'conflicted-a':
722
elif state == 'conflicted-b':
726
assert state in ('irrelevant', 'ghost-a', 'ghost-b',
727
'killed-base', 'killed-both'), state
728
for struct in outstanding_struct():
732
class WeaveMerge(PlanWeaveMerge):
733
"""Weave merge that takes a VersionedFile and two versions as its input."""
735
def __init__(self, versionedfile, ver_a, ver_b,
736
a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
737
plan = versionedfile.plan_merge(ver_a, ver_b)
738
PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
741
class InterVersionedFile(InterObject):
742
"""This class represents operations taking place between two VersionedFiles.
744
Its instances have methods like join, and contain
745
references to the source and target versionedfiles these operations can be
748
Often we will provide convenience methods on 'versionedfile' which carry out
749
operations with another versionedfile - they will always forward to
750
InterVersionedFile.get(other).method_name(parameters).
754
"""The available optimised InterVersionedFile types."""
756
def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
757
"""Integrate versions from self.source into self.target.
759
If version_ids is None all versions from source should be
760
incorporated into this versioned file.
762
Must raise RevisionNotPresent if any of the specified versions
763
are not present in the other file's history unless ignore_missing is
764
supplied in which case they are silently skipped.
767
# - if the target is empty, just add all the versions from
768
# source to target, otherwise:
769
# - make a temporary versioned file of type target
770
# - insert the source content into it one at a time
772
if not self.target.versions():
775
# Make a new target-format versioned file.
776
temp_source = self.target.create_empty("temp", MemoryTransport())
778
version_ids = self._get_source_version_ids(version_ids, ignore_missing)
779
graph = Graph(self.source)
780
search = graph._make_breadth_first_searcher(version_ids)
781
transitive_ids = set()
782
map(transitive_ids.update, list(search))
783
parent_map = self.source.get_parent_map(transitive_ids)
784
order = tsort.topo_sort(parent_map.items())
785
pb = ui.ui_factory.nested_progress_bar()
788
# TODO for incremental cross-format work:
789
# make a versioned file with the following content:
790
# all revisions we have been asked to join
791
# all their ancestors that are *not* in target already.
792
# the immediate parents of the above two sets, with
793
# empty parent lists - these versions are in target already
794
# and the incorrect version data will be ignored.
795
# TODO: for all ancestors that are present in target already,
796
# check them for consistent data, this requires moving sha1 from
798
# TODO: remove parent texts when they are not relevant any more for
799
# memory pressure reduction. RBC 20060313
800
# pb.update('Converting versioned data', 0, len(order))
802
for index, version in enumerate(order):
803
pb.update('Converting versioned data', index, total)
804
_, _, parent_text = target.add_lines(version,
806
self.source.get_lines(version),
807
parent_texts=parent_texts)
808
parent_texts[version] = parent_text
810
# this should hit the native code path for target
811
if target is not self.target:
812
return self.target.join(temp_source,
822
def _get_source_version_ids(self, version_ids, ignore_missing):
823
"""Determine the version ids to be used from self.source.
825
:param version_ids: The caller-supplied version ids to check. (None
826
for all). If None is in version_ids, it is stripped.
827
:param ignore_missing: if True, remove missing ids from the version
828
list. If False, raise RevisionNotPresent on
829
a missing version id.
830
:return: A set of version ids.
832
if version_ids is None:
833
# None cannot be in source.versions
834
return set(self.source.versions())
837
return set(self.source.versions()).intersection(set(version_ids))
839
new_version_ids = set()
840
for version in version_ids:
843
if not self.source.has_version(version):
844
raise errors.RevisionNotPresent(version, str(self.source))
846
new_version_ids.add(version)
847
return new_version_ids