mercurial/revset.py
changeset 27584 fc7c8cac6a4b
parent 27517 c60a9c16ae25
child 27586 42910f9fffeb
--- a/mercurial/revset.py	Tue Dec 29 23:58:30 2015 +0900
+++ b/mercurial/revset.py	Tue Dec 29 23:58:30 2015 +0900
@@ -22,6 +22,7 @@
     parser,
     pathutil,
     phases,
+    registrar,
     repoview,
     util,
 )
@@ -465,19 +466,47 @@
 
 # functions
 
+# symbols are callables like:
+#   fn(repo, subset, x)
+# with:
+#   repo - current repository instance
+#   subset - of revisions to be examined
+#   x - argument in tree form
+symbols = {}
+
+class predicate(registrar.funcregistrar):
+    """Decorator to register revset predicate
+
+    Usage::
+
+        @predicate('mypredicate(arg1, arg2[, arg3])')
+        def mypredicatefunc(repo, subset, x):
+            '''Explanation of this revset predicate ....
+            '''
+            pass
+
+    The first string argument of the constructor is used also in
+    online help.
+    """
+    table = symbols
+    formatdoc = "``%s``\n    %s"
+    getname = registrar.funcregistrar.parsefuncdecl
+
+@predicate('_destupdate')
 def _destupdate(repo, subset, x):
     # experimental revset for update destination
     args = getargsdict(x, 'limit', 'clean check')
     return subset & baseset([destutil.destupdate(repo, **args)[0]])
 
+@predicate('_destmerge')
 def _destmerge(repo, subset, x):
     # experimental revset for merge destination
     getargs(x, 0, 0, _("_mergedefaultdest takes no arguments"))
     return subset & baseset([destutil.destmerge(repo)])
 
+@predicate('adds(pattern)')
 def adds(repo, subset, x):
-    """``adds(pattern)``
-    Changesets that add a file matching pattern.
+    """Changesets that add a file matching pattern.
 
     The pattern without explicit kind like ``glob:`` is expected to be
     relative to the current directory and match against a file or a
@@ -487,9 +516,9 @@
     pat = getstring(x, _("adds requires a pattern"))
     return checkstatus(repo, subset, pat, 1)
 
+@predicate('ancestor(*changeset)')
 def ancestor(repo, subset, x):
-    """``ancestor(*changeset)``
-    A greatest common ancestor of the changesets.
+    """A greatest common ancestor of the changesets.
 
     Accepts 0 or more changesets.
     Will return empty list when passed no args.
@@ -519,12 +548,13 @@
     s = _revancestors(repo, heads, followfirst)
     return subset & s
 
+@predicate('ancestors(set)')
 def ancestors(repo, subset, x):
-    """``ancestors(set)``
-    Changesets that are ancestors of a changeset in set.
+    """Changesets that are ancestors of a changeset in set.
     """
     return _ancestors(repo, subset, x)
 
+@predicate('_firstancestors')
 def _firstancestors(repo, subset, x):
     # ``_firstancestors(set)``
     # Like ``ancestors(set)`` but follows only the first parents.
@@ -547,18 +577,18 @@
         ps.add(r)
     return subset & ps
 
+@predicate('author(string)')
 def author(repo, subset, x):
-    """``author(string)``
-    Alias for ``user(string)``.
+    """Alias for ``user(string)``.
     """
     # i18n: "author" is a keyword
     n = encoding.lower(getstring(x, _("author requires a string")))
     kind, pattern, matcher = _substringmatcher(n)
     return subset.filter(lambda x: matcher(encoding.lower(repo[x].user())))
 
+@predicate('bisect(string)')
 def bisect(repo, subset, x):
-    """``bisect(string)``
-    Changesets marked in the specified bisect status:
+    """Changesets marked in the specified bisect status:
 
     - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
     - ``goods``, ``bads``      : csets topologically good/bad
@@ -575,12 +605,13 @@
 
 # Backward-compatibility
 # - no help entry so that we do not advertise it any more
+@predicate('bisected')
 def bisected(repo, subset, x):
     return bisect(repo, subset, x)
 
+@predicate('bookmark([name])')
 def bookmark(repo, subset, x):
