~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/graph.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-08-03 04:57:59 UTC
  • mfrom: (6042.1.2 fix-log-2)
  • Revision ID: pqm@pqm.ubuntu.com-20110803045759-1lrr8eymve8ofldr
(mbp) Log levels are no longer reset to what the log formatter supports (bug
 747958) (Thomi Richards)

Show diffs side-by-side

added added

removed removed

Lines of Context:
58
58
    def __repr__(self):
59
59
        return 'DictParentsProvider(%r)' % self.ancestry
60
60
 
61
 
    # Note: DictParentsProvider does not implement get_cached_parent_map
62
 
    #       Arguably, the data is clearly cached in memory. However, this class
63
 
    #       is mostly used for testing, and it keeps the tests clean to not
64
 
    #       change it.
65
 
 
66
61
    def get_parent_map(self, keys):
67
62
        """See StackedParentsProvider.get_parent_map"""
68
63
        ancestry = self.ancestry
69
 
        return dict([(k, ancestry[k]) for k in keys if k in ancestry])
 
64
        return dict((k, ancestry[k]) for k in keys if k in ancestry)
70
65
 
71
66
 
72
67
class StackedParentsProvider(object):
73
68
    """A parents provider which stacks (or unions) multiple providers.
74
 
 
 
69
    
75
70
    The providers are queries in the order of the provided parent_providers.
76
71
    """
77
 
 
 
72
    
78
73
    def __init__(self, parent_providers):
79
74
        self._parent_providers = parent_providers
80
75
 
96
91
        """
97
92
        found = {}
98
93
        remaining = set(keys)
99
 
        # This adds getattr() overhead to each get_parent_map call. However,
100
 
        # this is StackedParentsProvider, which means we're dealing with I/O
101
 
        # (either local indexes, or remote RPCs), so CPU overhead should be
102
 
        # minimal.
103
 
        for parents_provider in self._parent_providers:
104
 
            get_cached = getattr(parents_provider, 'get_cached_parent_map',
105
 
                                 None)
106
 
            if get_cached is None:
107
 
                continue
108
 
            new_found = get_cached(remaining)
109
 
            found.update(new_found)
110
 
            remaining.difference_update(new_found)
111
 
            if not remaining:
112
 
                break
113
 
        if not remaining:
114
 
            return found
115
94
        for parents_provider in self._parent_providers:
116
95
            new_found = parents_provider.get_parent_map(remaining)
117
96
            found.update(new_found)
171
150
            return None
172
151
        return dict(self._cache)
173
152
 
174
 
    def get_cached_parent_map(self, keys):
175
 
        """Return items from the cache.
176
 
 
177
 
        This returns the same info as get_parent_map, but explicitly does not
178
 
        invoke the supplied ParentsProvider to search for uncached values.
179
 
        """
180
 
        cache = self._cache
181
 
        if cache is None:
182
 
            return {}
183
 
        return dict([(key, cache[key]) for key in keys if key in cache])
184
 
 
185
153
    def get_parent_map(self, keys):
186
154
        """See StackedParentsProvider.get_parent_map."""
187
155
        cache = self._cache
1346
1314
        return ('_BreadthFirstSearcher(iterations=%d, %s,'
1347
1315
                ' seen=%r)' % (self._iterations, search, list(self.seen)))
1348
1316
 
1349
 
    def get_state(self):
1350
 
        """Get the current state of this searcher.
 
1317
    def get_result(self):
 
1318
        """Get a SearchResult for the current state of this searcher.
1351
1319
 
1352
 
        :return: Tuple with started keys, excludes and included keys
 
1320
        :return: A SearchResult for this search so far. The SearchResult is
 
1321
            static - the search can be advanced and the search result will not
 
1322
            be invalidated or altered.
1353
1323
        """
1354
1324
        if self._returning == 'next':
1355
1325
            # We have to know the current nodes children to be able to list the
1366
1336
            next_query = self._next_query
1367
1337
        excludes = self._stopped_keys.union(next_query)
1368
1338
        included_keys = self.seen.difference(excludes)
1369
 
        return self._started_keys, excludes, included_keys
1370
 
 
1371
 
    def _get_result(self):
1372
 
        """Get a SearchResult for the current state of this searcher.
1373
 
 
1374
 
        :return: A SearchResult for this search so far. The SearchResult is
1375
 
            static - the search can be advanced and the search result will not
1376
 
            be invalidated or altered.
1377
 
        """
