~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/graph.py

  • Committer: Vincent Ladeuil
  • Date: 2012-02-14 17:22:37 UTC
  • mfrom: (6466 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120214172237-7dv7er3n4uy8d5m4
Merge trunk

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
 
17
19
import time
18
20
 
19
21
from bzrlib import (
23
25
    revision,
24
26
    trace,
25
27
    )
26
 
from bzrlib.symbol_versioning import deprecated_function, deprecated_in
27
28
 
28
29
STEP_UNIQUE_SEARCHER_EVERY = 5
29
30
 
59
60
    def __repr__(self):
60
61
        return 'DictParentsProvider(%r)' % self.ancestry
61
62
 
 
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
 
62
68
    def get_parent_map(self, keys):
63
69
        """See StackedParentsProvider.get_parent_map"""
64
70
        ancestry = self.ancestry
65
 
        return dict((k, ancestry[k]) for k in keys if k in ancestry)
 
71
        return dict([(k, ancestry[k]) for k in keys if k in ancestry])
66
72
 
67
 
@deprecated_function(deprecated_in((1, 16, 0)))
68
 
def _StackedParentsProvider(*args, **kwargs):
69
 
    return StackedParentsProvider(*args, **kwargs)
70
73
 
71
74
class StackedParentsProvider(object):
72
75
    """A parents provider which stacks (or unions) multiple providers.
73
 
    
 
76
 
74
77
    The providers are queries in the order of the provided parent_providers.
75
78
    """
76
 
    
 
79
 
77
80
    def __init__(self, parent_providers):
78
81
        self._parent_providers = parent_providers
79
82
 
95
98
        """
96
99
        found = {}
97
100
        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
98
117
        for parents_provider in self._parent_providers:
99
118
            new_found = parents_provider.get_parent_map(remaining)
100
119
            found.update(new_found)
154
173
            return None
155
174
        return dict(self._cache)
156
175
 
 
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
 
157
187
    def get_parent_map(self, keys):
158
188
        """See StackedParentsProvider.get_parent_map."""
159
189
        cache = self._cache
183
213
            self.missing_keys.add(key)
184
214
 
185
215
 
 
216
class CallableToParentsProviderAdapter(object):
 
217
    """A parents provider that adapts any callable to the parents provider API.
 
218
 
 
219
    i.e. it accepts calls to self.get_parent_map and relays them to the
 
220
    callable it was constructed with.
 
221
    """
 
222
 
 
223
    def __init__(self, a_callable):
 
224
        self.callable = a_callable
 
225
 
 
226
    def __repr__(self):
 
227
        return "%s(%r)" % (self.__class__.__name__, self.callable)
 
228
 
 
229
    def get_parent_map(self, keys):
 
230
        return self.callable(keys)
 
231
 
 
232
 
186
233
class Graph(object):
187
234
    """Provide incremental access to revision graphs.
188
235
 
237
284
        common ancestor of all border ancestors, because this shows that it
238
285
        cannot be a descendant of any border ancestor.
239
286
 
240
 
        The scaling of this operation should be proportional to
 
287
        The scaling of this operation should be proportional to:
 
288
 
241
289
        1. The number of uncommon ancestors
242
290
        2. The number of border ancestors
243
291
        3. The length of the shortest path between a border ancestor and an
375
423
 
376
424
        :param unique_revision: The revision_id whose ancestry we are
377
425
            interested in.
378
 
            XXX: Would this API be better if we allowed multiple revisions on
379
 
                 to be searched here?
 
426
            (XXX: Would this API be better if we allowed multiple revisions on
 
427
            to be searched here?)
380
428
        :param common_revisions: Revision_ids of ancestries to exclude.
381
429
        :return: A set of revisions in the ancestry of unique_revision
382
430
        """
1300
1348
        return ('_BreadthFirstSearcher(iterations=%d, %s,'
1301
1349
                ' seen=%r)' % (self._iterations, search, list(self.seen)))
1302
1350
 
1303
 
    def get_result(self):
1304
 
        """Get a SearchResult for the current state of this searcher.
 
1351
    def get_state(self):
 
1352
        """Get the current state of this searcher.
1305
1353
 
1306
 
        :return: A SearchResult for this search so far. The SearchResult is
1307
 
            static - the search can be advanced and the search result will not
1308
 
            be invalidated or altered.
 
1354
        :return: Tuple with started keys, excludes and included keys
1309
1355
        """
1310
1356
        if self._returning == 'next':
1311
1357
            # We have to know the current nodes children to be able to list the
1322
1368
            next_query = self._next_query
1323
1369
        excludes = self._stopped_keys.union(next_query)
1324
1370
        included_keys = self.seen.difference(excludes)
1325
 
        return SearchResult(self._started_keys, excludes, len(included_keys),
 
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),
1326
1383
            included_keys)
