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