-    """``bookmark([name])``
-    The named bookmark or all bookmarks.
+    """The named bookmark or all bookmarks.
 
     If `name` starts with `re:`, the remainder of the name is treated as
     a regular expression. To match a bookmark that actually starts with `re:`,
@@ -616,8 +647,9 @@
     bms -= set([node.nullrev])
     return subset & bms
 
+@predicate('branch(string or set)')
 def branch(repo, subset, x):
-    """``branch(string or set)``
+    """
     All changesets belonging to the given branch or the branches of the given
     changesets.
 
@@ -652,9 +684,9 @@
     c = s.__contains__
     return subset.filter(lambda r: c(r) or getbi(r)[0] in b)
 
+@predicate('bumped()')
 def bumped(repo, subset, x):
-    """``bumped()``
-    Mutable changesets marked as successors of public changesets.
+    """Mutable changesets marked as successors of public changesets.
 
     Only non-public and non-obsolete changesets can be `bumped`.
     """
@@ -663,9 +695,9 @@
     bumped = obsmod.getrevs(repo, 'bumped')
     return subset & bumped
 
+@predicate('bundle()')
 def bundle(repo, subset, x):
-    """``bundle()``
-    Changesets in the bundle.
+    """Changesets in the bundle.
 
     Bundle must be specified by the -R option."""
 
@@ -723,25 +755,25 @@
     # This does not break because of other fullreposet misbehavior.
     return baseset(cs)
 
+@predicate('children(set)')
 def children(repo, subset, x):
-    """``children(set)``
-    Child changesets of changesets in set.
+    """Child changesets of changesets in set.
     """
     s = getset(repo, fullreposet(repo), x)
     cs = _children(repo, subset, s)
     return subset & cs
 
+@predicate('closed()')
 def closed(repo, subset, x):
-    """``closed()``
-    Changeset is closed.
+    """Changeset is closed.
     """
     # i18n: "closed" is a keyword
     getargs(x, 0, 0, _("closed takes no arguments"))
     return subset.filter(lambda r: repo[r].closesbranch())
 
+@predicate('contains(pattern)')
 def contains(repo, subset, x):
-    """``contains(pattern)``
-    The revision's manifest contains a file matching pattern (but might not
+    """The revision's manifest contains a file matching pattern (but might not
     modify it). See :hg:`help patterns` for information about file patterns.
 
     The pattern without explicit kind like ``glob:`` is expected to be
@@ -766,9 +798,9 @@
 
     return subset.filter(matches)
 
+@predicate('converted([id])')
 def converted(repo, subset, x):
-    """``converted([id])``
-    Changesets converted from the given identifier in the old repository if
+    """Changesets converted from the given identifier in the old repository if
     present, or all converted changesets if no identifier is specified.
     """
 
@@ -788,18 +820,18 @@
 
     return subset.filter(lambda r: _matchvalue(r))
 
+@predicate('date(interval)')
 def date(repo, subset, x):
-    """``date(interval)``
-    Changesets within the interval, see :hg:`help dates`.
+    """Changesets within the interval, see :hg:`help dates`.
     """
     # i18n: "date" is a keyword
     ds = getstring(x, _("date requires a string"))
     dm = util.matchdate(ds)
     return subset.filter(lambda x: dm(repo[x].date()[0]))
 
+@predicate('desc(string)')
 def desc(repo, subset, x):
-    """``desc(string)``
-    Search commit message for string. The match is case-insensitive.
+    """Search commit message for string. The match is case-insensitive.
     """
     # i18n: "desc" is a keyword
     ds = encoding.lower(getstring(x, _("desc requires a string")))
@@ -829,20 +861,21 @@
         result = subset & result
     return result
 
+@predicate('descendants(set)')
 def descendants(repo, subset, x):
-    """``descendants(set)``
-    Changesets which are descendants of changesets in set.
+    """Changesets which are descendants of changesets in set.
     """
     return _descendants(repo, subset, x)
 
+@predicate('_firstdescendants')
 def _firstdescendants(repo, subset, x):
     # ``_firstdescendants(set)``
     # Like ``descendants(set)`` but follows only the first parents.
     return _descendants(repo, subset, x, followfirst=True)
 
+@predicate('destination([set])')
 def destination(repo, subset, x):
