changeset 3983:a689f07d5663

branching: merge with stable
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Tue, 21 Aug 2018 04:08:38 +0200
parents 7b1af75e12a9 (diff) b4d0245c7f40 (current diff)
children e5da40e74104
files tests/test-evolve.t
diffstat 16 files changed, 372 insertions(+), 268 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/dagutil.py	Tue Aug 21 04:08:38 2018 +0200
@@ -0,0 +1,287 @@
+# dagutil.py - dag utilities for mercurial
+#
+# Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
+# and Peter Arrenbrecht <peter@arrenbrecht.ch>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+#
+# Imported from Mercurial code at cee9043c7dba
+
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+from mercurial.node import nullrev
+
+class basedag(object):
+    '''generic interface for DAGs
+
+    terms:
+    "ix" (short for index) identifies a nodes internally,
+    "id" identifies one externally.
+
+    All params are ixs unless explicitly suffixed otherwise.
+    Pluralized params are lists or sets.
+    '''
+
+    def __init__(self):
+        self._inverse = None
+
+    def nodeset(self):
+        '''set of all node ixs'''
+        raise NotImplementedError
+
+    def heads(self):
+        '''list of head ixs'''
+        raise NotImplementedError
+
+    def parents(self, ix):
+        '''list of parents ixs of ix'''
+        raise NotImplementedError
+
+    def inverse(self):
+        '''inverse DAG, where parents becomes children, etc.'''
+        raise NotImplementedError
+
+    def ancestorset(self, starts, stops=None):
+        '''
+        set of all ancestors of starts (incl), but stop walk at stops (excl)
+        '''
+        raise NotImplementedError
+
+    def descendantset(self, starts, stops=None):
+        '''
+        set of all descendants of starts (incl), but stop walk at stops (excl)
+        '''
+        return self.inverse().ancestorset(starts, stops)
+
+    def headsetofconnecteds(self, ixs):
+        '''
+        subset of connected list of ixs so that no node has a descendant in it
+
+        By "connected list" we mean that if an ancestor and a descendant are in
+        the list, then so is at least one path connecting them.
+        '''
+        raise NotImplementedError
+
+    def externalize(self, ix):
+        '''return a node id'''
+        return self._externalize(ix)
+
+    def externalizeall(self, ixs):
+        '''return a list of (or set if given a set) of node ids'''
+        ids = self._externalizeall(ixs)
+        if isinstance(ixs, set):
+            return set(ids)
+        return list(ids)
+
+    def internalize(self, id):
+        '''return a node ix'''
+        return self._internalize(id)
+
+    def internalizeall(self, ids, filterunknown=False):
+        '''return a list of (or set if given a set) of node ixs'''
+        ixs = self._internalizeall(ids, filterunknown)
+        if isinstance(ids, set):
+            return set(ixs)
+        return list(ixs)
+
+class genericdag(basedag):
+    '''generic implementations for DAGs'''
+
+    def ancestorset(self, starts, stops=None):
+        if stops:
+            stops = set(stops)
+        else:
+            stops = set()
+        seen = set()
+        pending = list(starts)
+        while pending:
+            n = pending.pop()
+            if n not in seen and n not in stops:
+                seen.add(n)
+                pending.extend(self.parents(n))
+        return seen
+
+    def headsetofconnecteds(self, ixs):
+        hds = set(ixs)
+        if not hds:
+            return hds
+        for n in ixs:
+            for p in self.parents(n):
+                hds.discard(p)
+        assert hds
+        return hds
+
+class revlogbaseddag(basedag):
+    '''generic dag interface to a revlog'''
+
+    def __init__(self, revlog, nodeset):
+        basedag.__init__(self)
+        self._revlog = revlog
+        self._heads = None
+        self._nodeset = nodeset
+
+    def nodeset(self):
+        return self._nodeset
+
+    def heads(self):
+        if self._heads is None:
+            self._heads = self._getheads()
+        return self._heads
+
+    def _externalize(self, ix):
+        return self._revlog.index[ix][7]
+
+    def _externalizeall(self, ixs):
+        idx = self._revlog.index
+        return [idx[i][7] for i in ixs]
+
+    def _internalize(self, id):
+        ix = self._revlog.rev(id)
+        if ix == nullrev:
+            raise LookupError(id, self._revlog.indexfile, _('nullid'))
+        return ix
+
+    def _internalizeall(self, ids, filterunknown):
+        rl = self._revlog
+        if filterunknown:
+            return [r for r in map(rl.nodemap.get, ids)
+                    if (r is not None
+                        and r != nullrev
+                        and r not in rl.filteredrevs)]
+        return [self._internalize(i) for i in ids]
+
+class revlogdag(revlogbaseddag):
+    '''dag interface to a revlog'''
+
+    def __init__(self, revlog, localsubset=None):
+        revlogbaseddag.__init__(self, revlog, set(revlog))
+        self._heads = localsubset
+
+    def _getheads(self):
+        return [r for r in self._revlog.headrevs() if r != nullrev]
+
+    def parents(self, ix):
+        rlog = self._revlog
+        idx = rlog.index
+        revdata = idx[ix]
+        prev = revdata[5]
+        if prev != nullrev:
+            prev2 = revdata[6]
+            if prev2 == nullrev:
+                return [prev]
+            return [prev, prev2]
+        prev2 = revdata[6]
+        if prev2 != nullrev:
+            return [prev2]
+        return []
+
+    def inverse(self):
+        if self._inverse is None:
+            self._inverse = inverserevlogdag(self)
+        return self._inverse
+
+    def ancestorset(self, starts, stops=None):
+        rlog = self._revlog
+        idx = rlog.index
+        if stops:
+            stops = set(stops)
+        else:
+            stops = set()
+        seen = set()
+        pending = list(starts)
+        while pending:
+            rev = pending.pop()
+            if rev not in seen and rev not in stops:
+                seen.add(rev)
+                revdata = idx[rev]
+                for i in [5, 6]:
+                    prev = revdata[i]
+                    if prev != nullrev:
+                        pending.append(prev)
+        return seen
+
+    def headsetofconnecteds(self, ixs):
+        if not ixs:
+            return set()
+        rlog = self._revlog
+        idx = rlog.index
+        headrevs = set(ixs)
+        for rev in ixs:
+            revdata = idx[rev]
+            for i in [5, 6]:
+                prev = revdata[i]
+                if prev != nullrev:
+                    headrevs.discard(prev)
+        assert headrevs
+        return headrevs
+
+    def linearize(self, ixs):
+        '''linearize and topologically sort a list of revisions
+
+        The linearization process tries to create long runs of revs where
+        a child rev comes immediately after its first parent. This is done by
+        visiting the heads of the given revs in inverse topological order,
+        and for each visited rev, visiting its second parent, then its first
+        parent, then adding the rev itself to the output list.
+        '''
+        sorted = []
+        visit = list(self.headsetofconnecteds(ixs))
+        visit.sort(reverse=True)
+        finished = set()
+
+        while visit:
+            cur = visit.pop()
+            if cur < 0:
+                cur = -cur - 1
+                if cur not in finished:
+                    sorted.append(cur)
+                    finished.add(cur)
+            else:
+                visit.append(-cur - 1)
+                visit += [p for p in self.parents(cur)
+                          if p in ixs and p not in finished]
+        assert len(sorted) == len(ixs)
+        return sorted
+
+class inverserevlogdag(revlogbaseddag, genericdag):
+    '''inverse of an existing revlog dag; see revlogdag.inverse()'''
+
+    def __init__(self, orig):
+        revlogbaseddag.__init__(self, orig._revlog, orig._nodeset)
+        self._orig = orig
+        self._children = {}
+        self._roots = []
+        self._walkfrom = len(self._revlog) - 1
+
+    def _walkto(self, walkto):
+        rev = self._walkfrom
+        cs = self._children
+        roots = self._roots
+        idx = self._revlog.index
+        while rev >= walkto:
+            data = idx[rev]
+            isroot = True
+            for prev in [data[5], data[6]]: # parent revs
+                if prev != nullrev:
+                    cs.setdefault(prev, []).append(rev)
+                    isroot = False
+            if isroot:
+                roots.append(rev)
+            rev -= 1
+        self._walkfrom = rev
+
+    def _getheads(self):
+        self._walkto(nullrev)
+        return self._roots
+
+    def parents(self, ix):
+        if ix is None:
+            return []
+        if ix <= self._walkfrom:
+            self._walkto(ix)
+        return self._children.get(ix, [])
+
+    def inverse(self):
+        return self._orig
--- a/hgext3rd/evolve/depthcache.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/depthcache.py	Tue Aug 21 04:08:38 2018 +0200
@@ -10,11 +10,9 @@
 from __future__ import absolute_import
 
 import array
