~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-05-11 15:04:23 UTC
  • mfrom: (5848.1.1 2.4-cython-first)
  • Revision ID: pqm@pqm.ubuntu.com-20110511150423-tpm1ablukqalkvim
(jameinel) Default to using Cython for compiling code,
 rather than Pyrex. (John A Meinel)

Show diffs side-by-side

added added

removed removed

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