-    """``destination([set])``
-    Changesets that were created by a graft, transplant or rebase operation,
+    """Changesets that were created by a graft, transplant or rebase operation,
     with the given revisions specified as the source.  Omitting the optional set
     is the same as passing all().
     """
@@ -884,8 +917,9 @@
 
     return subset.filter(dests.__contains__)
 
+@predicate('divergent()')
 def divergent(repo, subset, x):
-    """``divergent()``
+    """
     Final successors of changesets with an alternative set of final successors.
     """
     # i18n: "divergent" is a keyword
@@ -893,18 +927,18 @@
     divergent = obsmod.getrevs(repo, 'divergent')
     return subset & divergent
 
+@predicate('extinct()')
 def extinct(repo, subset, x):
-    """``extinct()``
-    Obsolete changesets with obsolete descendants only.
+    """Obsolete changesets with obsolete descendants only.
     """
     # i18n: "extinct" is a keyword
     getargs(x, 0, 0, _("extinct takes no arguments"))
     extincts = obsmod.getrevs(repo, 'extinct')
     return subset & extincts
 
+@predicate('extra(label, [value])')
 def extra(repo, subset, x):
-    """``extra(label, [value])``
-    Changesets with the given label in the extra metadata, with the given
+    """Changesets with the given label in the extra metadata, with the given
     optional value.
 
     If `value` starts with `re:`, the remainder of the value is treated as
@@ -932,9 +966,9 @@
 
     return subset.filter(lambda r: _matchvalue(r))
 
+@predicate('filelog(pattern)')
 def filelog(repo, subset, x):
-    """``filelog(pattern)``
-    Changesets connected to the specified filelog.
+    """Changesets connected to the specified filelog.
 
     For performance reasons, visits only revisions mentioned in the file-level
     filelog, rather than filtering through all changesets (much faster, but
@@ -1047,9 +1081,9 @@
 
     return subset & s
 
+@predicate('first(set, [n])')
 def first(repo, subset, x):
-    """``first(set, [n])``
-    An alias for limit().
+    """An alias for limit().
     """
     return limit(repo, subset, x)
 
@@ -1073,31 +1107,33 @@
 
     return subset & s
 
+@predicate('follow([pattern])')
 def follow(repo, subset, x):