-import weakref
 
 from mercurial import (
     localrepo,
-    util,
     scmutil,
 )
 
@@ -85,30 +83,12 @@
                 self.depthcache.clear()
             super(depthcacherepo, self).destroyed()
 
-        if util.safehasattr(repo, 'updatecaches'):
-            @localrepo.unfilteredmethod
-            def updatecaches(self, tr=None, **kwargs):
-                if utility.shouldwarmcache(self, tr):
-                    self.depthcache.update(self)
-                    self.depthcache.save(self)
-                super(depthcacherepo, self).updatecaches(tr, **kwargs)
-
-        else:
-            def transaction(self, *args, **kwargs):
-                tr = super(depthcacherepo, self).transaction(*args, **kwargs)
-                reporef = weakref.ref(self)
-
-                def _warmcache(tr):
-                    repo = reporef()
-                    if repo is None:
-                        return
-                    repo = repo.unfiltered()
-                    repo.depthcache.update(repo)
-                    repo.depthcache.save(repo)
-
-                if utility.shouldwarmcache(self, tr):
-                    tr.addpostclose('warmcache-00depthcache', _warmcache)
-                return tr
+        @localrepo.unfilteredmethod
+        def updatecaches(self, tr=None, **kwargs):
+            if utility.shouldwarmcache(self, tr):
+                self.depthcache.update(self)
+                self.depthcache.save(self)
+            super(depthcacherepo, self).updatecaches(tr, **kwargs)
 
     repo.__class__ = depthcacherepo
 