1327
1384
 
1328
1385
    def step(self):
1405
1462
        parents_of_found = set()
1406
1463
        # revisions may contain nodes that point to other nodes in revisions:
1407
1464
        # we want to filter them out.
1408
 
        self.seen.update(revisions)
 
1465
        seen = self.seen
 
1466
        seen.update(revisions)
1409
1467
        parent_map = self._parents_provider.get_parent_map(revisions)
1410
1468
        found_revisions.update(parent_map)
1411
1469
        for rev_id, parents in parent_map.iteritems():
1412
1470
            if parents is None:
1413
1471
                continue
1414
 
            new_found_parents = [p for p in parents if p not in self.seen]
 
1472
            new_found_parents = [p for p in parents if p not in seen]
1415
1473
            if new_found_parents:
1416
1474
                # Calling set.update() with an empty generator is actually
1417
1475
                # rather expensive.
1536
1594
            return revs, ghosts
1537
1595
 
1538
1596
 
1539
 
class AbstractSearchResult(object):
1540
 
    """The result of a search, describing a set of keys.
1541
 
    
1542
 
    Search results are typically used as the 'fetch_spec' parameter when
1543
 
    fetching revisions.
1544
 
 
1545
 
    :seealso: AbstractSearch
1546
 
    """
1547
 
 
1548
 
    def get_recipe(self):
1549
 
        """Return a recipe that can be used to replay this search.
1550
 
 
1551
 
        The recipe allows reconstruction of the same results at a later date.
1552
 
 
1553
 
        :return: A tuple of (search_kind_str, *details).  The details vary by
1554
 
            kind of search result.
1555
 
        """
1556
 
        raise NotImplementedError(self.get_recipe)
1557
 
 
1558
 
    def get_network_struct(self):
1559
 
        """Return a tuple that can be transmitted via the HPSS protocol."""
1560
 
        raise NotImplementedError(self.get_network_struct)
1561
 
 
1562
 
    def get_keys(self):
1563
 
        """Return the keys found in this search.
1564
 
 
1565
 
        :return: A set of keys.
1566
 
        """
1567
 
        raise NotImplementedError(self.get_keys)
1568
 
 
1569
 
    def is_empty(self):
1570
 
        """Return false if the search lists 1 or more revisions."""
1571
 
        raise NotImplementedError(self.is_empty)
1572
 
 
1573
 
    def refine(self, seen, referenced):
1574
 
        """Create a new search by refining this search.
1575
 
 
1576
 
        :param seen: Revisions that have been satisfied.
1577
 
        :param referenced: Revision references observed while satisfying some
1578
 
            of this search.
1579
 
        :return: A search result.
1580
 
        """
1581
 
        raise NotImplementedError(self.refine)
1582
 
 
1583
 
 
1584
 
class AbstractSearch(object):
1585
 
    """A search that can be executed, producing a search result.
1586
 
 
1587
 
    :seealso: AbstractSearchResult
1588
 
    """
1589
 
 
1590
 
    def execute(self):
1591
 
        """Construct a network-ready search result from this search description.
1592
 
 
1593
 
        This may take some time to search repositories, etc.
1594
 
 
1595
 
        :return: A search result (an object that implements
1596
 
            AbstractSearchResult's API).
1597
 
        """
1598
 
        raise NotImplementedError(self.execute)
1599
 
 
1600
 
 
1601
 
class SearchResult(AbstractSearchResult):
1602
 
    """The result of a breadth first search.
1603
 
 
1604
 
    A SearchResult provides the ability to reconstruct the search or access a
1605
 
    set of the keys the search found.
1606
 
    """
1607
 
 
1608
 
    def __init__(self, start_keys, exclude_keys, key_count, keys):
