Patchwork templater: store revisions as ints so min/max won't compare them as strings

login
register
mail settings
Submitter Yuya Nishihara
Date Oct. 11, 2017, 3:02 p.m.
Message ID <71e49fbf7b471fd41478.1507734146@mimosa>
Download mbox | patch
Permalink /patch/24747/
State Accepted
Headers show

Comments

Yuya Nishihara - Oct. 11, 2017, 3:02 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1505830426 -32400
#      Tue Sep 19 23:13:46 2017 +0900
# Node ID 71e49fbf7b471fd41478705a4bd7e71f67b0c97e
# Parent  1b59287a1cfa3074c092837a5f8ecb63cb6e1933
templater: store revisions as ints so min/max won't compare them as strings

Because a template value has no explicit type (like ancient PHP), ifcontains()
has to coerce the type of the needle. Before, it was always converted to a
string, which meant any container type should be a list/dict of strings.
This no longer works since we've introduced min/max functions.

In order to work around the untyped nature of templater, this patch adds
a type specifier to hybrid dict/list. It isn't named as "valuetype" since
the _hybrid class can also wrap a dict.
Augie Fackler - Oct. 11, 2017, 3:48 p.m.
On Thu, Oct 12, 2017 at 12:02:26AM +0900, Yuya Nishihara wrote:
> # HG changeset patch
> # User Yuya Nishihara <yuya@tcha.org>
> # Date 1505830426 -32400
> #      Tue Sep 19 23:13:46 2017 +0900
> # Node ID 71e49fbf7b471fd41478705a4bd7e71f67b0c97e
> # Parent  1b59287a1cfa3074c092837a5f8ecb63cb6e1933
> templater: store revisions as ints so min/max won't compare them as strings

queued, thanks

Patch

diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -37,12 +37,13 @@  class _hybrid(object):
     - "{files|json}"
     """
 
-    def __init__(self, gen, values, makemap, joinfmt):
+    def __init__(self, gen, values, makemap, joinfmt, keytype=None):
         if gen is not None:
             self.gen = gen  # generator or function returning generator
         self._values = values
         self._makemap = makemap
         self.joinfmt = joinfmt
+        self.keytype = keytype  # hint for 'x in y' where type(x) is unresolved
     def gen(self):
         """Default generator to stringify this as {join(self, ' ')}"""
         for i, x in enumerate(self._values):
@@ -788,15 +789,14 @@  def showparents(**args):
     repo = args['repo']
     ctx = args['ctx']
     pctxs = scmutil.meaningfulparents(repo, ctx)
-    # ifcontains() needs a list of str
-    prevs = ["%d" % p.rev() for p in pctxs]
+    prevs = [p.rev() for p in pctxs]
     parents = [[('rev', p.rev()),
                 ('node', p.hex()),
                 ('phase', p.phasestr())]
                for p in pctxs]
     f = _showlist('parent', parents, args)
-    return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
-                   lambda x: scmutil.formatchangeid(repo[int(x)]))
+    return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
+                   lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
 
 @templatekeyword('phase')
 def showphase(repo, ctx, templ, **args):
@@ -818,12 +818,10 @@  def showrevslist(name, revs, **args):
     be evaluated"""
     args = pycompat.byteskwargs(args)
     repo = args['ctx'].repo()
-    # ifcontains() needs a list of str
-    revs = ["%d" % r for r in revs]
-    f = _showlist(name, revs, args)
+    f = _showlist(name, ['%d' % r for r in revs], args)
     return _hybrid(f, revs,
-                   lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
-                   pycompat.identity)
+                   lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
+                   pycompat.identity, keytype=int)
 
 @templatekeyword('subrepos')
 def showsubrepos(**args):
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -333,12 +333,12 @@  def evalboolean(context, mapping, arg):
     # empty dict/list should be False as they are expected to be ''
     return bool(stringify(thing))
 
-def evalinteger(context, mapping, arg, err):
+def evalinteger(context, mapping, arg, err=None):
     v = evalfuncarg(context, mapping, arg)
     try:
         return int(v)
     except (TypeError, ValueError):
-        raise error.ParseError(err)
+        raise error.ParseError(err or _('not an integer'))
 
 def evalstring(context, mapping, arg):
     return stringify(evalrawexp(context, mapping, arg))
@@ -353,6 +353,20 @@  def evalstringliteral(context, mapping, 
         thing = func(context, mapping, data)
     return stringify(thing)
 
+_evalfuncbytype = {
+    bool: evalboolean,
+    bytes: evalstring,
+    int: evalinteger,
+}
+
+def evalastype(context, mapping, arg, typ):
+    """Evaluate given argument and coerce its type"""
+    try:
+        f = _evalfuncbytype[typ]
+    except KeyError:
+        raise error.ProgrammingError('invalid type specified: %r' % typ)
+    return f(context, mapping, arg)
+
 def runinteger(context, mapping, data):
     return int(data)
 
@@ -782,8 +796,9 @@  def ifcontains(context, mapping, args):
         # i18n: "ifcontains" is a keyword
         raise error.ParseError(_("ifcontains expects three or four arguments"))
 
-    needle = evalstring(context, mapping, args[0])
     haystack = evalfuncarg(context, mapping, args[1])
+    needle = evalastype(context, mapping, args[0],
+                        getattr(haystack, 'keytype', None) or bytes)
 
     if needle in haystack:
         yield evalrawexp(context, mapping, args[2])
diff --git a/tests/test-command-template.t b/tests/test-command-template.t
--- a/tests/test-command-template.t
+++ b/tests/test-command-template.t
@@ -3147,6 +3147,13 @@  Test manifest/get() can be join()-ed as 
   $ hg log -R latesttag -r tip -T '{join(get(extras, "branch"), "")}\n'
   default
 
+Test min/max of integers
+
+  $ hg log -R latesttag -l1 -T '{min(revset("9:10"))}\n'
+  9
+  $ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
+  10
+
 Test dot operator precedence:
 
   $ hg debugtemplate -R latesttag -r0 -v '{manifest.node|short}\n'