--- a/hgext3rd/evolve/exthelper.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/exthelper.py	Tue Aug 21 04:08:38 2018 +0200
@@ -11,12 +11,6 @@
     util,
 )
 
-if util.safehasattr(registrar, 'command'):
-    command = registrar.command
-else: # compat with hg < 4.3
-    from mercurial import cmdutil
-    command = cmdutil.command
-
 configitem = None
 dynamicdefault = None
 if util.safehasattr(registrar, 'configitem'):
@@ -44,7 +38,7 @@
         self._functionwrappers = []
         self._duckpunchers = []
         self.cmdtable = {}
-        self.command = command(self.cmdtable)
+        self.command = registrar.command(self.cmdtable)
 
         self.configtable = {}
         self._configitem = None
--- a/hgext3rd/evolve/firstmergecache.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/firstmergecache.py	Tue Aug 21 04:08:38 2018 +0200
@@ -10,12 +10,10 @@
 from __future__ import absolute_import
 
 import array
-import weakref
 
 from mercurial import (
     localrepo,
     node as nodemod,
-    util,
 )
 
 from . import (
@@ -47,30 +45,12 @@
                 self.firstmergecache.clear()
             super(firstmergecacherepo, self).destroyed()
 
-        if util.safehasattr(repo, 'updatecaches'):
-            @localrepo.unfilteredmethod
-            def updatecaches(self, tr=None, **kwargs):
-                if utility.shouldwarmcache(self, tr):
-                    self.firstmergecache.update(self)
-                    self.firstmergecache.save(self)
-                super(firstmergecacherepo, self).updatecaches(tr, **kwargs)
-
-        else:
-            def transaction(self, *args, **kwargs):
-                tr = super(firstmergecacherepo, self).transaction(*args, **kwargs)
-                reporef = weakref.ref(self)
-
-                def _warmcache(tr):
-                    repo = reporef()
-                    if repo is None:
-                        return
-                    repo = repo.unfiltered()
-                    repo.firstmergecache.update(repo)
-                    repo.firstmergecache.save(repo)
-
-                if utility.shouldwarmcache(self, tr):
-                    tr.addpostclose('warmcache-01-firstparentcache', _warmcache)
-                return tr
+        @localrepo.unfilteredmethod
+        def updatecaches(self, tr=None, **kwargs):
+            if utility.shouldwarmcache(self, tr):
+                self.firstmergecache.update(self)
+                self.firstmergecache.save(self)
+            super(firstmergecacherepo, self).updatecaches(tr, **kwargs)
 
     repo.__class__ = firstmergecacherepo
 
--- a/hgext3rd/evolve/hack/drophack.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/hack/drophack.py	Tue Aug 21 04:08:38 2018 +0200
@@ -21,12 +21,7 @@
 
 cmdtable = {}
 
-if util.safehasattr(registrar, 'command'):
-    command = registrar.command(cmdtable)
-else: # compat with hg < 4.3
-    from mercurial import cmdutil
-    command = cmdutil.command(cmdtable)
-
+command = registrar.command(cmdtable)
 
 @contextlib.contextmanager
 def timed(ui, caption):
--- a/hgext3rd/evolve/legacy.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/legacy.py	Tue Aug 21 04:08:38 2018 +0200
@@ -36,11 +36,7 @@
     # compat with hg < 4.6
     from mercurial.util import makedate
 
-if util.safehasattr(registrar, 'command'):
-    commandfunc = registrar.command
-else: # compat with hg < 4.3
-    from mercurial import cmdutil
-    commandfunc = cmdutil.command
+commandfunc = registrar.command
 
 #####################################################################
 ### Older format management                                       ###
--- a/hgext3rd/evolve/metadata.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/metadata.py	Tue Aug 21 04:08:38 2018 +0200
@@ -5,7 +5,7 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-__version__ = '8.1.1.dev'
+__version__ = '8.2.0.dev'
 testedwith = '4.3.2 4.4.2 4.5.2 4.6.2 4.7'
 minimumhgversion = '4.3'
 buglink = 'https://bz.mercurial-scm.org/'
--- a/hgext3rd/evolve/obscache.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/obscache.py	Tue Aug 21 04:08:38 2018 +0200
@@ -10,7 +10,6 @@
 import errno
 import hashlib
 import struct
-import weakref
 
 from mercurial import (
     localrepo,
@@ -478,31 +477,10 @@
                 self.obsstore.obscache.clear()
             super(obscacherepo, self).destroyed()
 
-        if util.safehasattr(repo, 'updatecaches'):
-            @localrepo.unfilteredmethod
-            def updatecaches(self, tr=None, **kwargs):
-                super(obscacherepo, self).updatecaches(tr, **kwargs)
-                self.obsstore.obscache.update(self)
-                self.obsstore.obscache.save(self)
-
-        else:
-            def transaction(self, *args, **kwargs):
-                tr = super(obscacherepo, self).transaction(*args, **kwargs)
-                reporef = weakref.ref(self)
-
-                def _warmcache(tr):
-                    repo = reporef()
-                    if repo is None:
-                        return
-                    repo = repo.unfiltered()
-                    # As pointed in 'obscache.update', we could have the changelog
-                    # and the obsstore in charge of updating the cache when new
-                    # items goes it. The tranaction logic would then only be
-                    # involved for the 'pending' and final writing on disk.
-                    self.obsstore.obscache.update(repo)
-                    self.obsstore.obscache.save(repo)
-
-                tr.addpostclose('warmcache-obscache', _warmcache)
-                return tr
+        @localrepo.unfilteredmethod
+        def updatecaches(self, tr=None, **kwargs):
+            super(obscacherepo, self).updatecaches(tr, **kwargs)
+            self.obsstore.obscache.update(self)
+            self.obsstore.obscache.save(self)
 
     repo.__class__ = obscacherepo
--- a/hgext3rd/evolve/obsdiscovery.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/obsdiscovery.py	Tue Aug 21 04:08:38 2018 +0200
@@ -29,7 +29,6 @@
 import weakref
 
 from mercurial import (
-    dagutil,
     error,
     exchange,
     extensions,
@@ -60,6 +59,12 @@
     wireprotov1server = wireprototypes
     from mercurial.wireproto import wirepeer, encodelist, decodelist
 
+try:
+    from mercurial import dagutil
+    dagutil.revlogdag
+except (ImportError, AttributeError): # <= hg-4.7
+    from . import dagutil
+
 _pack = struct.pack
 _unpack = struct.unpack
 _calcsize = struct.calcsize
@@ -631,30 +636,12 @@
                 self.obsstore.rangeobshashcache.clear()
             super(obshashrepo, self).destroyed()
 
-        if util.safehasattr(repo, 'updatecaches'):
-            @localrepo.unfilteredmethod
-            def updatecaches(self, tr=None, **kwargs):
-                if utility.shouldwarmcache(self, tr):
-                    self.obsstore.rangeobshashcache.update(self)
-                    self.obsstore.rangeobshashcache.save(self)
-                super(obshashrepo, self).updatecaches(tr, **kwargs)
-
-        else:
-            def transaction(self, *args, **kwargs):
-                tr = super(obshashrepo, self).transaction(*args, **kwargs)
-                reporef = weakref.ref(self)
-
-                def _warmcache(tr):
-                    repo = reporef()
-                    if repo is None:
-                        return
-                    repo = repo.unfiltered()
-                    repo.obsstore.rangeobshashcache.update(repo)
-                    repo.obsstore.rangeobshashcache.save(repo)
-
-                if utility.shouldwarmcache(self, tr):
-                    tr.addpostclose('warmcache-20obshashrange', _warmcache)
-                return tr
+        @localrepo.unfilteredmethod
+        def updatecaches(self, tr=None, **kwargs):
+            if utility.shouldwarmcache(self, tr):
+                self.obsstore.rangeobshashcache.update(self)
+                self.obsstore.rangeobshashcache.save(self)
+            super(obshashrepo, self).updatecaches(tr, **kwargs)
 
     repo.__class__ = obshashrepo
 
--- a/hgext3rd/evolve/stablerangecache.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/stablerangecache.py	Tue Aug 21 04:08:38 2018 +0200
@@ -12,7 +12,6 @@
 import random
 import sqlite3
 import time
-import weakref
 
 from mercurial import (
     error,
@@ -378,30 +377,12 @@
                 del self.stablerange
             super(stablerangerepo, self).destroyed()
 
-        if util.safehasattr(repo, 'updatecaches'):
-            @localrepo.unfilteredmethod
-            def updatecaches(self, tr=None, **kwargs):
-                if utility.shouldwarmcache(self, tr):
-                    self.stablerange.update(self)
-                    self.stablerange.save(self)
-                super(stablerangerepo, self).updatecaches(tr, **kwargs)
-
-        else:
-            def transaction(self, *args, **kwargs):
-                tr = super(stablerangerepo, self).transaction(*args, **kwargs)
-                reporef = weakref.ref(self)
-
-                def _warmcache(tr):
-                    repo = reporef()
-                    if repo is None:
-                        return
-                    repo = repo.unfiltered()
-                    repo.stablerange.update(repo)
-                    repo.stablerange.save(repo)
-
-                if utility.shouldwarmcache(self, tr):
-                    tr.addpostclose('warmcache-10stablerange', _warmcache)
-                return tr
+        @localrepo.unfilteredmethod
+        def updatecaches(self, tr=None, **kwargs):
+            if utility.shouldwarmcache(self, tr):
+                self.stablerange.update(self)
+                self.stablerange.save(self)
+            super(stablerangerepo, self).updatecaches(tr, **kwargs)
 
     repo.__class__ = stablerangerepo
 
--- a/hgext3rd/evolve/stablesort.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/stablesort.py	Tue Aug 21 04:08:38 2018 +0200
@@ -10,7 +10,6 @@
 import array
 import collections
 import struct
-import weakref
 
 from mercurial import (
     commands,
@@ -18,7 +17,6 @@
     error,
     node as nodemod,
     scmutil,
-    util,
 )
 
 from mercurial.i18n import _
@@ -669,30 +667,12 @@
                 self.stablesort.clear()
             super(stablesortrepo, self).destroyed()
 
-        if util.safehasattr(repo, 'updatecaches'):
-            @localrepo.unfilteredmethod
-            def updatecaches(self, tr=None, **kwargs):
-                if utility.shouldwarmcache(self, tr):
-                    self.stablesort.update(self)
-                    self.stablesort.save(self)
-                super(stablesortrepo, self).updatecaches(tr, **kwargs)
-
-        else:
-            def transaction(self, *args, **kwargs):
-                tr = super(stablesortrepo, self).transaction(*args, **kwargs)
-                reporef = weakref.ref(self)
-
-                def _warmcache(tr):
-                    repo = reporef()
-                    if repo is None:
-                        return
-                    repo = repo.unfiltered()
-                    repo.stablesort.update(repo)
-                    repo.stablesort.save(repo)
-
-                if utility.shouldwarmcache(self, tr):
-                    tr.addpostclose('warmcache-02stablesort', _warmcache)
-                return tr
+        @localrepo.unfilteredmethod
+        def updatecaches(self, tr=None, **kwargs):
+            if utility.shouldwarmcache(self, tr):
+                self.stablesort.update(self)
+                self.stablesort.save(self)
+            super(stablesortrepo, self).updatecaches(tr, **kwargs)
 
     repo.__class__ = stablesortrepo
 
--- a/hgext3rd/evolve/templatekw.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/evolve/templatekw.py	Tue Aug 21 04:08:38 2018 +0200
@@ -24,17 +24,6 @@
 eh = exthelper.exthelper()
 
 ### template keywords
-# XXX it does not handle troubles well :-/
-
-if not util.safehasattr(templatekw, 'showobsolete'):
-    # hg < 4.2
-    @eh.templatekw('obsolete')
-    def obsoletekw(repo, ctx, templ, **args):
-        """String. Whether the changeset is ``obsolete``.
-        """
-        if ctx.obsolete():
-            return 'obsolete'
-        return ''
 
 if util.safehasattr(templatekw, 'compatlist'):
     @eh.templatekw('troubles', requires=set(['ctx', 'templ']))
--- a/hgext3rd/topic/__init__.py	Mon Aug 20 12:21:39 2018 +0300
+++ b/hgext3rd/topic/__init__.py	Tue Aug 21 04:08:38 2018 +0200
@@ -132,7 +132,6 @@
     registrar,
     scmutil,
     templatefilters,
-    templatekw,
     util,
 )
 
@@ -149,13 +148,8 @@
     topicmap,
 )
 
-if util.safehasattr(registrar, 'command'):
-    commandfunc = registrar.command
-else: # compat with hg < 4.3
-    commandfunc = cmdutil.command
-
 cmdtable = {}
-command = commandfunc(cmdtable)
+command = registrar.command(cmdtable)
 colortable = {'topic.active': 'green',
               'topic.list.troubledcount': 'red',
               'topic.list.headcount.multiple': 'yellow',
@@ -181,7 +175,7 @@
               'topic.active': 'green',
              }
 
-__version__ = '0.10.1.dev'
+__version__ = '0.11.0.dev'
 
 testedwith = '4.3.3 4.4.2 4.5.2 4.6.2 4.7'
 minimumhgversion = '4.3'
@@ -231,6 +225,8 @@
                       default=None,
             )
 
+templatekeyword = registrar.templatekeyword()
+
 def _contexttopic(self, force=False):
     if not (force or self.mutable()):
         return ''
@@ -338,7 +334,6 @@
 
     cmdutil.summaryhooks.add('topic', summaryhook)
 
-    templatekw.keywords['topic'] = topickw
     # Wrap workingctx extra to return the topic name
     extensions.wrapfunction(context.workingctx, '__init__', wrapinit)
     # Wrap changelog.add to drop empty topic
@@ -510,9 +505,11 @@
             'topics', 'topic', namemap=_namemap, nodemap=_nodemap,
             listnames=lambda repo: repo.topics))
 