1609
 
        """Create a SearchResult.
1610
 
 
1611
 
        :param start_keys: The keys the search started at.
1612
 
        :param exclude_keys: The keys the search excludes.
1613
 
        :param key_count: The total number of keys (from start to but not
1614
 
            including exclude).
1615
 
        :param keys: The keys the search found. Note that in future we may get
1616
 
            a SearchResult from a smart server, in which case the keys list is
1617
 
            not necessarily immediately available.
1618
 
        """
1619
 
        self._recipe = ('search', start_keys, exclude_keys, key_count)
1620
 
        self._keys = frozenset(keys)
1621
 
 
1622
 
    def __repr__(self):
1623
 
        kind, start_keys, exclude_keys, key_count = self._recipe
1624
 
        if len(start_keys) > 5:
1625
 
            start_keys_repr = repr(list(start_keys)[:5])[:-1] + ', ...]'
1626
 
        else:
1627
 
            start_keys_repr = repr(start_keys)
1628
 
        if len(exclude_keys) > 5:
1629
 
            exclude_keys_repr = repr(list(exclude_keys)[:5])[:-1] + ', ...]'
1630
 
        else:
1631
 
            exclude_keys_repr = repr(exclude_keys)
1632
 
        return '<%s %s:(%s, %s, %d)>' % (self.__class__.__name__,
1633
 
            kind, start_keys_repr, exclude_keys_repr, key_count)
1634
 
 
1635
 
    def get_recipe(self):
1636
 
        """Return a recipe that can be used to replay this search.
1637
 
 
1638
 
        The recipe allows reconstruction of the same results at a later date
1639
 
        without knowing all the found keys. The essential elements are a list
1640
 
        of keys to start and to stop at. In order to give reproducible
1641
 
        results when ghosts are encountered by a search they are automatically
1642
 
        added to the exclude list (or else ghost filling may alter the
1643
 
        results).
1644
 
 
1645
 
        :return: A tuple ('search', start_keys_set, exclude_keys_set,
1646
 
            revision_count). To recreate the results of this search, create a
1647
 
            breadth first searcher on the same graph starting at start_keys.
1648
 
            Then call next() (or next_with_ghosts()) repeatedly, and on every
1649
 
            result, call stop_searching_any on any keys from the exclude_keys
1650
 
            set. The revision_count value acts as a trivial cross-check - the
1651
 
            found revisions of the new search should have as many elements as
1652
 
            revision_count. If it does not, then additional revisions have been
1653
 
            ghosted since the search was executed the first time and the second
1654
 
            time.
1655
 
        """
1656
 
        return self._recipe
1657
 
 
1658
 
    def get_network_struct(self):
1659
 
        start_keys = ' '.join(self._recipe[1])
1660
 
        stop_keys = ' '.join(self._recipe[2])
1661
 
        count = str(self._recipe[3])
1662
 
        return (self._recipe[0], '\n'.join((start_keys, stop_keys, count)))
1663
 
 
1664
 
    def get_keys(self):
1665
 
        """Return the keys found in this search.
1666
 
 
1667
 
        :return: A set of keys.
1668
 
        """
1669
 
        return self._keys
1670
 
 
1671
 
    def is_empty(self):
1672
 
        """Return false if the search lists 1 or more revisions."""
1673
 
        return self._recipe[3] == 0
1674
 
 
1675
 
    def refine(self, seen, referenced):
1676
 
        """Create a new search by refining this search.
1677
 
 
1678
 
        :param seen: Revisions that have been satisfied.
1679
 
        :param referenced: Revision references observed while satisfying some
1680
 
            of this search.
1681
 
        """
1682
 
        start = self._recipe[1]
1683
 
        exclude = self._recipe[2]
1684
 
        count = self._recipe[3]
1685
 
        keys = self.get_keys()
1686
 
        # New heads = referenced + old heads - seen things - exclude
1687
 
        pending_refs = set(referenced)
1688
 
        pending_refs.update(start)
1689
 
        pending_refs.difference_update(seen)
1690
 
        pending_refs.difference_update(exclude)
1691
 
        # New exclude = old exclude + satisfied heads
1692
 
        seen_heads = start.intersection(seen)
1693
 
        exclude.update(seen_heads)
1694
 
        # keys gets seen removed
1695
 
        keys = keys - seen
1696
 
        # length is reduced by len(seen)
1697
 
        count -= len(seen)
1698
 
        return SearchResult(pending_refs, exclude, count, keys)
