##// END OF EJS Templates
templates: make {indent("", " ")} be empty...
Martin von Zweigbergk -
r44093:fa246ada default
parent child Browse files
Show More
@@ -1,549 +1,549 b''
1 1 # templatefilters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 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 from __future__ import absolute_import
9 9
10 10 import os
11 11 import re
12 12 import time
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 node,
19 19 pycompat,
20 20 registrar,
21 21 templateutil,
22 22 url,
23 23 util,
24 24 )
25 25 from .utils import (
26 26 cborutil,
27 27 dateutil,
28 28 stringutil,
29 29 )
30 30
31 31 urlerr = util.urlerr
32 32 urlreq = util.urlreq
33 33
34 34 # filters are callables like:
35 35 # fn(obj)
36 36 # with:
37 37 # obj - object to be filtered (text, date, list and so on)
38 38 filters = {}
39 39
40 40 templatefilter = registrar.templatefilter(filters)
41 41
42 42
43 43 @templatefilter(b'addbreaks', intype=bytes)
44 44 def addbreaks(text):
45 45 """Any text. Add an XHTML "<br />" tag before the end of
46 46 every line except the last.
47 47 """
48 48 return text.replace(b'\n', b'<br/>\n')
49 49
50 50
51 51 agescales = [
52 52 (b"year", 3600 * 24 * 365, b'Y'),
53 53 (b"month", 3600 * 24 * 30, b'M'),
54 54 (b"week", 3600 * 24 * 7, b'W'),
55 55 (b"day", 3600 * 24, b'd'),
56 56 (b"hour", 3600, b'h'),
57 57 (b"minute", 60, b'm'),
58 58 (b"second", 1, b's'),
59 59 ]
60 60
61 61
62 62 @templatefilter(b'age', intype=templateutil.date)
63 63 def age(date, abbrev=False):
64 64 """Date. Returns a human-readable date/time difference between the
65 65 given date/time and the current date/time.
66 66 """
67 67
68 68 def plural(t, c):
69 69 if c == 1:
70 70 return t
71 71 return t + b"s"
72 72
73 73 def fmt(t, c, a):
74 74 if abbrev:
75 75 return b"%d%s" % (c, a)
76 76 return b"%d %s" % (c, plural(t, c))
77 77
78 78 now = time.time()
79 79 then = date[0]
80 80 future = False
81 81 if then > now:
82 82 future = True
83 83 delta = max(1, int(then - now))
84 84 if delta > agescales[0][1] * 30:
85 85 return b'in the distant future'
86 86 else:
87 87 delta = max(1, int(now - then))
88 88 if delta > agescales[0][1] * 2:
89 89 return dateutil.shortdate(date)
90 90
91 91 for t, s, a in agescales:
92 92 n = delta // s
93 93 if n >= 2 or s == 1:
94 94 if future:
95 95 return b'%s from now' % fmt(t, n, a)
96 96 return b'%s ago' % fmt(t, n, a)
97 97
98 98
99 99 @templatefilter(b'basename', intype=bytes)
100 100 def basename(path):
101 101 """Any text. Treats the text as a path, and returns the last
102 102 component of the path after splitting by the path separator.
103 103 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
104 104 """
105 105 return os.path.basename(path)
106 106
107 107
108 108 @templatefilter(b'cbor')
109 109 def cbor(obj):
110 110 """Any object. Serializes the object to CBOR bytes."""
111 111 return b''.join(cborutil.streamencode(obj))
112 112
113 113
114 114 @templatefilter(b'commondir')
115 115 def commondir(filelist):
116 116 """List of text. Treats each list item as file name with /
117 117 as path separator and returns the longest common directory
118 118 prefix shared by all list items.
119 119 Returns the empty string if no common prefix exists.
120 120
121 121 The list items are not normalized, i.e. "foo/../bar" is handled as
122 122 file "bar" in the directory "foo/..". Leading slashes are ignored.
123 123
124 124 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
125 125 ["foo/bar", "baz"] becomes "".
126 126 """
127 127
128 128 def common(a, b):
129 129 if len(a) > len(b):
130 130 a = b[: len(a)]
131 131 elif len(b) > len(a):
132 132 b = b[: len(a)]
133 133 if a == b:
134 134 return a
135 135 for i in pycompat.xrange(len(a)):
136 136 if a[i] != b[i]:
137 137 return a[:i]
138 138 return a
139 139
140 140 try:
141 141 if not filelist:
142 142 return b""
143 143 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
144 144 if len(dirlist) == 1:
145 145 return b'/'.join(dirlist[0])
146 146 a = min(dirlist)
147 147 b = max(dirlist)
148 148 # The common prefix of a and b is shared with all
149 149 # elements of the list since Python sorts lexicographical
150 150 # and [1, x] after [1].
151 151 return b'/'.join(common(a, b))
152 152 except TypeError:
153 153 raise error.ParseError(_(b'argument is not a list of text'))
154 154
155 155
156 156 @templatefilter(b'count')
157 157 def count(i):
158 158 """List or text. Returns the length as an integer."""
159 159 try:
160 160 return len(i)
161 161 except TypeError:
162 162 raise error.ParseError(_(b'not countable'))
163 163
164 164
165 165 @templatefilter(b'dirname', intype=bytes)
166 166 def dirname(path):
167 167 """Any text. Treats the text as a path, and strips the last
168 168 component of the path after splitting by the path separator.
169 169 """
170 170 return os.path.dirname(path)
171 171
172 172
173 173 @templatefilter(b'domain', intype=bytes)
174 174 def domain(author):
175 175 """Any text. Finds the first string that looks like an email
176 176 address, and extracts just the domain component. Example: ``User
177 177 <user@example.com>`` becomes ``example.com``.
178 178 """
179 179 f = author.find(b'@')
180 180 if f == -1:
181 181 return b''
182 182 author = author[f + 1 :]
183 183 f = author.find(b'>')
184 184 if f >= 0:
185 185 author = author[:f]
186 186 return author
187 187
188 188
189 189 @templatefilter(b'email', intype=bytes)
190 190 def email(text):
191 191 """Any text. Extracts the first string that looks like an email
192 192 address. Example: ``User <user@example.com>`` becomes
193 193 ``user@example.com``.
194 194 """
195 195 return stringutil.email(text)
196 196
197 197
198 198 @templatefilter(b'escape', intype=bytes)
199 199 def escape(text):
200 200 """Any text. Replaces the special XML/XHTML characters "&", "<"
201 201 and ">" with XML entities, and filters out NUL characters.
202 202 """
203 203 return url.escape(text.replace(b'\0', b''), True)
204 204
205 205
206 206 para_re = None
207 207 space_re = None
208 208
209 209
210 210 def fill(text, width, initindent=b'', hangindent=b''):
211 211 '''fill many paragraphs with optional indentation.'''
212 212 global para_re, space_re
213 213 if para_re is None:
214 214 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
215 215 space_re = re.compile(br' +')
216 216
217 217 def findparas():
218 218 start = 0
219 219 while True:
220 220 m = para_re.search(text, start)
221 221 if not m:
222 222 uctext = encoding.unifromlocal(text[start:])
223 223 w = len(uctext)
224 224 while w > 0 and uctext[w - 1].isspace():
225 225 w -= 1
226 226 yield (
227 227 encoding.unitolocal(uctext[:w]),
228 228 encoding.unitolocal(uctext[w:]),
229 229 )
230 230 break
231 231 yield text[start : m.start(0)], m.group(1)
232 232 start = m.end(1)
233 233
234 234 return b"".join(
235 235 [
236 236 stringutil.wrap(
237 237 space_re.sub(b' ', stringutil.wrap(para, width)),
238 238 width,
239 239 initindent,
240 240 hangindent,
241 241 )
242 242 + rest
243 243 for para, rest in findparas()
244 244 ]
245 245 )
246 246
247 247
248 248 @templatefilter(b'fill68', intype=bytes)
249 249 def fill68(text):
250 250 """Any text. Wraps the text to fit in 68 columns."""
251 251 return fill(text, 68)
252 252
253 253
254 254 @templatefilter(b'fill76', intype=bytes)
255 255 def fill76(text):
256 256 """Any text. Wraps the text to fit in 76 columns."""
257 257 return fill(text, 76)
258 258
259 259
260 260 @templatefilter(b'firstline', intype=bytes)
261 261 def firstline(text):
262 262 """Any text. Returns the first line of text."""
263 263 try:
264 264 return text.splitlines(True)[0].rstrip(b'\r\n')
265 265 except IndexError:
266 266 return b''
267 267
268 268
269 269 @templatefilter(b'hex', intype=bytes)
270 270 def hexfilter(text):
271 271 """Any text. Convert a binary Mercurial node identifier into
272 272 its long hexadecimal representation.
273 273 """
274 274 return node.hex(text)
275 275
276 276
277 277 @templatefilter(b'hgdate', intype=templateutil.date)
278 278 def hgdate(text):
279 279 """Date. Returns the date as a pair of numbers: "1157407993
280 280 25200" (Unix timestamp, timezone offset).
281 281 """
282 282 return b"%d %d" % text
283 283
284 284
285 285 @templatefilter(b'isodate', intype=templateutil.date)
286 286 def isodate(text):
287 287 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
288 288 +0200".
289 289 """
290 290 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
291 291
292 292
293 293 @templatefilter(b'isodatesec', intype=templateutil.date)
294 294 def isodatesec(text):
295 295 """Date. Returns the date in ISO 8601 format, including
296 296 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
297 297 filter.
298 298 """
299 299 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
300 300
301 301
302 def indent(text, prefix):
302 def indent(text, prefix, firstline=b''):
303 303 '''indent each non-empty line of text after first with prefix.'''
304 304 lines = text.splitlines()
305 305 num_lines = len(lines)
306 306 endswithnewline = text[-1:] == b'\n'
307 307
308 308 def indenter():
309 309 for i in pycompat.xrange(num_lines):
310 310 l = lines[i]
311 if i and l.strip():
312 yield prefix
311 if l.strip():
312 yield prefix if i else firstline
313 313 yield l
314 314 if i < num_lines - 1 or endswithnewline:
315 315 yield b'\n'
316 316
317 317 return b"".join(indenter())
318 318
319 319
320 320 @templatefilter(b'json')
321 321 def json(obj, paranoid=True):
322 322 """Any object. Serializes the object to a JSON formatted text."""
323 323 if obj is None:
324 324 return b'null'
325 325 elif obj is False:
326 326 return b'false'
327 327 elif obj is True:
328 328 return b'true'
329 329 elif isinstance(obj, (int, pycompat.long, float)):
330 330 return pycompat.bytestr(obj)
331 331 elif isinstance(obj, bytes):
332 332 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
333 333 elif isinstance(obj, type(u'')):
334 334 raise error.ProgrammingError(
335 335 b'Mercurial only does output with bytes: %r' % obj
336 336 )
337 337 elif util.safehasattr(obj, b'keys'):
338 338 out = [
339 339 b'"%s": %s'
340 340 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
341 341 for k, v in sorted(pycompat.iteritems(obj))
342 342 ]
343 343 return b'{' + b', '.join(out) + b'}'
344 344 elif util.safehasattr(obj, b'__iter__'):
345 345 out = [json(i, paranoid) for i in obj]
346 346 return b'[' + b', '.join(out) + b']'
347 347 raise error.ProgrammingError(b'cannot encode %r' % obj)
348 348
349 349
350 350 @templatefilter(b'lower', intype=bytes)
351 351 def lower(text):
352 352 """Any text. Converts the text to lowercase."""
353 353 return encoding.lower(text)
354 354
355 355
356 356 @templatefilter(b'nonempty', intype=bytes)
357 357 def nonempty(text):
358 358 """Any text. Returns '(none)' if the string is empty."""
359 359 return text or b"(none)"
360 360
361 361
362 362 @templatefilter(b'obfuscate', intype=bytes)
363 363 def obfuscate(text):
364 364 """Any text. Returns the input text rendered as a sequence of
365 365 XML entities.
366 366 """
367 367 text = pycompat.unicode(
368 368 text, pycompat.sysstr(encoding.encoding), r'replace'
369 369 )
370 370 return b''.join([b'&#%d;' % ord(c) for c in text])
371 371
372 372
373 373 @templatefilter(b'permissions', intype=bytes)
374 374 def permissions(flags):
375 375 if b"l" in flags:
376 376 return b"lrwxrwxrwx"
377 377 if b"x" in flags:
378 378 return b"-rwxr-xr-x"
379 379 return b"-rw-r--r--"
380 380
381 381
382 382 @templatefilter(b'person', intype=bytes)
383 383 def person(author):
384 384 """Any text. Returns the name before an email address,
385 385 interpreting it as per RFC 5322.
386 386 """
387 387 return stringutil.person(author)
388 388
389 389
390 390 @templatefilter(b'revescape', intype=bytes)
391 391 def revescape(text):
392 392 """Any text. Escapes all "special" characters, except @.
393 393 Forward slashes are escaped twice to prevent web servers from prematurely
394 394 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
395 395 """
396 396 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
397 397
398 398
399 399 @templatefilter(b'rfc3339date', intype=templateutil.date)
400 400 def rfc3339date(text):
401 401 """Date. Returns a date using the Internet date format
402 402 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
403 403 """
404 404 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
405 405
406 406
407 407 @templatefilter(b'rfc822date', intype=templateutil.date)
408 408 def rfc822date(text):
409 409 """Date. Returns a date using the same format used in email
410 410 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
411 411 """
412 412 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
413 413
414 414
415 415 @templatefilter(b'short', intype=bytes)
416 416 def short(text):
417 417 """Changeset hash. Returns the short form of a changeset hash,
418 418 i.e. a 12 hexadecimal digit string.
419 419 """
420 420 return text[:12]
421 421
422 422
423 423 @templatefilter(b'shortbisect', intype=bytes)
424 424 def shortbisect(label):
425 425 """Any text. Treats `label` as a bisection status, and
426 426 returns a single-character representing the status (G: good, B: bad,
427 427 S: skipped, U: untested, I: ignored). Returns single space if `text`
428 428 is not a valid bisection status.
429 429 """
430 430 if label:
431 431 return label[0:1].upper()
432 432 return b' '
433 433
434 434
435 435 @templatefilter(b'shortdate', intype=templateutil.date)
436 436 def shortdate(text):
437 437 """Date. Returns a date like "2006-09-18"."""
438 438 return dateutil.shortdate(text)
439 439
440 440
441 441 @templatefilter(b'slashpath', intype=bytes)
442 442 def slashpath(path):
443 443 """Any text. Replaces the native path separator with slash."""
444 444 return util.pconvert(path)
445 445
446 446
447 447 @templatefilter(b'splitlines', intype=bytes)
448 448 def splitlines(text):
449 449 """Any text. Split text into a list of lines."""
450 450 return templateutil.hybridlist(text.splitlines(), name=b'line')
451 451
452 452
453 453 @templatefilter(b'stringescape', intype=bytes)
454 454 def stringescape(text):
455 455 return stringutil.escapestr(text)
456 456
457 457
458 458 @templatefilter(b'stringify', intype=bytes)
459 459 def stringify(thing):
460 460 """Any type. Turns the value into text by converting values into
461 461 text and concatenating them.
462 462 """
463 463 return thing # coerced by the intype
464 464
465 465
466 466 @templatefilter(b'stripdir', intype=bytes)
467 467 def stripdir(text):
468 468 """Treat the text as path and strip a directory level, if
469 469 possible. For example, "foo" and "foo/bar" becomes "foo".
470 470 """
471 471 dir = os.path.dirname(text)
472 472 if dir == b"":
473 473 return os.path.basename(text)
474 474 else:
475 475 return dir
476 476
477 477
478 478 @templatefilter(b'tabindent', intype=bytes)
479 479 def tabindent(text):
480 480 """Any text. Returns the text, with every non-empty line
481 481 except the first starting with a tab character.
482 482 """
483 483 return indent(text, b'\t')
484 484
485 485
486 486 @templatefilter(b'upper', intype=bytes)
487 487 def upper(text):
488 488 """Any text. Converts the text to uppercase."""
489 489 return encoding.upper(text)
490 490
491 491
492 492 @templatefilter(b'urlescape', intype=bytes)
493 493 def urlescape(text):
494 494 """Any text. Escapes all "special" characters. For example,
495 495 "foo bar" becomes "foo%20bar".
496 496 """
497 497 return urlreq.quote(text)
498 498
499 499
500 500 @templatefilter(b'user', intype=bytes)
501 501 def userfilter(text):
502 502 """Any text. Returns a short representation of a user name or email
503 503 address."""
504 504 return stringutil.shortuser(text)
505 505
506 506
507 507 @templatefilter(b'emailuser', intype=bytes)
508 508 def emailuser(text):
509 509 """Any text. Returns the user portion of an email address."""
510 510 return stringutil.emailuser(text)
511 511
512 512
513 513 @templatefilter(b'utf8', intype=bytes)
514 514 def utf8(text):
515 515 """Any text. Converts from the local character encoding to UTF-8."""
516 516 return encoding.fromlocal(text)
517 517
518 518
519 519 @templatefilter(b'xmlescape', intype=bytes)
520 520 def xmlescape(text):
521 521 text = (
522 522 text.replace(b'&', b'&amp;')
523 523 .replace(b'<', b'&lt;')
524 524 .replace(b'>', b'&gt;')
525 525 .replace(b'"', b'&quot;')
526 526 .replace(b"'", b'&#39;')
527 527 ) # &apos; invalid in HTML
528 528 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
529 529
530 530
531 531 def websub(text, websubtable):
532 532 """:websub: Any text. Only applies to hgweb. Applies the regular
533 533 expression replacements defined in the websub section.
534 534 """
535 535 if websubtable:
536 536 for regexp, format in websubtable:
537 537 text = regexp.sub(format, text)
538 538 return text
539 539
540 540
541 541 def loadfilter(ui, extname, registrarobj):
542 542 """Load template filter from specified registrarobj
543 543 """
544 544 for name, func in pycompat.iteritems(registrarobj._table):
545 545 filters[name] = func
546 546
547 547
548 548 # tell hggettext to extract docstrings from these functions:
549 549 i18nfunctions = filters.values()
@@ -1,882 +1,880 b''
1 1 # templatefuncs.py - common template functions
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 from __future__ import absolute_import
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 bin,
15 15 wdirid,
16 16 )
17 17 from . import (
18 18 color,
19 19 diffutil,
20 20 encoding,
21 21 error,
22 22 minirst,
23 23 obsutil,
24 24 pycompat,
25 25 registrar,
26 26 revset as revsetmod,
27 27 revsetlang,
28 28 scmutil,
29 29 templatefilters,
30 30 templatekw,
31 31 templateutil,
32 32 util,
33 33 )
34 34 from .utils import (
35 35 dateutil,
36 36 stringutil,
37 37 )
38 38
39 39 evalrawexp = templateutil.evalrawexp
40 40 evalwrapped = templateutil.evalwrapped
41 41 evalfuncarg = templateutil.evalfuncarg
42 42 evalboolean = templateutil.evalboolean
43 43 evaldate = templateutil.evaldate
44 44 evalinteger = templateutil.evalinteger
45 45 evalstring = templateutil.evalstring
46 46 evalstringliteral = templateutil.evalstringliteral
47 47
48 48 # dict of template built-in functions
49 49 funcs = {}
50 50 templatefunc = registrar.templatefunc(funcs)
51 51
52 52
53 53 @templatefunc(b'date(date[, fmt])')
54 54 def date(context, mapping, args):
55 55 """Format a date. See :hg:`help dates` for formatting
56 56 strings. The default is a Unix date format, including the timezone:
57 57 "Mon Sep 04 15:13:13 2006 0700"."""
58 58 if not (1 <= len(args) <= 2):
59 59 # i18n: "date" is a keyword
60 60 raise error.ParseError(_(b"date expects one or two arguments"))
61 61
62 62 date = evaldate(
63 63 context,
64 64 mapping,
65 65 args[0],
66 66 # i18n: "date" is a keyword
67 67 _(b"date expects a date information"),
68 68 )
69 69 fmt = None
70 70 if len(args) == 2:
71 71 fmt = evalstring(context, mapping, args[1])
72 72 if fmt is None:
73 73 return dateutil.datestr(date)
74 74 else:
75 75 return dateutil.datestr(date, fmt)
76 76
77 77
78 78 @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs')
79 79 def dict_(context, mapping, args):
80 80 """Construct a dict from key-value pairs. A key may be omitted if
81 81 a value expression can provide an unambiguous name."""
82 82 data = util.sortdict()
83 83
84 84 for v in args[b'args']:
85 85 k = templateutil.findsymbolicname(v)
86 86 if not k:
87 87 raise error.ParseError(_(b'dict key cannot be inferred'))
88 88 if k in data or k in args[b'kwargs']:
89 89 raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k)
90 90 data[k] = evalfuncarg(context, mapping, v)
91 91
92 92 data.update(
93 93 (k, evalfuncarg(context, mapping, v))
94 94 for k, v in pycompat.iteritems(args[b'kwargs'])
95 95 )
96 96 return templateutil.hybriddict(data)
97 97
98 98
99 99 @templatefunc(
100 100 b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'}
101 101 )
102 102 def diff(context, mapping, args):
103 103 """Show a diff, optionally
104 104 specifying files to include or exclude."""
105 105 if len(args) > 2:
106 106 # i18n: "diff" is a keyword
107 107 raise error.ParseError(_(b"diff expects zero, one, or two arguments"))
108 108
109 109 def getpatterns(i):
110 110 if i < len(args):
111 111 s = evalstring(context, mapping, args[i]).strip()
112 112 if s:
113 113 return [s]
114 114 return []
115 115
116 116 ctx = context.resource(mapping, b'ctx')
117 117 ui = context.resource(mapping, b'ui')
118 118 diffopts = diffutil.diffallopts(ui)
119 119 chunks = ctx.diff(
120 120 match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts
121 121 )
122 122
123 123 return b''.join(chunks)
124 124
125 125
126 126 @templatefunc(
127 127 b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'}
128 128 )
129 129 def extdata(context, mapping, args):
130 130 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
131 131 if b'source' not in args:
132 132 # i18n: "extdata" is a keyword
133 133 raise error.ParseError(_(b'extdata expects one argument'))
134 134
135 135 source = evalstring(context, mapping, args[b'source'])
136 136 if not source:
137 137 sym = templateutil.findsymbolicname(args[b'source'])
138 138 if sym:
139 139 raise error.ParseError(
140 140 _(b'empty data source specified'),
141 141 hint=_(b"did you mean extdata('%s')?") % sym,
142 142 )
143 143 else:
144 144 raise error.ParseError(_(b'empty data source specified'))
145 145 cache = context.resource(mapping, b'cache').setdefault(b'extdata', {})
146 146 ctx = context.resource(mapping, b'ctx')
147 147 if source in cache:
148 148 data = cache[source]
149 149 else:
150 150 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
151 151 return data.get(ctx.rev(), b'')
152 152
153 153
154 154 @templatefunc(b'files(pattern)', requires={b'ctx'})
155 155 def files(context, mapping, args):
156 156 """All files of the current changeset matching the pattern. See
157 157 :hg:`help patterns`."""
158 158 if not len(args) == 1:
159 159 # i18n: "files" is a keyword
160 160 raise error.ParseError(_(b"files expects one argument"))
161 161
162 162 raw = evalstring(context, mapping, args[0])
163 163 ctx = context.resource(mapping, b'ctx')
164 164 m = ctx.match([raw])
165 165 files = list(ctx.matches(m))
166 166 return templateutil.compatfileslist(context, mapping, b"file", files)
167 167
168 168
169 169 @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])')
170 170 def fill(context, mapping, args):
171 171 """Fill many
172 172 paragraphs with optional indentation. See the "fill" filter."""
173 173 if not (1 <= len(args) <= 4):
174 174 # i18n: "fill" is a keyword
175 175 raise error.ParseError(_(b"fill expects one to four arguments"))
176 176
177 177 text = evalstring(context, mapping, args[0])
178 178 width = 76
179 179 initindent = b''
180 180 hangindent = b''
181 181 if 2 <= len(args) <= 4:
182 182 width = evalinteger(
183 183 context,
184 184 mapping,
185 185 args[1],
186 186 # i18n: "fill" is a keyword
187 187 _(b"fill expects an integer width"),
188 188 )
189 189 try:
190 190 initindent = evalstring(context, mapping, args[2])
191 191 hangindent = evalstring(context, mapping, args[3])
192 192 except IndexError:
193 193 pass
194 194
195 195 return templatefilters.fill(text, width, initindent, hangindent)
196 196
197 197
198 198 @templatefunc(b'filter(iterable[, expr])')
199 199 def filter_(context, mapping, args):
200 200 """Remove empty elements from a list or a dict. If expr specified, it's
201 201 applied to each element to test emptiness."""
202 202 if not (1 <= len(args) <= 2):
203 203 # i18n: "filter" is a keyword
204 204 raise error.ParseError(_(b"filter expects one or two arguments"))
205 205 iterable = evalwrapped(context, mapping, args[0])
206 206 if len(args) == 1:
207 207
208 208 def select(w):
209 209 return w.tobool(context, mapping)
210 210
211 211 else:
212 212
213 213 def select(w):
214 214 if not isinstance(w, templateutil.mappable):
215 215 raise error.ParseError(_(b"not filterable by expression"))
216 216 lm = context.overlaymap(mapping, w.tomap(context))
217 217 return evalboolean(context, lm, args[1])
218 218
219 219 return iterable.filter(context, mapping, select)
220 220
221 221
222 222 @templatefunc(b'formatnode(node)', requires={b'ui'})
223 223 def formatnode(context, mapping, args):
224 224 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
225 225 if len(args) != 1:
226 226 # i18n: "formatnode" is a keyword
227 227 raise error.ParseError(_(b"formatnode expects one argument"))
228 228
229 229 ui = context.resource(mapping, b'ui')
230 230 node = evalstring(context, mapping, args[0])
231 231 if ui.debugflag:
232 232 return node
233 233 return templatefilters.short(node)
234 234
235 235
236 236 @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'})
237 237 def mailmap(context, mapping, args):
238 238 """Return the author, updated according to the value
239 239 set in the .mailmap file"""
240 240 if len(args) != 1:
241 241 raise error.ParseError(_(b"mailmap expects one argument"))
242 242
243 243 author = evalstring(context, mapping, args[0])
244 244
245 245 cache = context.resource(mapping, b'cache')
246 246 repo = context.resource(mapping, b'repo')
247 247
248 248 if b'mailmap' not in cache:
249 249 data = repo.wvfs.tryread(b'.mailmap')
250 250 cache[b'mailmap'] = stringutil.parsemailmap(data)
251 251
252 252 return stringutil.mapname(cache[b'mailmap'], author)
253 253
254 254
255 255 @templatefunc(
256 256 b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])',
257 257 argspec=b'text width fillchar left truncate',
258 258 )
259 259 def pad(context, mapping, args):
260 260 """Pad text with a
261 261 fill character."""
262 262 if b'text' not in args or b'width' not in args:
263 263 # i18n: "pad" is a keyword
264 264 raise error.ParseError(_(b"pad() expects two to four arguments"))
265 265
266 266 width = evalinteger(
267 267 context,
268 268 mapping,
269 269 args[b'width'],
270 270 # i18n: "pad" is a keyword
271 271 _(b"pad() expects an integer width"),
272 272 )
273 273
274 274 text = evalstring(context, mapping, args[b'text'])
275 275
276 276 truncate = False
277 277 left = False
278 278 fillchar = b' '
279 279 if b'fillchar' in args:
280 280 fillchar = evalstring(context, mapping, args[b'fillchar'])
281 281 if len(color.stripeffects(fillchar)) != 1:
282 282 # i18n: "pad" is a keyword
283 283 raise error.ParseError(_(b"pad() expects a single fill character"))
284 284 if b'left' in args:
285 285 left = evalboolean(context, mapping, args[b'left'])
286 286 if b'truncate' in args:
287 287 truncate = evalboolean(context, mapping, args[b'truncate'])
288 288
289 289 fillwidth = width - encoding.colwidth(color.stripeffects(text))
290 290 if fillwidth < 0 and truncate:
291 291 return encoding.trim(color.stripeffects(text), width, leftside=left)
292 292 if fillwidth <= 0:
293 293 return text
294 294 if left:
295 295 return fillchar * fillwidth + text
296 296 else:
297 297 return text + fillchar * fillwidth
298 298
299 299
300 300 @templatefunc(b'indent(text, indentchars[, firstline])')
301 301 def indent(context, mapping, args):
302 302 """Indents all non-empty lines
303 303 with the characters given in the indentchars string. An optional
304 304 third parameter will override the indent for the first line only
305 305 if present."""
306 306 if not (2 <= len(args) <= 3):
307 307 # i18n: "indent" is a keyword
308 308 raise error.ParseError(_(b"indent() expects two or three arguments"))
309 309
310 310 text = evalstring(context, mapping, args[0])
311 311 indent = evalstring(context, mapping, args[1])
312 312
313 firstline = indent
313 314 if len(args) == 3:
314 315 firstline = evalstring(context, mapping, args[2])
315 else:
316 firstline = indent
317 316
318 # the indent function doesn't indent the first line, so we do it here
319 return templatefilters.indent(firstline + text, indent)
317 return templatefilters.indent(text, indent, firstline=firstline)
320 318
321 319
322 320 @templatefunc(b'get(dict, key)')
323 321 def get(context, mapping, args):
324 322 """Get an attribute/key from an object. Some keywords
325 323 are complex types. This function allows you to obtain the value of an
326 324 attribute on these types."""
327 325 if len(args) != 2:
328 326 # i18n: "get" is a keyword
329 327 raise error.ParseError(_(b"get() expects two arguments"))
330 328
331 329 dictarg = evalwrapped(context, mapping, args[0])
332 330 key = evalrawexp(context, mapping, args[1])
333 331 try:
334 332 return dictarg.getmember(context, mapping, key)
335 333 except error.ParseError as err:
336 334 # i18n: "get" is a keyword
337 335 hint = _(b"get() expects a dict as first argument")
338 336 raise error.ParseError(bytes(err), hint=hint)
339 337
340 338
341 339 @templatefunc(b'config(section, name[, default])', requires={b'ui'})
342 340 def config(context, mapping, args):
343 341 """Returns the requested hgrc config option as a string."""
344 342 fn = context.resource(mapping, b'ui').config
345 343 return _config(context, mapping, args, fn, evalstring)
346 344
347 345
348 346 @templatefunc(b'configbool(section, name[, default])', requires={b'ui'})
349 347 def configbool(context, mapping, args):
350 348 """Returns the requested hgrc config option as a boolean."""
351 349 fn = context.resource(mapping, b'ui').configbool
352 350 return _config(context, mapping, args, fn, evalboolean)
353 351
354 352
355 353 @templatefunc(b'configint(section, name[, default])', requires={b'ui'})
356 354 def configint(context, mapping, args):
357 355 """Returns the requested hgrc config option as an integer."""
358 356 fn = context.resource(mapping, b'ui').configint
359 357 return _config(context, mapping, args, fn, evalinteger)
360 358
361 359
362 360 def _config(context, mapping, args, configfn, defaultfn):
363 361 if not (2 <= len(args) <= 3):
364 362 raise error.ParseError(_(b"config expects two or three arguments"))
365 363
366 364 # The config option can come from any section, though we specifically
367 365 # reserve the [templateconfig] section for dynamically defining options
368 366 # for this function without also requiring an extension.
369 367 section = evalstringliteral(context, mapping, args[0])
370 368 name = evalstringliteral(context, mapping, args[1])
371 369 if len(args) == 3:
372 370 default = defaultfn(context, mapping, args[2])
373 371 return configfn(section, name, default)
374 372 else:
375 373 return configfn(section, name)
376 374
377 375
378 376 @templatefunc(b'if(expr, then[, else])')
379 377 def if_(context, mapping, args):
380 378 """Conditionally execute based on the result of
381 379 an expression."""
382 380 if not (2 <= len(args) <= 3):
383 381 # i18n: "if" is a keyword
384 382 raise error.ParseError(_(b"if expects two or three arguments"))
385 383
386 384 test = evalboolean(context, mapping, args[0])
387 385 if test:
388 386 return evalrawexp(context, mapping, args[1])
389 387 elif len(args) == 3:
390 388 return evalrawexp(context, mapping, args[2])
391 389
392 390
393 391 @templatefunc(b'ifcontains(needle, haystack, then[, else])')
394 392 def ifcontains(context, mapping, args):
395 393 """Conditionally execute based
396 394 on whether the item "needle" is in "haystack"."""
397 395 if not (3 <= len(args) <= 4):
398 396 # i18n: "ifcontains" is a keyword
399 397 raise error.ParseError(_(b"ifcontains expects three or four arguments"))
400 398
401 399 haystack = evalwrapped(context, mapping, args[1])
402 400 try:
403 401 needle = evalrawexp(context, mapping, args[0])
404 402 found = haystack.contains(context, mapping, needle)
405 403 except error.ParseError:
406 404 found = False
407 405
408 406 if found:
409 407 return evalrawexp(context, mapping, args[2])
410 408 elif len(args) == 4:
411 409 return evalrawexp(context, mapping, args[3])
412 410
413 411
414 412 @templatefunc(b'ifeq(expr1, expr2, then[, else])')
415 413 def ifeq(context, mapping, args):
416 414 """Conditionally execute based on
417 415 whether 2 items are equivalent."""
418 416 if not (3 <= len(args) <= 4):
419 417 # i18n: "ifeq" is a keyword
420 418 raise error.ParseError(_(b"ifeq expects three or four arguments"))
421 419
422 420 test = evalstring(context, mapping, args[0])
423 421 match = evalstring(context, mapping, args[1])
424 422 if test == match:
425 423 return evalrawexp(context, mapping, args[2])
426 424 elif len(args) == 4:
427 425 return evalrawexp(context, mapping, args[3])
428 426
429 427
430 428 @templatefunc(b'join(list, sep)')
431 429 def join(context, mapping, args):
432 430 """Join items in a list with a delimiter."""
433 431 if not (1 <= len(args) <= 2):
434 432 # i18n: "join" is a keyword
435 433 raise error.ParseError(_(b"join expects one or two arguments"))
436 434
437 435 joinset = evalwrapped(context, mapping, args[0])
438 436 joiner = b" "
439 437 if len(args) > 1:
440 438 joiner = evalstring(context, mapping, args[1])
441 439 return joinset.join(context, mapping, joiner)
442 440
443 441
444 442 @templatefunc(b'label(label, expr)', requires={b'ui'})
445 443 def label(context, mapping, args):
446 444 """Apply a label to generated content. Content with
447 445 a label applied can result in additional post-processing, such as
448 446 automatic colorization."""
449 447 if len(args) != 2:
450 448 # i18n: "label" is a keyword
451 449 raise error.ParseError(_(b"label expects two arguments"))
452 450
453 451 ui = context.resource(mapping, b'ui')
454 452 thing = evalstring(context, mapping, args[1])
455 453 # preserve unknown symbol as literal so effects like 'red', 'bold',
456 454 # etc. don't need to be quoted
457 455 label = evalstringliteral(context, mapping, args[0])
458 456
459 457 return ui.label(thing, label)
460 458
461 459
462 460 @templatefunc(b'latesttag([pattern])')
463 461 def latesttag(context, mapping, args):
464 462 """The global tags matching the given pattern on the
465 463 most recent globally tagged ancestor of this changeset.
466 464 If no such tags exist, the "{tag}" template resolves to
467 465 the string "null". See :hg:`help revisions.patterns` for the pattern
468 466 syntax.
469 467 """
470 468 if len(args) > 1:
471 469 # i18n: "latesttag" is a keyword
472 470 raise error.ParseError(_(b"latesttag expects at most one argument"))
473 471
474 472 pattern = None
475 473 if len(args) == 1:
476 474 pattern = evalstring(context, mapping, args[0])
477 475 return templatekw.showlatesttags(context, mapping, pattern)
478 476
479 477
480 478 @templatefunc(b'localdate(date[, tz])')
481 479 def localdate(context, mapping, args):
482 480 """Converts a date to the specified timezone.
483 481 The default is local date."""
484 482 if not (1 <= len(args) <= 2):
485 483 # i18n: "localdate" is a keyword
486 484 raise error.ParseError(_(b"localdate expects one or two arguments"))
487 485
488 486 date = evaldate(
489 487 context,
490 488 mapping,
491 489 args[0],
492 490 # i18n: "localdate" is a keyword
493 491 _(b"localdate expects a date information"),
494 492 )
495 493 if len(args) >= 2:
496 494 tzoffset = None
497 495 tz = evalfuncarg(context, mapping, args[1])
498 496 if isinstance(tz, bytes):
499 497 tzoffset, remainder = dateutil.parsetimezone(tz)
500 498 if remainder:
501 499 tzoffset = None
502 500 if tzoffset is None:
503 501 try:
504 502 tzoffset = int(tz)
505 503 except (TypeError, ValueError):
506 504 # i18n: "localdate" is a keyword
507 505 raise error.ParseError(_(b"localdate expects a timezone"))
508 506 else:
509 507 tzoffset = dateutil.makedate()[1]
510 508 return templateutil.date((date[0], tzoffset))
511 509
512 510
513 511 @templatefunc(b'max(iterable)')
514 512 def max_(context, mapping, args, **kwargs):
515 513 """Return the max of an iterable"""
516 514 if len(args) != 1:
517 515 # i18n: "max" is a keyword
518 516 raise error.ParseError(_(b"max expects one argument"))
519 517
520 518 iterable = evalwrapped(context, mapping, args[0])
521 519 try:
522 520 return iterable.getmax(context, mapping)
523 521 except error.ParseError as err:
524 522 # i18n: "max" is a keyword
525 523 hint = _(b"max first argument should be an iterable")
526 524 raise error.ParseError(bytes(err), hint=hint)
527 525
528 526
529 527 @templatefunc(b'min(iterable)')
530 528 def min_(context, mapping, args, **kwargs):
531 529 """Return the min of an iterable"""
532 530 if len(args) != 1:
533 531 # i18n: "min" is a keyword
534 532 raise error.ParseError(_(b"min expects one argument"))
535 533
536 534 iterable = evalwrapped(context, mapping, args[0])
537 535 try:
538 536 return iterable.getmin(context, mapping)
539 537 except error.ParseError as err:
540 538 # i18n: "min" is a keyword
541 539 hint = _(b"min first argument should be an iterable")
542 540 raise error.ParseError(bytes(err), hint=hint)
543 541
544 542
545 543 @templatefunc(b'mod(a, b)')
546 544 def mod(context, mapping, args):
547 545 """Calculate a mod b such that a / b + a mod b == a"""
548 546 if not len(args) == 2:
549 547 # i18n: "mod" is a keyword
550 548 raise error.ParseError(_(b"mod expects two arguments"))
551 549
552 550 func = lambda a, b: a % b
553 551 return templateutil.runarithmetic(
554 552 context, mapping, (func, args[0], args[1])
555 553 )
556 554
557 555
558 556 @templatefunc(b'obsfateoperations(markers)')
559 557 def obsfateoperations(context, mapping, args):
560 558 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
561 559 if len(args) != 1:
562 560 # i18n: "obsfateoperations" is a keyword
563 561 raise error.ParseError(_(b"obsfateoperations expects one argument"))
564 562
565 563 markers = evalfuncarg(context, mapping, args[0])
566 564
567 565 try:
568 566 data = obsutil.markersoperations(markers)
569 567 return templateutil.hybridlist(data, name=b'operation')
570 568 except (TypeError, KeyError):
571 569 # i18n: "obsfateoperations" is a keyword
572 570 errmsg = _(b"obsfateoperations first argument should be an iterable")
573 571 raise error.ParseError(errmsg)
574 572
575 573
576 574 @templatefunc(b'obsfatedate(markers)')
577 575 def obsfatedate(context, mapping, args):
578 576 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
579 577 if len(args) != 1:
580 578 # i18n: "obsfatedate" is a keyword
581 579 raise error.ParseError(_(b"obsfatedate expects one argument"))
582 580
583 581 markers = evalfuncarg(context, mapping, args[0])
584 582
585 583 try:
586 584 # TODO: maybe this has to be a wrapped list of date wrappers?
587 585 data = obsutil.markersdates(markers)
588 586 return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d')
589 587 except (TypeError, KeyError):
590 588 # i18n: "obsfatedate" is a keyword
591 589 errmsg = _(b"obsfatedate first argument should be an iterable")
592 590 raise error.ParseError(errmsg)
593 591
594 592
595 593 @templatefunc(b'obsfateusers(markers)')
596 594 def obsfateusers(context, mapping, args):
597 595 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
598 596 if len(args) != 1:
599 597 # i18n: "obsfateusers" is a keyword
600 598 raise error.ParseError(_(b"obsfateusers expects one argument"))
601 599
602 600 markers = evalfuncarg(context, mapping, args[0])
603 601
604 602 try:
605 603 data = obsutil.markersusers(markers)
606 604 return templateutil.hybridlist(data, name=b'user')
607 605 except (TypeError, KeyError, ValueError):
608 606 # i18n: "obsfateusers" is a keyword
609 607 msg = _(
610 608 b"obsfateusers first argument should be an iterable of "
611 609 b"obsmakers"
612 610 )
613 611 raise error.ParseError(msg)
614 612
615 613
616 614 @templatefunc(b'obsfateverb(successors, markers)')
617 615 def obsfateverb(context, mapping, args):
618 616 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
619 617 if len(args) != 2:
620 618 # i18n: "obsfateverb" is a keyword
621 619 raise error.ParseError(_(b"obsfateverb expects two arguments"))
622 620
623 621 successors = evalfuncarg(context, mapping, args[0])
624 622 markers = evalfuncarg(context, mapping, args[1])
625 623
626 624 try:
627 625 return obsutil.obsfateverb(successors, markers)
628 626 except TypeError:
629 627 # i18n: "obsfateverb" is a keyword
630 628 errmsg = _(b"obsfateverb first argument should be countable")
631 629 raise error.ParseError(errmsg)
632 630
633 631
634 632 @templatefunc(b'relpath(path)', requires={b'repo'})
635 633 def relpath(context, mapping, args):
636 634 """Convert a repository-absolute path into a filesystem path relative to
637 635 the current working directory."""
638 636 if len(args) != 1:
639 637 # i18n: "relpath" is a keyword
640 638 raise error.ParseError(_(b"relpath expects one argument"))
641 639
642 640 repo = context.resource(mapping, b'repo')
643 641 path = evalstring(context, mapping, args[0])
644 642 return repo.pathto(path)
645 643
646 644
647 645 @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'})
648 646 def revset(context, mapping, args):
649 647 """Execute a revision set query. See
650 648 :hg:`help revset`."""
651 649 if not len(args) > 0:
652 650 # i18n: "revset" is a keyword
653 651 raise error.ParseError(_(b"revset expects one or more arguments"))
654 652
655 653 raw = evalstring(context, mapping, args[0])
656 654 repo = context.resource(mapping, b'repo')
657 655
658 656 def query(expr):
659 657 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
660 658 return m(repo)
661 659
662 660 if len(args) > 1:
663 661 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
664 662 revs = query(revsetlang.formatspec(raw, *formatargs))
665 663 else:
666 664 cache = context.resource(mapping, b'cache')
667 665 revsetcache = cache.setdefault(b"revsetcache", {})
668 666 if raw in revsetcache:
669 667 revs = revsetcache[raw]
670 668 else:
671 669 revs = query(raw)
672 670 revsetcache[raw] = revs
673 671 return templatekw.showrevslist(context, mapping, b"revision", revs)
674 672
675 673
676 674 @templatefunc(b'rstdoc(text, style)')
677 675 def rstdoc(context, mapping, args):
678 676 """Format reStructuredText."""
679 677 if len(args) != 2:
680 678 # i18n: "rstdoc" is a keyword
681 679 raise error.ParseError(_(b"rstdoc expects two arguments"))
682 680
683 681 text = evalstring(context, mapping, args[0])
684 682 style = evalstring(context, mapping, args[1])
685 683
686 684 return minirst.format(text, style=style, keep=[b'verbose'])
687 685
688 686
689 687 @templatefunc(b'search(pattern, text)')
690 688 def search(context, mapping, args):
691 689 """Look for the first text matching the regular expression pattern.
692 690 Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template."""
693 691 if len(args) != 2:
694 692 # i18n: "search" is a keyword
695 693 raise error.ParseError(_(b'search expects two arguments'))
696 694
697 695 pat = evalstring(context, mapping, args[0])
698 696 src = evalstring(context, mapping, args[1])
699 697 try:
700 698 patre = re.compile(pat)
701 699 except re.error:
702 700 # i18n: "search" is a keyword
703 701 raise error.ParseError(_(b'search got an invalid pattern: %s') % pat)
704 702 # named groups shouldn't shadow *reserved* resource keywords
705 703 badgroups = context.knownresourcekeys() & set(
706 704 pycompat.byteskwargs(patre.groupindex)
707 705 )
708 706 if badgroups:
709 707 raise error.ParseError(
710 708 # i18n: "search" is a keyword
711 709 _(b'invalid group %(group)s in search pattern: %(pat)s')
712 710 % {
713 711 b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)),
714 712 b'pat': pat,
715 713 }
716 714 )
717 715
718 716 match = patre.search(src)
719 717 if not match:
720 718 return templateutil.mappingnone()
721 719
722 720 lm = {b'0': match.group(0)}
723 721 lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
724 722 lm.update(pycompat.byteskwargs(match.groupdict()))
725 723 return templateutil.mappingdict(lm, tmpl=b'{0}')
726 724
727 725
728 726 @templatefunc(b'separate(sep, args...)', argspec=b'sep *args')
729 727 def separate(context, mapping, args):
730 728 """Add a separator between non-empty arguments."""
731 729 if b'sep' not in args:
732 730 # i18n: "separate" is a keyword
733 731 raise error.ParseError(_(b"separate expects at least one argument"))
734 732
735 733 sep = evalstring(context, mapping, args[b'sep'])
736 734 first = True
737 735 for arg in args[b'args']:
738 736 argstr = evalstring(context, mapping, arg)
739 737 if not argstr:
740 738 continue
741 739 if first:
742 740 first = False
743 741 else:
744 742 yield sep
745 743 yield argstr
746 744
747 745
748 746 @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'})
749 747 def shortest(context, mapping, args):
750 748 """Obtain the shortest representation of
751 749 a node."""
752 750 if not (1 <= len(args) <= 2):
753 751 # i18n: "shortest" is a keyword
754 752 raise error.ParseError(_(b"shortest() expects one or two arguments"))
755 753
756 754 hexnode = evalstring(context, mapping, args[0])
757 755
758 756 minlength = 4
759 757 if len(args) > 1:
760 758 minlength = evalinteger(
761 759 context,
762 760 mapping,
763 761 args[1],
764 762 # i18n: "shortest" is a keyword
765 763 _(b"shortest() expects an integer minlength"),
766 764 )
767 765
768 766 repo = context.resource(mapping, b'repo')
769 767 if len(hexnode) > 40:
770 768 return hexnode
771 769 elif len(hexnode) == 40:
772 770 try:
773 771 node = bin(hexnode)
774 772 except TypeError:
775 773 return hexnode
776 774 else:
777 775 try:
778 776 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
779 777 except error.WdirUnsupported:
780 778 node = wdirid
781 779 except error.LookupError:
782 780 return hexnode
783 781 if not node:
784 782 return hexnode
785 783 cache = context.resource(mapping, b'cache')
786 784 try:
787 785 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
788 786 except error.RepoLookupError:
789 787 return hexnode
790 788
791 789
792 790 @templatefunc(b'strip(text[, chars])')
793 791 def strip(context, mapping, args):
794 792 """Strip characters from a string. By default,
795 793 strips all leading and trailing whitespace."""
796 794 if not (1 <= len(args) <= 2):
797 795 # i18n: "strip" is a keyword
798 796 raise error.ParseError(_(b"strip expects one or two arguments"))
799 797
800 798 text = evalstring(context, mapping, args[0])
801 799 if len(args) == 2:
802 800 chars = evalstring(context, mapping, args[1])
803 801 return text.strip(chars)
804 802 return text.strip()
805 803
806 804
807 805 @templatefunc(b'sub(pattern, replacement, expression)')
808 806 def sub(context, mapping, args):
809 807 """Perform text substitution
810 808 using regular expressions."""
811 809 if len(args) != 3:
812 810 # i18n: "sub" is a keyword
813 811 raise error.ParseError(_(b"sub expects three arguments"))
814 812
815 813 pat = evalstring(context, mapping, args[0])
816 814 rpl = evalstring(context, mapping, args[1])
817 815 src = evalstring(context, mapping, args[2])
818 816 try:
819 817 patre = re.compile(pat)
820 818 except re.error:
821 819 # i18n: "sub" is a keyword
822 820 raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat)
823 821 try:
824 822 yield patre.sub(rpl, src)
825 823 except re.error:
826 824 # i18n: "sub" is a keyword
827 825 raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl)
828 826
829 827
830 828 @templatefunc(b'startswith(pattern, text)')
831 829 def startswith(context, mapping, args):
832 830 """Returns the value from the "text" argument
833 831 if it begins with the content from the "pattern" argument."""
834 832 if len(args) != 2:
835 833 # i18n: "startswith" is a keyword
836 834 raise error.ParseError(_(b"startswith expects two arguments"))
837 835
838 836 patn = evalstring(context, mapping, args[0])
839 837 text = evalstring(context, mapping, args[1])
840 838 if text.startswith(patn):
841 839 return text
842 840 return b''
843 841
844 842
845 843 @templatefunc(b'word(number, text[, separator])')
846 844 def word(context, mapping, args):
847 845 """Return the nth word from a string."""
848 846 if not (2 <= len(args) <= 3):
849 847 # i18n: "word" is a keyword
850 848 raise error.ParseError(
851 849 _(b"word expects two or three arguments, got %d") % len(args)
852 850 )
853 851
854 852 num = evalinteger(
855 853 context,
856 854 mapping,
857 855 args[0],
858 856 # i18n: "word" is a keyword
859 857 _(b"word expects an integer index"),
860 858 )
861 859 text = evalstring(context, mapping, args[1])
862 860 if len(args) == 3:
863 861 splitter = evalstring(context, mapping, args[2])
864 862 else:
865 863 splitter = None
866 864
867 865 tokens = text.split(splitter)
868 866 if num >= len(tokens) or num < -len(tokens):
869 867 return b''
870 868 else:
871 869 return tokens[num]
872 870
873 871
874 872 def loadfunction(ui, extname, registrarobj):
875 873 """Load template function from specified registrarobj
876 874 """
877 875 for name, func in pycompat.iteritems(registrarobj._table):
878 876 funcs[name] = func
879 877
880 878
881 879 # tell hggettext to extract docstrings from these functions:
882 880 i18nfunctions = funcs.values()
@@ -1,14 +1,17 b''
1 1 == New Features ==
2 2
3 3
4 4 == New Experimental Features ==
5 5
6 6
7 7 == Bug Fixes ==
8 8
9 * The `indent()` template function was documented to not indent empty lines,
10 but it still indented the first line even if it was empty. It no longer does
11 that.
9 12
10 13 == Backwards Compatibility Changes ==
11 14
12 15
13 16 == Internal API Changes ==
14 17
@@ -1,1653 +1,1653 b''
1 1 Test template filters and functions
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 Second branch starting at nullrev:
36 36
37 37 $ hg update null
38 38 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
39 39 $ echo second > second
40 40 $ hg add second
41 41 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
42 42 created new head
43 43
44 44 $ echo third > third
45 45 $ hg add third
46 46 $ hg mv second fourth
47 47 $ hg commit -m third -d "2020-01-01 10:01"
48 48
49 49 $ hg phase -r 5 --public
50 50 $ hg phase -r 7 --secret --force
51 51
52 52 Filters work:
53 53
54 54 $ hg log --template '{author|domain}\n'
55 55
56 56 hostname
57 57
58 58
59 59
60 60
61 61 place
62 62 place
63 63 hostname
64 64
65 65 $ hg log --template '{author|person}\n'
66 66 test
67 67 User Name
68 68 person
69 69 person
70 70 person
71 71 person
72 72 other
73 73 A. N. Other
74 74 User Name
75 75
76 76 $ hg log --template '{author|user}\n'
77 77 test
78 78 user
79 79 person
80 80 person
81 81 person
82 82 person
83 83 other
84 84 other
85 85 user
86 86
87 87 $ hg log --template '{date|date}\n'
88 88 Wed Jan 01 10:01:00 2020 +0000
89 89 Mon Jan 12 13:46:40 1970 +0000
90 90 Sun Jan 18 08:40:01 1970 +0000
91 91 Sun Jan 18 08:40:00 1970 +0000
92 92 Sat Jan 17 04:53:20 1970 +0000
93 93 Fri Jan 16 01:06:40 1970 +0000
94 94 Wed Jan 14 21:20:00 1970 +0000
95 95 Tue Jan 13 17:33:20 1970 +0000
96 96 Mon Jan 12 13:46:40 1970 +0000
97 97
98 98 $ hg log --template '{date|isodate}\n'
99 99 2020-01-01 10:01 +0000
100 100 1970-01-12 13:46 +0000
101 101 1970-01-18 08:40 +0000
102 102 1970-01-18 08:40 +0000
103 103 1970-01-17 04:53 +0000
104 104 1970-01-16 01:06 +0000
105 105 1970-01-14 21:20 +0000
106 106 1970-01-13 17:33 +0000
107 107 1970-01-12 13:46 +0000
108 108
109 109 $ hg log --template '{date|isodatesec}\n'
110 110 2020-01-01 10:01:00 +0000
111 111 1970-01-12 13:46:40 +0000
112 112 1970-01-18 08:40:01 +0000
113 113 1970-01-18 08:40:00 +0000
114 114 1970-01-17 04:53:20 +0000
115 115 1970-01-16 01:06:40 +0000
116 116 1970-01-14 21:20:00 +0000
117 117 1970-01-13 17:33:20 +0000
118 118 1970-01-12 13:46:40 +0000
119 119
120 120 $ hg log --template '{date|rfc822date}\n'
121 121 Wed, 01 Jan 2020 10:01:00 +0000
122 122 Mon, 12 Jan 1970 13:46:40 +0000
123 123 Sun, 18 Jan 1970 08:40:01 +0000
124 124 Sun, 18 Jan 1970 08:40:00 +0000
125 125 Sat, 17 Jan 1970 04:53:20 +0000
126 126 Fri, 16 Jan 1970 01:06:40 +0000
127 127 Wed, 14 Jan 1970 21:20:00 +0000
128 128 Tue, 13 Jan 1970 17:33:20 +0000
129 129 Mon, 12 Jan 1970 13:46:40 +0000
130 130
131 131 $ hg log --template '{desc|firstline}\n'
132 132 third
133 133 second
134 134 merge
135 135 new head
136 136 new branch
137 137 no user, no domain
138 138 no person
139 139 other 1
140 140 line 1
141 141
142 142 $ hg log --template '{node|short}\n'
143 143 95c24699272e
144 144 29114dbae42b
145 145 d41e714fe50d
146 146 13207e5a10d9
147 147 bbe44766e73d
148 148 10e46f2dcbf4
149 149 97054abb4ab8
150 150 b608e9d1a3f0
151 151 1e4e1b8f71e0
152 152
153 153 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
154 154 <changeset author="test"/>
155 155 <changeset author="User Name &lt;user@hostname&gt;"/>
156 156 <changeset author="person"/>
157 157 <changeset author="person"/>
158 158 <changeset author="person"/>
159 159 <changeset author="person"/>
160 160 <changeset author="other@place"/>
161 161 <changeset author="A. N. Other &lt;other@place&gt;"/>
162 162 <changeset author="User Name &lt;user@hostname&gt;"/>
163 163
164 164 $ hg log --template '{rev}: {children}\n'
165 165 8:
166 166 7: 8:95c24699272e
167 167 6:
168 168 5: 6:d41e714fe50d
169 169 4: 6:d41e714fe50d
170 170 3: 4:bbe44766e73d 5:13207e5a10d9
171 171 2: 3:10e46f2dcbf4
172 172 1: 2:97054abb4ab8
173 173 0: 1:b608e9d1a3f0
174 174
175 175 Formatnode filter works:
176 176
177 177 $ hg -q log -r 0 --template '{node|formatnode}\n'
178 178 1e4e1b8f71e0
179 179
180 180 $ hg log -r 0 --template '{node|formatnode}\n'
181 181 1e4e1b8f71e0
182 182
183 183 $ hg -v log -r 0 --template '{node|formatnode}\n'
184 184 1e4e1b8f71e0
185 185
186 186 $ hg --debug log -r 0 --template '{node|formatnode}\n'
187 187 1e4e1b8f71e05681d422154f5421e385fec3454f
188 188
189 189 Age filter:
190 190
191 191 $ hg init unstable-hash
192 192 $ cd unstable-hash
193 193 $ hg log --template '{date|age}\n' > /dev/null || exit 1
194 194
195 195 >>> from __future__ import absolute_import
196 196 >>> import datetime
197 197 >>> fp = open('a', 'wb')
198 198 >>> n = datetime.datetime.now() + datetime.timedelta(366 * 7)
199 199 >>> fp.write(b'%d-%d-%d 00:00' % (n.year, n.month, n.day)) and None
200 200 >>> fp.close()
201 201 $ hg add a
202 202 $ hg commit -m future -d "`cat a`"
203 203
204 204 $ hg log -l1 --template '{date|age}\n'
205 205 7 years from now
206 206
207 207 $ cd ..
208 208 $ rm -rf unstable-hash
209 209
210 210 Filename filters:
211 211
212 212 $ hg debugtemplate '{"foo/bar"|basename}|{"foo/"|basename}|{"foo"|basename}|\n'
213 213 bar||foo|
214 214 $ hg debugtemplate '{"foo/bar"|dirname}|{"foo/"|dirname}|{"foo"|dirname}|\n'
215 215 foo|foo||
216 216 $ hg debugtemplate '{"foo/bar"|stripdir}|{"foo/"|stripdir}|{"foo"|stripdir}|\n'
217 217 foo|foo|foo|
218 218
219 219 commondir() filter:
220 220
221 221 $ hg debugtemplate '{""|splitlines|commondir}\n'
222 222
223 223 $ hg debugtemplate '{"foo/bar\nfoo/baz\nfoo/foobar\n"|splitlines|commondir}\n'
224 224 foo
225 225 $ hg debugtemplate '{"foo/bar\nfoo/bar\n"|splitlines|commondir}\n'
226 226 foo
227 227 $ hg debugtemplate '{"/foo/bar\n/foo/bar\n"|splitlines|commondir}\n'
228 228 foo
229 229 $ hg debugtemplate '{"/foo\n/foo\n"|splitlines|commondir}\n'
230 230
231 231 $ hg debugtemplate '{"foo/bar\nbar/baz"|splitlines|commondir}\n'
232 232
233 233 $ hg debugtemplate '{"foo/bar\nbar/baz\nbar/foo\n"|splitlines|commondir}\n'
234 234
235 235 $ hg debugtemplate '{"foo/../bar\nfoo/bar"|splitlines|commondir}\n'
236 236 foo
237 237 $ hg debugtemplate '{"foo\n/foo"|splitlines|commondir}\n'
238 238
239 239
240 240 $ hg log -r null -T '{rev|commondir}'
241 241 hg: parse error: argument is not a list of text
242 242 (template filter 'commondir' is not compatible with keyword 'rev')
243 243 [255]
244 244
245 245 Add a dummy commit to make up for the instability of the above:
246 246
247 247 $ echo a > a
248 248 $ hg add a
249 249 $ hg ci -m future
250 250
251 251 Count filter:
252 252
253 253 $ hg log -l1 --template '{node|count} {node|short|count}\n'
254 254 40 12
255 255
256 256 $ hg log -l1 --template '{revset("null^")|count} {revset(".")|count} {revset("0::3")|count}\n'
257 257 0 1 4
258 258
259 259 $ hg log -G --template '{rev}: children: {children|count}, \
260 260 > tags: {tags|count}, file_adds: {file_adds|count}, \
261 261 > ancestors: {revset("ancestors(%s)", rev)|count}'
262 262 @ 9: children: 0, tags: 1, file_adds: 1, ancestors: 3
263 263 |
264 264 o 8: children: 1, tags: 0, file_adds: 2, ancestors: 2
265 265 |
266 266 o 7: children: 1, tags: 0, file_adds: 1, ancestors: 1
267 267
268 268 o 6: children: 0, tags: 0, file_adds: 0, ancestors: 7
269 269 |\
270 270 | o 5: children: 1, tags: 0, file_adds: 1, ancestors: 5
271 271 | |
272 272 o | 4: children: 1, tags: 0, file_adds: 0, ancestors: 5
273 273 |/
274 274 o 3: children: 2, tags: 0, file_adds: 0, ancestors: 4
275 275 |
276 276 o 2: children: 1, tags: 0, file_adds: 1, ancestors: 3
277 277 |
278 278 o 1: children: 1, tags: 0, file_adds: 1, ancestors: 2
279 279 |
280 280 o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1
281 281
282 282
283 283 $ hg log -l1 -T '{termwidth|count}\n'
284 284 hg: parse error: not countable
285 285 (template filter 'count' is not compatible with keyword 'termwidth')
286 286 [255]
287 287
288 288 Upper/lower filters:
289 289
290 290 $ hg log -r0 --template '{branch|upper}\n'
291 291 DEFAULT
292 292 $ hg log -r0 --template '{author|lower}\n'
293 293 user name <user@hostname>
294 294 $ hg log -r0 --template '{date|upper}\n'
295 295 1000000.00
296 296
297 297 Add a commit that does all possible modifications at once
298 298
299 299 $ echo modify >> third
300 300 $ touch b
301 301 $ hg add b
302 302 $ hg mv fourth fifth
303 303 $ hg rm a
304 304 $ hg ci -m "Modify, add, remove, rename"
305 305
306 306 Pass generator object created by template function to filter
307 307
308 308 $ hg log -l 1 --template '{if(author, author)|user}\n'
309 309 test
310 310
311 311 Test diff function:
312 312
313 313 $ hg diff -c 8
314 314 diff -r 29114dbae42b -r 95c24699272e fourth
315 315 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
316 316 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
317 317 @@ -0,0 +1,1 @@
318 318 +second
319 319 diff -r 29114dbae42b -r 95c24699272e second
320 320 --- a/second Mon Jan 12 13:46:40 1970 +0000
321 321 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
322 322 @@ -1,1 +0,0 @@
323 323 -second
324 324 diff -r 29114dbae42b -r 95c24699272e third
325 325 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
326 326 +++ b/third Wed Jan 01 10:01:00 2020 +0000
327 327 @@ -0,0 +1,1 @@
328 328 +third
329 329
330 330 $ hg log -r 8 -T "{diff()}"
331 331 diff -r 29114dbae42b -r 95c24699272e fourth
332 332 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
333 333 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
334 334 @@ -0,0 +1,1 @@
335 335 +second
336 336 diff -r 29114dbae42b -r 95c24699272e second
337 337 --- a/second Mon Jan 12 13:46:40 1970 +0000
338 338 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
339 339 @@ -1,1 +0,0 @@
340 340 -second
341 341 diff -r 29114dbae42b -r 95c24699272e third
342 342 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
343 343 +++ b/third Wed Jan 01 10:01:00 2020 +0000
344 344 @@ -0,0 +1,1 @@
345 345 +third
346 346
347 347 $ hg log -r 8 -T "{diff('glob:f*')}"
348 348 diff -r 29114dbae42b -r 95c24699272e fourth
349 349 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
350 350 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
351 351 @@ -0,0 +1,1 @@
352 352 +second
353 353
354 354 $ hg log -r 8 -T "{diff('', 'glob:f*')}"
355 355 diff -r 29114dbae42b -r 95c24699272e second
356 356 --- a/second Mon Jan 12 13:46:40 1970 +0000
357 357 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
358 358 @@ -1,1 +0,0 @@
359 359 -second
360 360 diff -r 29114dbae42b -r 95c24699272e third
361 361 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
362 362 +++ b/third Wed Jan 01 10:01:00 2020 +0000
363 363 @@ -0,0 +1,1 @@
364 364 +third
365 365
366 366 $ hg log -r 8 -T "{diff('FOURTH'|lower)}"
367 367 diff -r 29114dbae42b -r 95c24699272e fourth
368 368 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
369 369 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
370 370 @@ -0,0 +1,1 @@
371 371 +second
372 372
373 373 $ hg --config diff.git=true log -r 8 -T "{diff()}"
374 374 diff --git a/second b/fourth
375 375 rename from second
376 376 rename to fourth
377 377 diff --git a/third b/third
378 378 new file mode 100644
379 379 --- /dev/null
380 380 +++ b/third
381 381 @@ -0,0 +1,1 @@
382 382 +third
383 383
384 384 $ cd ..
385 385
386 386 latesttag() function:
387 387
388 388 $ hg init latesttag
389 389 $ cd latesttag
390 390
391 391 $ echo a > file
392 392 $ hg ci -Am a -d '0 0'
393 393 adding file
394 394
395 395 $ echo b >> file
396 396 $ hg ci -m b -d '1 0'
397 397
398 398 $ echo c >> head1
399 399 $ hg ci -Am h1c -d '2 0'
400 400 adding head1
401 401
402 402 $ hg update -q 1
403 403 $ echo d >> head2
404 404 $ hg ci -Am h2d -d '3 0'
405 405 adding head2
406 406 created new head
407 407
408 408 $ echo e >> head2
409 409 $ hg ci -m h2e -d '4 0'
410 410
411 411 $ hg merge -q
412 412 $ hg ci -m merge -d '5 -3600'
413 413
414 414 $ hg tag -r 1 -m t1 -d '6 0' t1
415 415 $ hg tag -r 2 -m t2 -d '7 0' t2
416 416 $ hg tag -r 3 -m t3 -d '8 0' t3
417 417 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
418 418 $ hg tag -r 5 -m t5 -d '9 0' t5
419 419 $ hg tag -r 3 -m at3 -d '10 0' at3
420 420
421 421 $ hg log -G --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
422 422 @ 11: t3, C: 9, D: 8
423 423 |
424 424 o 10: t3, C: 8, D: 7
425 425 |
426 426 o 9: t3, C: 7, D: 6
427 427 |
428 428 o 8: t3, C: 6, D: 5
429 429 |
430 430 o 7: t3, C: 5, D: 4
431 431 |
432 432 o 6: t3, C: 4, D: 3
433 433 |
434 434 o 5: t3, C: 3, D: 2
435 435 |\
436 436 | o 4: t3, C: 1, D: 1
437 437 | |
438 438 | o 3: t3, C: 0, D: 0
439 439 | |
440 440 o | 2: t1, C: 1, D: 1
441 441 |/
442 442 o 1: t1, C: 0, D: 0
443 443 |
444 444 o 0: null, C: 1, D: 1
445 445
446 446
447 447 $ cd ..
448 448
449 449 Test filter() empty values:
450 450
451 451 $ hg log -R a -r 1 -T '{filter(desc|splitlines) % "{line}\n"}'
452 452 other 1
453 453 other 2
454 454 other 3
455 455 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1) % "{ifeq(key, "a", "{value}\n")}")}'
456 456 0
457 457
458 458 0 should not be falsy
459 459
460 460 $ hg log -R a -r 0 -T '{filter(revset("0:2"))}\n'
461 461 0 1 2
462 462
463 463 Test filter() by expression:
464 464
465 465 $ hg log -R a -r 1 -T '{filter(desc|splitlines, ifcontains("1", line, "t"))}\n'
466 466 other 1
467 467 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1), ifeq(key, "b", "t"))}\n'
468 468 b=1
469 469
470 470 Test filter() shouldn't crash:
471 471
472 472 $ hg log -R a -r 0 -T '{filter(extras)}\n'
473 473 branch=default
474 474 $ hg log -R a -r 0 -T '{filter(files)}\n'
475 475 a
476 476
477 477 Test filter() unsupported arguments:
478 478
479 479 $ hg log -R a -r 0 -T '{filter()}\n'
480 480 hg: parse error: filter expects one or two arguments
481 481 [255]
482 482 $ hg log -R a -r 0 -T '{filter(date)}\n'
483 483 hg: parse error: date is not iterable
484 484 [255]
485 485 $ hg log -R a -r 0 -T '{filter(rev)}\n'
486 486 hg: parse error: 0 is not iterable
487 487 [255]
488 488 $ hg log -R a -r 0 -T '{filter(desc|firstline)}\n'
489 489 hg: parse error: 'line 1' is not filterable
490 490 [255]
491 491 $ hg log -R a -r 0 -T '{filter(manifest)}\n'
492 492 hg: parse error: '0:a0c8bcbbb45c' is not filterable
493 493 [255]
494 494 $ hg log -R a -r 0 -T '{filter(succsandmarkers)}\n'
495 495 hg: parse error: not filterable without template
496 496 [255]
497 497 $ hg log -R a -r 0 -T '{filter(desc|splitlines % "{line}", "")}\n'
498 498 hg: parse error: not filterable by expression
499 499 [255]
500 500
501 501 Test manifest/get() can be join()-ed as string, though it's silly:
502 502
503 503 $ hg log -R latesttag -r tip -T '{join(manifest, ".")}\n'
504 504 1.1.:.2.b.c.6.e.9.0.0.6.c.e.2
505 505 $ hg log -R latesttag -r tip -T '{join(get(extras, "branch"), ".")}\n'
506 506 d.e.f.a.u.l.t
507 507
508 508 Test join() over string
509 509
510 510 $ hg log -R latesttag -r tip -T '{join(rev|stringify, ".")}\n'
511 511 1.1
512 512
513 513 Test join() over uniterable
514 514
515 515 $ hg log -R latesttag -r tip -T '{join(rev, "")}\n'
516 516 hg: parse error: 11 is not iterable
517 517 [255]
518 518
519 519 Test min/max of integers
520 520
521 521 $ hg log -R latesttag -l1 -T '{min(revset("9:10"))}\n'
522 522 9
523 523 $ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
524 524 10
525 525
526 526 Test min/max over map operation:
527 527
528 528 $ hg log -R latesttag -r3 -T '{min(tags % "{tag}")}\n'
529 529 at3
530 530 $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
531 531 t3
532 532
533 533 Test min/max of strings:
534 534
535 535 $ hg log -R latesttag -l1 -T '{min(desc)}\n'
536 536 3
537 537 $ hg log -R latesttag -l1 -T '{max(desc)}\n'
538 538 t
539 539
540 540 Test min/max of non-iterable:
541 541
542 542 $ hg debugtemplate '{min(1)}'
543 543 hg: parse error: 1 is not iterable
544 544 (min first argument should be an iterable)
545 545 [255]
546 546 $ hg debugtemplate '{max(2)}'
547 547 hg: parse error: 2 is not iterable
548 548 (max first argument should be an iterable)
549 549 [255]
550 550
551 551 $ hg log -R latesttag -l1 -T '{min(date)}'
552 552 hg: parse error: date is not iterable
553 553 (min first argument should be an iterable)
554 554 [255]
555 555 $ hg log -R latesttag -l1 -T '{max(date)}'
556 556 hg: parse error: date is not iterable
557 557 (max first argument should be an iterable)
558 558 [255]
559 559
560 560 Test min/max of empty sequence:
561 561
562 562 $ hg debugtemplate '{min("")}'
563 563 hg: parse error: empty string
564 564 (min first argument should be an iterable)
565 565 [255]
566 566 $ hg debugtemplate '{max("")}'
567 567 hg: parse error: empty string
568 568 (max first argument should be an iterable)
569 569 [255]
570 570 $ hg debugtemplate '{min(dict())}'
571 571 hg: parse error: empty sequence
572 572 (min first argument should be an iterable)
573 573 [255]
574 574 $ hg debugtemplate '{max(dict())}'
575 575 hg: parse error: empty sequence
576 576 (max first argument should be an iterable)
577 577 [255]
578 578 $ hg debugtemplate '{min(dict() % "")}'
579 579 hg: parse error: empty sequence
580 580 (min first argument should be an iterable)
581 581 [255]
582 582 $ hg debugtemplate '{max(dict() % "")}'
583 583 hg: parse error: empty sequence
584 584 (max first argument should be an iterable)
585 585 [255]
586 586
587 587 Test min/max of if() result
588 588
589 589 $ cd latesttag
590 590 $ hg log -l1 -T '{min(if(true, revset("9:10"), ""))}\n'
591 591 9
592 592 $ hg log -l1 -T '{max(if(false, "", revset("9:10")))}\n'
593 593 10
594 594 $ hg log -l1 -T '{min(ifcontains("a", "aa", revset("9:10"), ""))}\n'
595 595 9
596 596 $ hg log -l1 -T '{max(ifcontains("a", "bb", "", revset("9:10")))}\n'
597 597 10
598 598 $ hg log -l1 -T '{min(ifeq(0, 0, revset("9:10"), ""))}\n'
599 599 9
600 600 $ hg log -l1 -T '{max(ifeq(0, 1, "", revset("9:10")))}\n'
601 601 10
602 602 $ cd ..
603 603
604 604 Test laziness of if() then/else clause
605 605
606 606 $ hg debugtemplate '{count(0)}'
607 607 hg: parse error: not countable
608 608 (incompatible use of template filter 'count')
609 609 [255]
610 610 $ hg debugtemplate '{if(true, "", count(0))}'
611 611 $ hg debugtemplate '{if(false, count(0), "")}'
612 612 $ hg debugtemplate '{ifcontains("a", "aa", "", count(0))}'
613 613 $ hg debugtemplate '{ifcontains("a", "bb", count(0), "")}'
614 614 $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
615 615 $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
616 616
617 617 Test search() function:
618 618
619 619 $ hg log -R a -r2 -T '{desc}\n'
620 620 no person
621 621
622 622 $ hg log -R a -r2 -T '{search(r"p.*", desc)}\n'
623 623 person
624 624
625 625 as bool
626 626
627 627 $ hg log -R a -r2 -T '{if(search(r"p.*", desc), "", "not ")}found\n'
628 628 found
629 629 $ hg log -R a -r2 -T '{if(search(r"q", desc), "", "not ")}found\n'
630 630 not found
631 631
632 632 match as json
633 633
634 634 $ hg log -R a -r2 -T '{search(r"(no) p.*", desc)|json}\n'
635 635 {"0": "no person", "1": "no"}
636 636 $ hg log -R a -r2 -T '{search(r"q", desc)|json}\n'
637 637 null
638 638
639 639 group reference
640 640
641 641 $ hg log -R a -r2 -T '{search(r"(no) (p.*)", desc) % "{1|upper} {2|hex}"}\n'
642 642 NO 706572736f6e
643 643 $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc) % "{foo}"}\n'
644 644 no
645 645 $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc).foo}\n'
646 646 no
647 647
648 648 group reference with no match
649 649
650 650 $ hg log -R a -r2 -T '{search(r"q", desc) % "match: {0}"}\n'
651 651
652 652
653 653 bad group names
654 654
655 655 $ hg log -R a -r2 -T '{search(r"(?P<0>.)", desc) % "{0}"}\n'
656 656 hg: parse error: search got an invalid pattern: (?P<0>.)
657 657 [255]
658 658 $ hg log -R a -r2 -T '{search(r"(?P<repo>.)", desc) % "{repo}"}\n'
659 659 hg: parse error: invalid group 'repo' in search pattern: (?P<repo>.)
660 660 [255]
661 661
662 662 Test the sub function of templating for expansion:
663 663
664 664 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
665 665 xx
666 666
667 667 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
668 668 hg: parse error: sub got an invalid pattern: [
669 669 [255]
670 670 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
671 671 hg: parse error: sub got an invalid replacement: \1
672 672 [255]
673 673
674 674 Test the strip function with chars specified:
675 675
676 676 $ hg log -R latesttag --template '{desc}\n'
677 677 at3
678 678 t5
679 679 t4
680 680 t3
681 681 t2
682 682 t1
683 683 merge
684 684 h2e
685 685 h2d
686 686 h1c
687 687 b
688 688 a
689 689
690 690 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
691 691 at3
692 692 5
693 693 4
694 694 3
695 695 2
696 696 1
697 697 merg
698 698 h2
699 699 h2d
700 700 h1c
701 701 b
702 702 a
703 703
704 704 Test date format:
705 705
706 706 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
707 707 date: 70 01 01 10 +0000
708 708 date: 70 01 01 09 +0000
709 709 date: 70 01 01 04 +0000
710 710 date: 70 01 01 08 +0000
711 711 date: 70 01 01 07 +0000
712 712 date: 70 01 01 06 +0000
713 713 date: 70 01 01 05 +0100
714 714 date: 70 01 01 04 +0000
715 715 date: 70 01 01 03 +0000
716 716 date: 70 01 01 02 +0000
717 717 date: 70 01 01 01 +0000
718 718 date: 70 01 01 00 +0000
719 719
720 720 Test invalid date:
721 721
722 722 $ hg log -R latesttag -T '{date(rev)}\n'
723 723 hg: parse error: date expects a date information
724 724 [255]
725 725
726 726 Set up repository containing template fragments in commit metadata:
727 727
728 728 $ hg init r
729 729 $ cd r
730 730 $ echo a > a
731 731 $ hg ci -Am '{rev}'
732 732 adding a
733 733
734 734 $ hg branch -q 'text.{rev}'
735 735 $ echo aa >> aa
736 736 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
737 737
738 738 color effect can be specified without quoting:
739 739
740 740 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
741 741 \x1b[0;31mtext\x1b[0m (esc)
742 742
743 743 color effects can be nested (issue5413)
744 744
745 745 $ hg debugtemplate --color=always \
746 746 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
747 747 \x1b[0;31mred\x1b[0;35mma\x1b[0;36mcyan\x1b[0m\x1b[0;31m\x1b[0;35m\x1b[0;33myellow\x1b[0m\x1b[0;31m\x1b[0;35mgenta\x1b[0m (esc)
748 748
749 749 pad() should interact well with color codes (issue5416)
750 750
751 751 $ hg debugtemplate --color=always \
752 752 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
753 753 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
754 754
755 755 pad() with truncate has to strip color codes, though
756 756
757 757 $ hg debugtemplate --color=always \
758 758 > '{pad(label(red, "scarlet"), 5, truncate=true)}\n'
759 759 scarl
760 760
761 761 label should be no-op if color is disabled:
762 762
763 763 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
764 764 text
765 765 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
766 766 text
767 767
768 768 Test branches inside if statement:
769 769
770 770 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
771 771 no
772 772
773 773 Test dict constructor:
774 774
775 775 $ hg log -r 0 -T '{dict(y=node|short, x=rev)}\n'
776 776 y=f7769ec2ab97 x=0
777 777 $ hg log -r 0 -T '{dict(x=rev, y=node|short) % "{key}={value}\n"}'
778 778 x=0
779 779 y=f7769ec2ab97
780 780 $ hg log -r 0 -T '{dict(x=rev, y=node|short)|json}\n'
781 781 {"x": 0, "y": "f7769ec2ab97"}
782 782 $ hg log -r 0 -T '{dict()|json}\n'
783 783 {}
784 784
785 785 $ hg log -r 0 -T '{dict(rev, node=node|short)}\n'
786 786 rev=0 node=f7769ec2ab97
787 787 $ hg log -r 0 -T '{dict(rev, node|short)}\n'
788 788 rev=0 node=f7769ec2ab97
789 789
790 790 $ hg log -r 0 -T '{dict(rev, rev=rev)}\n'
791 791 hg: parse error: duplicated dict key 'rev' inferred
792 792 [255]
793 793 $ hg log -r 0 -T '{dict(node, node|short)}\n'
794 794 hg: parse error: duplicated dict key 'node' inferred
795 795 [255]
796 796 $ hg log -r 0 -T '{dict(1 + 2)}'
797 797 hg: parse error: dict key cannot be inferred
798 798 [255]
799 799
800 800 $ hg log -r 0 -T '{dict(x=rev, x=node)}'
801 801 hg: parse error: dict got multiple values for keyword argument 'x'
802 802 [255]
803 803
804 804 Test get function:
805 805
806 806 $ hg log -r 0 --template '{get(extras, "branch")}\n'
807 807 default
808 808 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
809 809 default
810 810 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
811 811 hg: parse error: not a dictionary
812 812 (get() expects a dict as first argument)
813 813 [255]
814 814
815 815 Test json filter applied to wrapped object:
816 816
817 817 $ hg log -r0 -T '{files|json}\n'
818 818 ["a"]
819 819 $ hg log -r0 -T '{extras|json}\n'
820 820 {"branch": "default"}
821 821 $ hg log -r0 -T '{date|json}\n'
822 822 [0, 0]
823 823
824 824 Test json filter applied to map result:
825 825
826 826 $ hg log -r0 -T '{json(extras % "{key}")}\n'
827 827 ["branch"]
828 828
829 829 Test localdate(date, tz) function:
830 830
831 831 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
832 832 1970-01-01 09:00 +0900
833 833 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
834 834 1970-01-01 00:00 +0000
835 835 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
836 836 hg: parse error: localdate expects a timezone
837 837 [255]
838 838 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
839 839 1970-01-01 02:00 +0200
840 840 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
841 841 1970-01-01 00:00 +0000
842 842 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
843 843 1970-01-01 00:00 +0000
844 844 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
845 845 hg: parse error: localdate expects a timezone
846 846 [255]
847 847 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
848 848 hg: parse error: localdate expects a timezone
849 849 [255]
850 850
851 851 Test shortest(node) function:
852 852
853 853 $ echo b > b
854 854 $ hg ci -qAm b
855 855 $ hg log --template '{shortest(node)}\n'
856 856 e777
857 857 bcc7
858 858 f776
859 859 $ hg log --template '{shortest(node, 10)}\n'
860 860 e777603221
861 861 bcc7ff960b
862 862 f7769ec2ab
863 863 $ hg log --template '{shortest(node, 1)}\n' -r null
864 864 00
865 865 $ hg log --template '{node|shortest}\n' -l1
866 866 e777
867 867
868 868 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
869 869 f7769ec2ab
870 870 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
871 871 hg: parse error: shortest() expects an integer minlength
872 872 [255]
873 873
874 874 $ hg log -r 'wdir()' -T '{node|shortest}\n'
875 875 ffff
876 876
877 877 $ hg log --template '{shortest("f")}\n' -l1
878 878 f
879 879
880 880 $ hg log --template '{shortest("0123456789012345678901234567890123456789")}\n' -l1
881 881 0123456789012345678901234567890123456789
882 882
883 883 $ hg log --template '{shortest("01234567890123456789012345678901234567890123456789")}\n' -l1
884 884 01234567890123456789012345678901234567890123456789
885 885
886 886 $ hg log --template '{shortest("not a hex string")}\n' -l1
887 887 not a hex string
888 888
889 889 $ hg log --template '{shortest("not a hex string, but it'\''s 40 bytes long")}\n' -l1
890 890 not a hex string, but it's 40 bytes long
891 891
892 892 $ hg log --template '{shortest("ffffffffffffffffffffffffffffffffffffffff")}\n' -l1
893 893 ffff
894 894
895 895 $ hg log --template '{shortest("fffffff")}\n' -l1
896 896 ffff
897 897
898 898 $ hg log --template '{shortest("ff")}\n' -l1
899 899 ffff
900 900
901 901 $ cd ..
902 902
903 903 Test shortest(node) with the repo having short hash collision:
904 904
905 905 $ hg init hashcollision
906 906 $ cd hashcollision
907 907 $ cat <<EOF >> .hg/hgrc
908 908 > [experimental]
909 909 > evolution.createmarkers=True
910 910 > EOF
911 911 $ echo 0 > a
912 912 $ hg ci -qAm 0
913 913 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
914 914 > hg up -q 0
915 915 > echo $i > a
916 916 > hg ci -qm $i
917 917 > done
918 918 $ hg up -q null
919 919 $ hg log -r0: -T '{rev}:{node}\n'
920 920 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
921 921 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
922 922 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
923 923 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
924 924 4:10776689e627b465361ad5c296a20a487e153ca4
925 925 5:a00be79088084cb3aff086ab799f8790e01a976b
926 926 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
927 927 7:a0457b3450b8e1b778f1163b31a435802987fe5d
928 928 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
929 929 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
930 930 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
931 931 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
932 932 1 new obsolescence markers
933 933 obsoleted 1 changesets
934 934 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
935 935 1 new obsolescence markers
936 936 obsoleted 1 changesets
937 937 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
938 938 1 new obsolescence markers
939 939 obsoleted 1 changesets
940 940
941 941 nodes starting with '11' (we don't have the revision number '11' though)
942 942
943 943 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
944 944 1:1142
945 945 2:1140
946 946 3:11d
947 947
948 948 '5:a00' is hidden, but still we have two nodes starting with 'a0'
949 949
950 950 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
951 951 6:a0b
952 952 7:a04
953 953
954 954 node '10' conflicts with the revision number '10' even if it is hidden
955 955 (we could exclude hidden revision numbers, but currently we don't)
956 956
957 957 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
958 958 4:107
959 959 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
960 960 4:107
961 961
962 962 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n'
963 963 4:x10
964 964 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
965 965 4:x10
966 966
967 967 node 'c562' should be unique if the other 'c562' nodes are hidden
968 968 (but we don't try the slow path to filter out hidden nodes for now)
969 969
970 970 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
971 971 8:c5625
972 972 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
973 973 8:c5625
974 974 9:c5623
975 975 10:c562d
976 976
977 977 $ cd ..
978 978
979 979 Test prefixhexnode when the first character of the hash is 0.
980 980 $ hg init hashcollision2
981 981 $ cd hashcollision2
982 982 $ cat <<EOF >> .hg/hgrc
983 983 > [experimental]
984 984 > evolution.createmarkers=True
985 985 > EOF
986 986 $ echo 0 > a
987 987 $ hg ci -qAm 0
988 988 $ echo 21 > a
989 989 $ hg ci -qm 21
990 990 $ hg up -q null
991 991 $ hg log -r0: -T '{rev}:{node}\n'
992 992 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
993 993 1:0cf177ba2b1dc3862a00fb81715fec90950201be
994 994
995 995 we need the 'x' prefix to ensure we aren't colliding with rev0. We identify
996 996 the collision with nullid if we aren't using disambiguatewithin, so we need to set
997 997 that as well.
998 998 $ hg --config experimental.revisions.disambiguatewithin='descendants(0)' \
999 999 > --config experimental.revisions.prefixhexnode=yes \
1000 1000 > log -r 1 -T '{rev}:{shortest(node, 0)}\n'
1001 1001 1:x0
1002 1002
1003 1003 $ hg debugobsolete 0cf177ba2b1dc3862a00fb81715fec90950201be
1004 1004 1 new obsolescence markers
1005 1005 obsoleted 1 changesets
1006 1006 $ hg up -q 0
1007 1007 $ echo 61 > a
1008 1008 $ hg ci -m 61
1009 1009 $ hg log -r0: -T '{rev}:{node}\n'
1010 1010 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
1011 1011 2:01384dde84b3a511ae0835f35ac40bd806c99bb8
1012 1012
1013 1013 we still have the 'x' prefix because '0' is still the shortest prefix, since
1014 1014 rev1's '0c' is hidden.
1015 1015 $ hg --config experimental.revisions.disambiguatewithin=0:-1-0 \
1016 1016 > --config experimental.revisions.prefixhexnode=yes \
1017 1017 > log -r 0:-1-0 -T '{rev}:{shortest(node, 0)}\n'
1018 1018 2:x0
1019 1019
1020 1020 we don't have the 'x' prefix on 2 because '01' is not a synonym for rev1.
1021 1021 $ hg --config experimental.revisions.disambiguatewithin=0:-1-0 \
1022 1022 > --config experimental.revisions.prefixhexnode=yes \
1023 1023 > log -r 0:-1-0 -T '{rev}:{shortest(node, 0)}\n' --hidden
1024 1024 1:0c
1025 1025 2:01
1026 1026
1027 1027 $ cd ..
1028 1028
1029 1029 Test pad function
1030 1030
1031 1031 $ cd r
1032 1032
1033 1033 $ hg log --template '{pad(rev, 20)} {author|user}\n'
1034 1034 2 test
1035 1035 1 {node|short}
1036 1036 0 test
1037 1037
1038 1038 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
1039 1039 2 test
1040 1040 1 {node|short}
1041 1041 0 test
1042 1042
1043 1043 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
1044 1044 2------------------- test
1045 1045 1------------------- {node|short}
1046 1046 0------------------- test
1047 1047
1048 1048 $ hg log --template '{pad(author, 5, "-", False, True)}\n'
1049 1049 test-
1050 1050 {node
1051 1051 test-
1052 1052 $ hg log --template '{pad(author, 5, "-", True, True)}\n'
1053 1053 -test
1054 1054 hort}
1055 1055 -test
1056 1056
1057 1057 Test template string in pad function
1058 1058
1059 1059 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
1060 1060 {0} test
1061 1061
1062 1062 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
1063 1063 \{rev} test
1064 1064
1065 1065 Test width argument passed to pad function
1066 1066
1067 1067 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
1068 1068 0 test
1069 1069 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
1070 1070 hg: parse error: pad() expects an integer width
1071 1071 [255]
1072 1072
1073 1073 Test invalid fillchar passed to pad function
1074 1074
1075 1075 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
1076 1076 hg: parse error: pad() expects a single fill character
1077 1077 [255]
1078 1078 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
1079 1079 hg: parse error: pad() expects a single fill character
1080 1080 [255]
1081 1081
1082 1082 Test boolean argument passed to pad function
1083 1083
1084 1084 no crash
1085 1085
1086 1086 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
1087 1087 ---------0
1088 1088
1089 1089 string/literal
1090 1090
1091 1091 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
1092 1092 ---------0
1093 1093 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
1094 1094 0---------
1095 1095 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
1096 1096 0---------
1097 1097
1098 1098 unknown keyword is evaluated to ''
1099 1099
1100 1100 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
1101 1101 0---------
1102 1102
1103 1103 Test separate function
1104 1104
1105 1105 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
1106 1106 a-b-c
1107 1107 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
1108 1108 0:f7769ec2ab97 test default
1109 1109 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
1110 1110 a \x1b[0;31mb\x1b[0m c d (esc)
1111 1111
1112 1112 Test boolean expression/literal passed to if function
1113 1113
1114 1114 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
1115 1115 rev 0 is True
1116 1116 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
1117 1117 literal 0 is True as well
1118 1118 $ hg log -r 0 -T '{if(min(revset(r"0")), "0 of hybriditem is also True")}\n'
1119 1119 0 of hybriditem is also True
1120 1120 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
1121 1121 empty string is False
1122 1122 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
1123 1123 empty list is False
1124 1124 $ hg log -r 0 -T '{if(revset(r"0"), "non-empty list is True")}\n'
1125 1125 non-empty list is True
1126 1126 $ hg log -r 0 -T '{if(revset(r"0") % "", "list of empty strings is True")}\n'
1127 1127 list of empty strings is True
1128 1128 $ hg log -r 0 -T '{if(true, "true is True")}\n'
1129 1129 true is True
1130 1130 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
1131 1131 false is False
1132 1132 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
1133 1133 non-empty string is True
1134 1134
1135 1135 Test ifcontains function
1136 1136
1137 1137 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
1138 1138 2 is in the string
1139 1139 1 is not
1140 1140 0 is in the string
1141 1141
1142 1142 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
1143 1143 2 is in the string
1144 1144 1 is not
1145 1145 0 is in the string
1146 1146
1147 1147 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
1148 1148 2 did not add a
1149 1149 1 did not add a
1150 1150 0 added a
1151 1151
1152 1152 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
1153 1153 2 is parent of 1
1154 1154 1
1155 1155 0
1156 1156
1157 1157 $ hg log -l1 -T '{ifcontains("branch", extras, "t", "f")}\n'
1158 1158 t
1159 1159 $ hg log -l1 -T '{ifcontains("branch", extras % "{key}", "t", "f")}\n'
1160 1160 t
1161 1161 $ hg log -l1 -T '{ifcontains("branc", extras % "{key}", "t", "f")}\n'
1162 1162 f
1163 1163 $ hg log -l1 -T '{ifcontains("branc", stringify(extras % "{key}"), "t", "f")}\n'
1164 1164 t
1165 1165
1166 1166 Test revset function
1167 1167
1168 1168 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
1169 1169 2 current rev
1170 1170 1 not current rev
1171 1171 0 not current rev
1172 1172
1173 1173 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
1174 1174 2 match rev
1175 1175 1 match rev
1176 1176 0 not match rev
1177 1177
1178 1178 $ hg log -T '{ifcontains(desc, revset(":"), "", "type not match")}\n' -l1
1179 1179 type not match
1180 1180
1181 1181 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
1182 1182 2 Parents: 1
1183 1183 1 Parents: 0
1184 1184 0 Parents:
1185 1185
1186 1186 $ cat >> .hg/hgrc <<EOF
1187 1187 > [revsetalias]
1188 1188 > myparents(\$1) = parents(\$1)
1189 1189 > EOF
1190 1190 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
1191 1191 2 Parents: 1
1192 1192 1 Parents: 0
1193 1193 0 Parents:
1194 1194
1195 1195 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
1196 1196 Rev: 2
1197 1197 Ancestor: 0
1198 1198 Ancestor: 1
1199 1199 Ancestor: 2
1200 1200
1201 1201 Rev: 1
1202 1202 Ancestor: 0
1203 1203 Ancestor: 1
1204 1204
1205 1205 Rev: 0
1206 1206 Ancestor: 0
1207 1207
1208 1208 $ hg log --template '{revset("TIP"|lower)}\n' -l1
1209 1209 2
1210 1210
1211 1211 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
1212 1212 2
1213 1213
1214 1214 a list template is evaluated for each item of revset/parents
1215 1215
1216 1216 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
1217 1217 2 p: 1:bcc7ff960b8e
1218 1218 1 p: 0:f7769ec2ab97
1219 1219 0 p:
1220 1220
1221 1221 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
1222 1222 2 p: 1:bcc7ff960b8e -1:000000000000
1223 1223 1 p: 0:f7769ec2ab97 -1:000000000000
1224 1224 0 p: -1:000000000000 -1:000000000000
1225 1225
1226 1226 therefore, 'revcache' should be recreated for each rev
1227 1227
1228 1228 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
1229 1229 2 aa b
1230 1230 p
1231 1231 1
1232 1232 p a
1233 1233 0 a
1234 1234 p
1235 1235
1236 1236 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
1237 1237 2 aa b
1238 1238 p
1239 1239 1
1240 1240 p a
1241 1241 0 a
1242 1242 p
1243 1243
1244 1244 a revset item must be evaluated as an integer revision, not an offset from tip
1245 1245
1246 1246 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
1247 1247 -1:000000000000
1248 1248 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
1249 1249 -1:000000000000
1250 1250
1251 1251 join() should pick '{rev}' from revset items:
1252 1252
1253 1253 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
1254 1254 4, 5
1255 1255
1256 1256 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
1257 1257 default. join() should agree with the default formatting:
1258 1258
1259 1259 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
1260 1260 5:13207e5a10d9, 4:bbe44766e73d
1261 1261
1262 1262 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
1263 1263 5:13207e5a10d9fd28ec424934298e176197f2c67f,
1264 1264 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1265 1265
1266 1266 Invalid arguments passed to revset()
1267 1267
1268 1268 $ hg log -T '{revset("%whatever", 0)}\n'
1269 1269 hg: parse error: unexpected revspec format character w
1270 1270 [255]
1271 1271 $ hg log -T '{revset("%lwhatever", files)}\n'
1272 1272 hg: parse error: unexpected revspec format character w
1273 1273 [255]
1274 1274 $ hg log -T '{revset("%s %s", 0)}\n'
1275 1275 hg: parse error: missing argument for revspec
1276 1276 [255]
1277 1277 $ hg log -T '{revset("", 0)}\n'
1278 1278 hg: parse error: too many revspec arguments specified
1279 1279 [255]
1280 1280 $ hg log -T '{revset("%s", 0, 1)}\n'
1281 1281 hg: parse error: too many revspec arguments specified
1282 1282 [255]
1283 1283 $ hg log -T '{revset("%", 0)}\n'
1284 1284 hg: parse error: incomplete revspec format character
1285 1285 [255]
1286 1286 $ hg log -T '{revset("%l", 0)}\n'
1287 1287 hg: parse error: incomplete revspec format character
1288 1288 [255]
1289 1289 $ hg log -T '{revset("%d", 'foo')}\n'
1290 1290 hg: parse error: invalid argument for revspec
1291 1291 [255]
1292 1292 $ hg log -T '{revset("%ld", files)}\n'
1293 1293 hg: parse error: invalid argument for revspec
1294 1294 [255]
1295 1295 $ hg log -T '{revset("%ls", 0)}\n'
1296 1296 hg: parse error: invalid argument for revspec
1297 1297 [255]
1298 1298 $ hg log -T '{revset("%b", 'foo')}\n'
1299 1299 hg: parse error: invalid argument for revspec
1300 1300 [255]
1301 1301 $ hg log -T '{revset("%lb", files)}\n'
1302 1302 hg: parse error: invalid argument for revspec
1303 1303 [255]
1304 1304 $ hg log -T '{revset("%r", 0)}\n'
1305 1305 hg: parse error: invalid argument for revspec
1306 1306 [255]
1307 1307
1308 1308 Test files function
1309 1309
1310 1310 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
1311 1311 2
1312 1312 a
1313 1313 aa
1314 1314 b
1315 1315 1
1316 1316 a
1317 1317 0
1318 1318 a
1319 1319
1320 1320 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
1321 1321 2
1322 1322 aa
1323 1323 1
1324 1324
1325 1325 0
1326 1326
1327 1327
1328 1328 $ hg log -l1 -T "{files('aa') % '{file}\n'}"
1329 1329 aa
1330 1330 $ hg log -l1 -T "{files('aa') % '{path}\n'}"
1331 1331 aa
1332 1332
1333 1333 $ hg rm a
1334 1334 $ hg log -r "wdir()" -T "{rev}\n{join(files('*'), '\n')}\n"
1335 1335 2147483647
1336 1336 aa
1337 1337 b
1338 1338 $ hg revert a
1339 1339
1340 1340 Test relpath function
1341 1341
1342 1342 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
1343 1343 a
1344 1344 $ cd ..
1345 1345 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
1346 1346 r/a
1347 1347
1348 1348 Test stringify on sub expressions
1349 1349
1350 1350 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
1351 1351 fourth, second, third
1352 1352 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
1353 1353 abc
1354 1354
1355 1355 Test splitlines
1356 1356
1357 1357 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
1358 1358 @ foo Modify, add, remove, rename
1359 1359 |
1360 1360 o foo future
1361 1361 |
1362 1362 o foo third
1363 1363 |
1364 1364 o foo second
1365 1365
1366 1366 o foo merge
1367 1367 |\
1368 1368 | o foo new head
1369 1369 | |
1370 1370 o | foo new branch
1371 1371 |/
1372 1372 o foo no user, no domain
1373 1373 |
1374 1374 o foo no person
1375 1375 |
1376 1376 o foo other 1
1377 1377 | foo other 2
1378 1378 | foo
1379 1379 | foo other 3
1380 1380 o foo line 1
1381 1381 foo line 2
1382 1382
1383 1383 $ hg log -R a -r0 -T '{desc|splitlines}\n'
1384 1384 line 1 line 2
1385 1385 $ hg log -R a -r0 -T '{join(desc|splitlines, "|")}\n'
1386 1386 line 1|line 2
1387 1387
1388 1388 Test startswith
1389 1389 $ hg log -Gv -R a --template "{startswith(desc)}"
1390 1390 hg: parse error: startswith expects two arguments
1391 1391 [255]
1392 1392
1393 1393 $ hg log -Gv -R a --template "{startswith('line', desc)}"
1394 1394 @
1395 1395 |
1396 1396 o
1397 1397 |
1398 1398 o
1399 1399 |
1400 1400 o
1401 1401
1402 1402 o
1403 1403 |\
1404 1404 | o
1405 1405 | |
1406 1406 o |
1407 1407 |/
1408 1408 o
1409 1409 |
1410 1410 o
1411 1411 |
1412 1412 o
1413 1413 |
1414 1414 o line 1
1415 1415 line 2
1416 1416
1417 1417 Test word function (including index out of bounds graceful failure)
1418 1418
1419 1419 $ hg log -Gv -R a --template "{word('1', desc)}"
1420 1420 @ add,
1421 1421 |
1422 1422 o
1423 1423 |
1424 1424 o
1425 1425 |
1426 1426 o
1427 1427
1428 1428 o
1429 1429 |\
1430 1430 | o head
1431 1431 | |
1432 1432 o | branch
1433 1433 |/
1434 1434 o user,
1435 1435 |
1436 1436 o person
1437 1437 |
1438 1438 o 1
1439 1439 |
1440 1440 o 1
1441 1441
1442 1442
1443 1443 Test word third parameter used as splitter
1444 1444
1445 1445 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
1446 1446 @ M
1447 1447 |
1448 1448 o future
1449 1449 |
1450 1450 o third
1451 1451 |
1452 1452 o sec
1453 1453
1454 1454 o merge
1455 1455 |\
1456 1456 | o new head
1457 1457 | |
1458 1458 o | new branch
1459 1459 |/
1460 1460 o n
1461 1461 |
1462 1462 o n
1463 1463 |
1464 1464 o
1465 1465 |
1466 1466 o line 1
1467 1467 line 2
1468 1468
1469 1469 Test word error messages for not enough and too many arguments
1470 1470
1471 1471 $ hg log -Gv -R a --template "{word('0')}"
1472 1472 hg: parse error: word expects two or three arguments, got 1
1473 1473 [255]
1474 1474
1475 1475 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
1476 1476 hg: parse error: word expects two or three arguments, got 7
1477 1477 [255]
1478 1478
1479 1479 Test word for integer literal
1480 1480
1481 1481 $ hg log -R a --template "{word(2, desc)}\n" -r0
1482 1482 line
1483 1483
1484 1484 Test word for invalid numbers
1485 1485
1486 1486 $ hg log -Gv -R a --template "{word('a', desc)}"
1487 1487 hg: parse error: word expects an integer index
1488 1488 [255]
1489 1489
1490 1490 Test word for out of range
1491 1491
1492 1492 $ hg log -R a --template "{word(10000, desc)}"
1493 1493 $ hg log -R a --template "{word(-10000, desc)}"
1494 1494
1495 1495 Test indent and not adding to empty lines
1496 1496
1497 1497 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
1498 1498 -----
1499 1499 > line 1
1500 1500 >> line 2
1501 1501 -----
1502 1502 > other 1
1503 1503 >> other 2
1504 1504
1505 1505 >> other 3
1506 1506
1507 1507 Test indent with empty first line
1508 1508
1509 1509 $ hg version -T "{indent('', '>> ')}\n"
1510 >>
1510
1511 1511
1512 1512 $ hg version -T "{indent('
1513 1513 > second', '>> ')}\n"
1514 >>
1514
1515 1515 >> second
1516 1516
1517 1517 $ hg version -T "{indent('
1518 1518 > second', '>> ', ' > ')}\n"
1519 >
1519
1520 1520 >> second
1521 1521
1522 1522 Test with non-strings like dates
1523 1523
1524 1524 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
1525 1525 1200000.00
1526 1526 1300000.00
1527 1527
1528 1528 Test cbor filter:
1529 1529
1530 1530 $ cat <<'EOF' > "$TESTTMP/decodecbor.py"
1531 1531 > from __future__ import absolute_import
1532 1532 > from mercurial import (
1533 1533 > dispatch,
1534 1534 > pycompat,
1535 1535 > )
1536 1536 > from mercurial.utils import (
1537 1537 > cborutil,
1538 1538 > stringutil,
1539 1539 > )
1540 1540 > dispatch.initstdio()
1541 1541 > items = cborutil.decodeall(pycompat.stdin.read())
1542 1542 > pycompat.stdout.write(stringutil.pprint(items, indent=1) + b'\n')
1543 1543 > EOF
1544 1544
1545 1545 $ hg log -T "{rev|cbor}" -R a -l2 | "$PYTHON" "$TESTTMP/decodecbor.py"
1546 1546 [
1547 1547 10,
1548 1548 9
1549 1549 ]
1550 1550
1551 1551 $ hg log -T "{extras|cbor}" -R a -l1 | "$PYTHON" "$TESTTMP/decodecbor.py"
1552 1552 [
1553 1553 {
1554 1554 'branch': 'default'
1555 1555 }
1556 1556 ]
1557 1557
1558 1558 json filter should escape HTML tags so that the output can be embedded in hgweb:
1559 1559
1560 1560 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
1561 1561 "\u003cfoo@example.org\u003e"
1562 1562
1563 1563 Set up repository for non-ascii encoding tests:
1564 1564
1565 1565 $ hg init nonascii
1566 1566 $ cd nonascii
1567 1567 $ "$PYTHON" <<EOF
1568 1568 > open('latin1', 'wb').write(b'\xe9')
1569 1569 > open('utf-8', 'wb').write(b'\xc3\xa9')
1570 1570 > EOF
1571 1571 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
1572 1572 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
1573 1573
1574 1574 json filter should try round-trip conversion to utf-8:
1575 1575
1576 1576 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
1577 1577 "\u00e9"
1578 1578 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
1579 1579 "non-ascii branch: \u00e9"
1580 1580
1581 1581 json filter should take input as utf-8 if it was converted from utf-8:
1582 1582
1583 1583 $ HGENCODING=latin-1 hg log -T "{branch|json}\n" -r0
1584 1584 "\u00e9"
1585 1585 $ HGENCODING=latin-1 hg log -T "{desc|json}\n" -r0
1586 1586 "non-ascii branch: \u00e9"
1587 1587
1588 1588 json filter takes input as utf-8b:
1589 1589
1590 1590 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
1591 1591 "\u00e9"
1592 1592 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
1593 1593 "\udce9"
1594 1594
1595 1595 cbor filter is bytes transparent, which should handle bytes subtypes
1596 1596 as bytes:
1597 1597
1598 1598 $ HGENCODING=ascii hg log -T "{branch|cbor}" -r0 \
1599 1599 > | "$PYTHON" "$TESTTMP/decodecbor.py"
1600 1600 [
1601 1601 '?'
1602 1602 ]
1603 1603 $ HGENCODING=latin-1 hg log -T "{branch|cbor}" -r0 \
1604 1604 > | "$PYTHON" "$TESTTMP/decodecbor.py"
1605 1605 [
1606 1606 '\xe9'
1607 1607 ]
1608 1608
1609 1609 utf8 filter:
1610 1610
1611 1611 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
1612 1612 round-trip: c3a9
1613 1613 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
1614 1614 decoded: c3a9
1615 1615 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
1616 1616 abort: decoding near * (glob)
1617 1617 [255]
1618 1618 $ hg log -T "coerced to string: {rev|utf8}\n" -r0
1619 1619 coerced to string: 0
1620 1620
1621 1621 pad width:
1622 1622
1623 1623 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
1624 1624 \xc3\xa9- (esc)
1625 1625
1626 1626 read config options:
1627 1627
1628 1628 $ hg log -T "{config('templateconfig', 'knob', 'foo')}\n"
1629 1629 foo
1630 1630 $ hg log -T "{config('templateconfig', 'knob', 'foo')}\n" \
1631 1631 > --config templateconfig.knob=bar
1632 1632 bar
1633 1633 $ hg log -T "{configbool('templateconfig', 'knob', True)}\n"
1634 1634 True
1635 1635 $ hg log -T "{configbool('templateconfig', 'knob', True)}\n" \
1636 1636 > --config templateconfig.knob=0
1637 1637 False
1638 1638 $ hg log -T "{configint('templateconfig', 'knob', 123)}\n"
1639 1639 123
1640 1640 $ hg log -T "{configint('templateconfig', 'knob', 123)}\n" \
1641 1641 > --config templateconfig.knob=456
1642 1642 456
1643 1643 $ hg log -T "{config('templateconfig', 'knob')}\n"
1644 1644 devel-warn: config item requires an explicit default value: 'templateconfig.knob' at: * (glob)
1645 1645
1646 1646 $ hg log -T "{configbool('ui', 'interactive')}\n"
1647 1647 False
1648 1648 $ hg log -T "{configbool('ui', 'interactive')}\n" --config ui.interactive=1
1649 1649 True
1650 1650 $ hg log -T "{config('templateconfig', 'knob', if(true, 'foo', 'bar'))}\n"
1651 1651 foo
1652 1652
1653 1653 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now