-def topickw(**args):
+@templatekeyword('topic', requires={'ctx'})
+def topickw(context, mapping):
     """:topic: String. The topic of the changeset"""
-    return args['ctx'].topic()
+    ctx = context.resource(mapping, 'ctx')
+    return ctx.topic()
 
 def wrapinit(orig, self, repo, *args, **kwargs):
     orig(self, repo, *args, **kwargs)
--- a/tests/test-discovery-obshashrange.t	Mon Aug 20 12:21:39 2018 +0300
+++ b/tests/test-discovery-obshashrange.t	Tue Aug 21 04:08:38 2018 +0200
@@ -1118,3 +1118,31 @@
              5 c8d03c1b5e94            5            1            6 446c2dc3bce5
              6 f69452c5b1af            6            1            7 000000000000
 
+Cache warming capabilities
+--------------------------
+
+  $ hg config experimental.obshashrange
+  1
+  $ hg config experimental.obshashrange.warm-cache
+  [1]
+  $ hg debugupdatecache
+  $ ls -1 .hg/cache/evoext*
+  .hg/cache/evoext-depthcache-00
+  .hg/cache/evoext-firstmerge-00
+  .hg/cache/evoext-obscache-00
+  .hg/cache/evoext-stablesortcache-00
+  .hg/cache/evoext_obshashrange_v2.sqlite
+  .hg/cache/evoext_stablerange_v2.sqlite
+  $ rm -f .hg/cache/evoext*
+  $ ls -1 .hg/cache/ | grep evoext
+  [1]
+  $ hg debugupdatecache --debug
+  updating the branch cache
+  invalid branchheads cache (served): tip differs
+  $ f -s .hg/cache/evoext*
+  .hg/cache/evoext-depthcache-00: size=96
+  .hg/cache/evoext-firstmerge-00: size=96
+  .hg/cache/evoext-obscache-00: size=73
+  .hg/cache/evoext-stablesortcache-00: size=100
+  .hg/cache/evoext_obshashrange_v2.sqlite: size=??* (glob)
+  .hg/cache/evoext_stablerange_v2.sqlite: size=??* (glob)
--- a/tests/test-evolve.t	Mon Aug 20 12:21:39 2018 +0300
+++ b/tests/test-evolve.t	Tue Aug 21 04:08:38 2018 +0200
@@ -1164,55 +1164,11 @@
   > EOF
   $ hg next
   hg: unknown command 'next'
