Mercurial > public > mercurial-scm > hg-stable
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 == '-'