Patchwork [1,of,4] templater: introduce wrapper for smartset (API)

login
register
mail settings
Submitter Yuya Nishihara
Date March 24, 2020, 2:42 p.m.
Message ID <22fa586e96c80e85f433.1585060953@mimosa>
Download mbox | patch
Permalink /patch/45870/
State Accepted
Headers show

Comments

Yuya Nishihara - March 24, 2020, 2:42 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1584252764 -32400
#      Sun Mar 15 15:12:44 2020 +0900
# Node ID 22fa586e96c80e85f433565e39b14b17993310b6
# Parent  2a98b0cd4995d725104ea1e42b5009f7ee26ac4c
templater: introduce wrapper for smartset (API)

I want to add a template function which takes a revset as an argument:

  {somefunc(..., revset(...))}
                 ^^^^^^^^^^^
                 evaluates to a revslist

This wrapper will provide a method to get an underlying smartset. It should
also be good for performance since count(revset(...)) will no longer have to
fully consume the smartset for example, but that isn't the point of this
change.

Patch

diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -668,7 +668,7 @@  def revset(context, mapping, args):
         else:
             revs = query(raw)
             revsetcache[raw] = revs
-    return templatekw.showrevslist(context, mapping, b"revision", revs)
+    return templateutil.revslist(repo, revs, name=b'revision')
 
 
 @templatefunc(b'rstdoc(text, style)')
diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -873,24 +873,6 @@  def showrev(context, mapping):
     return scmutil.intrev(ctx)
 
 
-def showrevslist(context, mapping, name, revs):
-    """helper to generate a list of revisions in which a mapped template will
-    be evaluated"""
-    repo = context.resource(mapping, b'repo')
-    # revs may be a smartset; don't compute it until f() has to be evaluated
-    def f():
-        srevs = [b'%d' % r for r in revs]
-        return _showcompatlist(context, mapping, name, srevs)
-
-    return _hybrid(
-        f,
-        revs,
-        lambda x: {name: x, b'ctx': repo[x]},
-        pycompat.identity,
-        keytype=int,
-    )
-
-
 @templatekeyword(b'subrepos', requires={b'ctx'})
 def showsubrepos(context, mapping):
     """List of strings. Updated subrepositories in the changeset."""
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -45,6 +45,9 @@  hybrid
 hybriditem
     represents a scalar printable value, also supports % operator.
 