1378
 
        from bzrlib.vf_search import SearchResult
1379
 
        (started_keys, excludes, included_keys) = self.get_state()
1380
 
        return SearchResult(started_keys, excludes, len(included_keys),
 
1339
        return SearchResult(self._started_keys, excludes, len(included_keys),
1381
1340
            included_keys)
1382
1341
 
1383
1342
    def step(self):
1460
1419
        parents_of_found = set()
1461
1420
        # revisions may contain nodes that point to other nodes in revisions:
1462
1421
        # we want to filter them out.
1463
 
        seen = self.seen
1464
 
        seen.update(revisions)
 
1422
        self.seen.update(revisions)
1465
1423
        parent_map = self._parents_provider.get_parent_map(revisions)
1466
1424
        found_revisions.update(parent_map)
1467
1425
        for rev_id, parents in parent_map.iteritems():
1468
1426
            if parents is None:
1469
1427
                continue
1470
 
            new_found_parents = [p for p in parents if p not in seen]
 
1428
            new_found_parents = [p for p in parents if p not in self.seen]
1471
1429
            if new_found_parents:
1472
1430
                # Calling set.update() with an empty generator is actually
1473
1431
                # rather expensive.
1592
1550
            return revs, ghosts
1593
1551
 
1594
1552
 
1595
 
def invert_parent_map(parent_map):
1596
 
    """Given a map from child => parents, create a map of parent=>children"""
1597
 
    child_map = {}
1598
 
    for child, parents in parent_map.iteritems():
1599
 
        for p in parents:
1600
 
            # Any given parent is likely to have only a small handful
1601
 
            # of children, many will have only one. So we avoid mem overhead of
1602
 
            # a list, in exchange for extra copying of tuples
1603
 
            if p not in child_map:
1604
 
                child_map[p] = (child,)
1605
 
            else:
1606
 
                child_map[p] = child_map[p] + (child,)
1607
 
    return child_map
 
1553
class AbstractSearchResult(object):
 
1554
    """The result of a search, describing a set of keys.
 
1555
    
 
1556
    Search results are typically used as the 'fetch_spec' parameter when
 
1557
    fetching revisions.
 
1558
 
 
1559
    :seealso: AbstractSearch
 
1560
    """
 
1561
 
 
1562
    def get_recipe(self):
 
1563
        """Return a recipe that can be used to replay this search.
 
1564
 
 
1565
        The recipe allows reconstruction of the same results at a later date.
 
1566
 
 
1567
        :return: A tuple of `(search_kind_str, *details)`.  The details vary by
 
1568
            kind of search result.
 
1569
        """
 
1570
        raise NotImplementedError(self.get_recipe)
 
1571
 
 
1572
    def get_network_struct(self):
 
1573
        """Return a tuple that can be transmitted via the HPSS protocol."""
 
1574
        raise NotImplementedError(self.get_network_struct)
 
1575
 
 
1576
    def get_keys(self):
 
1577
        """Return the keys found in this search.
 
1578
 
 
1579
        :return: A set of keys.
 
1580
        """
 
1581
        raise NotImplementedError(self.get_keys)
 
1582
 
 
1583
    def is_empty(self):
 
1584
        """Return false if the search lists 1 or more revisions."""
 
1585
        raise NotImplementedError(self.is_empty)
 
1586
 
 
1587
    def refine(self, seen, referenced):
 
1588
        """Create a new search by refining this search.
 
1589
 
 
1590
        :param seen: Revisions that have been satisfied.
 
1591
        :param referenced: Revision references observed while satisfying some
 
1592
            of this search.
 
1593
        :return: A search result.
 
1594
        """
 
1595
        raise NotImplementedError(self.refine)
 
1596
 
 
1597
 
 
1598
class AbstractSearch(object):
 
1599
    """A search that can be executed, producing a search result.
 
1600
 
 
1601
    :seealso: AbstractSearchResult
 
1602
    """
 
1603
 
 
1604
    def execute(self):
 
1605
        """Construct a network-ready search result from this search description.
 
1606
 
 
1607
        This may take some time to search repositories, etc.
 
1608
 
 
1609
        :return: A search result (an object that implements
 
1610
            AbstractSearchResult's API).
 
1611
        """
 
1612
        raise NotImplementedError(self.execute)
 
1613
 
 
1614
 
 
1615
class SearchResult(AbstractSearchResult):
 
1616
    """The result of a breadth first search.
 
1617
 
 
1618
    A SearchResult provides the ability to reconstruct the search or access a
 
1619
    set of the keys the search found.
 
1620
    """
 
1621
 
 
1622
    def __init__(self, start_keys, exclude_keys, key_count, keys):
 
1623
        """Create a SearchResult.
 
1624
 
 
1625
        :param start_keys: The keys the search started at.
 
1626
        :param exclude_keys: The keys the search excludes.
 
1627
        :param key_count: The total number of keys (from start to but not
 
1628
            including exclude).
 
1629
        :param keys: The keys the search found. Note that in future we may get
 
1630
            a SearchResult from a smart server, in which case the keys list is
 
1631
            not necessarily immediately available.
 
1632
        """
 
1633
        self._recipe = ('search', start_keys, exclude_keys, key_count)
 
1634
        self._keys = frozenset(keys)
 
1635
 
 
1636
    def __repr__(self):
 
1637
        kind, start_keys, exclude_keys, key_count = self._recipe
 
1638
        if len(start_keys) > 5:
 
1639
            start_keys_repr = repr(list(start_keys)[:5])[:-1] + ', ...]'
 
1640
        else:
 
1641
            start_keys_repr = repr(start_keys)
 
1642
        if len(exclude_keys) > 5:
 
1643
            exclude_keys_repr = repr(list(exclude_keys)[:5])[:-1] + ', ...]'
 
1644
        else:
 
1645
            exclude_keys_repr = repr(exclude_keys)
 
1646
        return '<%s %s:(%s, %s, %d)>' % (self.__class__.__name__,
 
1647
            kind, start_keys_repr, exclude_keys_repr, key_count)
 
1648
 
 
1649
    def get_recipe(self):
 
1650
        """Return a recipe that can be used to replay this search.
 
1651
 
 
1652
        The recipe allows reconstruction of the same results at a later date
 
1653
        without knowing all the found keys. The essential elements are a list
 
1654
        of keys to start and to stop at. In order to give reproducible
 
1655
        results when ghosts are encountered by a search they are automatically
 
1656
        added to the exclude list (or else ghost filling may alter the
 
1657
        results).
 
1658
 
 
1659
        :return: A tuple ('search', start_keys_set, exclude_keys_set,
 
1660
            revision_count). To recreate the results of this search, create a
 
1661
            breadth first searcher on the same graph starting at start_keys.
 
1662
            Then call next() (or next_with_ghosts()) repeatedly, and on every
 
1663
            result, call stop_searching_any on any keys from the exclude_keys
 
1664
            set. The revision_count value acts as a trivial cross-check - the
 
1665
            found revisions of the new search should have as many elements as
 
1666
            revision_count. If it does not, then additional revisions have been
 
1667
            ghosted since the search was executed the first time and the second
 
1668
            time.
 
1669
        """
 
1670
        return self._recipe
 
1671
 
 
1672
    def get_network_struct(self):
 
1673
        start_keys = ' '.join(self._recipe[1])
 
1674
        stop_keys = ' '.join(self._recipe[2])
 
1675
        count = str(self._recipe[3])
 
1676
        return (self._recipe[0], '\n'.join((start_keys, stop_keys, count)))
 
1677
 
 
1678
    def get_keys(self):
 
1679
        """Return the keys found in this search.
 
1680
 
 
1681
        :return: A set of keys.
 
1682
        """
 
1683
        return self._keys
 
1684
 
 
1685
    def is_empty(self):
 
1686
        """Return false if the search lists 1 or more revisions."""
 
1687
        return self._recipe[3] == 0
 
1688
 
 
1689
    def refine(self, seen, referenced):
 
1690
        """Create a new search by refining this search.
 
1691
 
 
1692
        :param seen: Revisions that have been satisfied.
 
1693
        :param referenced: Revision references observed while satisfying some
 
1694
            of this search.
 
1695
        """
 
1696
        start = self._recipe[1]
 
1697
        exclude = self._recipe[2]
 
1698
        count = self._recipe[3]
 
1699
        keys = self.get_keys()
 
1700
        # New heads = referenced + old heads - seen things - exclude
 
1701
        pending_refs = set(referenced)
 
1702
        pending_refs.update(start)
 
1703
        pending_refs.difference_update(seen)
 
1704
        pending_refs.difference_update(exclude)
 
1705
        # New exclude = old exclude + satisfied heads
 
1706
        seen_heads = start.intersection(seen)
 
1707
        exclude.update(seen_heads)
 
1708
        # keys gets seen removed
 
1709
        keys = keys - seen
 
1710
        # length is reduced by len(seen)
 
1711
        count -= len(seen)
 
1712
        return SearchResult(pending_refs, exclude, count, keys)
 
1713
 
 
1714
 
 
1715
class PendingAncestryResult(AbstractSearchResult):
 
1716
    """A search result that will reconstruct the ancestry for some graph heads.
 
1717
 
 
1718
    Unlike SearchResult, this doesn't hold the complete search result in
 
1719
    memory, it just holds a description of how to generate it.
 
1720
    """
 
1721
 
 
1722
    def __init__(self, heads, repo):
 
1723
        """Constructor.
 
1724
 
 
1725
        :param heads: an iterable of graph heads.
 
1726
        :param repo: a repository to use to generate the ancestry for the given
 
1727
            heads.
 
1728
        """
 
1729
        self.heads = frozenset(heads)
 
1730
        self.repo = repo
 
1731
 
 
1732
    def __repr__(self):
 
1733
        if len(self.heads) > 5:
 
1734
            heads_repr = repr(list(self.heads)[:5])[:-1]
 
1735
            heads_repr += ', <%d more>...]' % (len(self.heads) - 5,)
 
1736
        else:
 
1737
            heads_repr = repr(self.heads)
 
1738
        return '<%s heads:%s repo:%r>' % (
 
1739
            self.__class__.__name__, heads_repr, self.repo)
 
1740
 
 
1741
    def get_recipe(self):
 
1742
        """Return a recipe that can be used to replay this search.
 
1743
 
 
1744
        The recipe allows reconstruction of the same results at a later date.
 
1745
 
 
1746
        :seealso SearchResult.get_recipe:
 
1747
 
 
1748
        :return: A tuple ('proxy-search', start_keys_set, set(), -1)
 
1749
            To recreate this result, create a PendingAncestryResult with the
 
1750
            start_keys_set.
 
1751
        """
 
1752
        return ('proxy-search', self.heads, set(), -1)
 
1753
 
 
1754
    def get_network_struct(self):
 
1755
        parts = ['ancestry-of']
 
1756
        parts.extend(self.heads)
 
1757
        return parts
 
1758
 
 
1759
    def get_keys(self):
 
1760
        """See SearchResult.get_keys.
 
1761
 
 
1762
        Returns all the keys for the ancestry of the heads, excluding
 
1763
        NULL_REVISION.
 
1764
        """
 
1765
        return self._get_keys(self.repo.get_graph())
 
1766
 
 
1767
    def _get_keys(self, graph):
 
1768
        NULL_REVISION = revision.NULL_REVISION
 
1769
        keys = [key for (key, parents) in graph.iter_ancestry(self.heads)
 
1770
                if key != NULL_REVISION and parents is not None]
 
1771
        return keys
 
1772
 
 
1773
    def is_empty(self):
 
1774
        """Return false if the search lists 1 or more revisions."""
 
1775
        if revision.NULL_REVISION in self.heads:
 
1776
            return len(self.heads) == 1
 
1777
        else:
 
1778
            return len(self.heads) == 0
 
1779
 
 
1780
    def refine(self, seen, referenced):
 
1781
        """Create a new search by refining this search.
 
1782
 
 
1783
        :param seen: Revisions that have been satisfied.
 
1784
        :param referenced: Revision references observed while satisfying some
 
1785
            of this search.
 
1786
        """
 
1787
        referenced = self.heads.union(referenced)
 
1788
        return PendingAncestryResult(referenced - seen, self.repo)
 
1789
 
 
1790
 
 
1791
class EmptySearchResult(AbstractSearchResult):
 
1792
    """An empty search result."""
 
1793
 
 
1794
    def is_empty(self):
 
1795
        return True
 
1796
    
 
1797
 
 
1798
class EverythingResult(AbstractSearchResult):
 
1799
    """A search result that simply requests everything in the repository."""
 
1800
 
 
1801
    def __init__(self, repo):
 
1802
        self._repo = repo
 
1803
 
 
1804
    def __repr__(self):
 
1805
        return '%s(%r)' % (self.__class__.__name__, self._repo)
 
1806
 
 
1807
    def get_recipe(self):
 
1808
        raise NotImplementedError(self.get_recipe)
 
1809
 
 
1810
    def get_network_struct(self):
 
1811
        return ('everything',)
 
1812
 
 
1813
    def get_keys(self):
 
1814
        if 'evil' in debug.debug_flags:
 
1815
            from bzrlib import remote
 
1816
            if isinstance(self._repo, remote.RemoteRepository):
 
1817
                # warn developers (not users) not to do this
 
1818
                trace.mutter_callsite(
 
1819
                    2, "EverythingResult(RemoteRepository).get_keys() is slow.")
 
1820
        return self._repo.all_revision_ids()
 
1821
 
 
1822
    def is_empty(self):
 
1823
        # It's ok for this to wrongly return False: the worst that can happen
 
1824
        # is that RemoteStreamSource will initiate a get_stream on an empty
 
1825
        # repository.  And almost all repositories are non-empty.
 
1826
        return False
 
1827
 
 
1828
    def refine(self, seen, referenced):
 
1829
        heads = set(self._repo.all_revision_ids())
 
1830
        heads.difference_update(seen)
 
1831
        heads.update(referenced)
 
1832
        return PendingAncestryResult(heads, self._repo)
 
1833
 
 
1834
 
 
1835
class EverythingNotInOther(AbstractSearch):
 
1836
    """Find all revisions in that are in one repo but not the other."""
 
1837
 
 
1838
    def __init__(self, to_repo, from_repo, find_ghosts=False):
 
1839
        self.to_repo = to_repo
 
1840
        self.from_repo = from_repo
 
1841
        self.find_ghosts = find_ghosts
 
1842
 
 
1843
    def execute(self):
 
1844
        return self.to_repo.search_missing_revision_ids(
 
1845
            self.from_repo, find_ghosts=self.find_ghosts)
 
1846
 
 
1847
 
 
1848
class NotInOtherForRevs(AbstractSearch):
 
1849
    """Find all revisions missing in one repo for a some specific heads."""
 
1850
 
 
1851
    def __init__(self, to_repo, from_repo, required_ids, if_present_ids=None,
 
1852
            find_ghosts=False, limit=None):
 
1853
        """Constructor.
 
1854
 
 
1855
        :param required_ids: revision IDs of heads that must be found, or else
 
1856
            the search will fail with NoSuchRevision.  All revisions in their
 
1857
            ancestry not already in the other repository will be included in
 
1858
            the search result.
 
1859
        :param if_present_ids: revision IDs of heads that may be absent in the
 
1860
            source repository.  If present, then their ancestry not already
 
1861
            found in other will be included in the search result.
 
1862
        :param limit: maximum number of revisions to fetch
 
1863
        """
 
1864
        self.to_repo = to_repo
 
1865
        self.from_repo = from_repo
 
1866
        self.find_ghosts = find_ghosts
 
1867
        self.required_ids = required_ids
 
1868
        self.if_present_ids = if_present_ids
 
1869
        self.limit = limit
 
1870
 
 
1871
    def __repr__(self):
 
1872
        if len(self.required_ids) > 5:
 
1873
            reqd_revs_repr = repr(list(self.required_ids)[:5])[:-1] + ', ...]'
 
1874
        else:
 
1875
            reqd_revs_repr = repr(self.required_ids)
 
1876
        if self.if_present_ids and len(self.if_present_ids) > 5:
 
1877
            ifp_revs_repr = repr(list(self.if_present_ids)[:5])[:-1] + ', ...]'
 
1878
        else:
 
1879
            ifp_revs_repr = repr(self.if_present_ids)
 
1880
 
 
1881
        return ("<%s from:%r to:%r find_ghosts:%r req'd:%r if-present:%r"
 
1882
                "limit:%r>") % (
 
1883
                self.__class__.__name__, self.from_repo, self.to_repo,
 
1884
                self.find_ghosts, reqd_revs_repr, ifp_revs_repr,
 
1885
                self.limit)
 
1886
 
 
1887
    def execute(self):
 
1888
        return self.to_repo.search_missing_revision_ids(
 
1889
            self.from_repo, revision_ids=self.required_ids,
 
1890
            if_present_ids=self.if_present_ids, find_ghosts=self.find_ghosts,
 
1891
            limit=self.limit)
1608
1892
 
1609
1893
 
1610
1894
def collapse_linear_regions(parent_map):