comparison 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
comparison
equal deleted inserted replaced
36539:638c012a87ef 36540:aa3294027936
40 revlog, 40 revlog,
41 rewriteutil, 41 rewriteutil,
42 scmutil, 42 scmutil,
43 smartset, 43 smartset,
44 subrepoutil, 44 subrepoutil,
45 templatekw,
45 templater, 46 templater,
46 util, 47 util,
47 vfs as vfsmod, 48 vfs as vfsmod,
48 ) 49 )
49 stringio = util.stringio 50 stringio = util.stringio
889 elif editform: 890 elif editform:
890 return lambda r, c, s: commiteditor(r, c, s, editform=editform) 891 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
891 else: 892 else:
892 return commiteditor 893 return commiteditor
893 894
894 def makefilename(ctx, pat, 895 def rendertemplate(ctx, tmpl, props=None):
895 total=None, seqno=None, revwidth=None, pathname=None): 896 """Expand a literal template 'tmpl' byte-string against one changeset
897
898 Each props item must be a stringify-able value or a callable returning
899 such value, i.e. no bare list nor dict should be passed.
900 """
901 repo = ctx.repo()
902 tres = formatter.templateresources(repo.ui, repo)
903 t = formatter.maketemplater(repo.ui, tmpl, defaults=templatekw.keywords,
904 resources=tres)
905 mapping = {'ctx': ctx, 'revcache': {}}
906 if props:
907 mapping.update(props)
908 return t.render(mapping)
909
910 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
911 r"""Convert old-style filename format string to template string
912
913 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
914 'foo-{reporoot|basename}-{seqno}.patch'
915 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
916 '{rev}{tags % "{tag}"}{node}'
917
918 '\' in outermost strings has to be escaped because it is a directory
919 separator on Windows:
920
921 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
922 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
923 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
924 '\\\\\\\\foo\\\\bar.patch'
925 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
926 '\\\\{tags % "{tag}"}'
927
928 but inner strings follow the template rules (i.e. '\' is taken as an
929 escape character):
930
931 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
932 '{"c:\\tmp"}'
933 """
896 expander = { 934 expander = {
897 'H': lambda: ctx.hex(), 935 b'H': b'{node}',
898 'R': lambda: '%d' % ctx.rev(), 936 b'R': b'{rev}',
899 'h': lambda: short(ctx.node()), 937 b'h': b'{node|short}',
900 'm': lambda: re.sub('[^\w]', '_', 938 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
901 ctx.description().strip().splitlines()[0]), 939 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
902 'r': lambda: ('%d' % ctx.rev()).zfill(revwidth or 0), 940 b'%': b'%',
903 '%': lambda: '%', 941 b'b': b'{reporoot|basename}',
904 'b': lambda: os.path.basename(ctx.repo().root), 942 }
905 }
906 if total is not None: 943 if total is not None:
907 expander['N'] = lambda: '%d' % total 944 expander[b'N'] = b'{total}'
908 if seqno is not None: 945 if seqno is not None:
909 expander['n'] = lambda: '%d' % seqno 946 expander[b'n'] = b'{seqno}'
910 if total is not None and seqno is not None: 947 if total is not None and seqno is not None:
911 expander['n'] = (lambda: ('%d' % seqno).zfill(len('%d' % total))) 948 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
912 if pathname is not None: 949 if pathname is not None:
913 expander['s'] = lambda: os.path.basename(pathname) 950 expander[b's'] = b'{pathname|basename}'
914 expander['d'] = lambda: os.path.dirname(pathname) or '.' 951 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
915 expander['p'] = lambda: pathname 952 expander[b'p'] = b'{pathname}'
916 953
917 newname = [] 954 newname = []
918 patlen = len(pat) 955 for typ, start, end in templater.scantemplate(pat, raw=True):
919 i = 0 956 if typ != b'string':
920 while i < patlen: 957 newname.append(pat[start:end])
921 c = pat[i:i + 1] 958 continue
922 if c == '%': 959 i = start
923 i += 1 960 while i < end:
924 c = pat[i:i + 1] 961 n = pat.find(b'%', i, end)
962 if n < 0:
963 newname.append(util.escapestr(pat[i:end]))
964 break
965 newname.append(util.escapestr(pat[i:n]))
966 if n + 2 > end:
967 raise error.Abort(_("incomplete format spec in output "
968 "filename"))
969 c = pat[n + 1:n + 2]
970 i = n + 2
925 try: 971 try:
926 c = expander[c]() 972 newname.append(expander[c])
927 except KeyError: 973 except KeyError:
928 raise error.Abort(_("invalid format spec '%%%s' in output " 974 raise error.Abort(_("invalid format spec '%%%s' in output "
929 "filename") % c) 975 "filename") % c)
930 newname.append(c)
931 i += 1
932 return ''.join(newname) 976 return ''.join(newname)
977
978 def makefilename(ctx, pat, **props):
979 if not pat:
980 return pat
981 tmpl = _buildfntemplate(pat, **props)
982 # BUG: alias expansion shouldn't be made against template fragments
983 # rewritten from %-format strings, but we have no easy way to partially
984 # disable the expansion.
985 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
933 986
934 def isstdiofilename(pat): 987 def isstdiofilename(pat):
935 """True if the given pat looks like a filename denoting stdin/stdout""" 988 """True if the given pat looks like a filename denoting stdin/stdout"""
936 return not pat or pat == '-' 989 return not pat or pat == '-'
937 990