-  Mercurial Distributed SCM
-  
-  basic commands:
-  
-   add           add the specified files on the next commit
-   annotate      show changeset information by line for each file
-   clone         make a copy of an existing repository
-   commit        commit the specified files or all outstanding changes
-   diff          diff repository (or selected files)
-   export        dump the header and diffs for one or more changesets
-   forget        forget the specified files on the next commit
-   init          create a new repository in the given directory
-   log           show revision history of entire repository or files
-   merge         merge another revision into working directory
-   pull          pull changes from the specified source
-   push          push changes to the specified destination
-   remove        remove the specified files on the next commit
-   serve         start stand-alone webserver
-   status        show changed files in the working directory
-   summary       summarize working directory state
-   update        update working directory (or switch revisions)
-  
-  (use 'hg help' for the full list of commands or 'hg -v' for details)
+  (use 'hg help' for a list of commands)
   [255]
   $ hg fold
   hg: unknown command 'fold'
-  Mercurial Distributed SCM
-  
-  basic commands:
-  
-   add           add the specified files on the next commit
-   annotate      show changeset information by line for each file
-   clone         make a copy of an existing repository
-   commit        commit the specified files or all outstanding changes
-   diff          diff repository (or selected files)
-   export        dump the header and diffs for one or more changesets
-   forget        forget the specified files on the next commit
-   init          create a new repository in the given directory
-   log           show revision history of entire repository or files
-   merge         merge another revision into working directory
-   pull          pull changes from the specified source
-   push          push changes to the specified destination
-   remove        remove the specified files on the next commit
-   serve         start stand-alone webserver
-   status        show changed files in the working directory
-   summary       summarize working directory state
-   update        update working directory (or switch revisions)
-  
-  (use 'hg help' for the full list of commands or 'hg -v' for details)
+  (use 'hg help' for a list of commands)
   [255]
 Enabling commands selectively, only fold enabled, next is still unknown
   $ cat >> $HGRCPATH <<EOF
