521
526
return ('_BreadthFirstSearcher(iterations=%d, %s,'
522
527
' seen=%r)' % (self._iterations, search, list(self.seen)))
529
def get_result(self):
530
"""Get a SearchResult for the current state of this searcher.
532
:return: A SearchResult for this search so far. The SearchResult is
533
static - the search can be advanced and the search result will not
534
be invalidated or altered.
536
if self._returning == 'next':
537
# We have to know the current nodes children to be able to list the
538
# exclude keys for them. However, while we could have a second
539
# look-ahead result buffer and shuffle things around, this method
540
# is typically only called once per search - when memoising the
541
# results of the search.
542
found, ghosts, next, parents = self._do_query(self._next_query)
543
# pretend we didn't query: perhaps we should tweak _do_query to be
544
# entirely stateless?
545
self.seen.difference_update(next)
546
next_query = next.union(ghosts)
548
next_query = self._next_query
549
excludes = self._stopped_keys.union(next_query)
550
included_keys = self.seen.difference(excludes)
551
return SearchResult(self._started_keys, excludes, len(included_keys),
525
555
"""Return the next ancestors of this revision.
576
608
self._current_ghosts = ghosts
577
609
self._next_query = next
578
610
self._current_parents = parents
611
# ghosts are implicit stop points, otherwise the search cannot be
612
# repeated when ghosts are filled.
613
self._stopped_keys.update(ghosts)
580
615
def _do_query(self, revisions):
581
616
"""Query for revisions.
618
Adds revisions to the seen set.
583
620
:param revisions: Revisions to query.
584
621
:return: A tuple: (set(found_revisions), set(ghost_revisions),
585
622
set(parents_of_found_revisions), dict(found_revisions:parents)).
587
624
found_parents = set()
588
625
parents_of_found = set()
626
# revisions may contain nodes that point to other nodes in revisions:
627
# we want to filter them out.
628
self.seen.update(revisions)
589
629
parent_map = self._parents_provider.get_parent_map(revisions)
590
630
for rev_id, parents in parent_map.iteritems():
591
631
found_parents.add(rev_id)
592
632
parents_of_found.update(p for p in parents if p not in self.seen)
593
633
ghost_parents = revisions - found_parents
594
self.seen.update(parents_of_found)
595
634
return found_parents, ghost_parents, parents_of_found, parent_map
597
636
def __iter__(self):
621
660
stopped = self._next_query.intersection(revisions)
622
661
self._next_query = self._next_query.difference(revisions)
625
stopped.update(self._current_present.intersection(revisions))
626
stopped.update(self._current_ghosts.intersection(revisions))
663
stopped_present = self._current_present.intersection(revisions)
664
stopped = stopped_present.union(
665
self._current_ghosts.intersection(revisions))
627
666
self._current_present.difference_update(stopped)
628
667
self._current_ghosts.difference_update(stopped)
629
668
# stopping 'x' should stop returning parents of 'x', but
630
669
# not if 'y' always references those same parents
631
670
stop_rev_references = {}
671
for rev in stopped_present:
633
672
for parent_id in self._current_parents[rev]:
634
673
if parent_id not in stop_rev_references:
635
674
stop_rev_references[parent_id] = 0
658
698
ghost/not ghost status of revisions. (A tuple (present, ghosted)).
660
700
revisions = frozenset(revisions)
701
self._started_keys.update(revisions)
702
new_revisions = revisions.difference(self.seen)
703
revs, ghosts, query, parents = self._do_query(revisions)
704
self._stopped_keys.update(ghosts)
661
705
if self._returning == 'next':
662
self._next_query.update(revisions.difference(self.seen))
663
self.seen.update(revisions)
706
self._next_query.update(new_revisions)
665
708
# perform a query on revisions
666
revs, ghosts, query, parents = self._do_query(revisions)
667
709
self._current_present.update(revs)
668
710
self._current_ghosts.update(ghosts)
669
711
self._next_query.update(query)
670
712
self._current_parents.update(parents)
671
713
return revs, ghosts
716
class SearchResult(object):
717
"""The result of a breadth first search.
719
A SearchResult provides the ability to reconstruct the search or access a
720
set of the keys the search found.
723
def __init__(self, start_keys, exclude_keys, key_count, keys):
724
"""Create a SearchResult.
726
:param start_keys: The keys the search started at.
727
:param exclude_keys: The keys the search excludes.
728
:param key_count: The total number of keys (from start to but not
730
:param keys: The keys the search found. Note that in future we may get
731
a SearchResult from a smart server, in which case the keys list is
732
not necessarily immediately available.
734
self._recipe = (start_keys, exclude_keys, key_count)
735
self._keys = frozenset(keys)
737
def get_recipe(self):
738
"""Return a recipe that can be used to replay this search.
740
The recipe allows reconstruction of the same results at a later date
741
without knowing all the found keys. The essential elements are a list
742
of keys to start and and to stop at. In order to give reproducible
743
results when ghosts are encountered by a search they are automatically
744
added to the exclude list (or else ghost filling may alter the
747
:return: A tuple (start_keys_set, exclude_keys_set, revision_count). To
748
recreate the results of this search, create a breadth first
749
searcher on the same graph starting at start_keys. Then call next()
750
(or next_with_ghosts()) repeatedly, and on every result, call
751
stop_searching_any on any keys from the exclude_keys set. The
752
revision_count value acts as a trivial cross-check - the found
753
revisions of the new search should have as many elements as
754
revision_count. If it does not, then additional revisions have been
755
ghosted since the search was executed the first time and the second
761
"""Return the keys found in this search.
763
:return: A set of keys.