+revslist
+    represents a list of revision numbers.
+
 mappinggenerator, mappinglist
     represents mappings (i.e. a list of dicts), which may have default
     output format.
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -15,6 +15,7 @@  from .pycompat import getattr
 from . import (
     error,
     pycompat,
+    smartset,
     util,
 )
 from .utils import (
@@ -408,6 +409,74 @@  class hybriditem(mappable, wrapped):
         return _unthunk(context, mapping, self._value)
 
 
+class revslist(wrapped):
+    """Wrapper for a smartset (a list/set of revision numbers)
+
+    If name specified, the revs will be rendered with the old-style list
+    template of the given name by default.
+    """
+
+    def __init__(self, repo, revs, name=None):
+        assert isinstance(revs, smartset.abstractsmartset)
+        self._repo = repo
+        self._revs = revs
+        self._name = name
+
+    def contains(self, context, mapping, item):
+        rev = unwrapinteger(context, mapping, item)
+        return rev in self._revs
+
+    def getmember(self, context, mapping, key):
+        raise error.ParseError(_(b'not a dictionary'))
+
+    def getmin(self, context, mapping):
+        makehybriditem = self._makehybriditemfunc()
+        return makehybriditem(self._revs.min())
+
+    def getmax(self, context, mapping):
+        makehybriditem = self._makehybriditemfunc()
+        return makehybriditem(self._revs.max())
+
+    def filter(self, context, mapping, select):
+        makehybriditem = self._makehybriditemfunc()
+        frevs = self._revs.filter(lambda r: select(makehybriditem(r)))
+        # once filtered, no need to support old-style list template
+        return revslist(self._repo, frevs, name=None)
+
+    def itermaps(self, context):
+        makemap = self._makemapfunc()
+        for r in self._revs:
+            yield makemap(r)
+
+    def _makehybriditemfunc(self):
+        makemap = self._makemapfunc()
+        return lambda r: hybriditem(None, r, r, makemap)
+
+    def _makemapfunc(self):
+        repo = self._repo
+        name = self._name
+        if name:
+            return lambda r: {name: r, b'ctx': repo[r]}
+        else:
+            return lambda r: {b'ctx': repo[r]}
+
+    def join(self, context, mapping, sep):
+        return joinitems(self._revs, sep)
+
+    def show(self, context, mapping):
+        if self._name:
+            srevs = [b'%d' % r for r in self._revs]
+            return _showcompatlist(context, mapping, self._name, srevs)
+        else:
+            return self.join(context, mapping, b' ')
+
+    def tobool(self, context, mapping):
+        return bool(self._revs)
+
+    def tovalue(self, context, mapping):
+        return list(self._revs)
+
+
 class _mappingsequence(wrapped):
     """Wrapper for sequence of template mappings
 
diff --git a/tests/test-template-functions.t b/tests/test-template-functions.t
--- a/tests/test-template-functions.t
+++ b/tests/test-template-functions.t
@@ -820,6 +820,8 @@  Test json filter applied to wrapped obje
   {"branch": "default"}
   $ hg log -r0 -T '{date|json}\n'
   [0, 0]
+  $ hg log -r0 -T '{revset(":")|json}\n'
+  [0, 1]
 
 Test json filter applied to map result:
 
@@ -1263,6 +1265,28 @@  default. join() should agree with the de
   5:13207e5a10d9fd28ec424934298e176197f2c67f,
   4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
 
+for historical reasons, revset() supports old-style list template
+
+  $ hg log -T '{revset(":")}\n' -l1 \
+  >        --config templates.start_revisions='"["' \
+  >        --config templates.end_revisions='"]"' \
+  >        --config templates.revision='"{revision}, "' \
+  >        --config templates.last_revision='"{revision}"'
+  [0, 1, 2]
+  $ hg log -T '{revset(":") % " {revision}"}\n' -l1
+   0 1 2
+
+but a filtered one doesn't
+
+  $ hg log -T '{filter(revset(":"), ifeq(rev, 1, "", "y"))}\n' -l1 \
+  >        --config templates.start_revisions='"["' \
+  >        --config templates.end_revisions='"]"' \
+  >        --config templates.revision='"{revision}, "' \
+  >        --config templates.last_revision='"{revision}"'
+  0 2
+  $ hg log -T '{filter(revset(":"), ifeq(rev, 1, "", "y")) % "x{revision}"}\n' -l1
+  xx
+
 %d parameter handling:
 
   $ hg log -T '{revset("%d", rev)}\n' -r'wdir()'
@@ -1318,6 +1342,13 @@  Invalid arguments passed to revset()
   hg: parse error: invalid argument for revspec
   [255]
 
+Invalid operation on revset()
+
+  $ hg log -T '{get(revset(":"), "foo")}\n'
+  hg: parse error: not a dictionary
+  (get() expects a dict as first argument)
+  [255]
+
 Test files function
 
   $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
@@ -1568,6 +1599,23 @@  Test cbor filter:
    }
   ]
 
+  $ hg log -T "{revset(':')|cbor}" -R a -l1 | "$PYTHON" "$TESTTMP/decodecbor.py"
+  [
+   [
+    0,
+    1,
+    2,
+    3,
+    4,
+    5,
+    6,
+    7,
+    8,
+    9,
+    10
+   ]
+  ]
+
 json filter should escape HTML tags so that the output can be embedded in hgweb:
 
   $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1