Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/alembic/script/revision.py : 15%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import collections
2import re
4from sqlalchemy import util as sqlautil
6from .. import util
7from ..util import compat
9_relative_destination = re.compile(r"(?:(.+?)@)?(\w+)?((?:\+|-)\d+)")
10_revision_illegal_chars = ["@", "-", "+"]
13class RevisionError(Exception):
14 pass
17class RangeNotAncestorError(RevisionError):
18 def __init__(self, lower, upper):
19 self.lower = lower
20 self.upper = upper
21 super(RangeNotAncestorError, self).__init__(
22 "Revision %s is not an ancestor of revision %s"
23 % (lower or "base", upper or "base")
24 )
27class MultipleHeads(RevisionError):
28 def __init__(self, heads, argument):
29 self.heads = heads
30 self.argument = argument
31 super(MultipleHeads, self).__init__(
32 "Multiple heads are present for given argument '%s'; "
33 "%s" % (argument, ", ".join(heads))
34 )
37class ResolutionError(RevisionError):
38 def __init__(self, message, argument):
39 super(ResolutionError, self).__init__(message)
40 self.argument = argument
43class RevisionMap(object):
44 """Maintains a map of :class:`.Revision` objects.
46 :class:`.RevisionMap` is used by :class:`.ScriptDirectory` to maintain
47 and traverse the collection of :class:`.Script` objects, which are
48 themselves instances of :class:`.Revision`.
50 """
52 def __init__(self, generator):
53 """Construct a new :class:`.RevisionMap`.
55 :param generator: a zero-arg callable that will generate an iterable
56 of :class:`.Revision` instances to be used. These are typically
57 :class:`.Script` subclasses within regular Alembic use.
59 """
60 self._generator = generator
62 @util.memoized_property
63 def heads(self):
64 """All "head" revisions as strings.
66 This is normally a tuple of length one,
67 unless unmerged branches are present.
69 :return: a tuple of string revision numbers.
71 """
72 self._revision_map
73 return self.heads
75 @util.memoized_property
76 def bases(self):
77 """All "base" revisions as strings.
79 These are revisions that have a ``down_revision`` of None,
80 or empty tuple.
82 :return: a tuple of string revision numbers.
84 """
85 self._revision_map
86 return self.bases
88 @util.memoized_property
89 def _real_heads(self):
90 """All "real" head revisions as strings.
92 :return: a tuple of string revision numbers.
94 """
95 self._revision_map
96 return self._real_heads
98 @util.memoized_property
99 def _real_bases(self):
100 """All "real" base revisions as strings.
102 :return: a tuple of string revision numbers.
104 """
105 self._revision_map
106 return self._real_bases
108 @util.memoized_property
109 def _revision_map(self):
110 """memoized attribute, initializes the revision map from the
111 initial collection.
113 """
114 map_ = {}
116 heads = sqlautil.OrderedSet()
117 _real_heads = sqlautil.OrderedSet()
118 self.bases = ()
119 self._real_bases = ()
121 has_branch_labels = set()
122 has_depends_on = set()
123 for revision in self._generator():
125 if revision.revision in map_:
126 util.warn(
127 "Revision %s is present more than once" % revision.revision
128 )
129 map_[revision.revision] = revision
130 if revision.branch_labels:
131 has_branch_labels.add(revision)
132 if revision.dependencies:
133 has_depends_on.add(revision)
134 heads.add(revision.revision)
135 _real_heads.add(revision.revision)
136 if revision.is_base:
137 self.bases += (revision.revision,)
138 if revision._is_real_base:
139 self._real_bases += (revision.revision,)
141 # add the branch_labels to the map_. We'll need these
142 # to resolve the dependencies.
143 for revision in has_branch_labels:
144 self._map_branch_labels(revision, map_)
146 for revision in has_depends_on:
147 self._add_depends_on(revision, map_)
149 for rev in map_.values():
150 for downrev in rev._all_down_revisions:
151 if downrev not in map_:
152 util.warn(
153 "Revision %s referenced from %s is not present"
154 % (downrev, rev)
155 )
156 down_revision = map_[downrev]
157 down_revision.add_nextrev(rev)
158 if downrev in rev._versioned_down_revisions:
159 heads.discard(downrev)
160 _real_heads.discard(downrev)
162 map_[None] = map_[()] = None
163 self.heads = tuple(heads)
164 self._real_heads = tuple(_real_heads)
166 for revision in has_branch_labels:
167 self._add_branches(revision, map_, map_branch_labels=False)
168 return map_
170 def _map_branch_labels(self, revision, map_):
171 if revision.branch_labels:
172 for branch_label in revision._orig_branch_labels:
173 if branch_label in map_:
174 raise RevisionError(
175 "Branch name '%s' in revision %s already "
176 "used by revision %s"
177 % (
178 branch_label,
179 revision.revision,
180 map_[branch_label].revision,
181 )
182 )
183 map_[branch_label] = revision
185 def _add_branches(self, revision, map_, map_branch_labels=True):
186 if map_branch_labels:
187 self._map_branch_labels(revision, map_)
189 if revision.branch_labels:
190 revision.branch_labels.update(revision.branch_labels)
191 for node in self._get_descendant_nodes(
192 [revision], map_, include_dependencies=False
193 ):
194 node.branch_labels.update(revision.branch_labels)
196 parent = node
197 while (
198 parent
199 and not parent._is_real_branch_point
200 and not parent.is_merge_point
201 ):
203 parent.branch_labels.update(revision.branch_labels)
204 if parent.down_revision:
205 parent = map_[parent.down_revision]
206 else:
207 break
209 def _add_depends_on(self, revision, map_):
210 if revision.dependencies:
211 deps = [map_[dep] for dep in util.to_tuple(revision.dependencies)]
212 revision._resolved_dependencies = tuple([d.revision for d in deps])
214 def add_revision(self, revision, _replace=False):
215 """add a single revision to an existing map.
217 This method is for single-revision use cases, it's not
218 appropriate for fully populating an entire revision map.
220 """
221 map_ = self._revision_map
222 if not _replace and revision.revision in map_:
223 util.warn(
224 "Revision %s is present more than once" % revision.revision
225 )
226 elif _replace and revision.revision not in map_:
227 raise Exception("revision %s not in map" % revision.revision)
229 map_[revision.revision] = revision
230 self._add_branches(revision, map_)
231 self._add_depends_on(revision, map_)
233 if revision.is_base:
234 self.bases += (revision.revision,)
235 if revision._is_real_base:
236 self._real_bases += (revision.revision,)
237 for downrev in revision._all_down_revisions:
238 if downrev not in map_:
239 util.warn(
240 "Revision %s referenced from %s is not present"
241 % (downrev, revision)
242 )
243 map_[downrev].add_nextrev(revision)
244 if revision._is_real_head:
245 self._real_heads = tuple(
246 head
247 for head in self._real_heads
248 if head
249 not in set(revision._all_down_revisions).union(
250 [revision.revision]
251 )
252 ) + (revision.revision,)
253 if revision.is_head:
254 self.heads = tuple(
255 head
256 for head in self.heads
257 if head
258 not in set(revision._versioned_down_revisions).union(
259 [revision.revision]
260 )
261 ) + (revision.revision,)
263 def get_current_head(self, branch_label=None):
264 """Return the current head revision.
266 If the script directory has multiple heads
267 due to branching, an error is raised;
268 :meth:`.ScriptDirectory.get_heads` should be
269 preferred.
271 :param branch_label: optional branch name which will limit the
272 heads considered to those which include that branch_label.
274 :return: a string revision number.
276 .. seealso::
278 :meth:`.ScriptDirectory.get_heads`
280 """
281 current_heads = self.heads
282 if branch_label:
283 current_heads = self.filter_for_lineage(
284 current_heads, branch_label
285 )
286 if len(current_heads) > 1:
287 raise MultipleHeads(
288 current_heads,
289 "%s@head" % branch_label if branch_label else "head",
290 )
292 if current_heads:
293 return current_heads[0]
294 else:
295 return None
297 def _get_base_revisions(self, identifier):
298 return self.filter_for_lineage(self.bases, identifier)
300 def get_revisions(self, id_):
301 """Return the :class:`.Revision` instances with the given rev id
302 or identifiers.
304 May be given a single identifier, a sequence of identifiers, or the
305 special symbols "head" or "base". The result is a tuple of one
306 or more identifiers, or an empty tuple in the case of "base".
308 In the cases where 'head', 'heads' is requested and the
309 revision map is empty, returns an empty tuple.
311 Supports partial identifiers, where the given identifier
312 is matched against all identifiers that start with the given
313 characters; if there is exactly one match, that determines the
314 full revision.
316 """
318 if isinstance(id_, (list, tuple, set, frozenset)):
319 return sum([self.get_revisions(id_elem) for id_elem in id_], ())
320 else:
321 resolved_id, branch_label = self._resolve_revision_number(id_)
322 return tuple(
323 self._revision_for_ident(rev_id, branch_label)
324 for rev_id in resolved_id
325 )
327 def get_revision(self, id_):
328 """Return the :class:`.Revision` instance with the given rev id.
330 If a symbolic name such as "head" or "base" is given, resolves
331 the identifier into the current head or base revision. If the symbolic
332 name refers to multiples, :class:`.MultipleHeads` is raised.
334 Supports partial identifiers, where the given identifier
335 is matched against all identifiers that start with the given
336 characters; if there is exactly one match, that determines the
337 full revision.
339 """
341 resolved_id, branch_label = self._resolve_revision_number(id_)
342 if len(resolved_id) > 1:
343 raise MultipleHeads(resolved_id, id_)
344 elif resolved_id:
345 resolved_id = resolved_id[0]
347 return self._revision_for_ident(resolved_id, branch_label)
349 def _resolve_branch(self, branch_label):
350 try:
351 branch_rev = self._revision_map[branch_label]
352 except KeyError:
353 try:
354 nonbranch_rev = self._revision_for_ident(branch_label)
355 except ResolutionError:
356 raise ResolutionError(
357 "No such branch: '%s'" % branch_label, branch_label
358 )
359 else:
360 return nonbranch_rev
361 else:
362 return branch_rev
364 def _revision_for_ident(self, resolved_id, check_branch=None):
365 if check_branch:
366 branch_rev = self._resolve_branch(check_branch)
367 else:
368 branch_rev = None
370 try:
371 revision = self._revision_map[resolved_id]
372 except KeyError:
373 # break out to avoid misleading py3k stack traces
374 revision = False
375 if revision is False:
376 # do a partial lookup
377 revs = [
378 x
379 for x in self._revision_map
380 if x and len(x) > 3 and x.startswith(resolved_id)
381 ]
383 if branch_rev:
384 revs = self.filter_for_lineage(revs, check_branch)
385 if not revs:
386 raise ResolutionError(
387 "No such revision or branch '%s'%s"
388 % (
389 resolved_id,
390 (
391 "; please ensure at least four characters are "
392 "present for partial revision identifier matches"
393 if len(resolved_id) < 4
394 else ""
395 ),
396 ),
397 resolved_id,
398 )
399 elif len(revs) > 1:
400 raise ResolutionError(
401 "Multiple revisions start "
402 "with '%s': %s..."
403 % (resolved_id, ", ".join("'%s'" % r for r in revs[0:3])),
404 resolved_id,
405 )
406 else:
407 revision = self._revision_map[revs[0]]
409 if check_branch and revision is not None:
410 if not self._shares_lineage(
411 revision.revision, branch_rev.revision
412 ):
413 raise ResolutionError(
414 "Revision %s is not a member of branch '%s'"
415 % (revision.revision, check_branch),
416 resolved_id,
417 )
418 return revision
420 def _filter_into_branch_heads(self, targets):
421 targets = set(targets)
423 for rev in list(targets):
424 if targets.intersection(
425 self._get_descendant_nodes([rev], include_dependencies=False)
426 ).difference([rev]):
427 targets.discard(rev)
428 return targets
430 def filter_for_lineage(
431 self, targets, check_against, include_dependencies=False
432 ):
433 id_, branch_label = self._resolve_revision_number(check_against)
435 shares = []
436 if branch_label:
437 shares.append(branch_label)
438 if id_:
439 shares.extend(id_)
441 return [
442 tg
443 for tg in targets
444 if self._shares_lineage(
445 tg, shares, include_dependencies=include_dependencies
446 )
447 ]
449 def _shares_lineage(
450 self, target, test_against_revs, include_dependencies=False
451 ):
452 if not test_against_revs:
453 return True
454 if not isinstance(target, Revision):
455 target = self._revision_for_ident(target)
457 test_against_revs = [
458 self._revision_for_ident(test_against_rev)
459 if not isinstance(test_against_rev, Revision)
460 else test_against_rev
461 for test_against_rev in util.to_tuple(
462 test_against_revs, default=()
463 )
464 ]
466 return bool(
467 set(
468 self._get_descendant_nodes(
469 [target], include_dependencies=include_dependencies
470 )
471 )
472 .union(
473 self._get_ancestor_nodes(
474 [target], include_dependencies=include_dependencies
475 )
476 )
477 .intersection(test_against_revs)
478 )
480 def _resolve_revision_number(self, id_):
481 if isinstance(id_, compat.string_types) and "@" in id_:
482 branch_label, id_ = id_.split("@", 1)
484 elif id_ is not None and (
485 (
486 isinstance(id_, tuple)
487 and id_
488 and not isinstance(id_[0], compat.string_types)
489 )
490 or not isinstance(id_, compat.string_types + (tuple,))
491 ):
492 raise RevisionError(
493 "revision identifier %r is not a string; ensure database "
494 "driver settings are correct" % (id_,)
495 )
497 else:
498 branch_label = None
500 # ensure map is loaded
501 self._revision_map
502 if id_ == "heads":
503 if branch_label:
504 return (
505 self.filter_for_lineage(self.heads, branch_label),
506 branch_label,
507 )
508 else:
509 return self._real_heads, branch_label
510 elif id_ == "head":
511 current_head = self.get_current_head(branch_label)
512 if current_head:
513 return (current_head,), branch_label
514 else:
515 return (), branch_label
516 elif id_ == "base" or id_ is None:
517 return (), branch_label
518 else:
519 return util.to_tuple(id_, default=None), branch_label
521 def _relative_iterate(
522 self,
523 destination,
524 source,
525 is_upwards,
526 implicit_base,
527 inclusive,
528 assert_relative_length,
529 ):
530 if isinstance(destination, compat.string_types):
531 match = _relative_destination.match(destination)
532 if not match:
533 return None
534 else:
535 return None
537 relative = int(match.group(3))
538 symbol = match.group(2)
539 branch_label = match.group(1)
541 reldelta = 1 if inclusive and not symbol else 0
543 if is_upwards:
544 if branch_label:
545 from_ = "%s@head" % branch_label
546 elif symbol:
547 if symbol.startswith("head"):
548 from_ = symbol
549 else:
550 from_ = "%s@head" % symbol
551 else:
552 from_ = "head"
553 to_ = source
554 else:
555 if branch_label:
556 to_ = "%s@base" % branch_label
557 elif symbol:
558 to_ = "%s@base" % symbol
559 else:
560 to_ = "base"
561 from_ = source
563 revs = list(
564 self._iterate_revisions(
565 from_, to_, inclusive=inclusive, implicit_base=implicit_base
566 )
567 )
569 if symbol:
570 if branch_label:
571 symbol_rev = self.get_revision(
572 "%s@%s" % (branch_label, symbol)
573 )
574 else:
575 symbol_rev = self.get_revision(symbol)
576 if symbol.startswith("head"):
577 index = 0
578 elif symbol == "base":
579 index = len(revs) - 1
580 else:
581 range_ = compat.range(len(revs) - 1, 0, -1)
582 for index in range_:
583 if symbol_rev.revision == revs[index].revision:
584 break
585 else:
586 index = 0
587 else:
588 index = 0
589 if is_upwards:
590 revs = revs[index - relative - reldelta :]
591 if (
592 not index
593 and assert_relative_length
594 and len(revs) < abs(relative - reldelta)
595 ):
596 raise RevisionError(
597 "Relative revision %s didn't "
598 "produce %d migrations" % (destination, abs(relative))
599 )
600 else:
601 revs = revs[0 : index - relative + reldelta]
602 if (
603 not index
604 and assert_relative_length
605 and len(revs) != abs(relative) + reldelta
606 ):
607 raise RevisionError(
608 "Relative revision %s didn't "
609 "produce %d migrations" % (destination, abs(relative))
610 )
612 return iter(revs)
614 def iterate_revisions(
615 self,
616 upper,
617 lower,
618 implicit_base=False,
619 inclusive=False,
620 assert_relative_length=True,
621 select_for_downgrade=False,
622 ):
623 """Iterate through script revisions, starting at the given
624 upper revision identifier and ending at the lower.
626 The traversal uses strictly the `down_revision`
627 marker inside each migration script, so
628 it is a requirement that upper >= lower,
629 else you'll get nothing back.
631 The iterator yields :class:`.Revision` objects.
633 """
635 relative_upper = self._relative_iterate(
636 upper,
637 lower,
638 True,
639 implicit_base,
640 inclusive,
641 assert_relative_length,
642 )
643 if relative_upper:
644 return relative_upper
646 relative_lower = self._relative_iterate(
647 lower,
648 upper,
649 False,
650 implicit_base,
651 inclusive,
652 assert_relative_length,
653 )
654 if relative_lower:
655 return relative_lower
657 return self._iterate_revisions(
658 upper,
659 lower,
660 inclusive=inclusive,
661 implicit_base=implicit_base,
662 select_for_downgrade=select_for_downgrade,
663 )
665 def _get_descendant_nodes(
666 self,
667 targets,
668 map_=None,
669 check=False,
670 omit_immediate_dependencies=False,
671 include_dependencies=True,
672 ):
674 if omit_immediate_dependencies:
676 def fn(rev):
677 if rev not in targets:
678 return rev._all_nextrev
679 else:
680 return rev.nextrev
682 elif include_dependencies:
684 def fn(rev):
685 return rev._all_nextrev
687 else:
689 def fn(rev):
690 return rev.nextrev
692 return self._iterate_related_revisions(
693 fn, targets, map_=map_, check=check
694 )
696 def _get_ancestor_nodes(
697 self, targets, map_=None, check=False, include_dependencies=True
698 ):
700 if include_dependencies:
702 def fn(rev):
703 return rev._all_down_revisions
705 else:
707 def fn(rev):
708 return rev._versioned_down_revisions
710 return self._iterate_related_revisions(
711 fn, targets, map_=map_, check=check
712 )
714 def _iterate_related_revisions(self, fn, targets, map_, check=False):
715 if map_ is None:
716 map_ = self._revision_map
718 seen = set()
719 todo = collections.deque()
720 for target in targets:
722 todo.append(target)
723 if check:
724 per_target = set()
726 while todo:
727 rev = todo.pop()
728 if check:
729 per_target.add(rev)
731 if rev in seen:
732 continue
733 seen.add(rev)
734 todo.extend(map_[rev_id] for rev_id in fn(rev))
735 yield rev
736 if check:
737 overlaps = per_target.intersection(targets).difference(
738 [target]
739 )
740 if overlaps:
741 raise RevisionError(
742 "Requested revision %s overlaps with "
743 "other requested revisions %s"
744 % (
745 target.revision,
746 ", ".join(r.revision for r in overlaps),
747 )
748 )
750 def _iterate_revisions(
751 self,
752 upper,
753 lower,
754 inclusive=True,
755 implicit_base=False,
756 select_for_downgrade=False,
757 ):
758 """iterate revisions from upper to lower.
760 The traversal is depth-first within branches, and breadth-first
761 across branches as a whole.
763 """
765 requested_lowers = self.get_revisions(lower)
767 # some complexity to accommodate an iteration where some
768 # branches are starting from nothing, and others are starting
769 # from a given point. Additionally, if the bottom branch
770 # is specified using a branch identifier, then we limit operations
771 # to just that branch.
773 limit_to_lower_branch = isinstance(
774 lower, compat.string_types
775 ) and lower.endswith("@base")
777 uppers = util.dedupe_tuple(self.get_revisions(upper))
779 if not uppers and not requested_lowers:
780 return
782 upper_ancestors = set(self._get_ancestor_nodes(uppers, check=True))
784 if limit_to_lower_branch:
785 lowers = self.get_revisions(self._get_base_revisions(lower))
786 elif implicit_base and requested_lowers:
787 lower_ancestors = set(self._get_ancestor_nodes(requested_lowers))
788 lower_descendants = set(
789 self._get_descendant_nodes(requested_lowers)
790 )
791 base_lowers = set()
792 candidate_lowers = upper_ancestors.difference(
793 lower_ancestors
794 ).difference(lower_descendants)
795 for rev in candidate_lowers:
796 for downrev in rev._all_down_revisions:
797 if self._revision_map[downrev] in candidate_lowers:
798 break
799 else:
800 base_lowers.add(rev)
801 lowers = base_lowers.union(requested_lowers)
802 elif implicit_base:
803 base_lowers = set(self.get_revisions(self._real_bases))
804 lowers = base_lowers.union(requested_lowers)
805 elif not requested_lowers:
806 lowers = set(self.get_revisions(self._real_bases))
807 else:
808 lowers = requested_lowers
810 # represents all nodes we will produce
811 total_space = set(
812 rev.revision for rev in upper_ancestors
813 ).intersection(
814 rev.revision
815 for rev in self._get_descendant_nodes(
816 lowers,
817 check=True,
818 omit_immediate_dependencies=(
819 select_for_downgrade and requested_lowers
820 ),
821 )
822 )
824 if not total_space:
825 # no nodes. determine if this is an invalid range
826 # or not.
827 start_from = set(requested_lowers)
828 start_from.update(
829 self._get_ancestor_nodes(
830 list(start_from), include_dependencies=True
831 )
832 )
834 # determine all the current branch points represented
835 # by requested_lowers
836 start_from = self._filter_into_branch_heads(start_from)
838 # if the requested start is one of those branch points,
839 # then just return empty set
840 if start_from.intersection(upper_ancestors):
841 return
842 else:
843 # otherwise, they requested nodes out of
844 # order
845 raise RangeNotAncestorError(lower, upper)
847 # organize branch points to be consumed separately from
848 # member nodes
849 branch_todo = set(
850 rev
851 for rev in (self._revision_map[rev] for rev in total_space)
852 if rev._is_real_branch_point
853 and len(total_space.intersection(rev._all_nextrev)) > 1
854 )
856 # it's not possible for any "uppers" to be in branch_todo,
857 # because the ._all_nextrev of those nodes is not in total_space
858 # assert not branch_todo.intersection(uppers)
860 todo = collections.deque(
861 r for r in uppers if r.revision in total_space
862 )
864 # iterate for total_space being emptied out
865 total_space_modified = True
866 while total_space:
868 if not total_space_modified:
869 raise RevisionError(
870 "Dependency resolution failed; iteration can't proceed"
871 )
872 total_space_modified = False
873 # when everything non-branch pending is consumed,
874 # add to the todo any branch nodes that have no
875 # descendants left in the queue
876 if not todo:
877 todo.extendleft(
878 sorted(
879 (
880 rev
881 for rev in branch_todo
882 if not rev._all_nextrev.intersection(total_space)
883 ),
884 # favor "revisioned" branch points before
885 # dependent ones
886 key=lambda rev: 0 if rev.is_branch_point else 1,
887 )
888 )
889 branch_todo.difference_update(todo)
890 # iterate nodes that are in the immediate todo
891 while todo:
892 rev = todo.popleft()
893 total_space.remove(rev.revision)
894 total_space_modified = True
896 # do depth first for elements within branches,
897 # don't consume any actual branch nodes
898 todo.extendleft(
899 [
900 self._revision_map[downrev]
901 for downrev in reversed(rev._all_down_revisions)
902 if self._revision_map[downrev] not in branch_todo
903 and downrev in total_space
904 ]
905 )
907 if not inclusive and rev in requested_lowers:
908 continue
909 yield rev
911 assert not branch_todo
914class Revision(object):
915 """Base class for revisioned objects.
917 The :class:`.Revision` class is the base of the more public-facing
918 :class:`.Script` object, which represents a migration script.
919 The mechanics of revision management and traversal are encapsulated
920 within :class:`.Revision`, while :class:`.Script` applies this logic
921 to Python files in a version directory.
923 """
925 nextrev = frozenset()
926 """following revisions, based on down_revision only."""
928 _all_nextrev = frozenset()
930 revision = None
931 """The string revision number."""
933 down_revision = None
934 """The ``down_revision`` identifier(s) within the migration script.
936 Note that the total set of "down" revisions is
937 down_revision + dependencies.
939 """
941 dependencies = None
942 """Additional revisions which this revision is dependent on.
944 From a migration standpoint, these dependencies are added to the
945 down_revision to form the full iteration. However, the separation
946 of down_revision from "dependencies" is to assist in navigating
947 a history that contains many branches, typically a multi-root scenario.
949 """
951 branch_labels = None
952 """Optional string/tuple of symbolic names to apply to this
953 revision's branch"""
955 @classmethod
956 def verify_rev_id(cls, revision):
957 illegal_chars = set(revision).intersection(_revision_illegal_chars)
958 if illegal_chars:
959 raise RevisionError(
960 "Character(s) '%s' not allowed in revision identifier '%s'"
961 % (", ".join(sorted(illegal_chars)), revision)
962 )
964 def __init__(
965 self, revision, down_revision, dependencies=None, branch_labels=None
966 ):
967 self.verify_rev_id(revision)
968 self.revision = revision
969 self.down_revision = tuple_rev_as_scalar(down_revision)
970 self.dependencies = tuple_rev_as_scalar(dependencies)
971 self._resolved_dependencies = ()
972 self._orig_branch_labels = util.to_tuple(branch_labels, default=())
973 self.branch_labels = set(self._orig_branch_labels)
975 def __repr__(self):
976 args = [repr(self.revision), repr(self.down_revision)]
977 if self.dependencies:
978 args.append("dependencies=%r" % (self.dependencies,))
979 if self.branch_labels:
980 args.append("branch_labels=%r" % (self.branch_labels,))
981 return "%s(%s)" % (self.__class__.__name__, ", ".join(args))
983 def add_nextrev(self, revision):
984 self._all_nextrev = self._all_nextrev.union([revision.revision])
985 if self.revision in revision._versioned_down_revisions:
986 self.nextrev = self.nextrev.union([revision.revision])
988 @property
989 def _all_down_revisions(self):
990 return (
991 util.to_tuple(self.down_revision, default=())
992 + self._resolved_dependencies
993 )
995 @property
996 def _versioned_down_revisions(self):
997 return util.to_tuple(self.down_revision, default=())
999 @property
1000 def is_head(self):
1001 """Return True if this :class:`.Revision` is a 'head' revision.
1003 This is determined based on whether any other :class:`.Script`
1004 within the :class:`.ScriptDirectory` refers to this
1005 :class:`.Script`. Multiple heads can be present.
1007 """
1008 return not bool(self.nextrev)
1010 @property
1011 def _is_real_head(self):
1012 return not bool(self._all_nextrev)
1014 @property
1015 def is_base(self):
1016 """Return True if this :class:`.Revision` is a 'base' revision."""
1018 return self.down_revision is None
1020 @property
1021 def _is_real_base(self):
1022 """Return True if this :class:`.Revision` is a "real" base revision,
1023 e.g. that it has no dependencies either."""
1025 # we use self.dependencies here because this is called up
1026 # in initialization where _real_dependencies isn't set up
1027 # yet
1028 return self.down_revision is None and self.dependencies is None
1030 @property
1031 def is_branch_point(self):
1032 """Return True if this :class:`.Script` is a branch point.
1034 A branchpoint is defined as a :class:`.Script` which is referred
1035 to by more than one succeeding :class:`.Script`, that is more
1036 than one :class:`.Script` has a `down_revision` identifier pointing
1037 here.
1039 """
1040 return len(self.nextrev) > 1
1042 @property
1043 def _is_real_branch_point(self):
1044 """Return True if this :class:`.Script` is a 'real' branch point,
1045 taking into account dependencies as well.
1047 """
1048 return len(self._all_nextrev) > 1
1050 @property
1051 def is_merge_point(self):
1052 """Return True if this :class:`.Script` is a merge point."""
1054 return len(self._versioned_down_revisions) > 1
1057def tuple_rev_as_scalar(rev):
1058 if not rev:
1059 return None
1060 elif len(rev) == 1:
1061 return rev[0]
1062 else:
1063 return rev