##// END OF EJS Templates
templater: check invalid use of list expression properly (issue5920)...
Yuya Nishihara -
r40652:ff8b2886 default
parent child Browse files
Show More
@@ -1,989 +1,992 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Slightly complicated template engine for commands and hgweb
8 """Slightly complicated template engine for commands and hgweb
9
9
10 This module provides low-level interface to the template engine. See the
10 This module provides low-level interface to the template engine. See the
11 formatter and cmdutil modules if you are looking for high-level functions
11 formatter and cmdutil modules if you are looking for high-level functions
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
13
14 Internal Data Types
14 Internal Data Types
15 -------------------
15 -------------------
16
16
17 Template keywords and functions take a dictionary of current symbols and
17 Template keywords and functions take a dictionary of current symbols and
18 resources (a "mapping") and return result. Inputs and outputs must be one
18 resources (a "mapping") and return result. Inputs and outputs must be one
19 of the following data types:
19 of the following data types:
20
20
21 bytes
21 bytes
22 a byte string, which is generally a human-readable text in local encoding.
22 a byte string, which is generally a human-readable text in local encoding.
23
23
24 generator
24 generator
25 a lazily-evaluated byte string, which is a possibly nested generator of
25 a lazily-evaluated byte string, which is a possibly nested generator of
26 values of any printable types, and will be folded by ``stringify()``
26 values of any printable types, and will be folded by ``stringify()``
27 or ``flatten()``.
27 or ``flatten()``.
28
28
29 None
29 None
30 sometimes represents an empty value, which can be stringified to ''.
30 sometimes represents an empty value, which can be stringified to ''.
31
31
32 True, False, int, float
32 True, False, int, float
33 can be stringified as such.
33 can be stringified as such.
34
34
35 wrappedbytes, wrappedvalue
35 wrappedbytes, wrappedvalue
36 a wrapper for the above printable types.
36 a wrapper for the above printable types.
37
37
38 date
38 date
39 represents a (unixtime, offset) tuple.
39 represents a (unixtime, offset) tuple.
40
40
41 hybrid
41 hybrid
42 represents a list/dict of printable values, which can also be converted
42 represents a list/dict of printable values, which can also be converted
43 to mappings by % operator.
43 to mappings by % operator.
44
44
45 hybriditem
45 hybriditem
46 represents a scalar printable value, also supports % operator.
46 represents a scalar printable value, also supports % operator.
47
47
48 mappinggenerator, mappinglist
48 mappinggenerator, mappinglist
49 represents mappings (i.e. a list of dicts), which may have default
49 represents mappings (i.e. a list of dicts), which may have default
50 output format.
50 output format.
51
51
52 mappingdict
52 mappingdict
53 represents a single mapping (i.e. a dict), which may have default output
53 represents a single mapping (i.e. a dict), which may have default output
54 format.
54 format.
55
55
56 mappedgenerator
56 mappedgenerator
57 a lazily-evaluated list of byte strings, which is e.g. a result of %
57 a lazily-evaluated list of byte strings, which is e.g. a result of %
58 operation.
58 operation.
59 """
59 """
60
60
61 from __future__ import absolute_import, print_function
61 from __future__ import absolute_import, print_function
62
62
63 import abc
63 import abc
64 import os
64 import os
65
65
66 from .i18n import _
66 from .i18n import _
67 from . import (
67 from . import (
68 config,
68 config,
69 encoding,
69 encoding,
70 error,
70 error,
71 parser,
71 parser,
72 pycompat,
72 pycompat,
73 templatefilters,
73 templatefilters,
74 templatefuncs,
74 templatefuncs,
75 templateutil,
75 templateutil,
76 util,
76 util,
77 )
77 )
78 from .utils import (
78 from .utils import (
79 stringutil,
79 stringutil,
80 )
80 )
81
81
82 # template parsing
82 # template parsing
83
83
84 elements = {
84 elements = {
85 # token-type: binding-strength, primary, prefix, infix, suffix
85 # token-type: binding-strength, primary, prefix, infix, suffix
86 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
86 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
87 ".": (18, None, None, (".", 18), None),
87 ".": (18, None, None, (".", 18), None),
88 "%": (15, None, None, ("%", 15), None),
88 "%": (15, None, None, ("%", 15), None),
89 "|": (15, None, None, ("|", 15), None),
89 "|": (15, None, None, ("|", 15), None),
90 "*": (5, None, None, ("*", 5), None),
90 "*": (5, None, None, ("*", 5), None),
91 "/": (5, None, None, ("/", 5), None),
91 "/": (5, None, None, ("/", 5), None),
92 "+": (4, None, None, ("+", 4), None),
92 "+": (4, None, None, ("+", 4), None),
93 "-": (4, None, ("negate", 19), ("-", 4), None),
93 "-": (4, None, ("negate", 19), ("-", 4), None),
94 "=": (3, None, None, ("keyvalue", 3), None),
94 "=": (3, None, None, ("keyvalue", 3), None),
95 ",": (2, None, None, ("list", 2), None),
95 ",": (2, None, None, ("list", 2), None),
96 ")": (0, None, None, None, None),
96 ")": (0, None, None, None, None),
97 "integer": (0, "integer", None, None, None),
97 "integer": (0, "integer", None, None, None),
98 "symbol": (0, "symbol", None, None, None),
98 "symbol": (0, "symbol", None, None, None),
99 "string": (0, "string", None, None, None),
99 "string": (0, "string", None, None, None),
100 "template": (0, "template", None, None, None),
100 "template": (0, "template", None, None, None),
101 "end": (0, None, None, None, None),
101 "end": (0, None, None, None, None),
102 }
102 }
103
103
104 def tokenize(program, start, end, term=None):
104 def tokenize(program, start, end, term=None):
105 """Parse a template expression into a stream of tokens, which must end
105 """Parse a template expression into a stream of tokens, which must end
106 with term if specified"""
106 with term if specified"""
107 pos = start
107 pos = start
108 program = pycompat.bytestr(program)
108 program = pycompat.bytestr(program)
109 while pos < end:
109 while pos < end:
110 c = program[pos]
110 c = program[pos]
111 if c.isspace(): # skip inter-token whitespace
111 if c.isspace(): # skip inter-token whitespace
112 pass
112 pass
113 elif c in "(=,).%|+-*/": # handle simple operators
113 elif c in "(=,).%|+-*/": # handle simple operators
114 yield (c, None, pos)
114 yield (c, None, pos)
115 elif c in '"\'': # handle quoted templates
115 elif c in '"\'': # handle quoted templates
116 s = pos + 1
116 s = pos + 1
117 data, pos = _parsetemplate(program, s, end, c)
117 data, pos = _parsetemplate(program, s, end, c)
118 yield ('template', data, s)
118 yield ('template', data, s)
119 pos -= 1
119 pos -= 1
120 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
120 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
121 # handle quoted strings
121 # handle quoted strings
122 c = program[pos + 1]
122 c = program[pos + 1]
123 s = pos = pos + 2
123 s = pos = pos + 2
124 while pos < end: # find closing quote
124 while pos < end: # find closing quote
125 d = program[pos]
125 d = program[pos]
126 if d == '\\': # skip over escaped characters
126 if d == '\\': # skip over escaped characters
127 pos += 2
127 pos += 2
128 continue
128 continue
129 if d == c:
129 if d == c:
130 yield ('string', program[s:pos], s)
130 yield ('string', program[s:pos], s)
131 break
131 break
132 pos += 1
132 pos += 1
133 else:
133 else:
134 raise error.ParseError(_("unterminated string"), s)
134 raise error.ParseError(_("unterminated string"), s)
135 elif c.isdigit():
135 elif c.isdigit():
136 s = pos
136 s = pos
137 while pos < end:
137 while pos < end:
138 d = program[pos]
138 d = program[pos]
139 if not d.isdigit():
139 if not d.isdigit():
140 break
140 break
141 pos += 1
141 pos += 1
142 yield ('integer', program[s:pos], s)
142 yield ('integer', program[s:pos], s)
143 pos -= 1
143 pos -= 1
144 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
144 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
145 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
145 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
146 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
146 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
147 # where some of nested templates were preprocessed as strings and
147 # where some of nested templates were preprocessed as strings and
148 # then compiled. therefore, \"...\" was allowed. (issue4733)
148 # then compiled. therefore, \"...\" was allowed. (issue4733)
149 #
149 #
150 # processing flow of _evalifliteral() at 5ab28a2e9962:
150 # processing flow of _evalifliteral() at 5ab28a2e9962:
151 # outer template string -> stringify() -> compiletemplate()
151 # outer template string -> stringify() -> compiletemplate()
152 # ------------------------ ------------ ------------------
152 # ------------------------ ------------ ------------------
153 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
153 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
154 # ~~~~~~~~
154 # ~~~~~~~~
155 # escaped quoted string
155 # escaped quoted string
156 if c == 'r':
156 if c == 'r':
157 pos += 1
157 pos += 1
158 token = 'string'
158 token = 'string'
159 else:
159 else:
160 token = 'template'
160 token = 'template'
161 quote = program[pos:pos + 2]
161 quote = program[pos:pos + 2]
162 s = pos = pos + 2
162 s = pos = pos + 2
163 while pos < end: # find closing escaped quote
163 while pos < end: # find closing escaped quote
164 if program.startswith('\\\\\\', pos, end):
164 if program.startswith('\\\\\\', pos, end):
165 pos += 4 # skip over double escaped characters
165 pos += 4 # skip over double escaped characters
166 continue
166 continue
167 if program.startswith(quote, pos, end):
167 if program.startswith(quote, pos, end):
168 # interpret as if it were a part of an outer string
168 # interpret as if it were a part of an outer string
169 data = parser.unescapestr(program[s:pos])
169 data = parser.unescapestr(program[s:pos])
170 if token == 'template':
170 if token == 'template':
171 data = _parsetemplate(data, 0, len(data))[0]
171 data = _parsetemplate(data, 0, len(data))[0]
172 yield (token, data, s)
172 yield (token, data, s)
173 pos += 1
173 pos += 1
174 break
174 break
175 pos += 1
175 pos += 1
176 else:
176 else:
177 raise error.ParseError(_("unterminated string"), s)
177 raise error.ParseError(_("unterminated string"), s)
178 elif c.isalnum() or c in '_':
178 elif c.isalnum() or c in '_':
179 s = pos
179 s = pos
180 pos += 1
180 pos += 1
181 while pos < end: # find end of symbol
181 while pos < end: # find end of symbol
182 d = program[pos]
182 d = program[pos]
183 if not (d.isalnum() or d == "_"):
183 if not (d.isalnum() or d == "_"):
184 break
184 break
185 pos += 1
185 pos += 1
186 sym = program[s:pos]
186 sym = program[s:pos]
187 yield ('symbol', sym, s)
187 yield ('symbol', sym, s)
188 pos -= 1
188 pos -= 1
189 elif c == term:
189 elif c == term:
190 yield ('end', None, pos)
190 yield ('end', None, pos)
191 return
191 return
192 else:
192 else:
193 raise error.ParseError(_("syntax error"), pos)
193 raise error.ParseError(_("syntax error"), pos)
194 pos += 1
194 pos += 1
195 if term:
195 if term:
196 raise error.ParseError(_("unterminated template expansion"), start)
196 raise error.ParseError(_("unterminated template expansion"), start)
197 yield ('end', None, pos)
197 yield ('end', None, pos)
198
198
199 def _parsetemplate(tmpl, start, stop, quote=''):
199 def _parsetemplate(tmpl, start, stop, quote=''):
200 r"""
200 r"""
201 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
201 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
202 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
202 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
203 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
203 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
204 ([('string', 'foo'), ('symbol', 'bar')], 9)
204 ([('string', 'foo'), ('symbol', 'bar')], 9)
205 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
205 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
206 ([('string', 'foo')], 4)
206 ([('string', 'foo')], 4)
207 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
207 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
208 ([('string', 'foo"'), ('string', 'bar')], 9)
208 ([('string', 'foo"'), ('string', 'bar')], 9)
209 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
209 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
210 ([('string', 'foo\\')], 6)
210 ([('string', 'foo\\')], 6)
211 """
211 """
212 parsed = []
212 parsed = []
213 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
213 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
214 if typ == 'string':
214 if typ == 'string':
215 parsed.append((typ, val))
215 parsed.append((typ, val))
216 elif typ == 'template':
216 elif typ == 'template':
217 parsed.append(val)
217 parsed.append(val)
218 elif typ == 'end':
218 elif typ == 'end':
219 return parsed, pos
219 return parsed, pos
220 else:
220 else:
221 raise error.ProgrammingError('unexpected type: %s' % typ)
221 raise error.ProgrammingError('unexpected type: %s' % typ)
222 raise error.ProgrammingError('unterminated scanning of template')
222 raise error.ProgrammingError('unterminated scanning of template')
223
223
224 def scantemplate(tmpl, raw=False):
224 def scantemplate(tmpl, raw=False):
225 r"""Scan (type, start, end) positions of outermost elements in template
225 r"""Scan (type, start, end) positions of outermost elements in template
226
226
227 If raw=True, a backslash is not taken as an escape character just like
227 If raw=True, a backslash is not taken as an escape character just like
228 r'' string in Python. Note that this is different from r'' literal in
228 r'' string in Python. Note that this is different from r'' literal in
229 template in that no template fragment can appear in r'', e.g. r'{foo}'
229 template in that no template fragment can appear in r'', e.g. r'{foo}'
230 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
230 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
231 'foo'.
231 'foo'.
232
232
233 >>> list(scantemplate(b'foo{bar}"baz'))
233 >>> list(scantemplate(b'foo{bar}"baz'))
234 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
234 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
235 >>> list(scantemplate(b'outer{"inner"}outer'))
235 >>> list(scantemplate(b'outer{"inner"}outer'))
236 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
236 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
237 >>> list(scantemplate(b'foo\\{escaped}'))
237 >>> list(scantemplate(b'foo\\{escaped}'))
238 [('string', 0, 5), ('string', 5, 13)]
238 [('string', 0, 5), ('string', 5, 13)]
239 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
239 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
240 [('string', 0, 4), ('template', 4, 13)]
240 [('string', 0, 4), ('template', 4, 13)]
241 """
241 """
242 last = None
242 last = None
243 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
243 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
244 if last:
244 if last:
245 yield last + (pos,)
245 yield last + (pos,)
246 if typ == 'end':
246 if typ == 'end':
247 return
247 return
248 else:
248 else:
249 last = (typ, pos)
249 last = (typ, pos)
250 raise error.ProgrammingError('unterminated scanning of template')
250 raise error.ProgrammingError('unterminated scanning of template')
251
251
252 def _scantemplate(tmpl, start, stop, quote='', raw=False):
252 def _scantemplate(tmpl, start, stop, quote='', raw=False):
253 """Parse template string into chunks of strings and template expressions"""
253 """Parse template string into chunks of strings and template expressions"""
254 sepchars = '{' + quote
254 sepchars = '{' + quote
255 unescape = [parser.unescapestr, pycompat.identity][raw]
255 unescape = [parser.unescapestr, pycompat.identity][raw]
256 pos = start
256 pos = start
257 p = parser.parser(elements)
257 p = parser.parser(elements)
258 try:
258 try:
259 while pos < stop:
259 while pos < stop:
260 n = min((tmpl.find(c, pos, stop)
260 n = min((tmpl.find(c, pos, stop)
261 for c in pycompat.bytestr(sepchars)),
261 for c in pycompat.bytestr(sepchars)),
262 key=lambda n: (n < 0, n))
262 key=lambda n: (n < 0, n))
263 if n < 0:
263 if n < 0:
264 yield ('string', unescape(tmpl[pos:stop]), pos)
264 yield ('string', unescape(tmpl[pos:stop]), pos)
265 pos = stop
265 pos = stop
266 break
266 break
267 c = tmpl[n:n + 1]
267 c = tmpl[n:n + 1]
268 bs = 0 # count leading backslashes
268 bs = 0 # count leading backslashes
269 if not raw:
269 if not raw:
270 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
270 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
271 if bs % 2 == 1:
271 if bs % 2 == 1:
272 # escaped (e.g. '\{', '\\\{', but not '\\{')
272 # escaped (e.g. '\{', '\\\{', but not '\\{')
273 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
273 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
274 pos = n + 1
274 pos = n + 1
275 continue
275 continue
276 if n > pos:
276 if n > pos:
277 yield ('string', unescape(tmpl[pos:n]), pos)
277 yield ('string', unescape(tmpl[pos:n]), pos)
278 if c == quote:
278 if c == quote:
279 yield ('end', None, n + 1)
279 yield ('end', None, n + 1)
280 return
280 return
281
281
282 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
282 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
283 if not tmpl.startswith('}', pos):
283 if not tmpl.startswith('}', pos):
284 raise error.ParseError(_("invalid token"), pos)
284 raise error.ParseError(_("invalid token"), pos)
285 yield ('template', parseres, n)
285 yield ('template', parseres, n)
286 pos += 1
286 pos += 1
287
287
288 if quote:
288 if quote:
289 raise error.ParseError(_("unterminated string"), start)
289 raise error.ParseError(_("unterminated string"), start)
290 except error.ParseError as inst:
290 except error.ParseError as inst:
291 if len(inst.args) > 1: # has location
291 if len(inst.args) > 1: # has location
292 loc = inst.args[1]
292 loc = inst.args[1]
293 # Offset the caret location by the number of newlines before the
293 # Offset the caret location by the number of newlines before the
294 # location of the error, since we will replace one-char newlines
294 # location of the error, since we will replace one-char newlines
295 # with the two-char literal r'\n'.
295 # with the two-char literal r'\n'.
296 offset = tmpl[:loc].count('\n')
296 offset = tmpl[:loc].count('\n')
297 tmpl = tmpl.replace('\n', br'\n')
297 tmpl = tmpl.replace('\n', br'\n')
298 # We want the caret to point to the place in the template that
298 # We want the caret to point to the place in the template that
299 # failed to parse, but in a hint we get a open paren at the
299 # failed to parse, but in a hint we get a open paren at the
300 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
300 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
301 # to line up the caret with the location of the error.
301 # to line up the caret with the location of the error.
302 inst.hint = (tmpl + '\n'
302 inst.hint = (tmpl + '\n'
303 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
303 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
304 raise
304 raise
305 yield ('end', None, pos)
305 yield ('end', None, pos)
306
306
307 def _unnesttemplatelist(tree):
307 def _unnesttemplatelist(tree):
308 """Expand list of templates to node tuple
308 """Expand list of templates to node tuple
309
309
310 >>> def f(tree):
310 >>> def f(tree):
311 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
311 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
312 >>> f((b'template', []))
312 >>> f((b'template', []))
313 (string '')
313 (string '')
314 >>> f((b'template', [(b'string', b'foo')]))
314 >>> f((b'template', [(b'string', b'foo')]))
315 (string 'foo')
315 (string 'foo')
316 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
316 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
317 (template
317 (template
318 (string 'foo')
318 (string 'foo')
319 (symbol 'rev'))
319 (symbol 'rev'))
320 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
320 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
321 (template
321 (template
322 (symbol 'rev'))
322 (symbol 'rev'))
323 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
323 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
324 (string 'foo')
324 (string 'foo')
325 """
325 """
326 if not isinstance(tree, tuple):
326 if not isinstance(tree, tuple):
327 return tree
327 return tree
328 op = tree[0]
328 op = tree[0]
329 if op != 'template':
329 if op != 'template':
330 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
330 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
331
331
332 assert len(tree) == 2
332 assert len(tree) == 2
333 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
333 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
334 if not xs:
334 if not xs:
335 return ('string', '') # empty template ""
335 return ('string', '') # empty template ""
336 elif len(xs) == 1 and xs[0][0] == 'string':
336 elif len(xs) == 1 and xs[0][0] == 'string':
337 return xs[0] # fast path for string with no template fragment "x"
337 return xs[0] # fast path for string with no template fragment "x"
338 else:
338 else:
339 return (op,) + xs
339 return (op,) + xs
340
340
341 def parse(tmpl):
341 def parse(tmpl):
342 """Parse template string into tree"""
342 """Parse template string into tree"""
343 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
343 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
344 assert pos == len(tmpl), 'unquoted template should be consumed'
344 assert pos == len(tmpl), 'unquoted template should be consumed'
345 return _unnesttemplatelist(('template', parsed))
345 return _unnesttemplatelist(('template', parsed))
346
346
347 def _parseexpr(expr):
347 def _parseexpr(expr):
348 """Parse a template expression into tree
348 """Parse a template expression into tree
349
349
350 >>> _parseexpr(b'"foo"')
350 >>> _parseexpr(b'"foo"')
351 ('string', 'foo')
351 ('string', 'foo')
352 >>> _parseexpr(b'foo(bar)')
352 >>> _parseexpr(b'foo(bar)')
353 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
353 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
354 >>> _parseexpr(b'foo(')
354 >>> _parseexpr(b'foo(')
355 Traceback (most recent call last):
355 Traceback (most recent call last):
356 ...
356 ...
357 ParseError: ('not a prefix: end', 4)
357 ParseError: ('not a prefix: end', 4)
358 >>> _parseexpr(b'"foo" "bar"')
358 >>> _parseexpr(b'"foo" "bar"')
359 Traceback (most recent call last):
359 Traceback (most recent call last):
360 ...
360 ...
361 ParseError: ('invalid token', 7)
361 ParseError: ('invalid token', 7)
362 """
362 """
363 p = parser.parser(elements)
363 p = parser.parser(elements)
364 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
364 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
365 if pos != len(expr):
365 if pos != len(expr):
366 raise error.ParseError(_('invalid token'), pos)
366 raise error.ParseError(_('invalid token'), pos)
367 return _unnesttemplatelist(tree)
367 return _unnesttemplatelist(tree)
368
368
369 def prettyformat(tree):
369 def prettyformat(tree):
370 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
370 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
371
371
372 def compileexp(exp, context, curmethods):
372 def compileexp(exp, context, curmethods):
373 """Compile parsed template tree to (func, data) pair"""
373 """Compile parsed template tree to (func, data) pair"""
374 if not exp:
374 if not exp:
375 raise error.ParseError(_("missing argument"))
375 raise error.ParseError(_("missing argument"))
376 t = exp[0]
376 t = exp[0]
377 if t in curmethods:
377 return curmethods[t](exp, context)
378 return curmethods[t](exp, context)
379 raise error.ParseError(_("unknown method '%s'") % t)
380
378
381 # template evaluation
379 # template evaluation
382
380
383 def getsymbol(exp):
381 def getsymbol(exp):
384 if exp[0] == 'symbol':
382 if exp[0] == 'symbol':
385 return exp[1]
383 return exp[1]
386 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
384 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
387
385
388 def getlist(x):
386 def getlist(x):
389 if not x:
387 if not x:
390 return []
388 return []
391 if x[0] == 'list':
389 if x[0] == 'list':
392 return getlist(x[1]) + [x[2]]
390 return getlist(x[1]) + [x[2]]
393 return [x]
391 return [x]
394
392
395 def gettemplate(exp, context):
393 def gettemplate(exp, context):
396 """Compile given template tree or load named template from map file;
394 """Compile given template tree or load named template from map file;
397 returns (func, data) pair"""
395 returns (func, data) pair"""
398 if exp[0] in ('template', 'string'):
396 if exp[0] in ('template', 'string'):
399 return compileexp(exp, context, methods)
397 return compileexp(exp, context, methods)
400 if exp[0] == 'symbol':
398 if exp[0] == 'symbol':
401 # unlike runsymbol(), here 'symbol' is always taken as template name
399 # unlike runsymbol(), here 'symbol' is always taken as template name
402 # even if it exists in mapping. this allows us to override mapping
400 # even if it exists in mapping. this allows us to override mapping
403 # by web templates, e.g. 'changelogtag' is redefined in map file.
401 # by web templates, e.g. 'changelogtag' is redefined in map file.
404 return context._load(exp[1])
402 return context._load(exp[1])
405 raise error.ParseError(_("expected template specifier"))
403 raise error.ParseError(_("expected template specifier"))
406
404
407 def _runrecursivesymbol(context, mapping, key):
405 def _runrecursivesymbol(context, mapping, key):
408 raise error.Abort(_("recursive reference '%s' in template") % key)
406 raise error.Abort(_("recursive reference '%s' in template") % key)
409
407
410 def buildtemplate(exp, context):
408 def buildtemplate(exp, context):
411 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
409 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
412 return (templateutil.runtemplate, ctmpl)
410 return (templateutil.runtemplate, ctmpl)
413
411
414 def buildfilter(exp, context):
412 def buildfilter(exp, context):
415 n = getsymbol(exp[2])
413 n = getsymbol(exp[2])
416 if n in context._filters:
414 if n in context._filters:
417 filt = context._filters[n]
415 filt = context._filters[n]
418 arg = compileexp(exp[1], context, methods)
416 arg = compileexp(exp[1], context, methods)
419 return (templateutil.runfilter, (arg, filt))
417 return (templateutil.runfilter, (arg, filt))
420 if n in context._funcs:
418 if n in context._funcs:
421 f = context._funcs[n]
419 f = context._funcs[n]
422 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
420 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
423 return (f, args)
421 return (f, args)
424 raise error.ParseError(_("unknown function '%s'") % n)
422 raise error.ParseError(_("unknown function '%s'") % n)
425
423
426 def buildmap(exp, context):
424 def buildmap(exp, context):
427 darg = compileexp(exp[1], context, methods)
425 darg = compileexp(exp[1], context, methods)
428 targ = gettemplate(exp[2], context)
426 targ = gettemplate(exp[2], context)
429 return (templateutil.runmap, (darg, targ))
427 return (templateutil.runmap, (darg, targ))
430
428
431 def buildmember(exp, context):
429 def buildmember(exp, context):
432 darg = compileexp(exp[1], context, methods)
430 darg = compileexp(exp[1], context, methods)
433 memb = getsymbol(exp[2])
431 memb = getsymbol(exp[2])
434 return (templateutil.runmember, (darg, memb))
432 return (templateutil.runmember, (darg, memb))
435
433
436 def buildnegate(exp, context):
434 def buildnegate(exp, context):
437 arg = compileexp(exp[1], context, exprmethods)
435 arg = compileexp(exp[1], context, exprmethods)
438 return (templateutil.runnegate, arg)
436 return (templateutil.runnegate, arg)
439
437
440 def buildarithmetic(exp, context, func):
438 def buildarithmetic(exp, context, func):
441 left = compileexp(exp[1], context, exprmethods)
439 left = compileexp(exp[1], context, exprmethods)
442 right = compileexp(exp[2], context, exprmethods)
440 right = compileexp(exp[2], context, exprmethods)
443 return (templateutil.runarithmetic, (func, left, right))
441 return (templateutil.runarithmetic, (func, left, right))
444
442
445 def buildfunc(exp, context):
443 def buildfunc(exp, context):
446 n = getsymbol(exp[1])
444 n = getsymbol(exp[1])
447 if n in context._funcs:
445 if n in context._funcs:
448 f = context._funcs[n]
446 f = context._funcs[n]
449 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
447 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
450 return (f, args)
448 return (f, args)
451 if n in context._filters:
449 if n in context._filters:
452 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
450 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
453 if len(args) != 1:
451 if len(args) != 1:
454 raise error.ParseError(_("filter %s expects one argument") % n)
452 raise error.ParseError(_("filter %s expects one argument") % n)
455 f = context._filters[n]
453 f = context._filters[n]
456 return (templateutil.runfilter, (args[0], f))
454 return (templateutil.runfilter, (args[0], f))
457 raise error.ParseError(_("unknown function '%s'") % n)
455 raise error.ParseError(_("unknown function '%s'") % n)
458
456
459 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
457 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
460 """Compile parsed tree of function arguments into list or dict of
458 """Compile parsed tree of function arguments into list or dict of
461 (func, data) pairs
459 (func, data) pairs
462
460
463 >>> context = engine(lambda t: (templateutil.runsymbol, t))
461 >>> context = engine(lambda t: (templateutil.runsymbol, t))
464 >>> def fargs(expr, argspec):
462 >>> def fargs(expr, argspec):
465 ... x = _parseexpr(expr)
463 ... x = _parseexpr(expr)
466 ... n = getsymbol(x[1])
464 ... n = getsymbol(x[1])
467 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
465 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
468 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
466 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
469 ['l', 'k']
467 ['l', 'k']
470 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
468 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
471 >>> list(args.keys()), list(args[b'opts'].keys())
469 >>> list(args.keys()), list(args[b'opts'].keys())
472 (['opts'], ['opts', 'k'])
470 (['opts'], ['opts', 'k'])
473 """
471 """
474 def compiledict(xs):
472 def compiledict(xs):
475 return util.sortdict((k, compileexp(x, context, curmethods))
473 return util.sortdict((k, compileexp(x, context, curmethods))
476 for k, x in xs.iteritems())
474 for k, x in xs.iteritems())
477 def compilelist(xs):
475 def compilelist(xs):
478 return [compileexp(x, context, curmethods) for x in xs]
476 return [compileexp(x, context, curmethods) for x in xs]
479
477
480 if not argspec:
478 if not argspec:
481 # filter or function with no argspec: return list of positional args
479 # filter or function with no argspec: return list of positional args
482 return compilelist(getlist(exp))
480 return compilelist(getlist(exp))
483
481
484 # function with argspec: return dict of named args
482 # function with argspec: return dict of named args
485 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
483 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
486 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
484 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
487 keyvaluenode='keyvalue', keynode='symbol')
485 keyvaluenode='keyvalue', keynode='symbol')
488 compargs = util.sortdict()
486 compargs = util.sortdict()
489 if varkey:
487 if varkey:
490 compargs[varkey] = compilelist(treeargs.pop(varkey))
488 compargs[varkey] = compilelist(treeargs.pop(varkey))
491 if optkey:
489 if optkey:
492 compargs[optkey] = compiledict(treeargs.pop(optkey))
490 compargs[optkey] = compiledict(treeargs.pop(optkey))
493 compargs.update(compiledict(treeargs))
491 compargs.update(compiledict(treeargs))
494 return compargs
492 return compargs
495
493
496 def buildkeyvaluepair(exp, content):
494 def buildkeyvaluepair(exp, content):
497 raise error.ParseError(_("can't use a key-value pair in this context"))
495 raise error.ParseError(_("can't use a key-value pair in this context"))
498
496
497 def buildlist(exp, context):
498 raise error.ParseError(_("can't use a list in this context"),
499 hint=_('check place of comma and parens'))
500
499 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
501 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
500 exprmethods = {
502 exprmethods = {
501 "integer": lambda e, c: (templateutil.runinteger, e[1]),
503 "integer": lambda e, c: (templateutil.runinteger, e[1]),
502 "string": lambda e, c: (templateutil.runstring, e[1]),
504 "string": lambda e, c: (templateutil.runstring, e[1]),
503 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
505 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
504 "template": buildtemplate,
506 "template": buildtemplate,
505 "group": lambda e, c: compileexp(e[1], c, exprmethods),
507 "group": lambda e, c: compileexp(e[1], c, exprmethods),
506 ".": buildmember,
508 ".": buildmember,
507 "|": buildfilter,
509 "|": buildfilter,
508 "%": buildmap,
510 "%": buildmap,
509 "func": buildfunc,
511 "func": buildfunc,
510 "keyvalue": buildkeyvaluepair,
512 "keyvalue": buildkeyvaluepair,
513 "list": buildlist,
511 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
514 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
512 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
515 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
513 "negate": buildnegate,
516 "negate": buildnegate,
514 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
517 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
515 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
518 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
516 }
519 }
517
520
518 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
521 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
519 methods = exprmethods.copy()
522 methods = exprmethods.copy()
520 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
523 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
521
524
522 class _aliasrules(parser.basealiasrules):
525 class _aliasrules(parser.basealiasrules):
523 """Parsing and expansion rule set of template aliases"""
526 """Parsing and expansion rule set of template aliases"""
524 _section = _('template alias')
527 _section = _('template alias')
525 _parse = staticmethod(_parseexpr)
528 _parse = staticmethod(_parseexpr)
526
529
527 @staticmethod
530 @staticmethod
528 def _trygetfunc(tree):
531 def _trygetfunc(tree):
529 """Return (name, args) if tree is func(...) or ...|filter; otherwise
532 """Return (name, args) if tree is func(...) or ...|filter; otherwise
530 None"""
533 None"""
531 if tree[0] == 'func' and tree[1][0] == 'symbol':
534 if tree[0] == 'func' and tree[1][0] == 'symbol':
532 return tree[1][1], getlist(tree[2])
535 return tree[1][1], getlist(tree[2])
533 if tree[0] == '|' and tree[2][0] == 'symbol':
536 if tree[0] == '|' and tree[2][0] == 'symbol':
534 return tree[2][1], [tree[1]]
537 return tree[2][1], [tree[1]]
535
538
536 def expandaliases(tree, aliases):
539 def expandaliases(tree, aliases):
537 """Return new tree of aliases are expanded"""
540 """Return new tree of aliases are expanded"""
538 aliasmap = _aliasrules.buildmap(aliases)
541 aliasmap = _aliasrules.buildmap(aliases)
539 return _aliasrules.expand(aliasmap, tree)
542 return _aliasrules.expand(aliasmap, tree)
540
543
541 # template engine
544 # template engine
542
545
543 def unquotestring(s):
546 def unquotestring(s):
544 '''unwrap quotes if any; otherwise returns unmodified string'''
547 '''unwrap quotes if any; otherwise returns unmodified string'''
545 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
548 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
546 return s
549 return s
547 return s[1:-1]
550 return s[1:-1]
548
551
549 class resourcemapper(object):
552 class resourcemapper(object):
550 """Mapper of internal template resources"""
553 """Mapper of internal template resources"""
551
554
552 __metaclass__ = abc.ABCMeta
555 __metaclass__ = abc.ABCMeta
553
556
554 @abc.abstractmethod
557 @abc.abstractmethod
555 def availablekeys(self, mapping):
558 def availablekeys(self, mapping):
556 """Return a set of available resource keys based on the given mapping"""
559 """Return a set of available resource keys based on the given mapping"""
557
560
558 @abc.abstractmethod
561 @abc.abstractmethod
559 def knownkeys(self):
562 def knownkeys(self):
560 """Return a set of supported resource keys"""
563 """Return a set of supported resource keys"""
561
564
562 @abc.abstractmethod
565 @abc.abstractmethod
563 def lookup(self, mapping, key):
566 def lookup(self, mapping, key):
564 """Return a resource for the key if available; otherwise None"""
567 """Return a resource for the key if available; otherwise None"""
565
568
566 @abc.abstractmethod
569 @abc.abstractmethod
567 def populatemap(self, context, origmapping, newmapping):
570 def populatemap(self, context, origmapping, newmapping):
568 """Return a dict of additional mapping items which should be paired
571 """Return a dict of additional mapping items which should be paired
569 with the given new mapping"""
572 with the given new mapping"""
570
573
571 class nullresourcemapper(resourcemapper):
574 class nullresourcemapper(resourcemapper):
572 def availablekeys(self, mapping):
575 def availablekeys(self, mapping):
573 return set()
576 return set()
574
577
575 def knownkeys(self):
578 def knownkeys(self):
576 return set()
579 return set()
577
580
578 def lookup(self, mapping, key):
581 def lookup(self, mapping, key):
579 return None
582 return None
580
583
581 def populatemap(self, context, origmapping, newmapping):
584 def populatemap(self, context, origmapping, newmapping):
582 return {}
585 return {}
583
586
584 class engine(object):
587 class engine(object):
585 '''template expansion engine.
588 '''template expansion engine.
586
589
587 template expansion works like this. a map file contains key=value
590 template expansion works like this. a map file contains key=value
588 pairs. if value is quoted, it is treated as string. otherwise, it
591 pairs. if value is quoted, it is treated as string. otherwise, it
589 is treated as name of template file.
592 is treated as name of template file.
590
593
591 templater is asked to expand a key in map. it looks up key, and
594 templater is asked to expand a key in map. it looks up key, and
592 looks for strings like this: {foo}. it expands {foo} by looking up
595 looks for strings like this: {foo}. it expands {foo} by looking up
593 foo in map, and substituting it. expansion is recursive: it stops
596 foo in map, and substituting it. expansion is recursive: it stops
594 when there is no more {foo} to replace.
597 when there is no more {foo} to replace.
595
598
596 expansion also allows formatting and filtering.
599 expansion also allows formatting and filtering.
597
600
598 format uses key to expand each item in list. syntax is
601 format uses key to expand each item in list. syntax is
599 {key%format}.
602 {key%format}.
600
603
601 filter uses function to transform value. syntax is
604 filter uses function to transform value. syntax is
602 {key|filter1|filter2|...}.'''
605 {key|filter1|filter2|...}.'''
603
606
604 def __init__(self, loader, filters=None, defaults=None, resources=None):
607 def __init__(self, loader, filters=None, defaults=None, resources=None):
605 self._loader = loader
608 self._loader = loader
606 if filters is None:
609 if filters is None:
607 filters = {}
610 filters = {}
608 self._filters = filters
611 self._filters = filters
609 self._funcs = templatefuncs.funcs # make this a parameter if needed
612 self._funcs = templatefuncs.funcs # make this a parameter if needed
610 if defaults is None:
613 if defaults is None:
611 defaults = {}
614 defaults = {}
612 if resources is None:
615 if resources is None:
613 resources = nullresourcemapper()
616 resources = nullresourcemapper()
614 self._defaults = defaults
617 self._defaults = defaults
615 self._resources = resources
618 self._resources = resources
616 self._cache = {} # key: (func, data)
619 self._cache = {} # key: (func, data)
617 self._tmplcache = {} # literal template: (func, data)
620 self._tmplcache = {} # literal template: (func, data)
618
621
619 def overlaymap(self, origmapping, newmapping):
622 def overlaymap(self, origmapping, newmapping):
620 """Create combined mapping from the original mapping and partial
623 """Create combined mapping from the original mapping and partial
621 mapping to override the original"""
624 mapping to override the original"""
622 # do not copy symbols which overrides the defaults depending on
625 # do not copy symbols which overrides the defaults depending on
623 # new resources, so the defaults will be re-evaluated (issue5612)
626 # new resources, so the defaults will be re-evaluated (issue5612)
624 knownres = self._resources.knownkeys()
627 knownres = self._resources.knownkeys()
625 newres = self._resources.availablekeys(newmapping)
628 newres = self._resources.availablekeys(newmapping)
626 mapping = {k: v for k, v in origmapping.iteritems()
629 mapping = {k: v for k, v in origmapping.iteritems()
627 if (k in knownres # not a symbol per self.symbol()
630 if (k in knownres # not a symbol per self.symbol()
628 or newres.isdisjoint(self._defaultrequires(k)))}
631 or newres.isdisjoint(self._defaultrequires(k)))}
629 mapping.update(newmapping)
632 mapping.update(newmapping)
630 mapping.update(
633 mapping.update(
631 self._resources.populatemap(self, origmapping, newmapping))
634 self._resources.populatemap(self, origmapping, newmapping))
632 return mapping
635 return mapping
633
636
634 def _defaultrequires(self, key):
637 def _defaultrequires(self, key):
635 """Resource keys required by the specified default symbol function"""
638 """Resource keys required by the specified default symbol function"""
636 v = self._defaults.get(key)
639 v = self._defaults.get(key)
637 if v is None or not callable(v):
640 if v is None or not callable(v):
638 return ()
641 return ()
639 return getattr(v, '_requires', ())
642 return getattr(v, '_requires', ())
640
643
641 def symbol(self, mapping, key):
644 def symbol(self, mapping, key):
642 """Resolve symbol to value or function; None if nothing found"""
645 """Resolve symbol to value or function; None if nothing found"""
643 v = None
646 v = None
644 if key not in self._resources.knownkeys():
647 if key not in self._resources.knownkeys():
645 v = mapping.get(key)
648 v = mapping.get(key)
646 if v is None:
649 if v is None:
647 v = self._defaults.get(key)
650 v = self._defaults.get(key)
648 return v
651 return v
649
652
650 def availableresourcekeys(self, mapping):
653 def availableresourcekeys(self, mapping):
651 """Return a set of available resource keys based on the given mapping"""
654 """Return a set of available resource keys based on the given mapping"""
652 return self._resources.availablekeys(mapping)
655 return self._resources.availablekeys(mapping)
653
656
654 def knownresourcekeys(self):
657 def knownresourcekeys(self):
655 """Return a set of supported resource keys"""
658 """Return a set of supported resource keys"""
656 return self._resources.knownkeys()
659 return self._resources.knownkeys()
657
660
658 def resource(self, mapping, key):
661 def resource(self, mapping, key):
659 """Return internal data (e.g. cache) used for keyword/function
662 """Return internal data (e.g. cache) used for keyword/function
660 evaluation"""
663 evaluation"""
661 v = self._resources.lookup(mapping, key)
664 v = self._resources.lookup(mapping, key)
662 if v is None:
665 if v is None:
663 raise templateutil.ResourceUnavailable(
666 raise templateutil.ResourceUnavailable(
664 _('template resource not available: %s') % key)
667 _('template resource not available: %s') % key)
665 return v
668 return v
666
669
667 def _load(self, t):
670 def _load(self, t):
668 '''load, parse, and cache a template'''
671 '''load, parse, and cache a template'''
669 if t not in self._cache:
672 if t not in self._cache:
670 x = self._loader(t)
673 x = self._loader(t)
671 # put poison to cut recursion while compiling 't'
674 # put poison to cut recursion while compiling 't'
672 self._cache[t] = (_runrecursivesymbol, t)
675 self._cache[t] = (_runrecursivesymbol, t)
673 try:
676 try:
674 self._cache[t] = compileexp(x, self, methods)
677 self._cache[t] = compileexp(x, self, methods)
675 except: # re-raises
678 except: # re-raises
676 del self._cache[t]
679 del self._cache[t]
677 raise
680 raise
678 return self._cache[t]
681 return self._cache[t]
679
682
680 def _parse(self, tmpl):
683 def _parse(self, tmpl):
681 """Parse and cache a literal template"""
684 """Parse and cache a literal template"""
682 if tmpl not in self._tmplcache:
685 if tmpl not in self._tmplcache:
683 x = parse(tmpl)
686 x = parse(tmpl)
684 self._tmplcache[tmpl] = compileexp(x, self, methods)
687 self._tmplcache[tmpl] = compileexp(x, self, methods)
685 return self._tmplcache[tmpl]
688 return self._tmplcache[tmpl]
686
689
687 def preload(self, t):
690 def preload(self, t):
688 """Load, parse, and cache the specified template if available"""
691 """Load, parse, and cache the specified template if available"""
689 try:
692 try:
690 self._load(t)
693 self._load(t)
691 return True
694 return True
692 except templateutil.TemplateNotFound:
695 except templateutil.TemplateNotFound:
693 return False
696 return False
694
697
695 def process(self, t, mapping):
698 def process(self, t, mapping):
696 '''Perform expansion. t is name of map element to expand.
699 '''Perform expansion. t is name of map element to expand.
697 mapping contains added elements for use during expansion. Is a
700 mapping contains added elements for use during expansion. Is a
698 generator.'''
701 generator.'''
699 func, data = self._load(t)
702 func, data = self._load(t)
700 return self._expand(func, data, mapping)
703 return self._expand(func, data, mapping)
701
704
702 def expand(self, tmpl, mapping):
705 def expand(self, tmpl, mapping):
703 """Perform expansion over a literal template
706 """Perform expansion over a literal template
704
707
705 No user aliases will be expanded since this is supposed to be called
708 No user aliases will be expanded since this is supposed to be called
706 with an internal template string.
709 with an internal template string.
707 """
710 """
708 func, data = self._parse(tmpl)
711 func, data = self._parse(tmpl)
709 return self._expand(func, data, mapping)
712 return self._expand(func, data, mapping)
710
713
711 def _expand(self, func, data, mapping):
714 def _expand(self, func, data, mapping):
712 # populate additional items only if they don't exist in the given
715 # populate additional items only if they don't exist in the given
713 # mapping. this is slightly different from overlaymap() because the
716 # mapping. this is slightly different from overlaymap() because the
714 # initial 'revcache' may contain pre-computed items.
717 # initial 'revcache' may contain pre-computed items.
715 extramapping = self._resources.populatemap(self, {}, mapping)
718 extramapping = self._resources.populatemap(self, {}, mapping)
716 if extramapping:
719 if extramapping:
717 extramapping.update(mapping)
720 extramapping.update(mapping)
718 mapping = extramapping
721 mapping = extramapping
719 return templateutil.flatten(self, mapping, func(self, mapping, data))
722 return templateutil.flatten(self, mapping, func(self, mapping, data))
720
723
721 def stylelist():
724 def stylelist():
722 paths = templatepaths()
725 paths = templatepaths()
723 if not paths:
726 if not paths:
724 return _('no templates found, try `hg debuginstall` for more info')
727 return _('no templates found, try `hg debuginstall` for more info')
725 dirlist = os.listdir(paths[0])
728 dirlist = os.listdir(paths[0])
726 stylelist = []
729 stylelist = []
727 for file in dirlist:
730 for file in dirlist:
728 split = file.split(".")
731 split = file.split(".")
729 if split[-1] in ('orig', 'rej'):
732 if split[-1] in ('orig', 'rej'):
730 continue
733 continue
731 if split[0] == "map-cmdline":
734 if split[0] == "map-cmdline":
732 stylelist.append(split[1])
735 stylelist.append(split[1])
733 return ", ".join(sorted(stylelist))
736 return ", ".join(sorted(stylelist))
734
737
735 def _readmapfile(mapfile):
738 def _readmapfile(mapfile):
736 """Load template elements from the given map file"""
739 """Load template elements from the given map file"""
737 if not os.path.exists(mapfile):
740 if not os.path.exists(mapfile):
738 raise error.Abort(_("style '%s' not found") % mapfile,
741 raise error.Abort(_("style '%s' not found") % mapfile,
739 hint=_("available styles: %s") % stylelist())
742 hint=_("available styles: %s") % stylelist())
740
743
741 base = os.path.dirname(mapfile)
744 base = os.path.dirname(mapfile)
742 conf = config.config(includepaths=templatepaths())
745 conf = config.config(includepaths=templatepaths())
743 conf.read(mapfile, remap={'': 'templates'})
746 conf.read(mapfile, remap={'': 'templates'})
744
747
745 cache = {}
748 cache = {}
746 tmap = {}
749 tmap = {}
747 aliases = []
750 aliases = []
748
751
749 val = conf.get('templates', '__base__')
752 val = conf.get('templates', '__base__')
750 if val and val[0] not in "'\"":
753 if val and val[0] not in "'\"":
751 # treat as a pointer to a base class for this style
754 # treat as a pointer to a base class for this style
752 path = util.normpath(os.path.join(base, val))
755 path = util.normpath(os.path.join(base, val))
753
756
754 # fallback check in template paths
757 # fallback check in template paths
755 if not os.path.exists(path):
758 if not os.path.exists(path):
756 for p in templatepaths():
759 for p in templatepaths():
757 p2 = util.normpath(os.path.join(p, val))
760 p2 = util.normpath(os.path.join(p, val))
758 if os.path.isfile(p2):
761 if os.path.isfile(p2):
759 path = p2
762 path = p2
760 break
763 break
761 p3 = util.normpath(os.path.join(p2, "map"))
764 p3 = util.normpath(os.path.join(p2, "map"))
762 if os.path.isfile(p3):
765 if os.path.isfile(p3):
763 path = p3
766 path = p3
764 break
767 break
765
768
766 cache, tmap, aliases = _readmapfile(path)
769 cache, tmap, aliases = _readmapfile(path)
767
770
768 for key, val in conf['templates'].items():
771 for key, val in conf['templates'].items():
769 if not val:
772 if not val:
770 raise error.ParseError(_('missing value'),
773 raise error.ParseError(_('missing value'),
771 conf.source('templates', key))
774 conf.source('templates', key))
772 if val[0] in "'\"":
775 if val[0] in "'\"":
773 if val[0] != val[-1]:
776 if val[0] != val[-1]:
774 raise error.ParseError(_('unmatched quotes'),
777 raise error.ParseError(_('unmatched quotes'),
775 conf.source('templates', key))
778 conf.source('templates', key))
776 cache[key] = unquotestring(val)
779 cache[key] = unquotestring(val)
777 elif key != '__base__':
780 elif key != '__base__':
778 tmap[key] = os.path.join(base, val)
781 tmap[key] = os.path.join(base, val)
779 aliases.extend(conf['templatealias'].items())
782 aliases.extend(conf['templatealias'].items())
780 return cache, tmap, aliases
783 return cache, tmap, aliases
781
784
782 class loader(object):
785 class loader(object):
783 """Load template fragments optionally from a map file"""
786 """Load template fragments optionally from a map file"""
784
787
785 def __init__(self, cache, aliases):
788 def __init__(self, cache, aliases):
786 if cache is None:
789 if cache is None:
787 cache = {}
790 cache = {}
788 self.cache = cache.copy()
791 self.cache = cache.copy()
789 self._map = {}
792 self._map = {}
790 self._aliasmap = _aliasrules.buildmap(aliases)
793 self._aliasmap = _aliasrules.buildmap(aliases)
791
794
792 def __contains__(self, key):
795 def __contains__(self, key):
793 return key in self.cache or key in self._map
796 return key in self.cache or key in self._map
794
797
795 def load(self, t):
798 def load(self, t):
796 """Get parsed tree for the given template name. Use a local cache."""
799 """Get parsed tree for the given template name. Use a local cache."""
797 if t not in self.cache:
800 if t not in self.cache:
798 try:
801 try:
799 self.cache[t] = util.readfile(self._map[t])
802 self.cache[t] = util.readfile(self._map[t])
800 except KeyError as inst:
803 except KeyError as inst:
801 raise templateutil.TemplateNotFound(
804 raise templateutil.TemplateNotFound(
802 _('"%s" not in template map') % inst.args[0])
805 _('"%s" not in template map') % inst.args[0])
803 except IOError as inst:
806 except IOError as inst:
804 reason = (_('template file %s: %s')
807 reason = (_('template file %s: %s')
805 % (self._map[t],
808 % (self._map[t],
806 stringutil.forcebytestr(inst.args[1])))
809 stringutil.forcebytestr(inst.args[1])))
807 raise IOError(inst.args[0], encoding.strfromlocal(reason))
810 raise IOError(inst.args[0], encoding.strfromlocal(reason))
808 return self._parse(self.cache[t])
811 return self._parse(self.cache[t])
809
812
810 def _parse(self, tmpl):
813 def _parse(self, tmpl):
811 x = parse(tmpl)
814 x = parse(tmpl)
812 if self._aliasmap:
815 if self._aliasmap:
813 x = _aliasrules.expand(self._aliasmap, x)
816 x = _aliasrules.expand(self._aliasmap, x)
814 return x
817 return x
815
818
816 def _findsymbolsused(self, tree, syms):
819 def _findsymbolsused(self, tree, syms):
817 if not tree:
820 if not tree:
818 return
821 return
819 op = tree[0]
822 op = tree[0]
820 if op == 'symbol':
823 if op == 'symbol':
821 s = tree[1]
824 s = tree[1]
822 if s in syms[0]:
825 if s in syms[0]:
823 return # avoid recursion: s -> cache[s] -> s
826 return # avoid recursion: s -> cache[s] -> s
824 syms[0].add(s)
827 syms[0].add(s)
825 if s in self.cache or s in self._map:
828 if s in self.cache or s in self._map:
826 # s may be a reference for named template
829 # s may be a reference for named template
827 self._findsymbolsused(self.load(s), syms)
830 self._findsymbolsused(self.load(s), syms)
828 return
831 return
829 if op in {'integer', 'string'}:
832 if op in {'integer', 'string'}:
830 return
833 return
831 # '{arg|func}' == '{func(arg)}'
834 # '{arg|func}' == '{func(arg)}'
832 if op == '|':
835 if op == '|':
833 syms[1].add(getsymbol(tree[2]))
836 syms[1].add(getsymbol(tree[2]))
834 self._findsymbolsused(tree[1], syms)
837 self._findsymbolsused(tree[1], syms)
835 return
838 return
836 if op == 'func':
839 if op == 'func':
837 syms[1].add(getsymbol(tree[1]))
840 syms[1].add(getsymbol(tree[1]))
838 self._findsymbolsused(tree[2], syms)
841 self._findsymbolsused(tree[2], syms)
839 return
842 return
840 for x in tree[1:]:
843 for x in tree[1:]:
841 self._findsymbolsused(x, syms)
844 self._findsymbolsused(x, syms)
842
845
843 def symbolsused(self, t):
846 def symbolsused(self, t):
844 """Look up (keywords, filters/functions) referenced from the name
847 """Look up (keywords, filters/functions) referenced from the name
845 template 't'
848 template 't'
846
849
847 This may load additional templates from the map file.
850 This may load additional templates from the map file.
848 """
851 """
849 syms = (set(), set())
852 syms = (set(), set())
850 self._findsymbolsused(self.load(t), syms)
853 self._findsymbolsused(self.load(t), syms)
851 return syms
854 return syms
852
855
853 class templater(object):
856 class templater(object):
854
857
855 def __init__(self, filters=None, defaults=None, resources=None,
858 def __init__(self, filters=None, defaults=None, resources=None,
856 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
859 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
857 """Create template engine optionally with preloaded template fragments
860 """Create template engine optionally with preloaded template fragments
858
861
859 - ``filters``: a dict of functions to transform a value into another.
862 - ``filters``: a dict of functions to transform a value into another.
860 - ``defaults``: a dict of symbol values/functions; may be overridden
863 - ``defaults``: a dict of symbol values/functions; may be overridden
861 by a ``mapping`` dict.
864 by a ``mapping`` dict.
862 - ``resources``: a resourcemapper object to look up internal data
865 - ``resources``: a resourcemapper object to look up internal data
863 (e.g. cache), inaccessible from user template.
866 (e.g. cache), inaccessible from user template.
864 - ``cache``: a dict of preloaded template fragments.
867 - ``cache``: a dict of preloaded template fragments.
865 - ``aliases``: a list of alias (name, replacement) pairs.
868 - ``aliases``: a list of alias (name, replacement) pairs.
866
869
867 self.cache may be updated later to register additional template
870 self.cache may be updated later to register additional template
868 fragments.
871 fragments.
869 """
872 """
870 allfilters = templatefilters.filters.copy()
873 allfilters = templatefilters.filters.copy()
871 if filters:
874 if filters:
872 allfilters.update(filters)
875 allfilters.update(filters)
873 self._loader = loader(cache, aliases)
876 self._loader = loader(cache, aliases)
874 self._proc = engine(self._loader.load, allfilters, defaults, resources)
877 self._proc = engine(self._loader.load, allfilters, defaults, resources)
875 self._minchunk, self._maxchunk = minchunk, maxchunk
878 self._minchunk, self._maxchunk = minchunk, maxchunk
876
879
877 @classmethod
880 @classmethod
878 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
881 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
879 cache=None, minchunk=1024, maxchunk=65536):
882 cache=None, minchunk=1024, maxchunk=65536):
880 """Create templater from the specified map file"""
883 """Create templater from the specified map file"""
881 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
884 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
882 cache, tmap, aliases = _readmapfile(mapfile)
885 cache, tmap, aliases = _readmapfile(mapfile)
883 t._loader.cache.update(cache)
886 t._loader.cache.update(cache)
884 t._loader._map = tmap
887 t._loader._map = tmap
885 t._loader._aliasmap = _aliasrules.buildmap(aliases)
888 t._loader._aliasmap = _aliasrules.buildmap(aliases)
886 return t
889 return t
887
890
888 def __contains__(self, key):
891 def __contains__(self, key):
889 return key in self._loader
892 return key in self._loader
890
893
891 @property
894 @property
892 def cache(self):
895 def cache(self):
893 return self._loader.cache
896 return self._loader.cache
894
897
895 # for highlight extension to insert one-time 'colorize' filter
898 # for highlight extension to insert one-time 'colorize' filter
896 @property
899 @property
897 def _filters(self):
900 def _filters(self):
898 return self._proc._filters
901 return self._proc._filters
899
902
900 @property
903 @property
901 def defaults(self):
904 def defaults(self):
902 return self._proc._defaults
905 return self._proc._defaults
903
906
904 def load(self, t):
907 def load(self, t):
905 """Get parsed tree for the given template name. Use a local cache."""
908 """Get parsed tree for the given template name. Use a local cache."""
906 return self._loader.load(t)
909 return self._loader.load(t)
907
910
908 def symbolsuseddefault(self):
911 def symbolsuseddefault(self):
909 """Look up (keywords, filters/functions) referenced from the default
912 """Look up (keywords, filters/functions) referenced from the default
910 unnamed template
913 unnamed template
911
914
912 This may load additional templates from the map file.
915 This may load additional templates from the map file.
913 """
916 """
914 return self.symbolsused('')
917 return self.symbolsused('')
915
918
916 def symbolsused(self, t):
919 def symbolsused(self, t):
917 """Look up (keywords, filters/functions) referenced from the name
920 """Look up (keywords, filters/functions) referenced from the name
918 template 't'
921 template 't'
919
922
920 This may load additional templates from the map file.
923 This may load additional templates from the map file.
921 """
924 """
922 return self._loader.symbolsused(t)
925 return self._loader.symbolsused(t)
923
926
924 def renderdefault(self, mapping):
927 def renderdefault(self, mapping):
925 """Render the default unnamed template and return result as string"""
928 """Render the default unnamed template and return result as string"""
926 return self.render('', mapping)
929 return self.render('', mapping)
927
930
928 def render(self, t, mapping):
931 def render(self, t, mapping):
929 """Render the specified named template and return result as string"""
932 """Render the specified named template and return result as string"""
930 return b''.join(self.generate(t, mapping))
933 return b''.join(self.generate(t, mapping))
931
934
932 def generate(self, t, mapping):
935 def generate(self, t, mapping):
933 """Return a generator that renders the specified named template and
936 """Return a generator that renders the specified named template and
934 yields chunks"""
937 yields chunks"""
935 stream = self._proc.process(t, mapping)
938 stream = self._proc.process(t, mapping)
936 if self._minchunk:
939 if self._minchunk:
937 stream = util.increasingchunks(stream, min=self._minchunk,
940 stream = util.increasingchunks(stream, min=self._minchunk,
938 max=self._maxchunk)
941 max=self._maxchunk)
939 return stream
942 return stream
940
943
941 def templatepaths():
944 def templatepaths():
942 '''return locations used for template files.'''
945 '''return locations used for template files.'''
943 pathsrel = ['templates']
946 pathsrel = ['templates']
944 paths = [os.path.normpath(os.path.join(util.datapath, f))
947 paths = [os.path.normpath(os.path.join(util.datapath, f))
945 for f in pathsrel]
948 for f in pathsrel]
946 return [p for p in paths if os.path.isdir(p)]
949 return [p for p in paths if os.path.isdir(p)]
947
950
948 def templatepath(name):
951 def templatepath(name):
949 '''return location of template file. returns None if not found.'''
952 '''return location of template file. returns None if not found.'''
950 for p in templatepaths():
953 for p in templatepaths():
951 f = os.path.join(p, name)
954 f = os.path.join(p, name)
952 if os.path.exists(f):
955 if os.path.exists(f):
953 return f
956 return f
954 return None
957 return None
955
958
956 def stylemap(styles, paths=None):
959 def stylemap(styles, paths=None):
957 """Return path to mapfile for a given style.
960 """Return path to mapfile for a given style.
958
961
959 Searches mapfile in the following locations:
962 Searches mapfile in the following locations:
960 1. templatepath/style/map
963 1. templatepath/style/map
961 2. templatepath/map-style
964 2. templatepath/map-style
962 3. templatepath/map
965 3. templatepath/map
963 """
966 """
964
967
965 if paths is None:
968 if paths is None:
966 paths = templatepaths()
969 paths = templatepaths()
967 elif isinstance(paths, bytes):
970 elif isinstance(paths, bytes):
968 paths = [paths]
971 paths = [paths]
969
972
970 if isinstance(styles, bytes):
973 if isinstance(styles, bytes):
971 styles = [styles]
974 styles = [styles]
972
975
973 for style in styles:
976 for style in styles:
974 # only plain name is allowed to honor template paths
977 # only plain name is allowed to honor template paths
975 if (not style
978 if (not style
976 or style in (pycompat.oscurdir, pycompat.ospardir)
979 or style in (pycompat.oscurdir, pycompat.ospardir)
977 or pycompat.ossep in style
980 or pycompat.ossep in style
978 or pycompat.osaltsep and pycompat.osaltsep in style):
981 or pycompat.osaltsep and pycompat.osaltsep in style):
979 continue
982 continue
980 locations = [os.path.join(style, 'map'), 'map-' + style]
983 locations = [os.path.join(style, 'map'), 'map-' + style]
981 locations.append('map')
984 locations.append('map')
982
985
983 for path in paths:
986 for path in paths:
984 for location in locations:
987 for location in locations:
985 mapfile = os.path.join(path, location)
988 mapfile = os.path.join(path, location)
986 if os.path.isfile(mapfile):
989 if os.path.isfile(mapfile):
987 return style, mapfile
990 return style, mapfile
988
991
989 raise RuntimeError("No hgweb templates found in %r" % paths)
992 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,1088 +1,1089 b''
1 Test template syntax and basic functionality
1 Test template syntax and basic functionality
2 ============================================
2 ============================================
3
3
4 $ hg init a
4 $ hg init a
5 $ cd a
5 $ cd a
6 $ echo a > a
6 $ echo a > a
7 $ hg add a
7 $ hg add a
8 $ echo line 1 > b
8 $ echo line 1 > b
9 $ echo line 2 >> b
9 $ echo line 2 >> b
10 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
10 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
11
11
12 $ hg add b
12 $ hg add b
13 $ echo other 1 > c
13 $ echo other 1 > c
14 $ echo other 2 >> c
14 $ echo other 2 >> c
15 $ echo >> c
15 $ echo >> c
16 $ echo other 3 >> c
16 $ echo other 3 >> c
17 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
17 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
18
18
19 $ hg add c
19 $ hg add c
20 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
20 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
21 $ echo c >> c
21 $ echo c >> c
22 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
22 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
23
23
24 $ echo foo > .hg/branch
24 $ echo foo > .hg/branch
25 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
25 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
26
26
27 $ hg co -q 3
27 $ hg co -q 3
28 $ echo other 4 >> d
28 $ echo other 4 >> d
29 $ hg add d
29 $ hg add d
30 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
30 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
31
31
32 $ hg merge -q foo
32 $ hg merge -q foo
33 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
33 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
34
34
35 Test arithmetic operators have the right precedence:
35 Test arithmetic operators have the right precedence:
36
36
37 $ hg log -l 1 -T '{date(date, "%Y") + 5 * 10} {date(date, "%Y") - 2 * 3}\n'
37 $ hg log -l 1 -T '{date(date, "%Y") + 5 * 10} {date(date, "%Y") - 2 * 3}\n'
38 2020 1964
38 2020 1964
39 $ hg log -l 1 -T '{date(date, "%Y") * 5 + 10} {date(date, "%Y") * 3 - 2}\n'
39 $ hg log -l 1 -T '{date(date, "%Y") * 5 + 10} {date(date, "%Y") * 3 - 2}\n'
40 9860 5908
40 9860 5908
41
41
42 Test division:
42 Test division:
43
43
44 $ hg debugtemplate -r0 -v '{5 / 2} {mod(5, 2)}\n'
44 $ hg debugtemplate -r0 -v '{5 / 2} {mod(5, 2)}\n'
45 (template
45 (template
46 (/
46 (/
47 (integer '5')
47 (integer '5')
48 (integer '2'))
48 (integer '2'))
49 (string ' ')
49 (string ' ')
50 (func
50 (func
51 (symbol 'mod')
51 (symbol 'mod')
52 (list
52 (list
53 (integer '5')
53 (integer '5')
54 (integer '2')))
54 (integer '2')))
55 (string '\n'))
55 (string '\n'))
56 * keywords:
56 * keywords:
57 * functions: mod
57 * functions: mod
58 2 1
58 2 1
59 $ hg debugtemplate -r0 -v '{5 / -2} {mod(5, -2)}\n'
59 $ hg debugtemplate -r0 -v '{5 / -2} {mod(5, -2)}\n'
60 (template
60 (template
61 (/
61 (/
62 (integer '5')
62 (integer '5')
63 (negate
63 (negate
64 (integer '2')))
64 (integer '2')))
65 (string ' ')
65 (string ' ')
66 (func
66 (func
67 (symbol 'mod')
67 (symbol 'mod')
68 (list
68 (list
69 (integer '5')
69 (integer '5')
70 (negate
70 (negate
71 (integer '2'))))
71 (integer '2'))))
72 (string '\n'))
72 (string '\n'))
73 * keywords:
73 * keywords:
74 * functions: mod
74 * functions: mod
75 -3 -1
75 -3 -1
76 $ hg debugtemplate -r0 -v '{-5 / 2} {mod(-5, 2)}\n'
76 $ hg debugtemplate -r0 -v '{-5 / 2} {mod(-5, 2)}\n'
77 (template
77 (template
78 (/
78 (/
79 (negate
79 (negate
80 (integer '5'))
80 (integer '5'))
81 (integer '2'))
81 (integer '2'))
82 (string ' ')
82 (string ' ')
83 (func
83 (func
84 (symbol 'mod')
84 (symbol 'mod')
85 (list
85 (list
86 (negate
86 (negate
87 (integer '5'))
87 (integer '5'))
88 (integer '2')))
88 (integer '2')))
89 (string '\n'))
89 (string '\n'))
90 * keywords:
90 * keywords:
91 * functions: mod
91 * functions: mod
92 -3 1
92 -3 1
93 $ hg debugtemplate -r0 -v '{-5 / -2} {mod(-5, -2)}\n'
93 $ hg debugtemplate -r0 -v '{-5 / -2} {mod(-5, -2)}\n'
94 (template
94 (template
95 (/
95 (/
96 (negate
96 (negate
97 (integer '5'))
97 (integer '5'))
98 (negate
98 (negate
99 (integer '2')))
99 (integer '2')))
100 (string ' ')
100 (string ' ')
101 (func
101 (func
102 (symbol 'mod')
102 (symbol 'mod')
103 (list
103 (list
104 (negate
104 (negate
105 (integer '5'))
105 (integer '5'))
106 (negate
106 (negate
107 (integer '2'))))
107 (integer '2'))))
108 (string '\n'))
108 (string '\n'))
109 * keywords:
109 * keywords:
110 * functions: mod
110 * functions: mod
111 2 -1
111 2 -1
112
112
113 Filters bind closer than arithmetic:
113 Filters bind closer than arithmetic:
114
114
115 $ hg debugtemplate -r0 -v '{revset(".")|count - 1}\n'
115 $ hg debugtemplate -r0 -v '{revset(".")|count - 1}\n'
116 (template
116 (template
117 (-
117 (-
118 (|
118 (|
119 (func
119 (func
120 (symbol 'revset')
120 (symbol 'revset')
121 (string '.'))
121 (string '.'))
122 (symbol 'count'))
122 (symbol 'count'))
123 (integer '1'))
123 (integer '1'))
124 (string '\n'))
124 (string '\n'))
125 * keywords:
125 * keywords:
126 * functions: count, revset
126 * functions: count, revset
127 0
127 0
128
128
129 But negate binds closer still:
129 But negate binds closer still:
130
130
131 $ hg debugtemplate -r0 -v '{1-3|stringify}\n'
131 $ hg debugtemplate -r0 -v '{1-3|stringify}\n'
132 (template
132 (template
133 (-
133 (-
134 (integer '1')
134 (integer '1')
135 (|
135 (|
136 (integer '3')
136 (integer '3')
137 (symbol 'stringify')))
137 (symbol 'stringify')))
138 (string '\n'))
138 (string '\n'))
139 * keywords:
139 * keywords:
140 * functions: stringify
140 * functions: stringify
141 hg: parse error: arithmetic only defined on integers
141 hg: parse error: arithmetic only defined on integers
142 [255]
142 [255]
143 $ hg debugtemplate -r0 -v '{-3|stringify}\n'
143 $ hg debugtemplate -r0 -v '{-3|stringify}\n'
144 (template
144 (template
145 (|
145 (|
146 (negate
146 (negate
147 (integer '3'))
147 (integer '3'))
148 (symbol 'stringify'))
148 (symbol 'stringify'))
149 (string '\n'))
149 (string '\n'))
150 * keywords:
150 * keywords:
151 * functions: stringify
151 * functions: stringify
152 -3
152 -3
153
153
154 Filters bind as close as map operator:
154 Filters bind as close as map operator:
155
155
156 $ hg debugtemplate -r0 -v '{desc|splitlines % "{line}\n"}'
156 $ hg debugtemplate -r0 -v '{desc|splitlines % "{line}\n"}'
157 (template
157 (template
158 (%
158 (%
159 (|
159 (|
160 (symbol 'desc')
160 (symbol 'desc')
161 (symbol 'splitlines'))
161 (symbol 'splitlines'))
162 (template
162 (template
163 (symbol 'line')
163 (symbol 'line')
164 (string '\n'))))
164 (string '\n'))))
165 * keywords: desc, line
165 * keywords: desc, line
166 * functions: splitlines
166 * functions: splitlines
167 line 1
167 line 1
168 line 2
168 line 2
169
169
170 Keyword arguments:
170 Keyword arguments:
171
171
172 $ hg debugtemplate -r0 -v '{foo=bar|baz}'
172 $ hg debugtemplate -r0 -v '{foo=bar|baz}'
173 (template
173 (template
174 (keyvalue
174 (keyvalue
175 (symbol 'foo')
175 (symbol 'foo')
176 (|
176 (|
177 (symbol 'bar')
177 (symbol 'bar')
178 (symbol 'baz'))))
178 (symbol 'baz'))))
179 * keywords: bar, foo
179 * keywords: bar, foo
180 * functions: baz
180 * functions: baz
181 hg: parse error: can't use a key-value pair in this context
181 hg: parse error: can't use a key-value pair in this context
182 [255]
182 [255]
183
183
184 $ hg debugtemplate '{pad("foo", width=10, left=true)}\n'
184 $ hg debugtemplate '{pad("foo", width=10, left=true)}\n'
185 foo
185 foo
186
186
187 Call function which takes named arguments by filter syntax:
187 Call function which takes named arguments by filter syntax:
188
188
189 $ hg debugtemplate '{" "|separate}'
189 $ hg debugtemplate '{" "|separate}'
190 $ hg debugtemplate '{("not", "an", "argument", "list")|separate}'
190 $ hg debugtemplate '{("not", "an", "argument", "list")|separate}'
191 hg: parse error: unknown method 'list'
191 hg: parse error: can't use a list in this context
192 (check place of comma and parens)
192 [255]
193 [255]
193
194
194 Second branch starting at nullrev:
195 Second branch starting at nullrev:
195
196
196 $ hg update null
197 $ hg update null
197 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
198 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
198 $ echo second > second
199 $ echo second > second
199 $ hg add second
200 $ hg add second
200 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
201 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
201 created new head
202 created new head
202
203
203 $ echo third > third
204 $ echo third > third
204 $ hg add third
205 $ hg add third
205 $ hg mv second fourth
206 $ hg mv second fourth
206 $ hg commit -m third -d "2020-01-01 10:01"
207 $ hg commit -m third -d "2020-01-01 10:01"
207
208
208 $ hg log --template '{join(file_copies, ",\n")}\n' -r .
209 $ hg log --template '{join(file_copies, ",\n")}\n' -r .
209 fourth (second)
210 fourth (second)
210 $ hg log -T '{file_copies % "{source} -> {name}\n"}' -r .
211 $ hg log -T '{file_copies % "{source} -> {name}\n"}' -r .
211 second -> fourth
212 second -> fourth
212 $ hg log -T '{rev} {ifcontains("fourth", file_copies, "t", "f")}\n' -r .:7
213 $ hg log -T '{rev} {ifcontains("fourth", file_copies, "t", "f")}\n' -r .:7
213 8 t
214 8 t
214 7 f
215 7 f
215
216
216 Internal resources shouldn't be exposed (issue5699):
217 Internal resources shouldn't be exposed (issue5699):
217
218
218 $ hg log -r. -T '{cache}{ctx}{repo}{revcache}{templ}{ui}'
219 $ hg log -r. -T '{cache}{ctx}{repo}{revcache}{templ}{ui}'
219
220
220 Never crash on internal resource not available:
221 Never crash on internal resource not available:
221
222
222 $ hg --cwd .. debugtemplate '{"c0bebeef"|shortest}\n'
223 $ hg --cwd .. debugtemplate '{"c0bebeef"|shortest}\n'
223 abort: template resource not available: repo
224 abort: template resource not available: repo
224 [255]
225 [255]
225
226
226 $ hg config -T '{author}'
227 $ hg config -T '{author}'
227
228
228 Quoting for ui.logtemplate
229 Quoting for ui.logtemplate
229
230
230 $ hg tip --config "ui.logtemplate={rev}\n"
231 $ hg tip --config "ui.logtemplate={rev}\n"
231 8
232 8
232 $ hg tip --config "ui.logtemplate='{rev}\n'"
233 $ hg tip --config "ui.logtemplate='{rev}\n'"
233 8
234 8
234 $ hg tip --config 'ui.logtemplate="{rev}\n"'
235 $ hg tip --config 'ui.logtemplate="{rev}\n"'
235 8
236 8
236 $ hg tip --config 'ui.logtemplate=n{rev}\n'
237 $ hg tip --config 'ui.logtemplate=n{rev}\n'
237 n8
238 n8
238
239
239 Check that recursive reference does not fall into RuntimeError (issue4758):
240 Check that recursive reference does not fall into RuntimeError (issue4758):
240
241
241 common mistake:
242 common mistake:
242
243
243 $ cat << EOF > issue4758
244 $ cat << EOF > issue4758
244 > changeset = '{changeset}\n'
245 > changeset = '{changeset}\n'
245 > EOF
246 > EOF
246 $ hg log --style ./issue4758
247 $ hg log --style ./issue4758
247 abort: recursive reference 'changeset' in template
248 abort: recursive reference 'changeset' in template
248 [255]
249 [255]
249
250
250 circular reference:
251 circular reference:
251
252
252 $ cat << EOF > issue4758
253 $ cat << EOF > issue4758
253 > changeset = '{foo}'
254 > changeset = '{foo}'
254 > foo = '{changeset}'
255 > foo = '{changeset}'
255 > EOF
256 > EOF
256 $ hg log --style ./issue4758
257 $ hg log --style ./issue4758
257 abort: recursive reference 'foo' in template
258 abort: recursive reference 'foo' in template
258 [255]
259 [255]
259
260
260 buildmap() -> gettemplate(), where no thunk was made:
261 buildmap() -> gettemplate(), where no thunk was made:
261
262
262 $ cat << EOF > issue4758
263 $ cat << EOF > issue4758
263 > changeset = '{files % changeset}\n'
264 > changeset = '{files % changeset}\n'
264 > EOF
265 > EOF
265 $ hg log --style ./issue4758
266 $ hg log --style ./issue4758
266 abort: recursive reference 'changeset' in template
267 abort: recursive reference 'changeset' in template
267 [255]
268 [255]
268
269
269 not a recursion if a keyword of the same name exists:
270 not a recursion if a keyword of the same name exists:
270
271
271 $ cat << EOF > issue4758
272 $ cat << EOF > issue4758
272 > changeset = '{tags % rev}'
273 > changeset = '{tags % rev}'
273 > rev = '{rev} {tag}\n'
274 > rev = '{rev} {tag}\n'
274 > EOF
275 > EOF
275 $ hg log --style ./issue4758 -r tip
276 $ hg log --style ./issue4758 -r tip
276 8 tip
277 8 tip
277
278
278 Set up phase:
279 Set up phase:
279
280
280 $ hg phase -r 5 --public
281 $ hg phase -r 5 --public
281 $ hg phase -r 7 --secret --force
282 $ hg phase -r 7 --secret --force
282
283
283 Add a dummy commit to make up for the instability of the above:
284 Add a dummy commit to make up for the instability of the above:
284
285
285 $ echo a > a
286 $ echo a > a
286 $ hg add a
287 $ hg add a
287 $ hg ci -m future
288 $ hg ci -m future
288
289
289 Add a commit that does all possible modifications at once
290 Add a commit that does all possible modifications at once
290
291
291 $ echo modify >> third
292 $ echo modify >> third
292 $ touch b
293 $ touch b
293 $ hg add b
294 $ hg add b
294 $ hg mv fourth fifth
295 $ hg mv fourth fifth
295 $ hg rm a
296 $ hg rm a
296 $ hg ci -m "Modify, add, remove, rename"
297 $ hg ci -m "Modify, add, remove, rename"
297
298
298 Error on syntax:
299 Error on syntax:
299
300
300 $ cat <<EOF > t
301 $ cat <<EOF > t
301 > changeset = '{c}'
302 > changeset = '{c}'
302 > c = q
303 > c = q
303 > x = "f
304 > x = "f
304 > EOF
305 > EOF
305 $ echo '[ui]' > .hg/hgrc
306 $ echo '[ui]' > .hg/hgrc
306 $ echo 'style = t' >> .hg/hgrc
307 $ echo 'style = t' >> .hg/hgrc
307 $ hg log
308 $ hg log
308 hg: parse error at t:3: unmatched quotes
309 hg: parse error at t:3: unmatched quotes
309 [255]
310 [255]
310
311
311 $ hg log -T '{date'
312 $ hg log -T '{date'
312 hg: parse error at 1: unterminated template expansion
313 hg: parse error at 1: unterminated template expansion
313 ({date
314 ({date
314 ^ here)
315 ^ here)
315 [255]
316 [255]
316 $ hg log -T '{date(}'
317 $ hg log -T '{date(}'
317 hg: parse error at 6: not a prefix: end
318 hg: parse error at 6: not a prefix: end
318 ({date(}
319 ({date(}
319 ^ here)
320 ^ here)
320 [255]
321 [255]
321 $ hg log -T '{date)}'
322 $ hg log -T '{date)}'
322 hg: parse error at 5: invalid token
323 hg: parse error at 5: invalid token
323 ({date)}
324 ({date)}
324 ^ here)
325 ^ here)
325 [255]
326 [255]
326 $ hg log -T '{date date}'
327 $ hg log -T '{date date}'
327 hg: parse error at 6: invalid token
328 hg: parse error at 6: invalid token
328 ({date date}
329 ({date date}
329 ^ here)
330 ^ here)
330 [255]
331 [255]
331
332
332 $ hg log -T '{}'
333 $ hg log -T '{}'
333 hg: parse error at 1: not a prefix: end
334 hg: parse error at 1: not a prefix: end
334 ({}
335 ({}
335 ^ here)
336 ^ here)
336 [255]
337 [255]
337 $ hg debugtemplate -v '{()}'
338 $ hg debugtemplate -v '{()}'
338 (template
339 (template
339 (group
340 (group
340 None))
341 None))
341 * keywords:
342 * keywords:
342 * functions:
343 * functions:
343 hg: parse error: missing argument
344 hg: parse error: missing argument
344 [255]
345 [255]
345
346
346 Behind the scenes, this would throw TypeError without intype=bytes
347 Behind the scenes, this would throw TypeError without intype=bytes
347
348
348 $ hg log -l 3 --template '{date|obfuscate}\n'
349 $ hg log -l 3 --template '{date|obfuscate}\n'
349 &#48;&#46;&#48;&#48;
350 &#48;&#46;&#48;&#48;
350 &#48;&#46;&#48;&#48;
351 &#48;&#46;&#48;&#48;
351 &#49;&#53;&#55;&#55;&#56;&#55;&#50;&#56;&#54;&#48;&#46;&#48;&#48;
352 &#49;&#53;&#55;&#55;&#56;&#55;&#50;&#56;&#54;&#48;&#46;&#48;&#48;
352
353
353 Behind the scenes, this will throw a ValueError
354 Behind the scenes, this will throw a ValueError
354
355
355 $ hg log -l 3 --template 'line: {desc|shortdate}\n'
356 $ hg log -l 3 --template 'line: {desc|shortdate}\n'
356 hg: parse error: invalid date: 'Modify, add, remove, rename'
357 hg: parse error: invalid date: 'Modify, add, remove, rename'
357 (template filter 'shortdate' is not compatible with keyword 'desc')
358 (template filter 'shortdate' is not compatible with keyword 'desc')
358 [255]
359 [255]
359
360
360 Behind the scenes, this would throw AttributeError without intype=bytes
361 Behind the scenes, this would throw AttributeError without intype=bytes
361
362
362 $ hg log -l 3 --template 'line: {date|escape}\n'
363 $ hg log -l 3 --template 'line: {date|escape}\n'
363 line: 0.00
364 line: 0.00
364 line: 0.00
365 line: 0.00
365 line: 1577872860.00
366 line: 1577872860.00
366
367
367 $ hg log -l 3 --template 'line: {extras|localdate}\n'
368 $ hg log -l 3 --template 'line: {extras|localdate}\n'
368 hg: parse error: localdate expects a date information
369 hg: parse error: localdate expects a date information
369 [255]
370 [255]
370
371
371 Behind the scenes, this will throw ValueError
372 Behind the scenes, this will throw ValueError
372
373
373 $ hg tip --template '{author|email|date}\n'
374 $ hg tip --template '{author|email|date}\n'
374 hg: parse error: date expects a date information
375 hg: parse error: date expects a date information
375 [255]
376 [255]
376
377
377 $ hg tip -T '{author|email|shortdate}\n'
378 $ hg tip -T '{author|email|shortdate}\n'
378 hg: parse error: invalid date: 'test'
379 hg: parse error: invalid date: 'test'
379 (template filter 'shortdate' is not compatible with keyword 'author')
380 (template filter 'shortdate' is not compatible with keyword 'author')
380 [255]
381 [255]
381
382
382 $ hg tip -T '{get(extras, "branch")|shortdate}\n'
383 $ hg tip -T '{get(extras, "branch")|shortdate}\n'
383 hg: parse error: invalid date: 'default'
384 hg: parse error: invalid date: 'default'
384 (incompatible use of template filter 'shortdate')
385 (incompatible use of template filter 'shortdate')
385 [255]
386 [255]
386
387
387 Error in nested template:
388 Error in nested template:
388
389
389 $ hg log -T '{"date'
390 $ hg log -T '{"date'
390 hg: parse error at 2: unterminated string
391 hg: parse error at 2: unterminated string
391 ({"date
392 ({"date
392 ^ here)
393 ^ here)
393 [255]
394 [255]
394
395
395 $ hg log -T '{"foo{date|?}"}'
396 $ hg log -T '{"foo{date|?}"}'
396 hg: parse error at 11: syntax error
397 hg: parse error at 11: syntax error
397 ({"foo{date|?}"}
398 ({"foo{date|?}"}
398 ^ here)
399 ^ here)
399 [255]
400 [255]
400
401
401 Thrown an error if a template function doesn't exist
402 Thrown an error if a template function doesn't exist
402
403
403 $ hg tip --template '{foo()}\n'
404 $ hg tip --template '{foo()}\n'
404 hg: parse error: unknown function 'foo'
405 hg: parse error: unknown function 'foo'
405 [255]
406 [255]
406
407
407 $ cd ..
408 $ cd ..
408
409
409 Set up latesttag repository:
410 Set up latesttag repository:
410
411
411 $ hg init latesttag
412 $ hg init latesttag
412 $ cd latesttag
413 $ cd latesttag
413
414
414 $ echo a > file
415 $ echo a > file
415 $ hg ci -Am a -d '0 0'
416 $ hg ci -Am a -d '0 0'
416 adding file
417 adding file
417
418
418 $ echo b >> file
419 $ echo b >> file
419 $ hg ci -m b -d '1 0'
420 $ hg ci -m b -d '1 0'
420
421
421 $ echo c >> head1
422 $ echo c >> head1
422 $ hg ci -Am h1c -d '2 0'
423 $ hg ci -Am h1c -d '2 0'
423 adding head1
424 adding head1
424
425
425 $ hg update -q 1
426 $ hg update -q 1
426 $ echo d >> head2
427 $ echo d >> head2
427 $ hg ci -Am h2d -d '3 0'
428 $ hg ci -Am h2d -d '3 0'
428 adding head2
429 adding head2
429 created new head
430 created new head
430
431
431 $ echo e >> head2
432 $ echo e >> head2
432 $ hg ci -m h2e -d '4 0'
433 $ hg ci -m h2e -d '4 0'
433
434
434 $ hg merge -q
435 $ hg merge -q
435 $ hg ci -m merge -d '5 -3600'
436 $ hg ci -m merge -d '5 -3600'
436
437
437 $ hg tag -r 1 -m t1 -d '6 0' t1
438 $ hg tag -r 1 -m t1 -d '6 0' t1
438 $ hg tag -r 2 -m t2 -d '7 0' t2
439 $ hg tag -r 2 -m t2 -d '7 0' t2
439 $ hg tag -r 3 -m t3 -d '8 0' t3
440 $ hg tag -r 3 -m t3 -d '8 0' t3
440 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
441 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
441 $ hg tag -r 5 -m t5 -d '9 0' t5
442 $ hg tag -r 5 -m t5 -d '9 0' t5
442 $ hg tag -r 3 -m at3 -d '10 0' at3
443 $ hg tag -r 3 -m at3 -d '10 0' at3
443
444
444 $ cd ..
445 $ cd ..
445
446
446 Test new-style inline templating:
447 Test new-style inline templating:
447
448
448 $ hg log -R latesttag -r tip --template 'modified files: {file_mods % " {file}\n"}\n'
449 $ hg log -R latesttag -r tip --template 'modified files: {file_mods % " {file}\n"}\n'
449 modified files: .hgtags
450 modified files: .hgtags
450
451
451
452
452 $ hg log -R latesttag -r tip -T '{rev % "a"}\n'
453 $ hg log -R latesttag -r tip -T '{rev % "a"}\n'
453 hg: parse error: 11 is not iterable of mappings
454 hg: parse error: 11 is not iterable of mappings
454 (keyword 'rev' does not support map operation)
455 (keyword 'rev' does not support map operation)
455 [255]
456 [255]
456 $ hg log -R latesttag -r tip -T '{get(extras, "unknown") % "a"}\n'
457 $ hg log -R latesttag -r tip -T '{get(extras, "unknown") % "a"}\n'
457 hg: parse error: None is not iterable of mappings
458 hg: parse error: None is not iterable of mappings
458 [255]
459 [255]
459 $ hg log -R latesttag -r tip -T '{extras % "{key}\n" % "{key}\n"}'
460 $ hg log -R latesttag -r tip -T '{extras % "{key}\n" % "{key}\n"}'
460 hg: parse error: list of strings is not mappable
461 hg: parse error: list of strings is not mappable
461 [255]
462 [255]
462
463
463 Test new-style inline templating of non-list/dict type:
464 Test new-style inline templating of non-list/dict type:
464
465
465 $ hg log -R latesttag -r tip -T '{manifest}\n'
466 $ hg log -R latesttag -r tip -T '{manifest}\n'
466 11:2bc6e9006ce2
467 11:2bc6e9006ce2
467 $ hg log -R latesttag -r tip -T 'string length: {manifest|count}\n'
468 $ hg log -R latesttag -r tip -T 'string length: {manifest|count}\n'
468 string length: 15
469 string length: 15
469 $ hg log -R latesttag -r tip -T '{manifest % "{rev}:{node}"}\n'
470 $ hg log -R latesttag -r tip -T '{manifest % "{rev}:{node}"}\n'
470 11:2bc6e9006ce29882383a22d39fd1f4e66dd3e2fc
471 11:2bc6e9006ce29882383a22d39fd1f4e66dd3e2fc
471
472
472 $ hg log -R latesttag -r tip -T '{get(extras, "branch") % "{key}: {value}\n"}'
473 $ hg log -R latesttag -r tip -T '{get(extras, "branch") % "{key}: {value}\n"}'
473 branch: default
474 branch: default
474 $ hg log -R latesttag -r tip -T '{get(extras, "unknown") % "{key}\n"}'
475 $ hg log -R latesttag -r tip -T '{get(extras, "unknown") % "{key}\n"}'
475 hg: parse error: None is not iterable of mappings
476 hg: parse error: None is not iterable of mappings
476 [255]
477 [255]
477 $ hg log -R latesttag -r tip -T '{min(extras) % "{key}: {value}\n"}'
478 $ hg log -R latesttag -r tip -T '{min(extras) % "{key}: {value}\n"}'
478 branch: default
479 branch: default
479 $ hg log -R latesttag -l1 -T '{min(revset("0:9")) % "{rev}:{node|short}\n"}'
480 $ hg log -R latesttag -l1 -T '{min(revset("0:9")) % "{rev}:{node|short}\n"}'
480 0:ce3cec86e6c2
481 0:ce3cec86e6c2
481 $ hg log -R latesttag -l1 -T '{max(revset("0:9")) % "{rev}:{node|short}\n"}'
482 $ hg log -R latesttag -l1 -T '{max(revset("0:9")) % "{rev}:{node|short}\n"}'
482 9:fbc7cd862e9c
483 9:fbc7cd862e9c
483
484
484 Test dot operator precedence:
485 Test dot operator precedence:
485
486
486 $ hg debugtemplate -R latesttag -r0 -v '{manifest.node|short}\n'
487 $ hg debugtemplate -R latesttag -r0 -v '{manifest.node|short}\n'
487 (template
488 (template
488 (|
489 (|
489 (.
490 (.
490 (symbol 'manifest')
491 (symbol 'manifest')
491 (symbol 'node'))
492 (symbol 'node'))
492 (symbol 'short'))
493 (symbol 'short'))
493 (string '\n'))
494 (string '\n'))
494 * keywords: manifest, node, rev
495 * keywords: manifest, node, rev
495 * functions: formatnode, short
496 * functions: formatnode, short
496 89f4071fec70
497 89f4071fec70
497
498
498 (the following examples are invalid, but seem natural in parsing POV)
499 (the following examples are invalid, but seem natural in parsing POV)
499
500
500 $ hg debugtemplate -R latesttag -r0 -v '{foo|bar.baz}\n' 2> /dev/null
501 $ hg debugtemplate -R latesttag -r0 -v '{foo|bar.baz}\n' 2> /dev/null
501 (template
502 (template
502 (|
503 (|
503 (symbol 'foo')
504 (symbol 'foo')
504 (.
505 (.
505 (symbol 'bar')
506 (symbol 'bar')
506 (symbol 'baz')))
507 (symbol 'baz')))
507 (string '\n'))
508 (string '\n'))
508 [255]
509 [255]
509 $ hg debugtemplate -R latesttag -r0 -v '{foo.bar()}\n' 2> /dev/null
510 $ hg debugtemplate -R latesttag -r0 -v '{foo.bar()}\n' 2> /dev/null
510 (template
511 (template
511 (.
512 (.
512 (symbol 'foo')
513 (symbol 'foo')
513 (func
514 (func
514 (symbol 'bar')
515 (symbol 'bar')
515 None))
516 None))
516 (string '\n'))
517 (string '\n'))
517 * keywords: foo
518 * keywords: foo
518 * functions: bar
519 * functions: bar
519 [255]
520 [255]
520
521
521 Test evaluation of dot operator:
522 Test evaluation of dot operator:
522
523
523 $ hg log -R latesttag -l1 -T '{min(revset("0:9")).node}\n'
524 $ hg log -R latesttag -l1 -T '{min(revset("0:9")).node}\n'
524 ce3cec86e6c26bd9bdfc590a6b92abc9680f1796
525 ce3cec86e6c26bd9bdfc590a6b92abc9680f1796
525 $ hg log -R latesttag -r0 -T '{extras.branch}\n'
526 $ hg log -R latesttag -r0 -T '{extras.branch}\n'
526 default
527 default
527 $ hg log -R latesttag -r0 -T '{date.unixtime} {localdate(date, "+0200").tzoffset}\n'
528 $ hg log -R latesttag -r0 -T '{date.unixtime} {localdate(date, "+0200").tzoffset}\n'
528 0 -7200
529 0 -7200
529
530
530 $ hg log -R latesttag -l1 -T '{author.invalid}\n'
531 $ hg log -R latesttag -l1 -T '{author.invalid}\n'
531 hg: parse error: 'test' is not a dictionary
532 hg: parse error: 'test' is not a dictionary
532 (keyword 'author' does not support member operation)
533 (keyword 'author' does not support member operation)
533 [255]
534 [255]
534 $ hg log -R latesttag -l1 -T '{min("abc").invalid}\n'
535 $ hg log -R latesttag -l1 -T '{min("abc").invalid}\n'
535 hg: parse error: 'a' is not a dictionary
536 hg: parse error: 'a' is not a dictionary
536 [255]
537 [255]
537
538
538 Test integer literal:
539 Test integer literal:
539
540
540 $ hg debugtemplate -v '{(0)}\n'
541 $ hg debugtemplate -v '{(0)}\n'
541 (template
542 (template
542 (group
543 (group
543 (integer '0'))
544 (integer '0'))
544 (string '\n'))
545 (string '\n'))
545 * keywords:
546 * keywords:
546 * functions:
547 * functions:
547 0
548 0
548 $ hg debugtemplate -v '{(123)}\n'
549 $ hg debugtemplate -v '{(123)}\n'
549 (template
550 (template
550 (group
551 (group
551 (integer '123'))
552 (integer '123'))
552 (string '\n'))
553 (string '\n'))
553 * keywords:
554 * keywords:
554 * functions:
555 * functions:
555 123
556 123
556 $ hg debugtemplate -v '{(-4)}\n'
557 $ hg debugtemplate -v '{(-4)}\n'
557 (template
558 (template
558 (group
559 (group
559 (negate
560 (negate
560 (integer '4')))
561 (integer '4')))
561 (string '\n'))
562 (string '\n'))
562 * keywords:
563 * keywords:
563 * functions:
564 * functions:
564 -4
565 -4
565 $ hg debugtemplate '{(-)}\n'
566 $ hg debugtemplate '{(-)}\n'
566 hg: parse error at 3: not a prefix: )
567 hg: parse error at 3: not a prefix: )
567 ({(-)}\n
568 ({(-)}\n
568 ^ here)
569 ^ here)
569 [255]
570 [255]
570 $ hg debugtemplate '{(-a)}\n'
571 $ hg debugtemplate '{(-a)}\n'
571 hg: parse error: negation needs an integer argument
572 hg: parse error: negation needs an integer argument
572 [255]
573 [255]
573
574
574 top-level integer literal is interpreted as symbol (i.e. variable name):
575 top-level integer literal is interpreted as symbol (i.e. variable name):
575
576
576 $ hg debugtemplate -D 1=one -v '{1}\n'
577 $ hg debugtemplate -D 1=one -v '{1}\n'
577 (template
578 (template
578 (integer '1')
579 (integer '1')
579 (string '\n'))
580 (string '\n'))
580 * keywords:
581 * keywords:
581 * functions:
582 * functions:
582 one
583 one
583 $ hg debugtemplate -D 1=one -v '{if("t", "{1}")}\n'
584 $ hg debugtemplate -D 1=one -v '{if("t", "{1}")}\n'
584 (template
585 (template
585 (func
586 (func
586 (symbol 'if')
587 (symbol 'if')
587 (list
588 (list
588 (string 't')
589 (string 't')
589 (template
590 (template
590 (integer '1'))))
591 (integer '1'))))
591 (string '\n'))
592 (string '\n'))
592 * keywords:
593 * keywords:
593 * functions: if
594 * functions: if
594 one
595 one
595 $ hg debugtemplate -D 1=one -v '{1|stringify}\n'
596 $ hg debugtemplate -D 1=one -v '{1|stringify}\n'
596 (template
597 (template
597 (|
598 (|
598 (integer '1')
599 (integer '1')
599 (symbol 'stringify'))
600 (symbol 'stringify'))
600 (string '\n'))
601 (string '\n'))
601 * keywords:
602 * keywords:
602 * functions: stringify
603 * functions: stringify
603 one
604 one
604
605
605 unless explicit symbol is expected:
606 unless explicit symbol is expected:
606
607
607 $ hg log -Ra -r0 -T '{desc|1}\n'
608 $ hg log -Ra -r0 -T '{desc|1}\n'
608 hg: parse error: expected a symbol, got 'integer'
609 hg: parse error: expected a symbol, got 'integer'
609 [255]
610 [255]
610 $ hg log -Ra -r0 -T '{1()}\n'
611 $ hg log -Ra -r0 -T '{1()}\n'
611 hg: parse error: expected a symbol, got 'integer'
612 hg: parse error: expected a symbol, got 'integer'
612 [255]
613 [255]
613
614
614 Test string literal:
615 Test string literal:
615
616
616 $ hg debugtemplate -Ra -r0 -v '{"string with no template fragment"}\n'
617 $ hg debugtemplate -Ra -r0 -v '{"string with no template fragment"}\n'
617 (template
618 (template
618 (string 'string with no template fragment')
619 (string 'string with no template fragment')
619 (string '\n'))
620 (string '\n'))
620 * keywords:
621 * keywords:
621 * functions:
622 * functions:
622 string with no template fragment
623 string with no template fragment
623 $ hg debugtemplate -Ra -r0 -v '{"template: {rev}"}\n'
624 $ hg debugtemplate -Ra -r0 -v '{"template: {rev}"}\n'
624 (template
625 (template
625 (template
626 (template
626 (string 'template: ')
627 (string 'template: ')
627 (symbol 'rev'))
628 (symbol 'rev'))
628 (string '\n'))
629 (string '\n'))
629 * keywords: rev
630 * keywords: rev
630 * functions:
631 * functions:
631 template: 0
632 template: 0
632 $ hg debugtemplate -Ra -r0 -v '{r"rawstring: {rev}"}\n'
633 $ hg debugtemplate -Ra -r0 -v '{r"rawstring: {rev}"}\n'
633 (template
634 (template
634 (string 'rawstring: {rev}')
635 (string 'rawstring: {rev}')
635 (string '\n'))
636 (string '\n'))
636 * keywords:
637 * keywords:
637 * functions:
638 * functions:
638 rawstring: {rev}
639 rawstring: {rev}
639 $ hg debugtemplate -Ra -r0 -v '{files % r"rawstring: {file}"}\n'
640 $ hg debugtemplate -Ra -r0 -v '{files % r"rawstring: {file}"}\n'
640 (template
641 (template
641 (%
642 (%
642 (symbol 'files')
643 (symbol 'files')
643 (string 'rawstring: {file}'))
644 (string 'rawstring: {file}'))
644 (string '\n'))
645 (string '\n'))
645 * keywords: files
646 * keywords: files
646 * functions:
647 * functions:
647 rawstring: {file}
648 rawstring: {file}
648
649
649 Test string escaping:
650 Test string escaping:
650
651
651 $ hg log -R latesttag -r 0 --template '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
652 $ hg log -R latesttag -r 0 --template '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
652 >
653 >
653 <>\n<[>
654 <>\n<[>
654 <>\n<]>
655 <>\n<]>
655 <>\n<
656 <>\n<
656
657
657 $ hg log -R latesttag -r 0 \
658 $ hg log -R latesttag -r 0 \
658 > --config ui.logtemplate='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
659 > --config ui.logtemplate='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
659 >
660 >
660 <>\n<[>
661 <>\n<[>
661 <>\n<]>
662 <>\n<]>
662 <>\n<
663 <>\n<
663
664
664 $ hg log -R latesttag -r 0 -T esc \
665 $ hg log -R latesttag -r 0 -T esc \
665 > --config templates.esc='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
666 > --config templates.esc='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
666 >
667 >
667 <>\n<[>
668 <>\n<[>
668 <>\n<]>
669 <>\n<]>
669 <>\n<
670 <>\n<
670
671
671 $ cat <<'EOF' > esctmpl
672 $ cat <<'EOF' > esctmpl
672 > changeset = '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
673 > changeset = '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
673 > EOF
674 > EOF
674 $ hg log -R latesttag -r 0 --style ./esctmpl
675 $ hg log -R latesttag -r 0 --style ./esctmpl
675 >
676 >
676 <>\n<[>
677 <>\n<[>
677 <>\n<]>
678 <>\n<]>
678 <>\n<
679 <>\n<
679
680
680 Test string escaping of quotes:
681 Test string escaping of quotes:
681
682
682 $ hg log -Ra -r0 -T '{"\""}\n'
683 $ hg log -Ra -r0 -T '{"\""}\n'
683 "
684 "
684 $ hg log -Ra -r0 -T '{"\\\""}\n'
685 $ hg log -Ra -r0 -T '{"\\\""}\n'
685 \"
686 \"
686 $ hg log -Ra -r0 -T '{r"\""}\n'
687 $ hg log -Ra -r0 -T '{r"\""}\n'
687 \"
688 \"
688 $ hg log -Ra -r0 -T '{r"\\\""}\n'
689 $ hg log -Ra -r0 -T '{r"\\\""}\n'
689 \\\"
690 \\\"
690
691
691
692
692 $ hg log -Ra -r0 -T '{"\""}\n'
693 $ hg log -Ra -r0 -T '{"\""}\n'
693 "
694 "
694 $ hg log -Ra -r0 -T '{"\\\""}\n'
695 $ hg log -Ra -r0 -T '{"\\\""}\n'
695 \"
696 \"
696 $ hg log -Ra -r0 -T '{r"\""}\n'
697 $ hg log -Ra -r0 -T '{r"\""}\n'
697 \"
698 \"
698 $ hg log -Ra -r0 -T '{r"\\\""}\n'
699 $ hg log -Ra -r0 -T '{r"\\\""}\n'
699 \\\"
700 \\\"
700
701
701 Test exception in quoted template. single backslash before quotation mark is
702 Test exception in quoted template. single backslash before quotation mark is
702 stripped before parsing:
703 stripped before parsing:
703
704
704 $ cat <<'EOF' > escquotetmpl
705 $ cat <<'EOF' > escquotetmpl
705 > changeset = "\" \\" \\\" \\\\" {files % \"{file}\"}\n"
706 > changeset = "\" \\" \\\" \\\\" {files % \"{file}\"}\n"
706 > EOF
707 > EOF
707 $ cd latesttag
708 $ cd latesttag
708 $ hg log -r 2 --style ../escquotetmpl
709 $ hg log -r 2 --style ../escquotetmpl
709 " \" \" \\" head1
710 " \" \" \\" head1
710
711
711 $ hg log -r 2 -T esc --config templates.esc='"{\"valid\"}\n"'
712 $ hg log -r 2 -T esc --config templates.esc='"{\"valid\"}\n"'
712 valid
713 valid
713 $ hg log -r 2 -T esc --config templates.esc="'"'{\'"'"'valid\'"'"'}\n'"'"
714 $ hg log -r 2 -T esc --config templates.esc="'"'{\'"'"'valid\'"'"'}\n'"'"
714 valid
715 valid
715
716
716 Test compatibility with 2.9.2-3.4 of escaped quoted strings in nested
717 Test compatibility with 2.9.2-3.4 of escaped quoted strings in nested
717 _evalifliteral() templates (issue4733):
718 _evalifliteral() templates (issue4733):
718
719
719 $ hg log -r 2 -T '{if(rev, "\"{rev}")}\n'
720 $ hg log -r 2 -T '{if(rev, "\"{rev}")}\n'
720 "2
721 "2
721 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\"{rev}\")}")}\n'
722 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\"{rev}\")}")}\n'
722 "2
723 "2
723 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\"{rev}\\\")}\")}")}\n'
724 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\"{rev}\\\")}\")}")}\n'
724 "2
725 "2
725
726
726 $ hg log -r 2 -T '{if(rev, "\\\"")}\n'
727 $ hg log -r 2 -T '{if(rev, "\\\"")}\n'
727 \"
728 \"
728 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\\\\\"\")}")}\n'
729 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\\\\\"\")}")}\n'
729 \"
730 \"
730 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
731 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
731 \"
732 \"
732
733
733 $ hg log -r 2 -T '{if(rev, r"\\\"")}\n'
734 $ hg log -r 2 -T '{if(rev, r"\\\"")}\n'
734 \\\"
735 \\\"
735 $ hg log -r 2 -T '{if(rev, "{if(rev, r\"\\\\\\\"\")}")}\n'
736 $ hg log -r 2 -T '{if(rev, "{if(rev, r\"\\\\\\\"\")}")}\n'
736 \\\"
737 \\\"
737 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, r\\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
738 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, r\\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
738 \\\"
739 \\\"
739
740
740 escaped single quotes and errors:
741 escaped single quotes and errors:
741
742
742 $ hg log -r 2 -T "{if(rev, '{if(rev, \'foo\')}')}"'\n'
743 $ hg log -r 2 -T "{if(rev, '{if(rev, \'foo\')}')}"'\n'
743 foo
744 foo
744 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
745 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
745 foo
746 foo
746 $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
747 $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
747 hg: parse error at 21: unterminated string
748 hg: parse error at 21: unterminated string
748 ({if(rev, "{if(rev, \")}")}\n
749 ({if(rev, "{if(rev, \")}")}\n
749 ^ here)
750 ^ here)
750 [255]
751 [255]
751 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
752 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
752 hg: parse error: trailing \ in string
753 hg: parse error: trailing \ in string
753 [255]
754 [255]
754 $ hg log -r 2 -T '{if(rev, r\"\\"")}\n'
755 $ hg log -r 2 -T '{if(rev, r\"\\"")}\n'
755 hg: parse error: trailing \ in string
756 hg: parse error: trailing \ in string
756 [255]
757 [255]
757
758
758 $ cd ..
759 $ cd ..
759
760
760 Test leading backslashes:
761 Test leading backslashes:
761
762
762 $ cd latesttag
763 $ cd latesttag
763 $ hg log -r 2 -T '\{rev} {files % "\{file}"}\n'
764 $ hg log -r 2 -T '\{rev} {files % "\{file}"}\n'
764 {rev} {file}
765 {rev} {file}
765 $ hg log -r 2 -T '\\{rev} {files % "\\{file}"}\n'
766 $ hg log -r 2 -T '\\{rev} {files % "\\{file}"}\n'
766 \2 \head1
767 \2 \head1
767 $ hg log -r 2 -T '\\\{rev} {files % "\\\{file}"}\n'
768 $ hg log -r 2 -T '\\\{rev} {files % "\\\{file}"}\n'
768 \{rev} \{file}
769 \{rev} \{file}
769 $ cd ..
770 $ cd ..
770
771
771 Test leading backslashes in "if" expression (issue4714):
772 Test leading backslashes in "if" expression (issue4714):
772
773
773 $ cd latesttag
774 $ cd latesttag
774 $ hg log -r 2 -T '{if("1", "\{rev}")} {if("1", r"\{rev}")}\n'
775 $ hg log -r 2 -T '{if("1", "\{rev}")} {if("1", r"\{rev}")}\n'
775 {rev} \{rev}
776 {rev} \{rev}
776 $ hg log -r 2 -T '{if("1", "\\{rev}")} {if("1", r"\\{rev}")}\n'
777 $ hg log -r 2 -T '{if("1", "\\{rev}")} {if("1", r"\\{rev}")}\n'
777 \2 \\{rev}
778 \2 \\{rev}
778 $ hg log -r 2 -T '{if("1", "\\\{rev}")} {if("1", r"\\\{rev}")}\n'
779 $ hg log -r 2 -T '{if("1", "\\\{rev}")} {if("1", r"\\\{rev}")}\n'
779 \{rev} \\\{rev}
780 \{rev} \\\{rev}
780 $ cd ..
781 $ cd ..
781
782
782 "string-escape"-ed "\x5c\x786e" becomes r"\x6e" (once) or r"n" (twice)
783 "string-escape"-ed "\x5c\x786e" becomes r"\x6e" (once) or r"n" (twice)
783
784
784 $ hg log -R a -r 0 --template '{if("1", "\x5c\x786e", "NG")}\n'
785 $ hg log -R a -r 0 --template '{if("1", "\x5c\x786e", "NG")}\n'
785 \x6e
786 \x6e
786 $ hg log -R a -r 0 --template '{if("1", r"\x5c\x786e", "NG")}\n'
787 $ hg log -R a -r 0 --template '{if("1", r"\x5c\x786e", "NG")}\n'
787 \x5c\x786e
788 \x5c\x786e
788 $ hg log -R a -r 0 --template '{if("", "NG", "\x5c\x786e")}\n'
789 $ hg log -R a -r 0 --template '{if("", "NG", "\x5c\x786e")}\n'
789 \x6e
790 \x6e
790 $ hg log -R a -r 0 --template '{if("", "NG", r"\x5c\x786e")}\n'
791 $ hg log -R a -r 0 --template '{if("", "NG", r"\x5c\x786e")}\n'
791 \x5c\x786e
792 \x5c\x786e
792
793
793 $ hg log -R a -r 2 --template '{ifeq("no perso\x6e", desc, "\x5c\x786e", "NG")}\n'
794 $ hg log -R a -r 2 --template '{ifeq("no perso\x6e", desc, "\x5c\x786e", "NG")}\n'
794 \x6e
795 \x6e
795 $ hg log -R a -r 2 --template '{ifeq(r"no perso\x6e", desc, "NG", r"\x5c\x786e")}\n'
796 $ hg log -R a -r 2 --template '{ifeq(r"no perso\x6e", desc, "NG", r"\x5c\x786e")}\n'
796 \x5c\x786e
797 \x5c\x786e
797 $ hg log -R a -r 2 --template '{ifeq(desc, "no perso\x6e", "\x5c\x786e", "NG")}\n'
798 $ hg log -R a -r 2 --template '{ifeq(desc, "no perso\x6e", "\x5c\x786e", "NG")}\n'
798 \x6e
799 \x6e
799 $ hg log -R a -r 2 --template '{ifeq(desc, r"no perso\x6e", "NG", r"\x5c\x786e")}\n'
800 $ hg log -R a -r 2 --template '{ifeq(desc, r"no perso\x6e", "NG", r"\x5c\x786e")}\n'
800 \x5c\x786e
801 \x5c\x786e
801
802
802 $ hg log -R a -r 8 --template '{join(files, "\n")}\n'
803 $ hg log -R a -r 8 --template '{join(files, "\n")}\n'
803 fourth
804 fourth
804 second
805 second
805 third
806 third
806 $ hg log -R a -r 8 --template '{join(files, r"\n")}\n'
807 $ hg log -R a -r 8 --template '{join(files, r"\n")}\n'
807 fourth\nsecond\nthird
808 fourth\nsecond\nthird
808
809
809 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", "htm\x6c")}'
810 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", "htm\x6c")}'
810 <p>
811 <p>
811 1st
812 1st
812 </p>
813 </p>
813 <p>
814 <p>
814 2nd
815 2nd
815 </p>
816 </p>
816 $ hg log -R a -r 2 --template '{rstdoc(r"1st\n\n2nd", "html")}'
817 $ hg log -R a -r 2 --template '{rstdoc(r"1st\n\n2nd", "html")}'
817 <p>
818 <p>
818 1st\n\n2nd
819 1st\n\n2nd
819 </p>
820 </p>
820 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", r"htm\x6c")}'
821 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", r"htm\x6c")}'
821 1st
822 1st
822
823
823 2nd
824 2nd
824
825
825 $ hg log -R a -r 2 --template '{strip(desc, "\x6e")}\n'
826 $ hg log -R a -r 2 --template '{strip(desc, "\x6e")}\n'
826 o perso
827 o perso
827 $ hg log -R a -r 2 --template '{strip(desc, r"\x6e")}\n'
828 $ hg log -R a -r 2 --template '{strip(desc, r"\x6e")}\n'
828 no person
829 no person
829 $ hg log -R a -r 2 --template '{strip("no perso\x6e", "\x6e")}\n'
830 $ hg log -R a -r 2 --template '{strip("no perso\x6e", "\x6e")}\n'
830 o perso
831 o perso
831 $ hg log -R a -r 2 --template '{strip(r"no perso\x6e", r"\x6e")}\n'
832 $ hg log -R a -r 2 --template '{strip(r"no perso\x6e", r"\x6e")}\n'
832 no perso
833 no perso
833
834
834 $ hg log -R a -r 2 --template '{sub("\\x6e", "\x2d", desc)}\n'
835 $ hg log -R a -r 2 --template '{sub("\\x6e", "\x2d", desc)}\n'
835 -o perso-
836 -o perso-
836 $ hg log -R a -r 2 --template '{sub(r"\\x6e", "-", desc)}\n'
837 $ hg log -R a -r 2 --template '{sub(r"\\x6e", "-", desc)}\n'
837 no person
838 no person
838 $ hg log -R a -r 2 --template '{sub("n", r"\\x2d", desc)}\n'
839 $ hg log -R a -r 2 --template '{sub("n", r"\\x2d", desc)}\n'
839 \x2do perso\x2d
840 \x2do perso\x2d
840 $ hg log -R a -r 2 --template '{sub("n", "\x2d", "no perso\x6e")}\n'
841 $ hg log -R a -r 2 --template '{sub("n", "\x2d", "no perso\x6e")}\n'
841 -o perso-
842 -o perso-
842 $ hg log -R a -r 2 --template '{sub("n", r"\\x2d", r"no perso\x6e")}\n'
843 $ hg log -R a -r 2 --template '{sub("n", r"\\x2d", r"no perso\x6e")}\n'
843 \x2do perso\x6e
844 \x2do perso\x6e
844
845
845 $ hg log -R a -r 8 --template '{files % "{file}\n"}'
846 $ hg log -R a -r 8 --template '{files % "{file}\n"}'
846 fourth
847 fourth
847 second
848 second
848 third
849 third
849
850
850 Test string escaping in nested expression:
851 Test string escaping in nested expression:
851
852
852 $ hg log -R a -r 8 --template '{ifeq(r"\x6e", if("1", "\x5c\x786e"), join(files, "\x5c\x786e"))}\n'
853 $ hg log -R a -r 8 --template '{ifeq(r"\x6e", if("1", "\x5c\x786e"), join(files, "\x5c\x786e"))}\n'
853 fourth\x6esecond\x6ethird
854 fourth\x6esecond\x6ethird
854 $ hg log -R a -r 8 --template '{ifeq(if("1", r"\x6e"), "\x5c\x786e", join(files, "\x5c\x786e"))}\n'
855 $ hg log -R a -r 8 --template '{ifeq(if("1", r"\x6e"), "\x5c\x786e", join(files, "\x5c\x786e"))}\n'
855 fourth\x6esecond\x6ethird
856 fourth\x6esecond\x6ethird
856
857
857 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", "\x5c\x786e"))}\n'
858 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", "\x5c\x786e"))}\n'
858 fourth\x6esecond\x6ethird
859 fourth\x6esecond\x6ethird
859 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", r"\x5c\x786e"))}\n'
860 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", r"\x5c\x786e"))}\n'
860 fourth\x5c\x786esecond\x5c\x786ethird
861 fourth\x5c\x786esecond\x5c\x786ethird
861
862
862 $ hg log -R a -r 3:4 --template '{rev}:{sub(if("1", "\x6e"), ifeq(branch, "foo", r"\x5c\x786e", "\x5c\x786e"), desc)}\n'
863 $ hg log -R a -r 3:4 --template '{rev}:{sub(if("1", "\x6e"), ifeq(branch, "foo", r"\x5c\x786e", "\x5c\x786e"), desc)}\n'
863 3:\x6eo user, \x6eo domai\x6e
864 3:\x6eo user, \x6eo domai\x6e
864 4:\x5c\x786eew bra\x5c\x786ech
865 4:\x5c\x786eew bra\x5c\x786ech
865
866
866 Test quotes in nested expression are evaluated just like a $(command)
867 Test quotes in nested expression are evaluated just like a $(command)
867 substitution in POSIX shells:
868 substitution in POSIX shells:
868
869
869 $ hg log -R a -r 8 -T '{"{"{rev}:{node|short}"}"}\n'
870 $ hg log -R a -r 8 -T '{"{"{rev}:{node|short}"}"}\n'
870 8:95c24699272e
871 8:95c24699272e
871 $ hg log -R a -r 8 -T '{"{"\{{rev}} \"{node|short}\""}"}\n'
872 $ hg log -R a -r 8 -T '{"{"\{{rev}} \"{node|short}\""}"}\n'
872 {8} "95c24699272e"
873 {8} "95c24699272e"
873
874
874 Test recursive evaluation:
875 Test recursive evaluation:
875
876
876 $ hg init r
877 $ hg init r
877 $ cd r
878 $ cd r
878 $ echo a > a
879 $ echo a > a
879 $ hg ci -Am '{rev}'
880 $ hg ci -Am '{rev}'
880 adding a
881 adding a
881 $ hg log -r 0 --template '{if(rev, desc)}\n'
882 $ hg log -r 0 --template '{if(rev, desc)}\n'
882 {rev}
883 {rev}
883 $ hg log -r 0 --template '{if(rev, "{author} {rev}")}\n'
884 $ hg log -r 0 --template '{if(rev, "{author} {rev}")}\n'
884 test 0
885 test 0
885
886
886 $ hg branch -q 'text.{rev}'
887 $ hg branch -q 'text.{rev}'
887 $ echo aa >> aa
888 $ echo aa >> aa
888 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
889 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
889
890
890 $ hg log -l1 --template '{fill(desc, "20", author, branch)}'
891 $ hg log -l1 --template '{fill(desc, "20", author, branch)}'
891 {node|short}desc to
892 {node|short}desc to
892 text.{rev}be wrapped
893 text.{rev}be wrapped
893 text.{rev}desc to be
894 text.{rev}desc to be
894 text.{rev}wrapped (no-eol)
895 text.{rev}wrapped (no-eol)
895 $ hg log -l1 --template '{fill(desc, "20", "{node|short}:", "text.{rev}:")}'
896 $ hg log -l1 --template '{fill(desc, "20", "{node|short}:", "text.{rev}:")}'
896 bcc7ff960b8e:desc to
897 bcc7ff960b8e:desc to
897 text.1:be wrapped
898 text.1:be wrapped
898 text.1:desc to be
899 text.1:desc to be
899 text.1:wrapped (no-eol)
900 text.1:wrapped (no-eol)
900 $ hg log -l1 -T '{fill(desc, date, "", "")}\n'
901 $ hg log -l1 -T '{fill(desc, date, "", "")}\n'
901 hg: parse error: fill expects an integer width
902 hg: parse error: fill expects an integer width
902 [255]
903 [255]
903
904
904 $ hg log -l 1 --template '{sub(r"[0-9]", "-", author)}'
905 $ hg log -l 1 --template '{sub(r"[0-9]", "-", author)}'
905 {node|short} (no-eol)
906 {node|short} (no-eol)
906 $ hg log -l 1 --template '{sub(r"[0-9]", "-", "{node|short}")}'
907 $ hg log -l 1 --template '{sub(r"[0-9]", "-", "{node|short}")}'
907 bcc-ff---b-e (no-eol)
908 bcc-ff---b-e (no-eol)
908
909
909 $ cat >> .hg/hgrc <<EOF
910 $ cat >> .hg/hgrc <<EOF
910 > [extensions]
911 > [extensions]
911 > color=
912 > color=
912 > [color]
913 > [color]
913 > mode=ansi
914 > mode=ansi
914 > text.{rev} = red
915 > text.{rev} = red
915 > text.1 = green
916 > text.1 = green
916 > EOF
917 > EOF
917 $ hg log --color=always -l 1 --template '{label(branch, "text\n")}'
918 $ hg log --color=always -l 1 --template '{label(branch, "text\n")}'
918 \x1b[0;31mtext\x1b[0m (esc)
919 \x1b[0;31mtext\x1b[0m (esc)
919 $ hg log --color=always -l 1 --template '{label("text.{rev}", "text\n")}'
920 $ hg log --color=always -l 1 --template '{label("text.{rev}", "text\n")}'
920 \x1b[0;32mtext\x1b[0m (esc)
921 \x1b[0;32mtext\x1b[0m (esc)
921
922
922 $ cd ..
923 $ cd ..
923
924
924 Test bad template with better error message
925 Test bad template with better error message
925
926
926 $ hg log -Gv -R a --template '{desc|user()}'
927 $ hg log -Gv -R a --template '{desc|user()}'
927 hg: parse error: expected a symbol, got 'func'
928 hg: parse error: expected a symbol, got 'func'
928 [255]
929 [255]
929
930
930 Test broken string escapes:
931 Test broken string escapes:
931
932
932 $ hg log -T "bogus\\" -R a
933 $ hg log -T "bogus\\" -R a
933 hg: parse error: trailing \ in string
934 hg: parse error: trailing \ in string
934 [255]
935 [255]
935 $ hg log -T "\\xy" -R a
936 $ hg log -T "\\xy" -R a
936 hg: parse error: invalid \x escape* (glob)
937 hg: parse error: invalid \x escape* (glob)
937 [255]
938 [255]
938
939
939 Templater supports aliases of symbol and func() styles:
940 Templater supports aliases of symbol and func() styles:
940
941
941 $ hg clone -q a aliases
942 $ hg clone -q a aliases
942 $ cd aliases
943 $ cd aliases
943 $ cat <<EOF >> .hg/hgrc
944 $ cat <<EOF >> .hg/hgrc
944 > [templatealias]
945 > [templatealias]
945 > r = rev
946 > r = rev
946 > rn = "{r}:{node|short}"
947 > rn = "{r}:{node|short}"
947 > status(c, files) = files % "{c} {file}\n"
948 > status(c, files) = files % "{c} {file}\n"
948 > utcdate(d) = localdate(d, "UTC")
949 > utcdate(d) = localdate(d, "UTC")
949 > EOF
950 > EOF
950
951
951 $ hg debugtemplate -vr0 '{rn} {utcdate(date)|isodate}\n'
952 $ hg debugtemplate -vr0 '{rn} {utcdate(date)|isodate}\n'
952 (template
953 (template
953 (symbol 'rn')
954 (symbol 'rn')
954 (string ' ')
955 (string ' ')
955 (|
956 (|
956 (func
957 (func
957 (symbol 'utcdate')
958 (symbol 'utcdate')
958 (symbol 'date'))
959 (symbol 'date'))
959 (symbol 'isodate'))
960 (symbol 'isodate'))
960 (string '\n'))
961 (string '\n'))
961 * expanded:
962 * expanded:
962 (template
963 (template
963 (template
964 (template
964 (symbol 'rev')
965 (symbol 'rev')
965 (string ':')
966 (string ':')
966 (|
967 (|
967 (symbol 'node')
968 (symbol 'node')
968 (symbol 'short')))
969 (symbol 'short')))
969 (string ' ')
970 (string ' ')
970 (|
971 (|
971 (func
972 (func
972 (symbol 'localdate')
973 (symbol 'localdate')
973 (list
974 (list
974 (symbol 'date')
975 (symbol 'date')
975 (string 'UTC')))
976 (string 'UTC')))
976 (symbol 'isodate'))
977 (symbol 'isodate'))
977 (string '\n'))
978 (string '\n'))
978 * keywords: date, node, rev
979 * keywords: date, node, rev
979 * functions: isodate, localdate, short
980 * functions: isodate, localdate, short
980 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
981 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
981
982
982 $ hg debugtemplate -vr0 '{status("A", file_adds)}'
983 $ hg debugtemplate -vr0 '{status("A", file_adds)}'
983 (template
984 (template
984 (func
985 (func
985 (symbol 'status')
986 (symbol 'status')
986 (list
987 (list
987 (string 'A')
988 (string 'A')
988 (symbol 'file_adds'))))
989 (symbol 'file_adds'))))
989 * expanded:
990 * expanded:
990 (template
991 (template
991 (%
992 (%
992 (symbol 'file_adds')
993 (symbol 'file_adds')
993 (template
994 (template
994 (string 'A')
995 (string 'A')
995 (string ' ')
996 (string ' ')
996 (symbol 'file')
997 (symbol 'file')
997 (string '\n'))))
998 (string '\n'))))
998 * keywords: file, file_adds
999 * keywords: file, file_adds
999 * functions:
1000 * functions:
1000 A a
1001 A a
1001
1002
1002 A unary function alias can be called as a filter:
1003 A unary function alias can be called as a filter:
1003
1004
1004 $ hg debugtemplate -vr0 '{date|utcdate|isodate}\n'
1005 $ hg debugtemplate -vr0 '{date|utcdate|isodate}\n'
1005 (template
1006 (template
1006 (|
1007 (|
1007 (|
1008 (|
1008 (symbol 'date')
1009 (symbol 'date')
1009 (symbol 'utcdate'))
1010 (symbol 'utcdate'))
1010 (symbol 'isodate'))
1011 (symbol 'isodate'))
1011 (string '\n'))
1012 (string '\n'))
1012 * expanded:
1013 * expanded:
1013 (template
1014 (template
1014 (|
1015 (|
1015 (func
1016 (func
1016 (symbol 'localdate')
1017 (symbol 'localdate')
1017 (list
1018 (list
1018 (symbol 'date')
1019 (symbol 'date')
1019 (string 'UTC')))
1020 (string 'UTC')))
1020 (symbol 'isodate'))
1021 (symbol 'isodate'))
1021 (string '\n'))
1022 (string '\n'))
1022 * keywords: date
1023 * keywords: date
1023 * functions: isodate, localdate
1024 * functions: isodate, localdate
1024 1970-01-12 13:46 +0000
1025 1970-01-12 13:46 +0000
1025
1026
1026 Aliases should be applied only to command arguments and templates in hgrc.
1027 Aliases should be applied only to command arguments and templates in hgrc.
1027 Otherwise, our stock styles and web templates could be corrupted:
1028 Otherwise, our stock styles and web templates could be corrupted:
1028
1029
1029 $ hg log -r0 -T '{rn} {utcdate(date)|isodate}\n'
1030 $ hg log -r0 -T '{rn} {utcdate(date)|isodate}\n'
1030 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
1031 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
1031
1032
1032 $ hg log -r0 --config ui.logtemplate='"{rn} {utcdate(date)|isodate}\n"'
1033 $ hg log -r0 --config ui.logtemplate='"{rn} {utcdate(date)|isodate}\n"'
1033 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
1034 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
1034
1035
1035 $ cat <<EOF > tmpl
1036 $ cat <<EOF > tmpl
1036 > changeset = 'nothing expanded:{rn}\n'
1037 > changeset = 'nothing expanded:{rn}\n'
1037 > EOF
1038 > EOF
1038 $ hg log -r0 --style ./tmpl
1039 $ hg log -r0 --style ./tmpl
1039 nothing expanded:
1040 nothing expanded:
1040
1041
1041 Aliases in formatter:
1042 Aliases in formatter:
1042
1043
1043 $ hg branches -T '{pad(branch, 7)} {rn}\n'
1044 $ hg branches -T '{pad(branch, 7)} {rn}\n'
1044 default 6:d41e714fe50d
1045 default 6:d41e714fe50d
1045 foo 4:bbe44766e73d
1046 foo 4:bbe44766e73d
1046
1047
1047 Aliases should honor HGPLAIN:
1048 Aliases should honor HGPLAIN:
1048
1049
1049 $ HGPLAIN= hg log -r0 -T 'nothing expanded:{rn}\n'
1050 $ HGPLAIN= hg log -r0 -T 'nothing expanded:{rn}\n'
1050 nothing expanded:
1051 nothing expanded:
1051 $ HGPLAINEXCEPT=templatealias hg log -r0 -T '{rn}\n'
1052 $ HGPLAINEXCEPT=templatealias hg log -r0 -T '{rn}\n'
1052 0:1e4e1b8f71e0
1053 0:1e4e1b8f71e0
1053
1054
1054 Unparsable alias:
1055 Unparsable alias:
1055
1056
1056 $ hg debugtemplate --config templatealias.bad='x(' -v '{bad}'
1057 $ hg debugtemplate --config templatealias.bad='x(' -v '{bad}'
1057 (template
1058 (template
1058 (symbol 'bad'))
1059 (symbol 'bad'))
1059 abort: bad definition of template alias "bad": at 2: not a prefix: end
1060 abort: bad definition of template alias "bad": at 2: not a prefix: end
1060 [255]
1061 [255]
1061 $ hg log --config templatealias.bad='x(' -T '{bad}'
1062 $ hg log --config templatealias.bad='x(' -T '{bad}'
1062 abort: bad definition of template alias "bad": at 2: not a prefix: end
1063 abort: bad definition of template alias "bad": at 2: not a prefix: end
1063 [255]
1064 [255]
1064
1065
1065 $ cd ..
1066 $ cd ..
1066
1067
1067 Test that template function in extension is registered as expected
1068 Test that template function in extension is registered as expected
1068
1069
1069 $ cd a
1070 $ cd a
1070
1071
1071 $ cat <<EOF > $TESTTMP/customfunc.py
1072 $ cat <<EOF > $TESTTMP/customfunc.py
1072 > from mercurial import registrar
1073 > from mercurial import registrar
1073 >
1074 >
1074 > templatefunc = registrar.templatefunc()
1075 > templatefunc = registrar.templatefunc()
1075 >
1076 >
1076 > @templatefunc(b'custom()')
1077 > @templatefunc(b'custom()')
1077 > def custom(context, mapping, args):
1078 > def custom(context, mapping, args):
1078 > return b'custom'
1079 > return b'custom'
1079 > EOF
1080 > EOF
1080 $ cat <<EOF > .hg/hgrc
1081 $ cat <<EOF > .hg/hgrc
1081 > [extensions]
1082 > [extensions]
1082 > customfunc = $TESTTMP/customfunc.py
1083 > customfunc = $TESTTMP/customfunc.py
1083 > EOF
1084 > EOF
1084
1085
1085 $ hg log -r . -T "{custom()}\n" --config customfunc.enabled=true
1086 $ hg log -r . -T "{custom()}\n" --config customfunc.enabled=true
1086 custom
1087 custom
1087
1088
1088 $ cd ..
1089 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now