1699
 
 
1700
 
 
1701
 
class PendingAncestryResult(AbstractSearchResult):
1702
 
    """A search result that will reconstruct the ancestry for some graph heads.
1703
 
 
1704
 
    Unlike SearchResult, this doesn't hold the complete search result in
1705
 
    memory, it just holds a description of how to generate it.
1706
 
    """
1707
 
 
1708
 
    def __init__(self, heads, repo):
1709
 
        """Constructor.
1710
 
 
1711
 
        :param heads: an iterable of graph heads.
1712
 
        :param repo: a repository to use to generate the ancestry for the given
1713
 
            heads.
1714
 
        """
1715
 
        self.heads = frozenset(heads)
1716
 
        self.repo = repo
1717
 
 
1718
 
    def __repr__(self):
1719
 
        if len(self.heads) > 5:
1720
 
            heads_repr = repr(list(self.heads)[:5])[:-1]
1721
 
            heads_repr += ', <%d more>...]' % (len(self.heads) - 5,)
1722
 
        else:
1723
 
            heads_repr = repr(self.heads)
1724
 
        return '<%s heads:%s repo:%r>' % (
1725
 
            self.__class__.__name__, heads_repr, self.repo)
1726
 
 
1727
 
    def get_recipe(self):
1728
 
        """Return a recipe that can be used to replay this search.
1729
 
 
1730
 
        The recipe allows reconstruction of the same results at a later date.
1731
 
 
1732
 
        :seealso SearchResult.get_recipe:
1733
 
 
1734
 
        :return: A tuple ('proxy-search', start_keys_set, set(), -1)
1735
 
            To recreate this result, create a PendingAncestryResult with the
1736
 
            start_keys_set.
1737
 
        """
1738
 
        return ('proxy-search', self.heads, set(), -1)
1739
 
 
1740
 
    def get_network_struct(self):
1741
 
        parts = ['ancestry-of']
1742
 
        parts.extend(self.heads)
1743
 
        return parts
1744
 
 
1745
 
    def get_keys(self):
1746
 
        """See SearchResult.get_keys.
1747
 
 
1748
 
        Returns all the keys for the ancestry of the heads, excluding
1749
 
        NULL_REVISION.
1750
 
        """
1751
 
        return self._get_keys(self.repo.get_graph())
1752
 
 
1753
 
    def _get_keys(self, graph):
1754
 
        NULL_REVISION = revision.NULL_REVISION
1755
 
        keys = [key for (key, parents) in graph.iter_ancestry(self.heads)
1756
 
                if key != NULL_REVISION and parents is not None]
1757
 
        return keys
1758
 
 
1759
 
    def is_empty(self):
1760
 
        """Return false if the search lists 1 or more revisions."""
1761
 
        if revision.NULL_REVISION in self.heads:
1762
 
            return len(self.heads) == 1
1763
 
        else:
1764
 
            return len(self.heads) == 0
1765
 
 
1766
 
    def refine(self, seen, referenced):
1767
 
        """Create a new search by refining this search.
1768
 
 
1769
 
        :param seen: Revisions that have been satisfied.
1770
 
        :param referenced: Revision references observed while satisfying some
1771
 
            of this search.
1772
 
        """
1773
 
        referenced = self.heads.union(referenced)
1774
 
        return PendingAncestryResult(referenced - seen, self.repo)
1775
 
 
1776
 
 
1777
 
class EmptySearchResult(AbstractSearchResult):
1778
 
    """An empty search result."""
1779
 
 
1780
 
    def is_empty(self):
1781
 
        return True
1782
 
    
1783
 
 
1784
 
class EverythingResult(AbstractSearchResult):
1785
 
    """A search result that simply requests everything in the repository."""
1786
 
 
1787
 
    def __init__(self, repo):
1788
 
        self._repo = repo
1789
 
 
1790
 
    def __repr__(self):
1791
 
        return '%s(%r)' % (self.__class__.__name__, self._repo)
1792
 
 
1793
 
    def get_recipe(self):
1794
 
        raise NotImplementedError(self.get_recipe)
1795
 
 
1796
 
    def get_network_struct(self):
1797
 
        return ('everything',)
1798
 
 
1799
 
    def get_keys(self):
1800
 
        if 'evil' in debug.debug_flags:
1801
 
            from bzrlib import remote
1802
 
            if isinstance(self._repo, remote.RemoteRepository):
