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