diff mercurial/cmdutil.py @ 36540:aa3294027936

cmdutil: expand filename format string by templater (BC) This is BC because '{}' could be a valid filename before, but I believe good programmers wouldn't use such catastrophic output filenames. On the other hand, '\' has to be escaped since it is a directory separator on Windows. Thanks to Matt Harbison for spotting this weird issue. This patch also adds cmdutil.rendertemplate(ctx, tmpl, props) as a simpler way of expanding template against single changeset. .. bc:: '{' in output filename passed to archive/cat/export is taken as a start of a template expression.
author Yuya Nishihara <yuya@tcha.org>
date Sun, 07 Jan 2018 11:53:07 +0900
parents d7a23d6184a2
children c6061cadb400
line wrap: on
line diff
--- a/mercurial/cmdutil.py	Sun Feb 18 11:53:26 2018 +0900
+++ b/mercurial/cmdutil.py	Sun Jan 07 11:53:07 2018 +0900
@@ -42,6 +42,7 @@
     scmutil,
     smartset,
     subrepoutil,
+    templatekw,
     templater,
     util,
     vfs as vfsmod,
@@ -891,46 +892,98 @@
     else:
         return commiteditor
 
-def makefilename(ctx, pat,
-                  total=None, seqno=None, revwidth=None, pathname=None):
+def rendertemplate(ctx, tmpl, props=None):
+    """Expand a literal template 'tmpl' byte-string against one changeset
+
+    Each props item must be a stringify-able value or a callable returning
+    such value, i.e. no bare list nor dict should be passed.
+    """
+    repo = ctx.repo()
+    tres = formatter.templateresources(repo.ui, repo)
+    t = formatter.maketemplater(repo.ui, tmpl, defaults=templatekw.keywords,
+                                resources=tres)
+    mapping = {'ctx': ctx, 'revcache': {}}
+    if props:
+        mapping.update(props)
+    return t.render(mapping)
+
+def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
+    r"""Convert old-style filename format string to template string
+
+    >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
+    'foo-{reporoot|basename}-{seqno}.patch'
+    >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
+    '{rev}{tags % "{tag}"}{node}'
+
+    '\' in outermost strings has to be escaped because it is a directory
+    separator on Windows:
+
+    >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
+    'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
+    >>> _buildfntemplate(b'\\\\foo\\bar.patch')
+    '\\\\\\\\foo\\\\bar.patch'
+    >>> _buildfntemplate(b'\\{tags % "{tag}"}')
+    '\\\\{tags % "{tag}"}'
+
+    but inner strings follow the template rules (i.e. '\' is taken as an
+    escape character):
+
+    >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
+    '{"c:\\tmp"}'
+    """
     expander = {
-        'H': lambda: ctx.hex(),
-        'R': lambda: '%d' % ctx.rev(),
-        'h': lambda: short(ctx.node()),
-        'm': lambda: re.sub('[^\w]', '_',
-                            ctx.description().strip().splitlines()[0]),
-        'r': lambda: ('%d' % ctx.rev()).zfill(revwidth or 0),
-        '%': lambda: '%',
-        'b': lambda: os.path.basename(ctx.repo().root),
-        }
+        b'H': b'{node}',
+        b'R': b'{rev}',
+        b'h': b'{node|short}',
+        b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
+        b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
+        b'%': b'%',
+        b'b': b'{reporoot|basename}',
+    }
     if total is not None:
-        expander['N'] = lambda: '%d' % total
+        expander[b'N'] = b'{total}'
     if seqno is not None:
-        expander['n'] = lambda: '%d' % seqno
+        expander[b'n'] = b'{seqno}'
     if total is not None and seqno is not None:
-        expander['n'] = (lambda: ('%d' % seqno).zfill(len('%d' % total)))
+        expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
     if pathname is not None:
-        expander['s'] = lambda: os.path.basename(pathname)
-        expander['d'] = lambda: os.path.dirname(pathname) or '.'
-        expander['p'] = lambda: pathname
+        expander[b's'] = b'{pathname|basename}'
+        expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
+        expander[b'p'] = b'{pathname}'
 
     newname = []
-    patlen = len(pat)
-    i = 0
-    while i < patlen:
-        c = pat[i:i + 1]
-        if c == '%':
-            i += 1
-            c = pat[i:i + 1]
+    for typ, start, end in templater.scantemplate(pat, raw=True):
+        if typ != b'string':
+            newname.append(pat[start:end])
+            continue
+        i = start
+        while i < end:
+            n = pat.find(b'%', i, end)
+            if n < 0:
+                newname.append(util.escapestr(pat[i:end]))
+                break
+            newname.append(util.escapestr(pat[i:n]))
+            if n + 2 > end:
+                raise error.Abort(_("incomplete format spec in output "
+                                    "filename"))
+            c = pat[n + 1:n + 2]
+            i = n + 2
             try:
-                c = expander[c]()
+                newname.append(expander[c])
             except KeyError:
                 raise error.Abort(_("invalid format spec '%%%s' in output "
                                     "filename") % c)
-        newname.append(c)
-        i += 1
     return ''.join(newname)
 
+def makefilename(ctx, pat, **props):
+    if not pat:
+        return pat
+    tmpl = _buildfntemplate(pat, **props)
+    # BUG: alias expansion shouldn't be made against template fragments
+    # rewritten from %-format strings, but we have no easy way to partially
+    # disable the expansion.
+    return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
+
 def isstdiofilename(pat):
     """True if the given pat looks like a filename denoting stdin/stdout"""
     return not pat or pat == '-'