##// END OF EJS Templates
templatefuncs: add truncate parameter to pad...
Mark Thomas -
r40225:9458dbfa default
parent child Browse files
Show More
@@ -1,718 +1,724
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 encoding,
20 20 error,
21 21 minirst,
22 22 obsutil,
23 23 registrar,
24 24 revset as revsetmod,
25 25 revsetlang,
26 26 scmutil,
27 27 templatefilters,
28 28 templatekw,
29 29 templateutil,
30 30 util,
31 31 )
32 32 from .utils import (
33 33 dateutil,
34 34 stringutil,
35 35 )
36 36
37 37 evalrawexp = templateutil.evalrawexp
38 38 evalwrapped = templateutil.evalwrapped
39 39 evalfuncarg = templateutil.evalfuncarg
40 40 evalboolean = templateutil.evalboolean
41 41 evaldate = templateutil.evaldate
42 42 evalinteger = templateutil.evalinteger
43 43 evalstring = templateutil.evalstring
44 44 evalstringliteral = templateutil.evalstringliteral
45 45
46 46 # dict of template built-in functions
47 47 funcs = {}
48 48 templatefunc = registrar.templatefunc(funcs)
49 49
50 50 @templatefunc('date(date[, fmt])')
51 51 def date(context, mapping, args):
52 52 """Format a date. See :hg:`help dates` for formatting
53 53 strings. The default is a Unix date format, including the timezone:
54 54 "Mon Sep 04 15:13:13 2006 0700"."""
55 55 if not (1 <= len(args) <= 2):
56 56 # i18n: "date" is a keyword
57 57 raise error.ParseError(_("date expects one or two arguments"))
58 58
59 59 date = evaldate(context, mapping, args[0],
60 60 # i18n: "date" is a keyword
61 61 _("date expects a date information"))
62 62 fmt = None
63 63 if len(args) == 2:
64 64 fmt = evalstring(context, mapping, args[1])
65 65 if fmt is None:
66 66 return dateutil.datestr(date)
67 67 else:
68 68 return dateutil.datestr(date, fmt)
69 69
70 70 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
71 71 def dict_(context, mapping, args):
72 72 """Construct a dict from key-value pairs. A key may be omitted if
73 73 a value expression can provide an unambiguous name."""
74 74 data = util.sortdict()
75 75
76 76 for v in args['args']:
77 77 k = templateutil.findsymbolicname(v)
78 78 if not k:
79 79 raise error.ParseError(_('dict key cannot be inferred'))
80 80 if k in data or k in args['kwargs']:
81 81 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
82 82 data[k] = evalfuncarg(context, mapping, v)
83 83
84 84 data.update((k, evalfuncarg(context, mapping, v))
85 85 for k, v in args['kwargs'].iteritems())
86 86 return templateutil.hybriddict(data)
87 87
88 88 @templatefunc('diff([includepattern [, excludepattern]])', requires={'ctx'})
89 89 def diff(context, mapping, args):
90 90 """Show a diff, optionally
91 91 specifying files to include or exclude."""
92 92 if len(args) > 2:
93 93 # i18n: "diff" is a keyword
94 94 raise error.ParseError(_("diff expects zero, one, or two arguments"))
95 95
96 96 def getpatterns(i):
97 97 if i < len(args):
98 98 s = evalstring(context, mapping, args[i]).strip()
99 99 if s:
100 100 return [s]
101 101 return []
102 102
103 103 ctx = context.resource(mapping, 'ctx')
104 104 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
105 105
106 106 return ''.join(chunks)
107 107
108 108 @templatefunc('extdata(source)', argspec='source', requires={'ctx', 'cache'})
109 109 def extdata(context, mapping, args):
110 110 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
111 111 if 'source' not in args:
112 112 # i18n: "extdata" is a keyword
113 113 raise error.ParseError(_('extdata expects one argument'))
114 114
115 115 source = evalstring(context, mapping, args['source'])
116 116 if not source:
117 117 sym = templateutil.findsymbolicname(args['source'])
118 118 if sym:
119 119 raise error.ParseError(_('empty data source specified'),
120 120 hint=_("did you mean extdata('%s')?") % sym)
121 121 else:
122 122 raise error.ParseError(_('empty data source specified'))
123 123 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
124 124 ctx = context.resource(mapping, 'ctx')
125 125 if source in cache:
126 126 data = cache[source]
127 127 else:
128 128 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
129 129 return data.get(ctx.rev(), '')
130 130
131 131 @templatefunc('files(pattern)', requires={'ctx'})
132 132 def files(context, mapping, args):
133 133 """All files of the current changeset matching the pattern. See
134 134 :hg:`help patterns`."""
135 135 if not len(args) == 1:
136 136 # i18n: "files" is a keyword
137 137 raise error.ParseError(_("files expects one argument"))
138 138
139 139 raw = evalstring(context, mapping, args[0])
140 140 ctx = context.resource(mapping, 'ctx')
141 141 m = ctx.match([raw])
142 142 files = list(ctx.matches(m))
143 143 return templateutil.compatfileslist(context, mapping, "file", files)
144 144
145 145 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
146 146 def fill(context, mapping, args):
147 147 """Fill many
148 148 paragraphs with optional indentation. See the "fill" filter."""
149 149 if not (1 <= len(args) <= 4):
150 150 # i18n: "fill" is a keyword
151 151 raise error.ParseError(_("fill expects one to four arguments"))
152 152
153 153 text = evalstring(context, mapping, args[0])
154 154 width = 76
155 155 initindent = ''
156 156 hangindent = ''
157 157 if 2 <= len(args) <= 4:
158 158 width = evalinteger(context, mapping, args[1],
159 159 # i18n: "fill" is a keyword
160 160 _("fill expects an integer width"))
161 161 try:
162 162 initindent = evalstring(context, mapping, args[2])
163 163 hangindent = evalstring(context, mapping, args[3])
164 164 except IndexError:
165 165 pass
166 166
167 167 return templatefilters.fill(text, width, initindent, hangindent)
168 168
169 169 @templatefunc('filter(iterable[, expr])')
170 170 def filter_(context, mapping, args):
171 171 """Remove empty elements from a list or a dict. If expr specified, it's
172 172 applied to each element to test emptiness."""
173 173 if not (1 <= len(args) <= 2):
174 174 # i18n: "filter" is a keyword
175 175 raise error.ParseError(_("filter expects one or two arguments"))
176 176 iterable = evalwrapped(context, mapping, args[0])
177 177 if len(args) == 1:
178 178 def select(w):
179 179 return w.tobool(context, mapping)
180 180 else:
181 181 def select(w):
182 182 if not isinstance(w, templateutil.mappable):
183 183 raise error.ParseError(_("not filterable by expression"))
184 184 lm = context.overlaymap(mapping, w.tomap(context))
185 185 return evalboolean(context, lm, args[1])
186 186 return iterable.filter(context, mapping, select)
187 187
188 188 @templatefunc('formatnode(node)', requires={'ui'})
189 189 def formatnode(context, mapping, args):
190 190 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
191 191 if len(args) != 1:
192 192 # i18n: "formatnode" is a keyword
193 193 raise error.ParseError(_("formatnode expects one argument"))
194 194
195 195 ui = context.resource(mapping, 'ui')
196 196 node = evalstring(context, mapping, args[0])
197 197 if ui.debugflag:
198 198 return node
199 199 return templatefilters.short(node)
200 200
201 201 @templatefunc('mailmap(author)', requires={'repo', 'cache'})
202 202 def mailmap(context, mapping, args):
203 203 """Return the author, updated according to the value
204 204 set in the .mailmap file"""
205 205 if len(args) != 1:
206 206 raise error.ParseError(_("mailmap expects one argument"))
207 207
208 208 author = evalstring(context, mapping, args[0])
209 209
210 210 cache = context.resource(mapping, 'cache')
211 211 repo = context.resource(mapping, 'repo')
212 212
213 213 if 'mailmap' not in cache:
214 214 data = repo.wvfs.tryread('.mailmap')
215 215 cache['mailmap'] = stringutil.parsemailmap(data)
216 216
217 217 return stringutil.mapname(cache['mailmap'], author)
218 218
219 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
220 argspec='text width fillchar left')
219 @templatefunc(
220 'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])',
221 argspec='text width fillchar left truncate')
221 222 def pad(context, mapping, args):
222 223 """Pad text with a
223 224 fill character."""
224 225 if 'text' not in args or 'width' not in args:
225 226 # i18n: "pad" is a keyword
226 227 raise error.ParseError(_("pad() expects two to four arguments"))
227 228
228 229 width = evalinteger(context, mapping, args['width'],
229 230 # i18n: "pad" is a keyword
230 231 _("pad() expects an integer width"))
231 232
232 233 text = evalstring(context, mapping, args['text'])
233 234
235 truncate = False
234 236 left = False
235 237 fillchar = ' '
236 238 if 'fillchar' in args:
237 239 fillchar = evalstring(context, mapping, args['fillchar'])
238 240 if len(color.stripeffects(fillchar)) != 1:
239 241 # i18n: "pad" is a keyword
240 242 raise error.ParseError(_("pad() expects a single fill character"))
241 243 if 'left' in args:
242 244 left = evalboolean(context, mapping, args['left'])
245 if 'truncate' in args:
246 truncate = evalboolean(context, mapping, args['truncate'])
243 247
244 248 fillwidth = width - encoding.colwidth(color.stripeffects(text))
249 if fillwidth < 0 and truncate:
250 return encoding.trim(color.stripeffects(text), width, leftside=left)
245 251 if fillwidth <= 0:
246 252 return text
247 253 if left:
248 254 return fillchar * fillwidth + text
249 255 else:
250 256 return text + fillchar * fillwidth
251 257
252 258 @templatefunc('indent(text, indentchars[, firstline])')
253 259 def indent(context, mapping, args):
254 260 """Indents all non-empty lines
255 261 with the characters given in the indentchars string. An optional
256 262 third parameter will override the indent for the first line only
257 263 if present."""
258 264 if not (2 <= len(args) <= 3):
259 265 # i18n: "indent" is a keyword
260 266 raise error.ParseError(_("indent() expects two or three arguments"))
261 267
262 268 text = evalstring(context, mapping, args[0])
263 269 indent = evalstring(context, mapping, args[1])
264 270
265 271 if len(args) == 3:
266 272 firstline = evalstring(context, mapping, args[2])
267 273 else:
268 274 firstline = indent
269 275
270 276 # the indent function doesn't indent the first line, so we do it here
271 277 return templatefilters.indent(firstline + text, indent)
272 278
273 279 @templatefunc('get(dict, key)')
274 280 def get(context, mapping, args):
275 281 """Get an attribute/key from an object. Some keywords
276 282 are complex types. This function allows you to obtain the value of an
277 283 attribute on these types."""
278 284 if len(args) != 2:
279 285 # i18n: "get" is a keyword
280 286 raise error.ParseError(_("get() expects two arguments"))
281 287
282 288 dictarg = evalwrapped(context, mapping, args[0])
283 289 key = evalrawexp(context, mapping, args[1])
284 290 try:
285 291 return dictarg.getmember(context, mapping, key)
286 292 except error.ParseError as err:
287 293 # i18n: "get" is a keyword
288 294 hint = _("get() expects a dict as first argument")
289 295 raise error.ParseError(bytes(err), hint=hint)
290 296
291 297 @templatefunc('if(expr, then[, else])')
292 298 def if_(context, mapping, args):
293 299 """Conditionally execute based on the result of
294 300 an expression."""
295 301 if not (2 <= len(args) <= 3):
296 302 # i18n: "if" is a keyword
297 303 raise error.ParseError(_("if expects two or three arguments"))
298 304
299 305 test = evalboolean(context, mapping, args[0])
300 306 if test:
301 307 return evalrawexp(context, mapping, args[1])
302 308 elif len(args) == 3:
303 309 return evalrawexp(context, mapping, args[2])
304 310
305 311 @templatefunc('ifcontains(needle, haystack, then[, else])')
306 312 def ifcontains(context, mapping, args):
307 313 """Conditionally execute based
308 314 on whether the item "needle" is in "haystack"."""
309 315 if not (3 <= len(args) <= 4):
310 316 # i18n: "ifcontains" is a keyword
311 317 raise error.ParseError(_("ifcontains expects three or four arguments"))
312 318
313 319 haystack = evalwrapped(context, mapping, args[1])
314 320 try:
315 321 needle = evalrawexp(context, mapping, args[0])
316 322 found = haystack.contains(context, mapping, needle)
317 323 except error.ParseError:
318 324 found = False
319 325
320 326 if found:
321 327 return evalrawexp(context, mapping, args[2])
322 328 elif len(args) == 4:
323 329 return evalrawexp(context, mapping, args[3])
324 330
325 331 @templatefunc('ifeq(expr1, expr2, then[, else])')
326 332 def ifeq(context, mapping, args):
327 333 """Conditionally execute based on
328 334 whether 2 items are equivalent."""
329 335 if not (3 <= len(args) <= 4):
330 336 # i18n: "ifeq" is a keyword
331 337 raise error.ParseError(_("ifeq expects three or four arguments"))
332 338
333 339 test = evalstring(context, mapping, args[0])
334 340 match = evalstring(context, mapping, args[1])
335 341 if test == match:
336 342 return evalrawexp(context, mapping, args[2])
337 343 elif len(args) == 4:
338 344 return evalrawexp(context, mapping, args[3])
339 345
340 346 @templatefunc('join(list, sep)')
341 347 def join(context, mapping, args):
342 348 """Join items in a list with a delimiter."""
343 349 if not (1 <= len(args) <= 2):
344 350 # i18n: "join" is a keyword
345 351 raise error.ParseError(_("join expects one or two arguments"))
346 352
347 353 joinset = evalwrapped(context, mapping, args[0])
348 354 joiner = " "
349 355 if len(args) > 1:
350 356 joiner = evalstring(context, mapping, args[1])
351 357 return joinset.join(context, mapping, joiner)
352 358
353 359 @templatefunc('label(label, expr)', requires={'ui'})
354 360 def label(context, mapping, args):
355 361 """Apply a label to generated content. Content with
356 362 a label applied can result in additional post-processing, such as
357 363 automatic colorization."""
358 364 if len(args) != 2:
359 365 # i18n: "label" is a keyword
360 366 raise error.ParseError(_("label expects two arguments"))
361 367
362 368 ui = context.resource(mapping, 'ui')
363 369 thing = evalstring(context, mapping, args[1])
364 370 # preserve unknown symbol as literal so effects like 'red', 'bold',
365 371 # etc. don't need to be quoted
366 372 label = evalstringliteral(context, mapping, args[0])
367 373
368 374 return ui.label(thing, label)
369 375
370 376 @templatefunc('latesttag([pattern])')
371 377 def latesttag(context, mapping, args):
372 378 """The global tags matching the given pattern on the
373 379 most recent globally tagged ancestor of this changeset.
374 380 If no such tags exist, the "{tag}" template resolves to
375 381 the string "null". See :hg:`help revisions.patterns` for the pattern
376 382 syntax.
377 383 """
378 384 if len(args) > 1:
379 385 # i18n: "latesttag" is a keyword
380 386 raise error.ParseError(_("latesttag expects at most one argument"))
381 387
382 388 pattern = None
383 389 if len(args) == 1:
384 390 pattern = evalstring(context, mapping, args[0])
385 391 return templatekw.showlatesttags(context, mapping, pattern)
386 392
387 393 @templatefunc('localdate(date[, tz])')
388 394 def localdate(context, mapping, args):
389 395 """Converts a date to the specified timezone.
390 396 The default is local date."""
391 397 if not (1 <= len(args) <= 2):
392 398 # i18n: "localdate" is a keyword
393 399 raise error.ParseError(_("localdate expects one or two arguments"))
394 400
395 401 date = evaldate(context, mapping, args[0],
396 402 # i18n: "localdate" is a keyword
397 403 _("localdate expects a date information"))
398 404 if len(args) >= 2:
399 405 tzoffset = None
400 406 tz = evalfuncarg(context, mapping, args[1])
401 407 if isinstance(tz, bytes):
402 408 tzoffset, remainder = dateutil.parsetimezone(tz)
403 409 if remainder:
404 410 tzoffset = None
405 411 if tzoffset is None:
406 412 try:
407 413 tzoffset = int(tz)
408 414 except (TypeError, ValueError):
409 415 # i18n: "localdate" is a keyword
410 416 raise error.ParseError(_("localdate expects a timezone"))
411 417 else:
412 418 tzoffset = dateutil.makedate()[1]
413 419 return templateutil.date((date[0], tzoffset))
414 420
415 421 @templatefunc('max(iterable)')
416 422 def max_(context, mapping, args, **kwargs):
417 423 """Return the max of an iterable"""
418 424 if len(args) != 1:
419 425 # i18n: "max" is a keyword
420 426 raise error.ParseError(_("max expects one argument"))
421 427
422 428 iterable = evalwrapped(context, mapping, args[0])
423 429 try:
424 430 return iterable.getmax(context, mapping)
425 431 except error.ParseError as err:
426 432 # i18n: "max" is a keyword
427 433 hint = _("max first argument should be an iterable")
428 434 raise error.ParseError(bytes(err), hint=hint)
429 435
430 436 @templatefunc('min(iterable)')
431 437 def min_(context, mapping, args, **kwargs):
432 438 """Return the min of an iterable"""
433 439 if len(args) != 1:
434 440 # i18n: "min" is a keyword
435 441 raise error.ParseError(_("min expects one argument"))
436 442
437 443 iterable = evalwrapped(context, mapping, args[0])
438 444 try:
439 445 return iterable.getmin(context, mapping)
440 446 except error.ParseError as err:
441 447 # i18n: "min" is a keyword
442 448 hint = _("min first argument should be an iterable")
443 449 raise error.ParseError(bytes(err), hint=hint)
444 450
445 451 @templatefunc('mod(a, b)')
446 452 def mod(context, mapping, args):
447 453 """Calculate a mod b such that a / b + a mod b == a"""
448 454 if not len(args) == 2:
449 455 # i18n: "mod" is a keyword
450 456 raise error.ParseError(_("mod expects two arguments"))
451 457
452 458 func = lambda a, b: a % b
453 459 return templateutil.runarithmetic(context, mapping,
454 460 (func, args[0], args[1]))
455 461
456 462 @templatefunc('obsfateoperations(markers)')
457 463 def obsfateoperations(context, mapping, args):
458 464 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
459 465 if len(args) != 1:
460 466 # i18n: "obsfateoperations" is a keyword
461 467 raise error.ParseError(_("obsfateoperations expects one argument"))
462 468
463 469 markers = evalfuncarg(context, mapping, args[0])
464 470
465 471 try:
466 472 data = obsutil.markersoperations(markers)
467 473 return templateutil.hybridlist(data, name='operation')
468 474 except (TypeError, KeyError):
469 475 # i18n: "obsfateoperations" is a keyword
470 476 errmsg = _("obsfateoperations first argument should be an iterable")
471 477 raise error.ParseError(errmsg)
472 478
473 479 @templatefunc('obsfatedate(markers)')
474 480 def obsfatedate(context, mapping, args):
475 481 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
476 482 if len(args) != 1:
477 483 # i18n: "obsfatedate" is a keyword
478 484 raise error.ParseError(_("obsfatedate expects one argument"))
479 485
480 486 markers = evalfuncarg(context, mapping, args[0])
481 487
482 488 try:
483 489 # TODO: maybe this has to be a wrapped list of date wrappers?
484 490 data = obsutil.markersdates(markers)
485 491 return templateutil.hybridlist(data, name='date', fmt='%d %d')
486 492 except (TypeError, KeyError):
487 493 # i18n: "obsfatedate" is a keyword
488 494 errmsg = _("obsfatedate first argument should be an iterable")
489 495 raise error.ParseError(errmsg)
490 496
491 497 @templatefunc('obsfateusers(markers)')
492 498 def obsfateusers(context, mapping, args):
493 499 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
494 500 if len(args) != 1:
495 501 # i18n: "obsfateusers" is a keyword
496 502 raise error.ParseError(_("obsfateusers expects one argument"))
497 503
498 504 markers = evalfuncarg(context, mapping, args[0])
499 505
500 506 try:
501 507 data = obsutil.markersusers(markers)
502 508 return templateutil.hybridlist(data, name='user')
503 509 except (TypeError, KeyError, ValueError):
504 510 # i18n: "obsfateusers" is a keyword
505 511 msg = _("obsfateusers first argument should be an iterable of "
506 512 "obsmakers")
507 513 raise error.ParseError(msg)
508 514
509 515 @templatefunc('obsfateverb(successors, markers)')
510 516 def obsfateverb(context, mapping, args):
511 517 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
512 518 if len(args) != 2:
513 519 # i18n: "obsfateverb" is a keyword
514 520 raise error.ParseError(_("obsfateverb expects two arguments"))
515 521
516 522 successors = evalfuncarg(context, mapping, args[0])
517 523 markers = evalfuncarg(context, mapping, args[1])
518 524
519 525 try:
520 526 return obsutil.obsfateverb(successors, markers)
521 527 except TypeError:
522 528 # i18n: "obsfateverb" is a keyword
523 529 errmsg = _("obsfateverb first argument should be countable")
524 530 raise error.ParseError(errmsg)
525 531
526 532 @templatefunc('relpath(path)', requires={'repo'})
527 533 def relpath(context, mapping, args):
528 534 """Convert a repository-absolute path into a filesystem path relative to
529 535 the current working directory."""
530 536 if len(args) != 1:
531 537 # i18n: "relpath" is a keyword
532 538 raise error.ParseError(_("relpath expects one argument"))
533 539
534 540 repo = context.resource(mapping, 'repo')
535 541 path = evalstring(context, mapping, args[0])
536 542 return repo.pathto(path)
537 543
538 544 @templatefunc('revset(query[, formatargs...])', requires={'repo', 'cache'})
539 545 def revset(context, mapping, args):
540 546 """Execute a revision set query. See
541 547 :hg:`help revset`."""
542 548 if not len(args) > 0:
543 549 # i18n: "revset" is a keyword
544 550 raise error.ParseError(_("revset expects one or more arguments"))
545 551
546 552 raw = evalstring(context, mapping, args[0])
547 553 repo = context.resource(mapping, 'repo')
548 554
549 555 def query(expr):
550 556 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
551 557 return m(repo)
552 558
553 559 if len(args) > 1:
554 560 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
555 561 revs = query(revsetlang.formatspec(raw, *formatargs))
556 562 revs = list(revs)
557 563 else:
558 564 cache = context.resource(mapping, 'cache')
559 565 revsetcache = cache.setdefault("revsetcache", {})
560 566 if raw in revsetcache:
561 567 revs = revsetcache[raw]
562 568 else:
563 569 revs = query(raw)
564 570 revs = list(revs)
565 571 revsetcache[raw] = revs
566 572 return templatekw.showrevslist(context, mapping, "revision", revs)
567 573
568 574 @templatefunc('rstdoc(text, style)')
569 575 def rstdoc(context, mapping, args):
570 576 """Format reStructuredText."""
571 577 if len(args) != 2:
572 578 # i18n: "rstdoc" is a keyword
573 579 raise error.ParseError(_("rstdoc expects two arguments"))
574 580
575 581 text = evalstring(context, mapping, args[0])
576 582 style = evalstring(context, mapping, args[1])
577 583
578 584 return minirst.format(text, style=style, keep=['verbose'])
579 585
580 586 @templatefunc('separate(sep, args...)', argspec='sep *args')
581 587 def separate(context, mapping, args):
582 588 """Add a separator between non-empty arguments."""
583 589 if 'sep' not in args:
584 590 # i18n: "separate" is a keyword
585 591 raise error.ParseError(_("separate expects at least one argument"))
586 592
587 593 sep = evalstring(context, mapping, args['sep'])
588 594 first = True
589 595 for arg in args['args']:
590 596 argstr = evalstring(context, mapping, arg)
591 597 if not argstr:
592 598 continue
593 599 if first:
594 600 first = False
595 601 else:
596 602 yield sep
597 603 yield argstr
598 604
599 605 @templatefunc('shortest(node, minlength=4)', requires={'repo', 'cache'})
600 606 def shortest(context, mapping, args):
601 607 """Obtain the shortest representation of
602 608 a node."""
603 609 if not (1 <= len(args) <= 2):
604 610 # i18n: "shortest" is a keyword
605 611 raise error.ParseError(_("shortest() expects one or two arguments"))
606 612
607 613 hexnode = evalstring(context, mapping, args[0])
608 614
609 615 minlength = 4
610 616 if len(args) > 1:
611 617 minlength = evalinteger(context, mapping, args[1],
612 618 # i18n: "shortest" is a keyword
613 619 _("shortest() expects an integer minlength"))
614 620
615 621 repo = context.resource(mapping, 'repo')
616 622 if len(hexnode) > 40:
617 623 return hexnode
618 624 elif len(hexnode) == 40:
619 625 try:
620 626 node = bin(hexnode)
621 627 except TypeError:
622 628 return hexnode
623 629 else:
624 630 try:
625 631 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
626 632 except error.WdirUnsupported:
627 633 node = wdirid
628 634 except error.LookupError:
629 635 return hexnode
630 636 if not node:
631 637 return hexnode
632 638 cache = context.resource(mapping, 'cache')
633 639 try:
634 640 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
635 641 except error.RepoLookupError:
636 642 return hexnode
637 643
638 644 @templatefunc('strip(text[, chars])')
639 645 def strip(context, mapping, args):
640 646 """Strip characters from a string. By default,
641 647 strips all leading and trailing whitespace."""
642 648 if not (1 <= len(args) <= 2):
643 649 # i18n: "strip" is a keyword
644 650 raise error.ParseError(_("strip expects one or two arguments"))
645 651
646 652 text = evalstring(context, mapping, args[0])
647 653 if len(args) == 2:
648 654 chars = evalstring(context, mapping, args[1])
649 655 return text.strip(chars)
650 656 return text.strip()
651 657
652 658 @templatefunc('sub(pattern, replacement, expression)')
653 659 def sub(context, mapping, args):
654 660 """Perform text substitution
655 661 using regular expressions."""
656 662 if len(args) != 3:
657 663 # i18n: "sub" is a keyword
658 664 raise error.ParseError(_("sub expects three arguments"))
659 665
660 666 pat = evalstring(context, mapping, args[0])
661 667 rpl = evalstring(context, mapping, args[1])
662 668 src = evalstring(context, mapping, args[2])
663 669 try:
664 670 patre = re.compile(pat)
665 671 except re.error:
666 672 # i18n: "sub" is a keyword
667 673 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
668 674 try:
669 675 yield patre.sub(rpl, src)
670 676 except re.error:
671 677 # i18n: "sub" is a keyword
672 678 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
673 679
674 680 @templatefunc('startswith(pattern, text)')
675 681 def startswith(context, mapping, args):
676 682 """Returns the value from the "text" argument
677 683 if it begins with the content from the "pattern" argument."""
678 684 if len(args) != 2:
679 685 # i18n: "startswith" is a keyword
680 686 raise error.ParseError(_("startswith expects two arguments"))
681 687
682 688 patn = evalstring(context, mapping, args[0])
683 689 text = evalstring(context, mapping, args[1])
684 690 if text.startswith(patn):
685 691 return text
686 692 return ''
687 693
688 694 @templatefunc('word(number, text[, separator])')
689 695 def word(context, mapping, args):
690 696 """Return the nth word from a string."""
691 697 if not (2 <= len(args) <= 3):
692 698 # i18n: "word" is a keyword
693 699 raise error.ParseError(_("word expects two or three arguments, got %d")
694 700 % len(args))
695 701
696 702 num = evalinteger(context, mapping, args[0],
697 703 # i18n: "word" is a keyword
698 704 _("word expects an integer index"))
699 705 text = evalstring(context, mapping, args[1])
700 706 if len(args) == 3:
701 707 splitter = evalstring(context, mapping, args[2])
702 708 else:
703 709 splitter = None
704 710
705 711 tokens = text.split(splitter)
706 712 if num >= len(tokens) or num < -len(tokens):
707 713 return ''
708 714 else:
709 715 return tokens[num]
710 716
711 717 def loadfunction(ui, extname, registrarobj):
712 718 """Load template function from specified registrarobj
713 719 """
714 720 for name, func in registrarobj._table.iteritems():
715 721 funcs[name] = func
716 722
717 723 # tell hggettext to extract docstrings from these functions:
718 724 i18nfunctions = funcs.values()
@@ -1,1441 +1,1456
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 $ cd ..
374 374
375 375 latesttag() function:
376 376
377 377 $ hg init latesttag
378 378 $ cd latesttag
379 379
380 380 $ echo a > file
381 381 $ hg ci -Am a -d '0 0'
382 382 adding file
383 383
384 384 $ echo b >> file
385 385 $ hg ci -m b -d '1 0'
386 386
387 387 $ echo c >> head1
388 388 $ hg ci -Am h1c -d '2 0'
389 389 adding head1
390 390
391 391 $ hg update -q 1
392 392 $ echo d >> head2
393 393 $ hg ci -Am h2d -d '3 0'
394 394 adding head2
395 395 created new head
396 396
397 397 $ echo e >> head2
398 398 $ hg ci -m h2e -d '4 0'
399 399
400 400 $ hg merge -q
401 401 $ hg ci -m merge -d '5 -3600'
402 402
403 403 $ hg tag -r 1 -m t1 -d '6 0' t1
404 404 $ hg tag -r 2 -m t2 -d '7 0' t2
405 405 $ hg tag -r 3 -m t3 -d '8 0' t3
406 406 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
407 407 $ hg tag -r 5 -m t5 -d '9 0' t5
408 408 $ hg tag -r 3 -m at3 -d '10 0' at3
409 409
410 410 $ hg log -G --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
411 411 @ 11: t3, C: 9, D: 8
412 412 |
413 413 o 10: t3, C: 8, D: 7
414 414 |
415 415 o 9: t3, C: 7, D: 6
416 416 |
417 417 o 8: t3, C: 6, D: 5
418 418 |
419 419 o 7: t3, C: 5, D: 4
420 420 |
421 421 o 6: t3, C: 4, D: 3
422 422 |
423 423 o 5: t3, C: 3, D: 2
424 424 |\
425 425 | o 4: t3, C: 1, D: 1
426 426 | |
427 427 | o 3: t3, C: 0, D: 0
428 428 | |
429 429 o | 2: t1, C: 1, D: 1
430 430 |/
431 431 o 1: t1, C: 0, D: 0
432 432 |
433 433 o 0: null, C: 1, D: 1
434 434
435 435
436 436 $ cd ..
437 437
438 438 Test filter() empty values:
439 439
440 440 $ hg log -R a -r 1 -T '{filter(desc|splitlines) % "{line}\n"}'
441 441 other 1
442 442 other 2
443 443 other 3
444 444 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1) % "{ifeq(key, "a", "{value}\n")}")}'
445 445 0
446 446
447 447 0 should not be falsy
448 448
449 449 $ hg log -R a -r 0 -T '{filter(revset("0:2"))}\n'
450 450 0 1 2
451 451
452 452 Test filter() by expression:
453 453
454 454 $ hg log -R a -r 1 -T '{filter(desc|splitlines, ifcontains("1", line, "t"))}\n'
455 455 other 1
456 456 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1), ifeq(key, "b", "t"))}\n'
457 457 b=1
458 458
459 459 Test filter() shouldn't crash:
460 460
461 461 $ hg log -R a -r 0 -T '{filter(extras)}\n'
462 462 branch=default
463 463 $ hg log -R a -r 0 -T '{filter(files)}\n'
464 464 a
465 465
466 466 Test filter() unsupported arguments:
467 467
468 468 $ hg log -R a -r 0 -T '{filter()}\n'
469 469 hg: parse error: filter expects one or two arguments
470 470 [255]
471 471 $ hg log -R a -r 0 -T '{filter(date)}\n'
472 472 hg: parse error: date is not iterable
473 473 [255]
474 474 $ hg log -R a -r 0 -T '{filter(rev)}\n'
475 475 hg: parse error: 0 is not iterable
476 476 [255]
477 477 $ hg log -R a -r 0 -T '{filter(desc|firstline)}\n'
478 478 hg: parse error: 'line 1' is not filterable
479 479 [255]
480 480 $ hg log -R a -r 0 -T '{filter(manifest)}\n'
481 481 hg: parse error: '0:a0c8bcbbb45c' is not filterable
482 482 [255]
483 483 $ hg log -R a -r 0 -T '{filter(succsandmarkers)}\n'
484 484 hg: parse error: not filterable without template
485 485 [255]
486 486 $ hg log -R a -r 0 -T '{filter(desc|splitlines % "{line}", "")}\n'
487 487 hg: parse error: not filterable by expression
488 488 [255]
489 489
490 490 Test manifest/get() can be join()-ed as string, though it's silly:
491 491
492 492 $ hg log -R latesttag -r tip -T '{join(manifest, ".")}\n'
493 493 1.1.:.2.b.c.6.e.9.0.0.6.c.e.2
494 494 $ hg log -R latesttag -r tip -T '{join(get(extras, "branch"), ".")}\n'
495 495 d.e.f.a.u.l.t
496 496
497 497 Test join() over string
498 498
499 499 $ hg log -R latesttag -r tip -T '{join(rev|stringify, ".")}\n'
500 500 1.1
501 501
502 502 Test join() over uniterable
503 503
504 504 $ hg log -R latesttag -r tip -T '{join(rev, "")}\n'
505 505 hg: parse error: 11 is not iterable
506 506 [255]
507 507
508 508 Test min/max of integers
509 509
510 510 $ hg log -R latesttag -l1 -T '{min(revset("9:10"))}\n'
511 511 9
512 512 $ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
513 513 10
514 514
515 515 Test min/max over map operation:
516 516
517 517 $ hg log -R latesttag -r3 -T '{min(tags % "{tag}")}\n'
518 518 at3
519 519 $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
520 520 t3
521 521
522 522 Test min/max of strings:
523 523
524 524 $ hg log -R latesttag -l1 -T '{min(desc)}\n'
525 525 3
526 526 $ hg log -R latesttag -l1 -T '{max(desc)}\n'
527 527 t
528 528
529 529 Test min/max of non-iterable:
530 530
531 531 $ hg debugtemplate '{min(1)}'
532 532 hg: parse error: 1 is not iterable
533 533 (min first argument should be an iterable)
534 534 [255]
535 535 $ hg debugtemplate '{max(2)}'
536 536 hg: parse error: 2 is not iterable
537 537 (max first argument should be an iterable)
538 538 [255]
539 539
540 540 $ hg log -R latesttag -l1 -T '{min(date)}'
541 541 hg: parse error: date is not iterable
542 542 (min first argument should be an iterable)
543 543 [255]
544 544 $ hg log -R latesttag -l1 -T '{max(date)}'
545 545 hg: parse error: date is not iterable
546 546 (max first argument should be an iterable)
547 547 [255]
548 548
549 549 Test min/max of empty sequence:
550 550
551 551 $ hg debugtemplate '{min("")}'
552 552 hg: parse error: empty string
553 553 (min first argument should be an iterable)
554 554 [255]
555 555 $ hg debugtemplate '{max("")}'
556 556 hg: parse error: empty string
557 557 (max first argument should be an iterable)
558 558 [255]
559 559 $ hg debugtemplate '{min(dict())}'
560 560 hg: parse error: empty sequence
561 561 (min first argument should be an iterable)
562 562 [255]
563 563 $ hg debugtemplate '{max(dict())}'
564 564 hg: parse error: empty sequence
565 565 (max first argument should be an iterable)
566 566 [255]
567 567 $ hg debugtemplate '{min(dict() % "")}'
568 568 hg: parse error: empty sequence
569 569 (min first argument should be an iterable)
570 570 [255]
571 571 $ hg debugtemplate '{max(dict() % "")}'
572 572 hg: parse error: empty sequence
573 573 (max first argument should be an iterable)
574 574 [255]
575 575
576 576 Test min/max of if() result
577 577
578 578 $ cd latesttag
579 579 $ hg log -l1 -T '{min(if(true, revset("9:10"), ""))}\n'
580 580 9
581 581 $ hg log -l1 -T '{max(if(false, "", revset("9:10")))}\n'
582 582 10
583 583 $ hg log -l1 -T '{min(ifcontains("a", "aa", revset("9:10"), ""))}\n'
584 584 9
585 585 $ hg log -l1 -T '{max(ifcontains("a", "bb", "", revset("9:10")))}\n'
586 586 10
587 587 $ hg log -l1 -T '{min(ifeq(0, 0, revset("9:10"), ""))}\n'
588 588 9
589 589 $ hg log -l1 -T '{max(ifeq(0, 1, "", revset("9:10")))}\n'
590 590 10
591 591 $ cd ..
592 592
593 593 Test laziness of if() then/else clause
594 594
595 595 $ hg debugtemplate '{count(0)}'
596 596 hg: parse error: not countable
597 597 (incompatible use of template filter 'count')
598 598 [255]
599 599 $ hg debugtemplate '{if(true, "", count(0))}'
600 600 $ hg debugtemplate '{if(false, count(0), "")}'
601 601 $ hg debugtemplate '{ifcontains("a", "aa", "", count(0))}'
602 602 $ hg debugtemplate '{ifcontains("a", "bb", count(0), "")}'
603 603 $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
604 604 $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
605 605
606 606 Test the sub function of templating for expansion:
607 607
608 608 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
609 609 xx
610 610
611 611 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
612 612 hg: parse error: sub got an invalid pattern: [
613 613 [255]
614 614 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
615 615 hg: parse error: sub got an invalid replacement: \1
616 616 [255]
617 617
618 618 Test the strip function with chars specified:
619 619
620 620 $ hg log -R latesttag --template '{desc}\n'
621 621 at3
622 622 t5
623 623 t4
624 624 t3
625 625 t2
626 626 t1
627 627 merge
628 628 h2e
629 629 h2d
630 630 h1c
631 631 b
632 632 a
633 633
634 634 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
635 635 at3
636 636 5
637 637 4
638 638 3
639 639 2
640 640 1
641 641 merg
642 642 h2
643 643 h2d
644 644 h1c
645 645 b
646 646 a
647 647
648 648 Test date format:
649 649
650 650 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
651 651 date: 70 01 01 10 +0000
652 652 date: 70 01 01 09 +0000
653 653 date: 70 01 01 04 +0000
654 654 date: 70 01 01 08 +0000
655 655 date: 70 01 01 07 +0000
656 656 date: 70 01 01 06 +0000
657 657 date: 70 01 01 05 +0100
658 658 date: 70 01 01 04 +0000
659 659 date: 70 01 01 03 +0000
660 660 date: 70 01 01 02 +0000
661 661 date: 70 01 01 01 +0000
662 662 date: 70 01 01 00 +0000
663 663
664 664 Test invalid date:
665 665
666 666 $ hg log -R latesttag -T '{date(rev)}\n'
667 667 hg: parse error: date expects a date information
668 668 [255]
669 669
670 670 Set up repository containing template fragments in commit metadata:
671 671
672 672 $ hg init r
673 673 $ cd r
674 674 $ echo a > a
675 675 $ hg ci -Am '{rev}'
676 676 adding a
677 677
678 678 $ hg branch -q 'text.{rev}'
679 679 $ echo aa >> aa
680 680 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
681 681
682 682 color effect can be specified without quoting:
683 683
684 684 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
685 685 \x1b[0;31mtext\x1b[0m (esc)
686 686
687 687 color effects can be nested (issue5413)
688 688
689 689 $ hg debugtemplate --color=always \
690 690 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
691 691 \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)
692 692
693 693 pad() should interact well with color codes (issue5416)
694 694
695 695 $ hg debugtemplate --color=always \
696 696 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
697 697 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
698 698
699 pad() with truncate has to strip color codes, though
700
701 $ hg debugtemplate --color=always \
702 > '{pad(label(red, "scarlet"), 5, truncate=true)}\n'
703 scarl
704
699 705 label should be no-op if color is disabled:
700 706
701 707 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
702 708 text
703 709 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
704 710 text
705 711
706 712 Test branches inside if statement:
707 713
708 714 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
709 715 no
710 716
711 717 Test dict constructor:
712 718
713 719 $ hg log -r 0 -T '{dict(y=node|short, x=rev)}\n'
714 720 y=f7769ec2ab97 x=0
715 721 $ hg log -r 0 -T '{dict(x=rev, y=node|short) % "{key}={value}\n"}'
716 722 x=0
717 723 y=f7769ec2ab97
718 724 $ hg log -r 0 -T '{dict(x=rev, y=node|short)|json}\n'
719 725 {"x": 0, "y": "f7769ec2ab97"}
720 726 $ hg log -r 0 -T '{dict()|json}\n'
721 727 {}
722 728
723 729 $ hg log -r 0 -T '{dict(rev, node=node|short)}\n'
724 730 rev=0 node=f7769ec2ab97
725 731 $ hg log -r 0 -T '{dict(rev, node|short)}\n'
726 732 rev=0 node=f7769ec2ab97
727 733
728 734 $ hg log -r 0 -T '{dict(rev, rev=rev)}\n'
729 735 hg: parse error: duplicated dict key 'rev' inferred
730 736 [255]
731 737 $ hg log -r 0 -T '{dict(node, node|short)}\n'
732 738 hg: parse error: duplicated dict key 'node' inferred
733 739 [255]
734 740 $ hg log -r 0 -T '{dict(1 + 2)}'
735 741 hg: parse error: dict key cannot be inferred
736 742 [255]
737 743
738 744 $ hg log -r 0 -T '{dict(x=rev, x=node)}'
739 745 hg: parse error: dict got multiple values for keyword argument 'x'
740 746 [255]
741 747
742 748 Test get function:
743 749
744 750 $ hg log -r 0 --template '{get(extras, "branch")}\n'
745 751 default
746 752 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
747 753 default
748 754 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
749 755 hg: parse error: not a dictionary
750 756 (get() expects a dict as first argument)
751 757 [255]
752 758
753 759 Test json filter applied to wrapped object:
754 760
755 761 $ hg log -r0 -T '{files|json}\n'
756 762 ["a"]
757 763 $ hg log -r0 -T '{extras|json}\n'
758 764 {"branch": "default"}
759 765 $ hg log -r0 -T '{date|json}\n'
760 766 [0, 0]
761 767
762 768 Test json filter applied to map result:
763 769
764 770 $ hg log -r0 -T '{json(extras % "{key}")}\n'
765 771 ["branch"]
766 772
767 773 Test localdate(date, tz) function:
768 774
769 775 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
770 776 1970-01-01 09:00 +0900
771 777 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
772 778 1970-01-01 00:00 +0000
773 779 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
774 780 hg: parse error: localdate expects a timezone
775 781 [255]
776 782 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
777 783 1970-01-01 02:00 +0200
778 784 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
779 785 1970-01-01 00:00 +0000
780 786 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
781 787 1970-01-01 00:00 +0000
782 788 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
783 789 hg: parse error: localdate expects a timezone
784 790 [255]
785 791 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
786 792 hg: parse error: localdate expects a timezone
787 793 [255]
788 794
789 795 Test shortest(node) function:
790 796
791 797 $ echo b > b
792 798 $ hg ci -qAm b
793 799 $ hg log --template '{shortest(node)}\n'
794 800 e777
795 801 bcc7
796 802 f776
797 803 $ hg log --template '{shortest(node, 10)}\n'
798 804 e777603221
799 805 bcc7ff960b
800 806 f7769ec2ab
801 807 $ hg log --template '{node|shortest}\n' -l1
802 808 e777
803 809
804 810 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
805 811 f7769ec2ab
806 812 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
807 813 hg: parse error: shortest() expects an integer minlength
808 814 [255]
809 815
810 816 $ hg log -r 'wdir()' -T '{node|shortest}\n'
811 817 ffff
812 818
813 819 $ hg log --template '{shortest("f")}\n' -l1
814 820 f
815 821
816 822 $ hg log --template '{shortest("0123456789012345678901234567890123456789")}\n' -l1
817 823 0123456789012345678901234567890123456789
818 824
819 825 $ hg log --template '{shortest("01234567890123456789012345678901234567890123456789")}\n' -l1
820 826 01234567890123456789012345678901234567890123456789
821 827
822 828 $ hg log --template '{shortest("not a hex string")}\n' -l1
823 829 not a hex string
824 830
825 831 $ hg log --template '{shortest("not a hex string, but it'\''s 40 bytes long")}\n' -l1
826 832 not a hex string, but it's 40 bytes long
827 833
828 834 $ hg log --template '{shortest("ffffffffffffffffffffffffffffffffffffffff")}\n' -l1
829 835 ffff
830 836
831 837 $ hg log --template '{shortest("fffffff")}\n' -l1
832 838 ffff
833 839
834 840 $ hg log --template '{shortest("ff")}\n' -l1
835 841 ffff
836 842
837 843 $ cd ..
838 844
839 845 Test shortest(node) with the repo having short hash collision:
840 846
841 847 $ hg init hashcollision
842 848 $ cd hashcollision
843 849 $ cat <<EOF >> .hg/hgrc
844 850 > [experimental]
845 851 > evolution.createmarkers=True
846 852 > EOF
847 853 $ echo 0 > a
848 854 $ hg ci -qAm 0
849 855 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
850 856 > hg up -q 0
851 857 > echo $i > a
852 858 > hg ci -qm $i
853 859 > done
854 860 $ hg up -q null
855 861 $ hg log -r0: -T '{rev}:{node}\n'
856 862 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
857 863 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
858 864 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
859 865 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
860 866 4:10776689e627b465361ad5c296a20a487e153ca4
861 867 5:a00be79088084cb3aff086ab799f8790e01a976b
862 868 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
863 869 7:a0457b3450b8e1b778f1163b31a435802987fe5d
864 870 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
865 871 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
866 872 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
867 873 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
868 874 obsoleted 1 changesets
869 875 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
870 876 obsoleted 1 changesets
871 877 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
872 878 obsoleted 1 changesets
873 879
874 880 nodes starting with '11' (we don't have the revision number '11' though)
875 881
876 882 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
877 883 1:1142
878 884 2:1140
879 885 3:11d
880 886
881 887 '5:a00' is hidden, but still we have two nodes starting with 'a0'
882 888
883 889 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
884 890 6:a0b
885 891 7:a04
886 892
887 893 node '10' conflicts with the revision number '10' even if it is hidden
888 894 (we could exclude hidden revision numbers, but currently we don't)
889 895
890 896 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
891 897 4:107
892 898 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
893 899 4:107
894 900
895 901 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n'
896 902 4:x10
897 903 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
898 904 4:x10
899 905
900 906 node 'c562' should be unique if the other 'c562' nodes are hidden
901 907 (but we don't try the slow path to filter out hidden nodes for now)
902 908
903 909 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
904 910 8:c5625
905 911 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
906 912 8:c5625
907 913 9:c5623
908 914 10:c562d
909 915
910 916 $ cd ..
911 917
912 918 Test pad function
913 919
914 920 $ cd r
915 921
916 922 $ hg log --template '{pad(rev, 20)} {author|user}\n'
917 923 2 test
918 924 1 {node|short}
919 925 0 test
920 926
921 927 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
922 928 2 test
923 929 1 {node|short}
924 930 0 test
925 931
926 932 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
927 933 2------------------- test
928 934 1------------------- {node|short}
929 935 0------------------- test
930 936
937 $ hg log --template '{pad(author, 5, "-", False, True)}\n'
938 test-
939 {node
940 test-
941 $ hg log --template '{pad(author, 5, "-", True, True)}\n'
942 -test
943 hort}
944 -test
945
931 946 Test template string in pad function
932 947
933 948 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
934 949 {0} test
935 950
936 951 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
937 952 \{rev} test
938 953
939 954 Test width argument passed to pad function
940 955
941 956 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
942 957 0 test
943 958 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
944 959 hg: parse error: pad() expects an integer width
945 960 [255]
946 961
947 962 Test invalid fillchar passed to pad function
948 963
949 964 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
950 965 hg: parse error: pad() expects a single fill character
951 966 [255]
952 967 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
953 968 hg: parse error: pad() expects a single fill character
954 969 [255]
955 970
956 971 Test boolean argument passed to pad function
957 972
958 973 no crash
959 974
960 975 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
961 976 ---------0
962 977
963 978 string/literal
964 979
965 980 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
966 981 ---------0
967 982 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
968 983 0---------
969 984 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
970 985 0---------
971 986
972 987 unknown keyword is evaluated to ''
973 988
974 989 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
975 990 0---------
976 991
977 992 Test separate function
978 993
979 994 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
980 995 a-b-c
981 996 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
982 997 0:f7769ec2ab97 test default
983 998 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
984 999 a \x1b[0;31mb\x1b[0m c d (esc)
985 1000
986 1001 Test boolean expression/literal passed to if function
987 1002
988 1003 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
989 1004 rev 0 is True
990 1005 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
991 1006 literal 0 is True as well
992 1007 $ hg log -r 0 -T '{if(min(revset(r"0")), "0 of hybriditem is also True")}\n'
993 1008 0 of hybriditem is also True
994 1009 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
995 1010 empty string is False
996 1011 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
997 1012 empty list is False
998 1013 $ hg log -r 0 -T '{if(revset(r"0"), "non-empty list is True")}\n'
999 1014 non-empty list is True
1000 1015 $ hg log -r 0 -T '{if(revset(r"0") % "", "list of empty strings is True")}\n'
1001 1016 list of empty strings is True
1002 1017 $ hg log -r 0 -T '{if(true, "true is True")}\n'
1003 1018 true is True
1004 1019 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
1005 1020 false is False
1006 1021 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
1007 1022 non-empty string is True
1008 1023
1009 1024 Test ifcontains function
1010 1025
1011 1026 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
1012 1027 2 is in the string
1013 1028 1 is not
1014 1029 0 is in the string
1015 1030
1016 1031 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
1017 1032 2 is in the string
1018 1033 1 is not
1019 1034 0 is in the string
1020 1035
1021 1036 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
1022 1037 2 did not add a
1023 1038 1 did not add a
1024 1039 0 added a
1025 1040
1026 1041 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
1027 1042 2 is parent of 1
1028 1043 1
1029 1044 0
1030 1045
1031 1046 $ hg log -l1 -T '{ifcontains("branch", extras, "t", "f")}\n'
1032 1047 t
1033 1048 $ hg log -l1 -T '{ifcontains("branch", extras % "{key}", "t", "f")}\n'
1034 1049 t
1035 1050 $ hg log -l1 -T '{ifcontains("branc", extras % "{key}", "t", "f")}\n'
1036 1051 f
1037 1052 $ hg log -l1 -T '{ifcontains("branc", stringify(extras % "{key}"), "t", "f")}\n'
1038 1053 t
1039 1054
1040 1055 Test revset function
1041 1056
1042 1057 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
1043 1058 2 current rev
1044 1059 1 not current rev
1045 1060 0 not current rev
1046 1061
1047 1062 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
1048 1063 2 match rev
1049 1064 1 match rev
1050 1065 0 not match rev
1051 1066
1052 1067 $ hg log -T '{ifcontains(desc, revset(":"), "", "type not match")}\n' -l1
1053 1068 type not match
1054 1069
1055 1070 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
1056 1071 2 Parents: 1
1057 1072 1 Parents: 0
1058 1073 0 Parents:
1059 1074
1060 1075 $ cat >> .hg/hgrc <<EOF
1061 1076 > [revsetalias]
1062 1077 > myparents(\$1) = parents(\$1)
1063 1078 > EOF
1064 1079 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
1065 1080 2 Parents: 1
1066 1081 1 Parents: 0
1067 1082 0 Parents:
1068 1083
1069 1084 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
1070 1085 Rev: 2
1071 1086 Ancestor: 0
1072 1087 Ancestor: 1
1073 1088 Ancestor: 2
1074 1089
1075 1090 Rev: 1
1076 1091 Ancestor: 0
1077 1092 Ancestor: 1
1078 1093
1079 1094 Rev: 0
1080 1095 Ancestor: 0
1081 1096
1082 1097 $ hg log --template '{revset("TIP"|lower)}\n' -l1
1083 1098 2
1084 1099
1085 1100 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
1086 1101 2
1087 1102
1088 1103 a list template is evaluated for each item of revset/parents
1089 1104
1090 1105 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
1091 1106 2 p: 1:bcc7ff960b8e
1092 1107 1 p: 0:f7769ec2ab97
1093 1108 0 p:
1094 1109
1095 1110 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
1096 1111 2 p: 1:bcc7ff960b8e -1:000000000000
1097 1112 1 p: 0:f7769ec2ab97 -1:000000000000
1098 1113 0 p: -1:000000000000 -1:000000000000
1099 1114
1100 1115 therefore, 'revcache' should be recreated for each rev
1101 1116
1102 1117 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
1103 1118 2 aa b
1104 1119 p
1105 1120 1
1106 1121 p a
1107 1122 0 a
1108 1123 p
1109 1124
1110 1125 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
1111 1126 2 aa b
1112 1127 p
1113 1128 1
1114 1129 p a
1115 1130 0 a
1116 1131 p
1117 1132
1118 1133 a revset item must be evaluated as an integer revision, not an offset from tip
1119 1134
1120 1135 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
1121 1136 -1:000000000000
1122 1137 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
1123 1138 -1:000000000000
1124 1139
1125 1140 join() should pick '{rev}' from revset items:
1126 1141
1127 1142 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
1128 1143 4, 5
1129 1144
1130 1145 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
1131 1146 default. join() should agree with the default formatting:
1132 1147
1133 1148 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
1134 1149 5:13207e5a10d9, 4:bbe44766e73d
1135 1150
1136 1151 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
1137 1152 5:13207e5a10d9fd28ec424934298e176197f2c67f,
1138 1153 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1139 1154
1140 1155 Invalid arguments passed to revset()
1141 1156
1142 1157 $ hg log -T '{revset("%whatever", 0)}\n'
1143 1158 hg: parse error: unexpected revspec format character w
1144 1159 [255]
1145 1160 $ hg log -T '{revset("%lwhatever", files)}\n'
1146 1161 hg: parse error: unexpected revspec format character w
1147 1162 [255]
1148 1163 $ hg log -T '{revset("%s %s", 0)}\n'
1149 1164 hg: parse error: missing argument for revspec
1150 1165 [255]
1151 1166 $ hg log -T '{revset("", 0)}\n'
1152 1167 hg: parse error: too many revspec arguments specified
1153 1168 [255]
1154 1169 $ hg log -T '{revset("%s", 0, 1)}\n'
1155 1170 hg: parse error: too many revspec arguments specified
1156 1171 [255]
1157 1172 $ hg log -T '{revset("%", 0)}\n'
1158 1173 hg: parse error: incomplete revspec format character
1159 1174 [255]
1160 1175 $ hg log -T '{revset("%l", 0)}\n'
1161 1176 hg: parse error: incomplete revspec format character
1162 1177 [255]
1163 1178 $ hg log -T '{revset("%d", 'foo')}\n'
1164 1179 hg: parse error: invalid argument for revspec
1165 1180 [255]
1166 1181 $ hg log -T '{revset("%ld", files)}\n'
1167 1182 hg: parse error: invalid argument for revspec
1168 1183 [255]
1169 1184 $ hg log -T '{revset("%ls", 0)}\n'
1170 1185 hg: parse error: invalid argument for revspec
1171 1186 [255]
1172 1187 $ hg log -T '{revset("%b", 'foo')}\n'
1173 1188 hg: parse error: invalid argument for revspec
1174 1189 [255]
1175 1190 $ hg log -T '{revset("%lb", files)}\n'
1176 1191 hg: parse error: invalid argument for revspec
1177 1192 [255]
1178 1193 $ hg log -T '{revset("%r", 0)}\n'
1179 1194 hg: parse error: invalid argument for revspec
1180 1195 [255]
1181 1196
1182 1197 Test files function
1183 1198
1184 1199 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
1185 1200 2
1186 1201 a
1187 1202 aa
1188 1203 b
1189 1204 1
1190 1205 a
1191 1206 0
1192 1207 a
1193 1208
1194 1209 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
1195 1210 2
1196 1211 aa
1197 1212 1
1198 1213
1199 1214 0
1200 1215
1201 1216
1202 1217 $ hg log -l1 -T "{files('aa') % '{file}\n'}"
1203 1218 aa
1204 1219 $ hg log -l1 -T "{files('aa') % '{path}\n'}"
1205 1220 aa
1206 1221
1207 1222 $ hg rm a
1208 1223 $ hg log -r "wdir()" -T "{rev}\n{join(files('*'), '\n')}\n"
1209 1224 2147483647
1210 1225 aa
1211 1226 b
1212 1227 $ hg revert a
1213 1228
1214 1229 Test relpath function
1215 1230
1216 1231 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
1217 1232 a
1218 1233 $ cd ..
1219 1234 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
1220 1235 r/a
1221 1236
1222 1237 Test stringify on sub expressions
1223 1238
1224 1239 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
1225 1240 fourth, second, third
1226 1241 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
1227 1242 abc
1228 1243
1229 1244 Test splitlines
1230 1245
1231 1246 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
1232 1247 @ foo Modify, add, remove, rename
1233 1248 |
1234 1249 o foo future
1235 1250 |
1236 1251 o foo third
1237 1252 |
1238 1253 o foo second
1239 1254
1240 1255 o foo merge
1241 1256 |\
1242 1257 | o foo new head
1243 1258 | |
1244 1259 o | foo new branch
1245 1260 |/
1246 1261 o foo no user, no domain
1247 1262 |
1248 1263 o foo no person
1249 1264 |
1250 1265 o foo other 1
1251 1266 | foo other 2
1252 1267 | foo
1253 1268 | foo other 3
1254 1269 o foo line 1
1255 1270 foo line 2
1256 1271
1257 1272 $ hg log -R a -r0 -T '{desc|splitlines}\n'
1258 1273 line 1 line 2
1259 1274 $ hg log -R a -r0 -T '{join(desc|splitlines, "|")}\n'
1260 1275 line 1|line 2
1261 1276
1262 1277 Test startswith
1263 1278 $ hg log -Gv -R a --template "{startswith(desc)}"
1264 1279 hg: parse error: startswith expects two arguments
1265 1280 [255]
1266 1281
1267 1282 $ hg log -Gv -R a --template "{startswith('line', desc)}"
1268 1283 @
1269 1284 |
1270 1285 o
1271 1286 |
1272 1287 o
1273 1288 |
1274 1289 o
1275 1290
1276 1291 o
1277 1292 |\
1278 1293 | o
1279 1294 | |
1280 1295 o |
1281 1296 |/
1282 1297 o
1283 1298 |
1284 1299 o
1285 1300 |
1286 1301 o
1287 1302 |
1288 1303 o line 1
1289 1304 line 2
1290 1305
1291 1306 Test word function (including index out of bounds graceful failure)
1292 1307
1293 1308 $ hg log -Gv -R a --template "{word('1', desc)}"
1294 1309 @ add,
1295 1310 |
1296 1311 o
1297 1312 |
1298 1313 o
1299 1314 |
1300 1315 o
1301 1316
1302 1317 o
1303 1318 |\
1304 1319 | o head
1305 1320 | |
1306 1321 o | branch
1307 1322 |/
1308 1323 o user,
1309 1324 |
1310 1325 o person
1311 1326 |
1312 1327 o 1
1313 1328 |
1314 1329 o 1
1315 1330
1316 1331
1317 1332 Test word third parameter used as splitter
1318 1333
1319 1334 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
1320 1335 @ M
1321 1336 |
1322 1337 o future
1323 1338 |
1324 1339 o third
1325 1340 |
1326 1341 o sec
1327 1342
1328 1343 o merge
1329 1344 |\
1330 1345 | o new head
1331 1346 | |
1332 1347 o | new branch
1333 1348 |/
1334 1349 o n
1335 1350 |
1336 1351 o n
1337 1352 |
1338 1353 o
1339 1354 |
1340 1355 o line 1
1341 1356 line 2
1342 1357
1343 1358 Test word error messages for not enough and too many arguments
1344 1359
1345 1360 $ hg log -Gv -R a --template "{word('0')}"
1346 1361 hg: parse error: word expects two or three arguments, got 1
1347 1362 [255]
1348 1363
1349 1364 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
1350 1365 hg: parse error: word expects two or three arguments, got 7
1351 1366 [255]
1352 1367
1353 1368 Test word for integer literal
1354 1369
1355 1370 $ hg log -R a --template "{word(2, desc)}\n" -r0
1356 1371 line
1357 1372
1358 1373 Test word for invalid numbers
1359 1374
1360 1375 $ hg log -Gv -R a --template "{word('a', desc)}"
1361 1376 hg: parse error: word expects an integer index
1362 1377 [255]
1363 1378
1364 1379 Test word for out of range
1365 1380
1366 1381 $ hg log -R a --template "{word(10000, desc)}"
1367 1382 $ hg log -R a --template "{word(-10000, desc)}"
1368 1383
1369 1384 Test indent and not adding to empty lines
1370 1385
1371 1386 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
1372 1387 -----
1373 1388 > line 1
1374 1389 >> line 2
1375 1390 -----
1376 1391 > other 1
1377 1392 >> other 2
1378 1393
1379 1394 >> other 3
1380 1395
1381 1396 Test with non-strings like dates
1382 1397
1383 1398 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
1384 1399 1200000.00
1385 1400 1300000.00
1386 1401
1387 1402 json filter should escape HTML tags so that the output can be embedded in hgweb:
1388 1403
1389 1404 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
1390 1405 "\u003cfoo@example.org\u003e"
1391 1406
1392 1407 Set up repository for non-ascii encoding tests:
1393 1408
1394 1409 $ hg init nonascii
1395 1410 $ cd nonascii
1396 1411 $ "$PYTHON" <<EOF
1397 1412 > open('latin1', 'wb').write(b'\xe9')
1398 1413 > open('utf-8', 'wb').write(b'\xc3\xa9')
1399 1414 > EOF
1400 1415 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
1401 1416 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
1402 1417
1403 1418 json filter should try round-trip conversion to utf-8:
1404 1419
1405 1420 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
1406 1421 "\u00e9"
1407 1422 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
1408 1423 "non-ascii branch: \u00e9"
1409 1424
1410 1425 json filter should take input as utf-8 if it was converted from utf-8:
1411 1426
1412 1427 $ HGENCODING=latin-1 hg log -T "{branch|json}\n" -r0
1413 1428 "\u00e9"
1414 1429 $ HGENCODING=latin-1 hg log -T "{desc|json}\n" -r0
1415 1430 "non-ascii branch: \u00e9"
1416 1431
1417 1432 json filter takes input as utf-8b:
1418 1433
1419 1434 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
1420 1435 "\u00e9"
1421 1436 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
1422 1437 "\udce9"
1423 1438
1424 1439 utf8 filter:
1425 1440
1426 1441 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
1427 1442 round-trip: c3a9
1428 1443 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
1429 1444 decoded: c3a9
1430 1445 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
1431 1446 abort: decoding near * (glob)
1432 1447 [255]
1433 1448 $ hg log -T "coerced to string: {rev|utf8}\n" -r0
1434 1449 coerced to string: 0
1435 1450
1436 1451 pad width:
1437 1452
1438 1453 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
1439 1454 \xc3\xa9- (esc)
1440 1455
1441 1456 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now