-    """``follow([pattern])``
+    """
     An alias for ``::.`` (ancestors of the working directory's first parent).
     If pattern is specified, the histories of files matching given
     pattern is followed, including copies.
     """
     return _follow(repo, subset, x, 'follow')
 
+@predicate('_followfirst')
 def _followfirst(repo, subset, x):
     # ``followfirst([pattern])``
     # Like ``follow([pattern])`` but follows only the first parent of
     # every revisions or files revisions.
     return _follow(repo, subset, x, '_followfirst', followfirst=True)
 
+@predicate('all()')
 def getall(repo, subset, x):
-    """``all()``
-    All changesets, the same as ``0:tip``.
+    """All changesets, the same as ``0:tip``.
     """
     # i18n: "all" is a keyword
     getargs(x, 0, 0, _("all takes no arguments"))
     return subset & spanset(repo)  # drop "null" if any
 
+@predicate('grep(regex)')
 def grep(repo, subset, x):
-    """``grep(regex)``
-    Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
+    """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
     to ensure special escape characters are handled correctly. Unlike
     ``keyword(string)``, the match is case-sensitive.
     """
@@ -1116,6 +1152,7 @@
 
     return subset.filter(matches)
 
+@predicate('_matchfiles')
 def _matchfiles(repo, subset, x):
     # _matchfiles takes a revset list of prefixed arguments:
     #
@@ -1181,9 +1218,9 @@
 
     return subset.filter(matches)
 
+@predicate('file(pattern)')
 def hasfile(repo, subset, x):
-    """``file(pattern)``
-    Changesets affecting files matched by pattern.
+    """Changesets affecting files matched by pattern.
 
     For a faster but less accurate result, consider using ``filelog()``
     instead.
@@ -1194,9 +1231,9 @@
     pat = getstring(x, _("file requires a pattern"))
     return _matchfiles(repo, subset, ('string', 'p:' + pat))
 
+@predicate('head()')
 def head(repo, subset, x):
-    """``head()``
-    Changeset is a named branch head.
+    """Changeset is a named branch head.
     """
     # i18n: "head" is a keyword
     getargs(x, 0, 0, _("head takes no arguments"))
@@ -1210,26 +1247,26 @@
     # necessary to ensure we preserve the order in subset.
     return baseset(hs) & subset
 
+@predicate('heads(set)')
 def heads(repo, subset, x):
-    """``heads(set)``
-    Members of set with no children in set.
+    """Members of set with no children in set.
     """
     s = getset(repo, subset, x)
     ps = parents(repo, subset, x)
     return s - ps
 
+@predicate('hidden()')
 def hidden(repo, subset, x):
-    """``hidden()``
-    Hidden changesets.
+    """Hidden changesets.
     """
     # i18n: "hidden" is a keyword
     getargs(x, 0, 0, _("hidden takes no arguments"))
     hiddenrevs = repoview.filterrevs(repo, 'visible')
     return subset & hiddenrevs
 
+@predicate('keyword(string)')
 def keyword(repo, subset, x):
-    """``keyword(string)``
-    Search commit message, user name, and names of changed files for
+    """Search commit message, user name, and names of changed files for
     string. The match is case-insensitive.
     """
     # i18n: "keyword" is a keyword
@@ -1242,9 +1279,9 @@
 
     return subset.filter(matches)
 
+@predicate('limit(set[, n[, offset]])')
 def limit(repo, subset, x):
-    """``limit(set[, n[, offset]])``
-    First n members of set, defaulting to 1, starting from offset.
+    """First n members of set, defaulting to 1, starting from offset.
     """
     args = getargsdict(x, 'limit', 'set n offset')
     if 'set' not in args:
@@ -1278,9 +1315,9 @@
             result.append(y)
     return baseset(result)
 
+@predicate('last(set, [n])')
 def last(repo, subset, x):
-    """``last(set, [n])``
-    Last n members of set, defaulting to 1.
+    """Last n members of set, defaulting to 1.
     """
     # i18n: "last" is a keyword
     l = getargs(x, 1, 2, _("last requires one or two arguments"))
@@ -1304,9 +1341,9 @@
             result.append(y)
     return baseset(result)
 
+@predicate('max(set)')
 def maxrev(repo, subset, x):
-    """``max(set)``
-    Changeset with highest revision number in set.
+    """Changeset with highest revision number in set.
     """
     os = getset(repo, fullreposet(repo), x)
     try:
@@ -1319,18 +1356,18 @@
         pass
     return baseset()
 
+@predicate('merge()')
 def merge(repo, subset, x):
-    """``merge()``
-    Changeset is a merge changeset.
+    """Changeset is a merge changeset.
     """
     # i18n: "merge" is a keyword
     getargs(x, 0, 0, _("merge takes no arguments"))
     cl = repo.changelog
     return subset.filter(lambda r: cl.parentrevs(r)[1] != -1)
 
+@predicate('branchpoint()')
 def branchpoint(repo, subset, x):
-    """``branchpoint()``
-    Changesets with more than one child.
+    """Changesets with more than one child.
     """
     # i18n: "branchpoint" is a keyword
     getargs(x, 0, 0, _("branchpoint takes no arguments"))
@@ -1347,9 +1384,9 @@
                 parentscount[p - baserev] += 1
     return subset.filter(lambda r: parentscount[r - baserev] > 1)
 
+@predicate('min(set)')
 def minrev(repo, subset, x):
-    """``min(set)``
-    Changeset with lowest revision number in set.
+    """Changeset with lowest revision number in set.
     """
     os = getset(repo, fullreposet(repo), x)
     try:
@@ -1362,9 +1399,9 @@
         pass
     return baseset()
 
+@predicate('modifies(pattern)')
 def modifies(repo, subset, x):
-    """``modifies(pattern)``
-    Changesets modifying files matched by pattern.
+    """Changesets modifying files matched by pattern.
 
     The pattern without explicit kind like ``glob:`` is expected to be
     relative to the current directory and match against a file or a
@@ -1374,9 +1411,9 @@
     pat = getstring(x, _("modifies requires a pattern"))
     return checkstatus(repo, subset, pat, 0)
 
+@predicate('named(namespace)')
 def named(repo, subset, x):
-    """``named(namespace)``
-    The changesets in a given namespace.
+    """The changesets in a given namespace.
 
     If `namespace` starts with `re:`, the remainder of the string is treated as
     a regular expression. To match a namespace that actually starts with `re:`,
@@ -1412,9 +1449,9 @@
     names -= set([node.nullrev])
     return subset & names
 
+@predicate('id(string)')
 def node_(repo, subset, x):
-    """``id(string)``
-    Revision non-ambiguously specified by the given hex string prefix.
+    """Revision non-ambiguously specified by the given hex string prefix.
     """
     # i18n: "id" is a keyword
     l = getargs(x, 1, 1, _("id requires one argument"))
@@ -1436,17 +1473,17 @@
     result = baseset([rn])
     return result & subset
 
+@predicate('obsolete()')
 def obsolete(repo, subset, x):
-    """``obsolete()``
-    Mutable changeset with a newer version."""
+    """Mutable changeset with a newer version."""
     # i18n: "obsolete" is a keyword
     getargs(x, 0, 0, _("obsolete takes no arguments"))
     obsoletes = obsmod.getrevs(repo, 'obsolete')
     return subset & obsoletes
 
+@predicate('only(set, [set])')
 def only(repo, subset, x):
-    """``only(set, [set])``
-    Changesets that are ancestors of the first set that are not ancestors
+    """Changesets that are ancestors of the first set that are not ancestors
     of any other head in the repo. If a second set is specified, the result
     is ancestors of the first set that are not ancestors of the second set
     (i.e. ::<set1> - ::<set2>).
@@ -1470,8 +1507,9 @@
     # some optimisations from the fact this is a baseset.
     return subset & results
 
+@predicate('origin([set])')
 def origin(repo, subset, x):
-    """``origin([set])``
+    """
     Changesets that were specified as a source for the grafts, transplants or
     rebases that created the given revisions.  Omitting the optional set is the
     same as passing all().  If a changeset created by these operations is itself
@@ -1501,9 +1539,9 @@
     # some optimisations from the fact this is a baseset.
     return subset & o
 
+@predicate('outgoing([path])')
 def outgoing(repo, subset, x):
-    """``outgoing([path])``
-    Changesets not found in the specified destination repository, or the
+    """Changesets not found in the specified destination repository, or the
     default push location.
     """
     # Avoid cycles.
@@ -1528,9 +1566,9 @@
     o = set([cl.rev(r) for r in outgoing.missing])
     return subset & o
 
+@predicate('p1([set])')
 def p1(repo, subset, x):
-    """``p1([set])``
-    First parent of changesets in set, or the working directory.
+    """First parent of changesets in set, or the working directory.
     """
     if x is None:
         p = repo[x].p1().rev()
@@ -1547,9 +1585,9 @@
     # some optimisations from the fact this is a baseset.
     return subset & ps
 
+@predicate('p2([set])')
 def p2(repo, subset, x):
-    """``p2([set])``
-    Second parent of changesets in set, or the working directory.
+    """Second parent of changesets in set, or the working directory.
     """
     if x is None:
         ps = repo[x].parents()
@@ -1570,8 +1608,9 @@
     # some optimisations from the fact this is a baseset.
     return subset & ps
 
+@predicate('parents([set])')
 def parents(repo, subset, x):
-    """``parents([set])``
+    """
     The set of all parents for all changesets in set, or the working directory.
     """
     if x is None:
@@ -1602,17 +1641,17 @@
         condition = lambda r: phase(repo, r) == target
         return subset.filter(condition, cache=False)
 
+@predicate('draft()')
 def draft(repo, subset, x):
-    """``draft()``
-    Changeset in draft phase."""
+    """Changeset in draft phase."""
     # i18n: "draft" is a keyword
     getargs(x, 0, 0, _("draft takes no arguments"))
     target = phases.draft
     return _phase(repo, subset, target)
 
+@predicate('secret()')
 def secret(repo, subset, x):
-    """``secret()``
-    Changeset in secret phase."""
+    """Changeset in secret phase."""
     # i18n: "secret" is a keyword
     getargs(x, 0, 0, _("secret takes no arguments"))
     target = phases.secret
@@ -1643,9 +1682,9 @@
                 ps.add(parents[1])
     return subset & ps
 
+@predicate('present(set)')
 def present(repo, subset, x):
-    """``present(set)``
-    An empty set, if any revision in set isn't found; otherwise,
+    """An empty set, if any revision in set isn't found; otherwise,
     all revisions in set.
 
     If any of specified revisions is not present in the local repository,
@@ -1658,6 +1697,7 @@
         return baseset()
 
 # for internal use
+@predicate('_notpublic')
 def _notpublic(repo, subset, x):
     getargs(x, 0, 0, "_notpublic takes no arguments")
     repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded
@@ -1674,9 +1714,9 @@
         condition = lambda r: phase(repo, r) != target
         return subset.filter(condition, cache=False)
 
+@predicate('public()')
 def public(repo, subset, x):
-    """``public()``
-    Changeset in public phase."""
+    """Changeset in public phase."""
     # i18n: "public" is a keyword
     getargs(x, 0, 0, _("public takes no arguments"))
     phase = repo._phasecache.phase
@@ -1684,9 +1724,9 @@
     condition = lambda r: phase(repo, r) == target
     return subset.filter(condition, cache=False)
 
+@predicate('remote([id [,path]])')
 def remote(repo, subset, x):
-    """``remote([id [,path]])``
-    Local revision that corresponds to the given identifier in a
+    """Local revision that corresponds to the given identifier in a
     remote repository, if present. Here, the '.' identifier is a
     synonym for the current local branch.
     """
@@ -1719,9 +1759,9 @@
             return baseset([r])
     return baseset()
 
+@predicate('removes(pattern)')
 def removes(repo, subset, x):
-    """``removes(pattern)``
-    Changesets which remove files matching pattern.
+    """Changesets which remove files matching pattern.
 
     The pattern without explicit kind like ``glob:`` is expected to be
     relative to the current directory and match against a file or a
@@ -1731,9 +1771,9 @@
     pat = getstring(x, _("removes requires a pattern"))
     return checkstatus(repo, subset, pat, 2)
 
+@predicate('rev(number)')
 def rev(repo, subset, x):
-    """``rev(number)``
-    Revision with the given numeric identifier.
+    """Revision with the given numeric identifier.
     """
     # i18n: "rev" is a keyword
     l = getargs(x, 1, 1, _("rev requires one argument"))
@@ -1747,9 +1787,9 @@
         return baseset()
     return subset & baseset([l])
 
+@predicate('matching(revision [, field])')
 def matching(repo, subset, x):
-    """``matching(revision [, field])``
-    Changesets in which a given set of fields match the set of fields in the
+    """Changesets in which a given set of fields match the set of fields in the
     selected revision or set.
 
     To match more than one field pass the list of fields to match separated
@@ -1859,17 +1899,17 @@
 
     return subset.filter(matches)
 
+@predicate('reverse(set)')
 def reverse(repo, subset, x):
-    """``reverse(set)``
-    Reverse order of set.
+    """Reverse order of set.
     """
     l = getset(repo, subset, x)
     l.reverse()
     return l
 
+@predicate('roots(set)')
 def roots(repo, subset, x):
-    """``roots(set)``
-    Changesets in set with no parent changeset in set.
+    """Changesets in set with no parent changeset in set.
     """
     s = getset(repo, fullreposet(repo), x)
     parents = repo.changelog.parentrevs
@@ -1880,9 +1920,9 @@
         return True
     return subset & s.filter(filter)
 
+@predicate('sort(set[, [-]key...])')
 def sort(repo, subset, x):
-    """``sort(set[, [-]key...])``
-    Sort set by keys. The default sort order is ascending, specify a key
+    """Sort set by keys. The default sort order is ascending, specify a key
     as ``-key`` to sort in descending order.
 
     The keys can be:
@@ -1943,9 +1983,9 @@
     l.sort()
     return baseset([e[-1] for e in l])
 
+@predicate('subrepo([pattern])')
 def subrepo(repo, subset, x):
-    """``subrepo([pattern])``
-    Changesets that add, modify or remove the given subrepo.  If no subrepo
+    """Changesets that add, modify or remove the given subrepo.  If no subrepo
     pattern is named, any subrepo changes are returned.
     """
     # i18n: "subrepo" is a keyword
@@ -1992,9 +2032,9 @@
         matcher = lambda s: pattern in s
     return kind, pattern, matcher
 
+@predicate('tag([name])')
 def tag(repo, subset, x):
-    """``tag([name])``
-    The specified tag by name, or all tagged revisions if no name is given.
+    """The specified tag by name, or all tagged revisions if no name is given.
 
     If `name` starts with `re:`, the remainder of the name is treated as
     a regular expression. To match a tag that actually starts with `re:`,
@@ -2021,12 +2061,13 @@
         s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
     return subset & s
 
+@predicate('tagged')
 def tagged(repo, subset, x):
     return tag(repo, subset, x)
 
+@predicate('unstable()')
 def unstable(repo, subset, x):
-    """``unstable()``
-    Non-obsolete changesets with obsolete ancestors.
+    """Non-obsolete changesets with obsolete ancestors.
     """
     # i18n: "unstable" is a keyword
     getargs(x, 0, 0, _("unstable takes no arguments"))
@@ -2034,9 +2075,9 @@
     return subset & unstables
 
 
+@predicate('user(string)')
 def user(repo, subset, x):
-    """``user(string)``
-    User name contains string. The match is case-insensitive.
+    """User name contains string. The match is case-insensitive.
 
     If `string` starts with `re:`, the remainder of the string is treated as
     a regular expression. To match a user that actually contains `re:`, use
@@ -2045,6 +2086,7 @@
     return author(repo, subset, x)
 
 # experimental
+@predicate('wdir')
 def wdir(repo, subset, x):
     # i18n: "wdir" is a keyword
     getargs(x, 0, 0, _("wdir takes no arguments"))
@@ -2053,6 +2095,7 @@
     return baseset()
 
 # for internal use
+@predicate('_list')
 def _list(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2082,6 +2125,7 @@
     return baseset(ls)
 
 # for internal use
+@predicate('_intlist')
 def _intlist(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2091,6 +2135,7 @@
     return baseset([r for r in ls if r in s])
 
 # for internal use
+@predicate('_hexlist')
 def _hexlist(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2100,83 +2145,6 @@
     s = subset
     return baseset([r for r in ls if r in s])
 
-symbols = {
-    "_destupdate": _destupdate,
-    "_destmerge": _destmerge,
-    "adds": adds,
-    "all": getall,
-    "ancestor": ancestor,
-    "ancestors": ancestors,
-    "_firstancestors": _firstancestors,
-    "author": author,
-    "bisect": bisect,
-    "bisected": bisected,
-    "bookmark": bookmark,
-    "branch": branch,
-    "branchpoint": branchpoint,
-    "bumped": bumped,
-    "bundle": bundle,
-    "children": children,
-    "closed": closed,
-    "contains": contains,
-    "converted": converted,
-    "date": date,
-    "desc": desc,
-    "descendants": descendants,
-    "_firstdescendants": _firstdescendants,
-    "destination": destination,
-    "divergent": divergent,
-    "draft": draft,
-    "extinct": extinct,
-    "extra": extra,
-    "file": hasfile,
-    "filelog": filelog,
-    "first": first,
-    "follow": follow,
-    "_followfirst": _followfirst,
-    "grep": grep,
-    "head": head,
-    "heads": heads,
-    "hidden": hidden,
-    "id": node_,
-    "keyword": keyword,
-    "last": last,
-    "limit": limit,
-    "_matchfiles": _matchfiles,
-    "max": maxrev,
-    "merge": merge,
-    "min": minrev,
-    "modifies": modifies,
-    "named": named,
-    "obsolete": obsolete,
-    "only": only,
-    "origin": origin,
-    "outgoing": outgoing,
-    "p1": p1,
-    "p2": p2,
-    "parents": parents,
-    "present": present,
-    "public": public,
-    "_notpublic": _notpublic,
-    "remote": remote,
-    "removes": removes,
-    "rev": rev,
-    "reverse": reverse,
-    "roots": roots,
-    "sort": sort,
-    "secret": secret,
-    "subrepo": subrepo,
-    "matching": matching,
-    "tag": tag,
-    "tagged": tagged,
-    "user": user,
-    "unstable": unstable,
-    "wdir": wdir,
-    "_list": _list,
-    "_intlist": _intlist,
-    "_hexlist": _hexlist,
-}
-
 # symbols which can't be used for a DoS attack for any given input
 # (e.g. those which accept regexes as plain strings shouldn't be included)
 # functions that just return a lot of changesets (like all) don't count here