@@ -1225,30 +1181,7 @@
   [255]
   $ hg next
   hg: unknown command 'next'
-  Mercurial Distributed SCM
-  
-  basic commands:
-  
-   add           add the specified files on the next commit
-   annotate      show changeset information by line for each file
-   clone         make a copy of an existing repository
-   commit        commit the specified files or all outstanding changes
-   diff          diff repository (or selected files)
-   export        dump the header and diffs for one or more changesets
-   fold          fold multiple revisions into a single one
-   forget        forget the specified files on the next commit
-   init          create a new repository in the given directory
-   log           show revision history of entire repository or files
-   merge         merge another revision into working directory
-   pull          pull changes from the specified source
-   push          push changes to the specified destination
-   remove        remove the specified files on the next commit
-   serve         start stand-alone webserver
-   status        show changed files in the working directory
-   summary       summarize working directory state
-   update        update working directory (or switch revisions)
-  
-  (use 'hg help' for the full list of commands or 'hg -v' for details)
+  (use 'hg help' for a list of commands)
   [255]
 
 Shows "use 'hg evolve' to..." hints iff the evolve command is enabled
--- a/tests/test-options.t	Mon Aug 20 12:21:39 2018 +0300
+++ b/tests/test-options.t	Tue Aug 21 04:08:38 2018 +0200
@@ -26,5 +26,4 @@
   > EOF
   $ hg prune | head -n 2
   hg: unknown command 'prune'
-  Mercurial Distributed SCM
-  
+  (use 'hg help' for a list of commands)