1803
 
                # warn developers (not users) not to do this
1804
 
                trace.mutter_callsite(
1805
 
                    2, "EverythingResult(RemoteRepository).get_keys() is slow.")
1806
 
        return self._repo.all_revision_ids()
1807
 
 
1808
 
    def is_empty(self):
1809
 
        # It's ok for this to wrongly return False: the worst that can happen
1810
 
        # is that RemoteStreamSource will initiate a get_stream on an empty
1811
 
        # repository.  And almost all repositories are non-empty.
1812
 
        return False
1813
 
 
1814
 
    def refine(self, seen, referenced):
1815
 
        heads = set(self._repo.all_revision_ids())
1816
 
        heads.difference_update(seen)
1817
 
        heads.update(referenced)
1818
 
        return PendingAncestryResult(heads, self._repo)
1819
 
 
1820
 
 
1821
 
class EverythingNotInOther(AbstractSearch):
1822
 
    """Find all revisions in that are in one repo but not the other."""
1823
 
 
1824
 
    def __init__(self, to_repo, from_repo, find_ghosts=False):
1825
 
        self.to_repo = to_repo
1826
 
        self.from_repo = from_repo
1827
 
        self.find_ghosts = find_ghosts
1828
 
 
1829
 
    def execute(self):
1830
 
        return self.to_repo.search_missing_revision_ids(
1831
 
            self.from_repo, find_ghosts=self.find_ghosts)
1832
 
 
1833
 
 
1834
 
class NotInOtherForRevs(AbstractSearch):
1835
 
    """Find all revisions missing in one repo for a some specific heads."""
1836
 
 
1837
 
    def __init__(self, to_repo, from_repo, required_ids, if_present_ids=None,
1838
 
            find_ghosts=False):
1839
 
        """Constructor.
1840
 
 
1841
 
        :param required_ids: revision IDs of heads that must be found, or else
1842
 
            the search will fail with NoSuchRevision.  All revisions in their
1843
 
            ancestry not already in the other repository will be included in
1844
 
            the search result.
1845
 
        :param if_present_ids: revision IDs of heads that may be absent in the
1846
 
            source repository.  If present, then their ancestry not already
1847
 
            found in other will be included in the search result.
1848
 
        """
1849
 
        self.to_repo = to_repo
1850
 
        self.from_repo = from_repo
1851
 
        self.find_ghosts = find_ghosts
1852
 
        self.required_ids = required_ids
1853
 
        self.if_present_ids = if_present_ids
1854
 
 
1855
 
    def __repr__(self):
1856
 
        if len(self.required_ids) > 5:
1857
 
            reqd_revs_repr = repr(list(self.required_ids)[:5])[:-1] + ', ...]'
1858
 
        else:
1859
 
            reqd_revs_repr = repr(self.required_ids)
1860
 
        if self.if_present_ids and len(self.if_present_ids) > 5:
1861
 
            ifp_revs_repr = repr(list(self.if_present_ids)[:5])[:-1] + ', ...]'
1862
 
        else:
1863
 
            ifp_revs_repr = repr(self.if_present_ids)
1864
 
 
1865
 
        return "<%s from:%r to:%r find_ghosts:%r req'd:%r if-present:%r>" % (
1866
 
            self.__class__.__name__, self.from_repo, self.to_repo,
1867
 
            self.find_ghosts, reqd_revs_repr, ifp_revs_repr)
1868
 
 
1869
 
    def execute(self):
1870
 
        return self.to_repo.search_missing_revision_ids(
1871
 
            self.from_repo, revision_ids=self.required_ids,
1872
 
            if_present_ids=self.if_present_ids, find_ghosts=self.find_ghosts)
 
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
1873
1610
 
1874
1611
 
1875
1612
def collapse_linear_regions(parent_map):
1961
1698
        return set([h[0] for h in head_keys])
1962
1699
 
1963
1700
    def merge_sort(self, tip_revision):
1964
 
        return self._graph.merge_sort((tip_revision,))
 
1701
        nodes = self._graph.merge_sort((tip_revision,))
 
1702
        for node in nodes:
 
1703
            node.key = node.key[0]
 
1704
        return nodes
1965
1705
 
1966
1706
    def add_node(self, revision, parents):
1967
1707
        self._graph.add_node((revision,), [(p,) for p in parents])