##// END OF EJS Templates
templater: fix truth testing of integer 0 taken from a list/dict...
Yuya Nishihara -
r38466:b6294c11 default
parent child Browse files
Show More
@@ -1,886 +1,887 b''
1 1 # templateutil.py - utility for template evaluation
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 abc
11 11 import types
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 pycompat,
17 17 util,
18 18 )
19 19 from .utils import (
20 20 dateutil,
21 21 stringutil,
22 22 )
23 23
24 24 class ResourceUnavailable(error.Abort):
25 25 pass
26 26
27 27 class TemplateNotFound(error.Abort):
28 28 pass
29 29
30 30 class wrapped(object):
31 31 """Object requiring extra conversion prior to displaying or processing
32 32 as value
33 33
34 34 Use unwrapvalue() or unwrapastype() to obtain the inner object.
35 35 """
36 36
37 37 __metaclass__ = abc.ABCMeta
38 38
39 39 @abc.abstractmethod
40 40 def contains(self, context, mapping, item):
41 41 """Test if the specified item is in self
42 42
43 43 The item argument may be a wrapped object.
44 44 """
45 45
46 46 @abc.abstractmethod
47 47 def getmember(self, context, mapping, key):
48 48 """Return a member item for the specified key
49 49
50 50 The key argument may be a wrapped object.
51 51 A returned object may be either a wrapped object or a pure value
52 52 depending on the self type.
53 53 """
54 54
55 55 @abc.abstractmethod
56 56 def getmin(self, context, mapping):
57 57 """Return the smallest item, which may be either a wrapped or a pure
58 58 value depending on the self type"""
59 59
60 60 @abc.abstractmethod
61 61 def getmax(self, context, mapping):
62 62 """Return the largest item, which may be either a wrapped or a pure
63 63 value depending on the self type"""
64 64
65 65 @abc.abstractmethod
66 66 def itermaps(self, context):
67 67 """Yield each template mapping"""
68 68
69 69 @abc.abstractmethod
70 70 def join(self, context, mapping, sep):
71 71 """Join items with the separator; Returns a bytes or (possibly nested)
72 72 generator of bytes
73 73
74 74 A pre-configured template may be rendered per item if this container
75 75 holds unprintable items.
76 76 """
77 77
78 78 @abc.abstractmethod
79 79 def show(self, context, mapping):
80 80 """Return a bytes or (possibly nested) generator of bytes representing
81 81 the underlying object
82 82
83 83 A pre-configured template may be rendered if the underlying object is
84 84 not printable.
85 85 """
86 86
87 87 @abc.abstractmethod
88 88 def tobool(self, context, mapping):
89 89 """Return a boolean representation of the inner value"""
90 90
91 91 @abc.abstractmethod
92 92 def tovalue(self, context, mapping):
93 93 """Move the inner value object out or create a value representation
94 94
95 95 A returned value must be serializable by templaterfilters.json().
96 96 """
97 97
98 98 class mappable(object):
99 99 """Object which can be converted to a single template mapping"""
100 100
101 101 def itermaps(self, context):
102 102 yield self.tomap(context)
103 103
104 104 @abc.abstractmethod
105 105 def tomap(self, context):
106 106 """Create a single template mapping representing this"""
107 107
108 108 class wrappedbytes(wrapped):
109 109 """Wrapper for byte string"""
110 110
111 111 def __init__(self, value):
112 112 self._value = value
113 113
114 114 def contains(self, context, mapping, item):
115 115 item = stringify(context, mapping, item)
116 116 return item in self._value
117 117
118 118 def getmember(self, context, mapping, key):
119 119 raise error.ParseError(_('%r is not a dictionary')
120 120 % pycompat.bytestr(self._value))
121 121
122 122 def getmin(self, context, mapping):
123 123 return self._getby(context, mapping, min)
124 124
125 125 def getmax(self, context, mapping):
126 126 return self._getby(context, mapping, max)
127 127
128 128 def _getby(self, context, mapping, func):
129 129 if not self._value:
130 130 raise error.ParseError(_('empty string'))
131 131 return func(pycompat.iterbytestr(self._value))
132 132
133 133 def itermaps(self, context):
134 134 raise error.ParseError(_('%r is not iterable of mappings')
135 135 % pycompat.bytestr(self._value))
136 136
137 137 def join(self, context, mapping, sep):
138 138 return joinitems(pycompat.iterbytestr(self._value), sep)
139 139
140 140 def show(self, context, mapping):
141 141 return self._value
142 142
143 143 def tobool(self, context, mapping):
144 144 return bool(self._value)
145 145
146 146 def tovalue(self, context, mapping):
147 147 return self._value
148 148
149 149 class wrappedvalue(wrapped):
150 150 """Generic wrapper for pure non-list/dict/bytes value"""
151 151
152 152 def __init__(self, value):
153 153 self._value = value
154 154
155 155 def contains(self, context, mapping, item):
156 156 raise error.ParseError(_("%r is not iterable") % self._value)
157 157
158 158 def getmember(self, context, mapping, key):
159 159 raise error.ParseError(_('%r is not a dictionary') % self._value)
160 160
161 161 def getmin(self, context, mapping):
162 162 raise error.ParseError(_("%r is not iterable") % self._value)
163 163
164 164 def getmax(self, context, mapping):
165 165 raise error.ParseError(_("%r is not iterable") % self._value)
166 166
167 167 def itermaps(self, context):
168 168 raise error.ParseError(_('%r is not iterable of mappings')
169 169 % self._value)
170 170
171 171 def join(self, context, mapping, sep):
172 172 raise error.ParseError(_('%r is not iterable') % self._value)
173 173
174 174 def show(self, context, mapping):
175 175 if self._value is None:
176 176 return b''
177 177 return pycompat.bytestr(self._value)
178 178
179 179 def tobool(self, context, mapping):
180 180 if self._value is None:
181 181 return False
182 182 if isinstance(self._value, bool):
183 183 return self._value
184 184 # otherwise evaluate as string, which means 0 is True
185 185 return bool(pycompat.bytestr(self._value))
186 186
187 187 def tovalue(self, context, mapping):
188 188 return self._value
189 189
190 190 class date(mappable, wrapped):
191 191 """Wrapper for date tuple"""
192 192
193 193 def __init__(self, value, showfmt='%d %d'):
194 194 # value may be (float, int), but public interface shouldn't support
195 195 # floating-point timestamp
196 196 self._unixtime, self._tzoffset = map(int, value)
197 197 self._showfmt = showfmt
198 198
199 199 def contains(self, context, mapping, item):
200 200 raise error.ParseError(_('date is not iterable'))
201 201
202 202 def getmember(self, context, mapping, key):
203 203 raise error.ParseError(_('date is not a dictionary'))
204 204
205 205 def getmin(self, context, mapping):
206 206 raise error.ParseError(_('date is not iterable'))
207 207
208 208 def getmax(self, context, mapping):
209 209 raise error.ParseError(_('date is not iterable'))
210 210
211 211 def join(self, context, mapping, sep):
212 212 raise error.ParseError(_("date is not iterable"))
213 213
214 214 def show(self, context, mapping):
215 215 return self._showfmt % (self._unixtime, self._tzoffset)
216 216
217 217 def tomap(self, context):
218 218 return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
219 219
220 220 def tobool(self, context, mapping):
221 221 return True
222 222
223 223 def tovalue(self, context, mapping):
224 224 return (self._unixtime, self._tzoffset)
225 225
226 226 class hybrid(wrapped):
227 227 """Wrapper for list or dict to support legacy template
228 228
229 229 This class allows us to handle both:
230 230 - "{files}" (legacy command-line-specific list hack) and
231 231 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
232 232 and to access raw values:
233 233 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
234 234 - "{get(extras, key)}"
235 235 - "{files|json}"
236 236 """
237 237
238 238 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
239 239 self._gen = gen # generator or function returning generator
240 240 self._values = values
241 241 self._makemap = makemap
242 242 self._joinfmt = joinfmt
243 243 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
244 244
245 245 def contains(self, context, mapping, item):
246 246 item = unwrapastype(context, mapping, item, self._keytype)
247 247 return item in self._values
248 248
249 249 def getmember(self, context, mapping, key):
250 250 # TODO: maybe split hybrid list/dict types?
251 251 if not util.safehasattr(self._values, 'get'):
252 252 raise error.ParseError(_('not a dictionary'))
253 253 key = unwrapastype(context, mapping, key, self._keytype)
254 254 return self._wrapvalue(key, self._values.get(key))
255 255
256 256 def getmin(self, context, mapping):
257 257 return self._getby(context, mapping, min)
258 258
259 259 def getmax(self, context, mapping):
260 260 return self._getby(context, mapping, max)
261 261
262 262 def _getby(self, context, mapping, func):
263 263 if not self._values:
264 264 raise error.ParseError(_('empty sequence'))
265 265 val = func(self._values)
266 266 return self._wrapvalue(val, val)
267 267
268 268 def _wrapvalue(self, key, val):
269 269 if val is None:
270 270 return
271 271 if util.safehasattr(val, '_makemap'):
272 272 # a nested hybrid list/dict, which has its own way of map operation
273 273 return val
274 274 return hybriditem(None, key, val, self._makemap)
275 275
276 276 def itermaps(self, context):
277 277 makemap = self._makemap
278 278 for x in self._values:
279 279 yield makemap(x)
280 280
281 281 def join(self, context, mapping, sep):
282 282 # TODO: switch gen to (context, mapping) API?
283 283 return joinitems((self._joinfmt(x) for x in self._values), sep)
284 284
285 285 def show(self, context, mapping):
286 286 # TODO: switch gen to (context, mapping) API?
287 287 gen = self._gen
288 288 if gen is None:
289 289 return self.join(context, mapping, ' ')
290 290 if callable(gen):
291 291 return gen()
292 292 return gen
293 293
294 294 def tobool(self, context, mapping):
295 295 return bool(self._values)
296 296
297 297 def tovalue(self, context, mapping):
298 298 # TODO: make it non-recursive for trivial lists/dicts
299 299 xs = self._values
300 300 if util.safehasattr(xs, 'get'):
301 301 return {k: unwrapvalue(context, mapping, v)
302 302 for k, v in xs.iteritems()}
303 303 return [unwrapvalue(context, mapping, x) for x in xs]
304 304
305 305 class hybriditem(mappable, wrapped):
306 306 """Wrapper for non-list/dict object to support map operation
307 307
308 308 This class allows us to handle both:
309 309 - "{manifest}"
310 310 - "{manifest % '{rev}:{node}'}"
311 311 - "{manifest.rev}"
312 312 """
313 313
314 314 def __init__(self, gen, key, value, makemap):
315 315 self._gen = gen # generator or function returning generator
316 316 self._key = key
317 317 self._value = value # may be generator of strings
318 318 self._makemap = makemap
319 319
320 320 def tomap(self, context):
321 321 return self._makemap(self._key)
322 322
323 323 def contains(self, context, mapping, item):
324 324 w = makewrapped(context, mapping, self._value)
325 325 return w.contains(context, mapping, item)
326 326
327 327 def getmember(self, context, mapping, key):
328 328 w = makewrapped(context, mapping, self._value)
329 329 return w.getmember(context, mapping, key)
330 330
331 331 def getmin(self, context, mapping):
332 332 w = makewrapped(context, mapping, self._value)
333 333 return w.getmin(context, mapping)
334 334
335 335 def getmax(self, context, mapping):
336 336 w = makewrapped(context, mapping, self._value)
337 337 return w.getmax(context, mapping)
338 338
339 339 def join(self, context, mapping, sep):
340 340 w = makewrapped(context, mapping, self._value)
341 341 return w.join(context, mapping, sep)
342 342
343 343 def show(self, context, mapping):
344 344 # TODO: switch gen to (context, mapping) API?
345 345 gen = self._gen
346 346 if gen is None:
347 347 return pycompat.bytestr(self._value)
348 348 if callable(gen):
349 349 return gen()
350 350 return gen
351 351
352 352 def tobool(self, context, mapping):
353 return bool(self.tovalue(context, mapping))
353 w = makewrapped(context, mapping, self._value)
354 return w.tobool(context, mapping)
354 355
355 356 def tovalue(self, context, mapping):
356 357 return _unthunk(context, mapping, self._value)
357 358
358 359 class _mappingsequence(wrapped):
359 360 """Wrapper for sequence of template mappings
360 361
361 362 This represents an inner template structure (i.e. a list of dicts),
362 363 which can also be rendered by the specified named/literal template.
363 364
364 365 Template mappings may be nested.
365 366 """
366 367
367 368 def __init__(self, name=None, tmpl=None, sep=''):
368 369 if name is not None and tmpl is not None:
369 370 raise error.ProgrammingError('name and tmpl are mutually exclusive')
370 371 self._name = name
371 372 self._tmpl = tmpl
372 373 self._defaultsep = sep
373 374
374 375 def contains(self, context, mapping, item):
375 376 raise error.ParseError(_('not comparable'))
376 377
377 378 def getmember(self, context, mapping, key):
378 379 raise error.ParseError(_('not a dictionary'))
379 380
380 381 def getmin(self, context, mapping):
381 382 raise error.ParseError(_('not comparable'))
382 383
383 384 def getmax(self, context, mapping):
384 385 raise error.ParseError(_('not comparable'))
385 386
386 387 def join(self, context, mapping, sep):
387 388 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
388 389 if self._name:
389 390 itemiter = (context.process(self._name, m) for m in mapsiter)
390 391 elif self._tmpl:
391 392 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
392 393 else:
393 394 raise error.ParseError(_('not displayable without template'))
394 395 return joinitems(itemiter, sep)
395 396
396 397 def show(self, context, mapping):
397 398 return self.join(context, mapping, self._defaultsep)
398 399
399 400 def tovalue(self, context, mapping):
400 401 knownres = context.knownresourcekeys()
401 402 items = []
402 403 for nm in self.itermaps(context):
403 404 # drop internal resources (recursively) which shouldn't be displayed
404 405 lm = context.overlaymap(mapping, nm)
405 406 items.append({k: unwrapvalue(context, lm, v)
406 407 for k, v in nm.iteritems() if k not in knownres})
407 408 return items
408 409
409 410 class mappinggenerator(_mappingsequence):
410 411 """Wrapper for generator of template mappings
411 412
412 413 The function ``make(context, *args)`` should return a generator of
413 414 mapping dicts.
414 415 """
415 416
416 417 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
417 418 super(mappinggenerator, self).__init__(name, tmpl, sep)
418 419 self._make = make
419 420 self._args = args
420 421
421 422 def itermaps(self, context):
422 423 return self._make(context, *self._args)
423 424
424 425 def tobool(self, context, mapping):
425 426 return _nonempty(self.itermaps(context))
426 427
427 428 class mappinglist(_mappingsequence):
428 429 """Wrapper for list of template mappings"""
429 430
430 431 def __init__(self, mappings, name=None, tmpl=None, sep=''):
431 432 super(mappinglist, self).__init__(name, tmpl, sep)
432 433 self._mappings = mappings
433 434
434 435 def itermaps(self, context):
435 436 return iter(self._mappings)
436 437
437 438 def tobool(self, context, mapping):
438 439 return bool(self._mappings)
439 440
440 441 class mappedgenerator(wrapped):
441 442 """Wrapper for generator of strings which acts as a list
442 443
443 444 The function ``make(context, *args)`` should return a generator of
444 445 byte strings, or a generator of (possibly nested) generators of byte
445 446 strings (i.e. a generator for a list of byte strings.)
446 447 """
447 448
448 449 def __init__(self, make, args=()):
449 450 self._make = make
450 451 self._args = args
451 452
452 453 def contains(self, context, mapping, item):
453 454 item = stringify(context, mapping, item)
454 455 return item in self.tovalue(context, mapping)
455 456
456 457 def _gen(self, context):
457 458 return self._make(context, *self._args)
458 459
459 460 def getmember(self, context, mapping, key):
460 461 raise error.ParseError(_('not a dictionary'))
461 462
462 463 def getmin(self, context, mapping):
463 464 return self._getby(context, mapping, min)
464 465
465 466 def getmax(self, context, mapping):
466 467 return self._getby(context, mapping, max)
467 468
468 469 def _getby(self, context, mapping, func):
469 470 xs = self.tovalue(context, mapping)
470 471 if not xs:
471 472 raise error.ParseError(_('empty sequence'))
472 473 return func(xs)
473 474
474 475 def itermaps(self, context):
475 476 raise error.ParseError(_('list of strings is not mappable'))
476 477
477 478 def join(self, context, mapping, sep):
478 479 return joinitems(self._gen(context), sep)
479 480
480 481 def show(self, context, mapping):
481 482 return self.join(context, mapping, '')
482 483
483 484 def tobool(self, context, mapping):
484 485 return _nonempty(self._gen(context))
485 486
486 487 def tovalue(self, context, mapping):
487 488 return [stringify(context, mapping, x) for x in self._gen(context)]
488 489
489 490 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
490 491 """Wrap data to support both dict-like and string-like operations"""
491 492 prefmt = pycompat.identity
492 493 if fmt is None:
493 494 fmt = '%s=%s'
494 495 prefmt = pycompat.bytestr
495 496 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
496 497 lambda k: fmt % (prefmt(k), prefmt(data[k])))
497 498
498 499 def hybridlist(data, name, fmt=None, gen=None):
499 500 """Wrap data to support both list-like and string-like operations"""
500 501 prefmt = pycompat.identity
501 502 if fmt is None:
502 503 fmt = '%s'
503 504 prefmt = pycompat.bytestr
504 505 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
505 506
506 507 def compatdict(context, mapping, name, data, key='key', value='value',
507 508 fmt=None, plural=None, separator=' '):
508 509 """Wrap data like hybriddict(), but also supports old-style list template
509 510
510 511 This exists for backward compatibility with the old-style template. Use
511 512 hybriddict() for new template keywords.
512 513 """
513 514 c = [{key: k, value: v} for k, v in data.iteritems()]
514 515 f = _showcompatlist(context, mapping, name, c, plural, separator)
515 516 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
516 517
517 518 def compatlist(context, mapping, name, data, element=None, fmt=None,
518 519 plural=None, separator=' '):
519 520 """Wrap data like hybridlist(), but also supports old-style list template
520 521
521 522 This exists for backward compatibility with the old-style template. Use
522 523 hybridlist() for new template keywords.
523 524 """
524 525 f = _showcompatlist(context, mapping, name, data, plural, separator)
525 526 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
526 527
527 528 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
528 529 """Return a generator that renders old-style list template
529 530
530 531 name is name of key in template map.
531 532 values is list of strings or dicts.
532 533 plural is plural of name, if not simply name + 's'.
533 534 separator is used to join values as a string
534 535
535 536 expansion works like this, given name 'foo'.
536 537
537 538 if values is empty, expand 'no_foos'.
538 539
539 540 if 'foo' not in template map, return values as a string,
540 541 joined by 'separator'.
541 542
542 543 expand 'start_foos'.
543 544
544 545 for each value, expand 'foo'. if 'last_foo' in template
545 546 map, expand it instead of 'foo' for last key.
546 547
547 548 expand 'end_foos'.
548 549 """
549 550 if not plural:
550 551 plural = name + 's'
551 552 if not values:
552 553 noname = 'no_' + plural
553 554 if context.preload(noname):
554 555 yield context.process(noname, mapping)
555 556 return
556 557 if not context.preload(name):
557 558 if isinstance(values[0], bytes):
558 559 yield separator.join(values)
559 560 else:
560 561 for v in values:
561 562 r = dict(v)
562 563 r.update(mapping)
563 564 yield r
564 565 return
565 566 startname = 'start_' + plural
566 567 if context.preload(startname):
567 568 yield context.process(startname, mapping)
568 569 def one(v, tag=name):
569 570 vmapping = {}
570 571 try:
571 572 vmapping.update(v)
572 573 # Python 2 raises ValueError if the type of v is wrong. Python
573 574 # 3 raises TypeError.
574 575 except (AttributeError, TypeError, ValueError):
575 576 try:
576 577 # Python 2 raises ValueError trying to destructure an e.g.
577 578 # bytes. Python 3 raises TypeError.
578 579 for a, b in v:
579 580 vmapping[a] = b
580 581 except (TypeError, ValueError):
581 582 vmapping[name] = v
582 583 vmapping = context.overlaymap(mapping, vmapping)
583 584 return context.process(tag, vmapping)
584 585 lastname = 'last_' + name
585 586 if context.preload(lastname):
586 587 last = values.pop()
587 588 else:
588 589 last = None
589 590 for v in values:
590 591 yield one(v)
591 592 if last is not None:
592 593 yield one(last, tag=lastname)
593 594 endname = 'end_' + plural
594 595 if context.preload(endname):
595 596 yield context.process(endname, mapping)
596 597
597 598 def flatten(context, mapping, thing):
598 599 """Yield a single stream from a possibly nested set of iterators"""
599 600 if isinstance(thing, wrapped):
600 601 thing = thing.show(context, mapping)
601 602 if isinstance(thing, bytes):
602 603 yield thing
603 604 elif isinstance(thing, str):
604 605 # We can only hit this on Python 3, and it's here to guard
605 606 # against infinite recursion.
606 607 raise error.ProgrammingError('Mercurial IO including templates is done'
607 608 ' with bytes, not strings, got %r' % thing)
608 609 elif thing is None:
609 610 pass
610 611 elif not util.safehasattr(thing, '__iter__'):
611 612 yield pycompat.bytestr(thing)
612 613 else:
613 614 for i in thing:
614 615 if isinstance(i, wrapped):
615 616 i = i.show(context, mapping)
616 617 if isinstance(i, bytes):
617 618 yield i
618 619 elif i is None:
619 620 pass
620 621 elif not util.safehasattr(i, '__iter__'):
621 622 yield pycompat.bytestr(i)
622 623 else:
623 624 for j in flatten(context, mapping, i):
624 625 yield j
625 626
626 627 def stringify(context, mapping, thing):
627 628 """Turn values into bytes by converting into text and concatenating them"""
628 629 if isinstance(thing, bytes):
629 630 return thing # retain localstr to be round-tripped
630 631 return b''.join(flatten(context, mapping, thing))
631 632
632 633 def findsymbolicname(arg):
633 634 """Find symbolic name for the given compiled expression; returns None
634 635 if nothing found reliably"""
635 636 while True:
636 637 func, data = arg
637 638 if func is runsymbol:
638 639 return data
639 640 elif func is runfilter:
640 641 arg = data[0]
641 642 else:
642 643 return None
643 644
644 645 def _nonempty(xiter):
645 646 try:
646 647 next(xiter)
647 648 return True
648 649 except StopIteration:
649 650 return False
650 651
651 652 def _unthunk(context, mapping, thing):
652 653 """Evaluate a lazy byte string into value"""
653 654 if not isinstance(thing, types.GeneratorType):
654 655 return thing
655 656 return stringify(context, mapping, thing)
656 657
657 658 def evalrawexp(context, mapping, arg):
658 659 """Evaluate given argument as a bare template object which may require
659 660 further processing (such as folding generator of strings)"""
660 661 func, data = arg
661 662 return func(context, mapping, data)
662 663
663 664 def evalwrapped(context, mapping, arg):
664 665 """Evaluate given argument to wrapped object"""
665 666 thing = evalrawexp(context, mapping, arg)
666 667 return makewrapped(context, mapping, thing)
667 668
668 669 def makewrapped(context, mapping, thing):
669 670 """Lift object to a wrapped type"""
670 671 if isinstance(thing, wrapped):
671 672 return thing
672 673 thing = _unthunk(context, mapping, thing)
673 674 if isinstance(thing, bytes):
674 675 return wrappedbytes(thing)
675 676 return wrappedvalue(thing)
676 677
677 678 def evalfuncarg(context, mapping, arg):
678 679 """Evaluate given argument as value type"""
679 680 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
680 681
681 682 def unwrapvalue(context, mapping, thing):
682 683 """Move the inner value object out of the wrapper"""
683 684 if isinstance(thing, wrapped):
684 685 return thing.tovalue(context, mapping)
685 686 # evalrawexp() may return string, generator of strings or arbitrary object
686 687 # such as date tuple, but filter does not want generator.
687 688 return _unthunk(context, mapping, thing)
688 689
689 690 def evalboolean(context, mapping, arg):
690 691 """Evaluate given argument as boolean, but also takes boolean literals"""
691 692 func, data = arg
692 693 if func is runsymbol:
693 694 thing = func(context, mapping, data, default=None)
694 695 if thing is None:
695 696 # not a template keyword, takes as a boolean literal
696 697 thing = stringutil.parsebool(data)
697 698 else:
698 699 thing = func(context, mapping, data)
699 700 return makewrapped(context, mapping, thing).tobool(context, mapping)
700 701
701 702 def evaldate(context, mapping, arg, err=None):
702 703 """Evaluate given argument as a date tuple or a date string; returns
703 704 a (unixtime, offset) tuple"""
704 705 thing = evalrawexp(context, mapping, arg)
705 706 return unwrapdate(context, mapping, thing, err)
706 707
707 708 def unwrapdate(context, mapping, thing, err=None):
708 709 if isinstance(thing, date):
709 710 return thing.tovalue(context, mapping)
710 711 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
711 712 thing = unwrapvalue(context, mapping, thing)
712 713 try:
713 714 return dateutil.parsedate(thing)
714 715 except AttributeError:
715 716 raise error.ParseError(err or _('not a date tuple nor a string'))
716 717 except error.ParseError:
717 718 if not err:
718 719 raise
719 720 raise error.ParseError(err)
720 721
721 722 def evalinteger(context, mapping, arg, err=None):
722 723 thing = evalrawexp(context, mapping, arg)
723 724 return unwrapinteger(context, mapping, thing, err)
724 725
725 726 def unwrapinteger(context, mapping, thing, err=None):
726 727 thing = unwrapvalue(context, mapping, thing)
727 728 try:
728 729 return int(thing)
729 730 except (TypeError, ValueError):
730 731 raise error.ParseError(err or _('not an integer'))
731 732
732 733 def evalstring(context, mapping, arg):
733 734 return stringify(context, mapping, evalrawexp(context, mapping, arg))
734 735
735 736 def evalstringliteral(context, mapping, arg):
736 737 """Evaluate given argument as string template, but returns symbol name
737 738 if it is unknown"""
738 739 func, data = arg
739 740 if func is runsymbol:
740 741 thing = func(context, mapping, data, default=data)
741 742 else:
742 743 thing = func(context, mapping, data)
743 744 return stringify(context, mapping, thing)
744 745
745 746 _unwrapfuncbytype = {
746 747 None: unwrapvalue,
747 748 bytes: stringify,
748 749 date: unwrapdate,
749 750 int: unwrapinteger,
750 751 }
751 752
752 753 def unwrapastype(context, mapping, thing, typ):
753 754 """Move the inner value object out of the wrapper and coerce its type"""
754 755 try:
755 756 f = _unwrapfuncbytype[typ]
756 757 except KeyError:
757 758 raise error.ProgrammingError('invalid type specified: %r' % typ)
758 759 return f(context, mapping, thing)
759 760
760 761 def runinteger(context, mapping, data):
761 762 return int(data)
762 763
763 764 def runstring(context, mapping, data):
764 765 return data
765 766
766 767 def _recursivesymbolblocker(key):
767 768 def showrecursion(**args):
768 769 raise error.Abort(_("recursive reference '%s' in template") % key)
769 770 return showrecursion
770 771
771 772 def runsymbol(context, mapping, key, default=''):
772 773 v = context.symbol(mapping, key)
773 774 if v is None:
774 775 # put poison to cut recursion. we can't move this to parsing phase
775 776 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
776 777 safemapping = mapping.copy()
777 778 safemapping[key] = _recursivesymbolblocker(key)
778 779 try:
779 780 v = context.process(key, safemapping)
780 781 except TemplateNotFound:
781 782 v = default
782 783 if callable(v) and getattr(v, '_requires', None) is None:
783 784 # old templatekw: expand all keywords and resources
784 785 # (TODO: deprecate this after porting web template keywords to new API)
785 786 props = {k: context._resources.lookup(context, mapping, k)
786 787 for k in context._resources.knownkeys()}
787 788 # pass context to _showcompatlist() through templatekw._showlist()
788 789 props['templ'] = context
789 790 props.update(mapping)
790 791 return v(**pycompat.strkwargs(props))
791 792 if callable(v):
792 793 # new templatekw
793 794 try:
794 795 return v(context, mapping)
795 796 except ResourceUnavailable:
796 797 # unsupported keyword is mapped to empty just like unknown keyword
797 798 return None
798 799 return v
799 800
800 801 def runtemplate(context, mapping, template):
801 802 for arg in template:
802 803 yield evalrawexp(context, mapping, arg)
803 804
804 805 def runfilter(context, mapping, data):
805 806 arg, filt = data
806 807 thing = evalrawexp(context, mapping, arg)
807 808 intype = getattr(filt, '_intype', None)
808 809 try:
809 810 thing = unwrapastype(context, mapping, thing, intype)
810 811 return filt(thing)
811 812 except error.ParseError as e:
812 813 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
813 814
814 815 def _formatfiltererror(arg, filt):
815 816 fn = pycompat.sysbytes(filt.__name__)
816 817 sym = findsymbolicname(arg)
817 818 if not sym:
818 819 return _("incompatible use of template filter '%s'") % fn
819 820 return (_("template filter '%s' is not compatible with keyword '%s'")
820 821 % (fn, sym))
821 822
822 823 def _iteroverlaymaps(context, origmapping, newmappings):
823 824 """Generate combined mappings from the original mapping and an iterable
824 825 of partial mappings to override the original"""
825 826 for i, nm in enumerate(newmappings):
826 827 lm = context.overlaymap(origmapping, nm)
827 828 lm['index'] = i
828 829 yield lm
829 830
830 831 def _applymap(context, mapping, d, darg, targ):
831 832 try:
832 833 diter = d.itermaps(context)
833 834 except error.ParseError as err:
834 835 sym = findsymbolicname(darg)
835 836 if not sym:
836 837 raise
837 838 hint = _("keyword '%s' does not support map operation") % sym
838 839 raise error.ParseError(bytes(err), hint=hint)
839 840 for lm in _iteroverlaymaps(context, mapping, diter):
840 841 yield evalrawexp(context, lm, targ)
841 842
842 843 def runmap(context, mapping, data):
843 844 darg, targ = data
844 845 d = evalwrapped(context, mapping, darg)
845 846 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
846 847
847 848 def runmember(context, mapping, data):
848 849 darg, memb = data
849 850 d = evalwrapped(context, mapping, darg)
850 851 if isinstance(d, mappable):
851 852 lm = context.overlaymap(mapping, d.tomap(context))
852 853 return runsymbol(context, lm, memb)
853 854 try:
854 855 return d.getmember(context, mapping, memb)
855 856 except error.ParseError as err:
856 857 sym = findsymbolicname(darg)
857 858 if not sym:
858 859 raise
859 860 hint = _("keyword '%s' does not support member operation") % sym
860 861 raise error.ParseError(bytes(err), hint=hint)
861 862
862 863 def runnegate(context, mapping, data):
863 864 data = evalinteger(context, mapping, data,
864 865 _('negation needs an integer argument'))
865 866 return -data
866 867
867 868 def runarithmetic(context, mapping, data):
868 869 func, left, right = data
869 870 left = evalinteger(context, mapping, left,
870 871 _('arithmetic only defined on integers'))
871 872 right = evalinteger(context, mapping, right,
872 873 _('arithmetic only defined on integers'))
873 874 try:
874 875 return func(left, right)
875 876 except ZeroDivisionError:
876 877 raise error.Abort(_('division by zero is not defined'))
877 878
878 879 def joinitems(itemiter, sep):
879 880 """Join items with the separator; Returns generator of bytes"""
880 881 first = True
881 882 for x in itemiter:
882 883 if first:
883 884 first = False
884 885 elif sep:
885 886 yield sep
886 887 yield x
@@ -1,1376 +1,1378 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 $ 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 manifest/get() can be join()-ed as string, though it's silly:
439 439
440 440 $ hg log -R latesttag -r tip -T '{join(manifest, ".")}\n'
441 441 1.1.:.2.b.c.6.e.9.0.0.6.c.e.2
442 442 $ hg log -R latesttag -r tip -T '{join(get(extras, "branch"), ".")}\n'
443 443 d.e.f.a.u.l.t
444 444
445 445 Test join() over string
446 446
447 447 $ hg log -R latesttag -r tip -T '{join(rev|stringify, ".")}\n'
448 448 1.1
449 449
450 450 Test join() over uniterable
451 451
452 452 $ hg log -R latesttag -r tip -T '{join(rev, "")}\n'
453 453 hg: parse error: 11 is not iterable
454 454 [255]
455 455
456 456 Test min/max of integers
457 457
458 458 $ hg log -R latesttag -l1 -T '{min(revset("9:10"))}\n'
459 459 9
460 460 $ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
461 461 10
462 462
463 463 Test min/max over map operation:
464 464
465 465 $ hg log -R latesttag -r3 -T '{min(tags % "{tag}")}\n'
466 466 at3
467 467 $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
468 468 t3
469 469
470 470 Test min/max of strings:
471 471
472 472 $ hg log -R latesttag -l1 -T '{min(desc)}\n'
473 473 3
474 474 $ hg log -R latesttag -l1 -T '{max(desc)}\n'
475 475 t
476 476
477 477 Test min/max of non-iterable:
478 478
479 479 $ hg debugtemplate '{min(1)}'
480 480 hg: parse error: 1 is not iterable
481 481 (min first argument should be an iterable)
482 482 [255]
483 483 $ hg debugtemplate '{max(2)}'
484 484 hg: parse error: 2 is not iterable
485 485 (max first argument should be an iterable)
486 486 [255]
487 487
488 488 $ hg log -R latesttag -l1 -T '{min(date)}'
489 489 hg: parse error: date is not iterable
490 490 (min first argument should be an iterable)
491 491 [255]
492 492 $ hg log -R latesttag -l1 -T '{max(date)}'
493 493 hg: parse error: date is not iterable
494 494 (max first argument should be an iterable)
495 495 [255]
496 496
497 497 Test min/max of empty sequence:
498 498
499 499 $ hg debugtemplate '{min("")}'
500 500 hg: parse error: empty string
501 501 (min first argument should be an iterable)
502 502 [255]
503 503 $ hg debugtemplate '{max("")}'
504 504 hg: parse error: empty string
505 505 (max first argument should be an iterable)
506 506 [255]
507 507 $ hg debugtemplate '{min(dict())}'
508 508 hg: parse error: empty sequence
509 509 (min first argument should be an iterable)
510 510 [255]
511 511 $ hg debugtemplate '{max(dict())}'
512 512 hg: parse error: empty sequence
513 513 (max first argument should be an iterable)
514 514 [255]
515 515 $ hg debugtemplate '{min(dict() % "")}'
516 516 hg: parse error: empty sequence
517 517 (min first argument should be an iterable)
518 518 [255]
519 519 $ hg debugtemplate '{max(dict() % "")}'
520 520 hg: parse error: empty sequence
521 521 (max first argument should be an iterable)
522 522 [255]
523 523
524 524 Test min/max of if() result
525 525
526 526 $ cd latesttag
527 527 $ hg log -l1 -T '{min(if(true, revset("9:10"), ""))}\n'
528 528 9
529 529 $ hg log -l1 -T '{max(if(false, "", revset("9:10")))}\n'
530 530 10
531 531 $ hg log -l1 -T '{min(ifcontains("a", "aa", revset("9:10"), ""))}\n'
532 532 9
533 533 $ hg log -l1 -T '{max(ifcontains("a", "bb", "", revset("9:10")))}\n'
534 534 10
535 535 $ hg log -l1 -T '{min(ifeq(0, 0, revset("9:10"), ""))}\n'
536 536 9
537 537 $ hg log -l1 -T '{max(ifeq(0, 1, "", revset("9:10")))}\n'
538 538 10
539 539 $ cd ..
540 540
541 541 Test laziness of if() then/else clause
542 542
543 543 $ hg debugtemplate '{count(0)}'
544 544 hg: parse error: not countable
545 545 (incompatible use of template filter 'count')
546 546 [255]
547 547 $ hg debugtemplate '{if(true, "", count(0))}'
548 548 $ hg debugtemplate '{if(false, count(0), "")}'
549 549 $ hg debugtemplate '{ifcontains("a", "aa", "", count(0))}'
550 550 $ hg debugtemplate '{ifcontains("a", "bb", count(0), "")}'
551 551 $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
552 552 $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
553 553
554 554 Test the sub function of templating for expansion:
555 555
556 556 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
557 557 xx
558 558
559 559 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
560 560 hg: parse error: sub got an invalid pattern: [
561 561 [255]
562 562 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
563 563 hg: parse error: sub got an invalid replacement: \1
564 564 [255]
565 565
566 566 Test the strip function with chars specified:
567 567
568 568 $ hg log -R latesttag --template '{desc}\n'
569 569 at3
570 570 t5
571 571 t4
572 572 t3
573 573 t2
574 574 t1
575 575 merge
576 576 h2e
577 577 h2d
578 578 h1c
579 579 b
580 580 a
581 581
582 582 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
583 583 at3
584 584 5
585 585 4
586 586 3
587 587 2
588 588 1
589 589 merg
590 590 h2
591 591 h2d
592 592 h1c
593 593 b
594 594 a
595 595
596 596 Test date format:
597 597
598 598 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
599 599 date: 70 01 01 10 +0000
600 600 date: 70 01 01 09 +0000
601 601 date: 70 01 01 04 +0000
602 602 date: 70 01 01 08 +0000
603 603 date: 70 01 01 07 +0000
604 604 date: 70 01 01 06 +0000
605 605 date: 70 01 01 05 +0100
606 606 date: 70 01 01 04 +0000
607 607 date: 70 01 01 03 +0000
608 608 date: 70 01 01 02 +0000
609 609 date: 70 01 01 01 +0000
610 610 date: 70 01 01 00 +0000
611 611
612 612 Test invalid date:
613 613
614 614 $ hg log -R latesttag -T '{date(rev)}\n'
615 615 hg: parse error: date expects a date information
616 616 [255]
617 617
618 618 Set up repository containing template fragments in commit metadata:
619 619
620 620 $ hg init r
621 621 $ cd r
622 622 $ echo a > a
623 623 $ hg ci -Am '{rev}'
624 624 adding a
625 625
626 626 $ hg branch -q 'text.{rev}'
627 627 $ echo aa >> aa
628 628 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
629 629
630 630 color effect can be specified without quoting:
631 631
632 632 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
633 633 \x1b[0;31mtext\x1b[0m (esc)
634 634
635 635 color effects can be nested (issue5413)
636 636
637 637 $ hg debugtemplate --color=always \
638 638 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
639 639 \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)
640 640
641 641 pad() should interact well with color codes (issue5416)
642 642
643 643 $ hg debugtemplate --color=always \
644 644 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
645 645 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
646 646
647 647 label should be no-op if color is disabled:
648 648
649 649 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
650 650 text
651 651 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
652 652 text
653 653
654 654 Test branches inside if statement:
655 655
656 656 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
657 657 no
658 658
659 659 Test dict constructor:
660 660
661 661 $ hg log -r 0 -T '{dict(y=node|short, x=rev)}\n'
662 662 y=f7769ec2ab97 x=0
663 663 $ hg log -r 0 -T '{dict(x=rev, y=node|short) % "{key}={value}\n"}'
664 664 x=0
665 665 y=f7769ec2ab97
666 666 $ hg log -r 0 -T '{dict(x=rev, y=node|short)|json}\n'
667 667 {"x": 0, "y": "f7769ec2ab97"}
668 668 $ hg log -r 0 -T '{dict()|json}\n'
669 669 {}
670 670
671 671 $ hg log -r 0 -T '{dict(rev, node=node|short)}\n'
672 672 rev=0 node=f7769ec2ab97
673 673 $ hg log -r 0 -T '{dict(rev, node|short)}\n'
674 674 rev=0 node=f7769ec2ab97
675 675
676 676 $ hg log -r 0 -T '{dict(rev, rev=rev)}\n'
677 677 hg: parse error: duplicated dict key 'rev' inferred
678 678 [255]
679 679 $ hg log -r 0 -T '{dict(node, node|short)}\n'
680 680 hg: parse error: duplicated dict key 'node' inferred
681 681 [255]
682 682 $ hg log -r 0 -T '{dict(1 + 2)}'
683 683 hg: parse error: dict key cannot be inferred
684 684 [255]
685 685
686 686 $ hg log -r 0 -T '{dict(x=rev, x=node)}'
687 687 hg: parse error: dict got multiple values for keyword argument 'x'
688 688 [255]
689 689
690 690 Test get function:
691 691
692 692 $ hg log -r 0 --template '{get(extras, "branch")}\n'
693 693 default
694 694 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
695 695 default
696 696 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
697 697 hg: parse error: not a dictionary
698 698 (get() expects a dict as first argument)
699 699 [255]
700 700
701 701 Test json filter applied to wrapped object:
702 702
703 703 $ hg log -r0 -T '{files|json}\n'
704 704 ["a"]
705 705 $ hg log -r0 -T '{extras|json}\n'
706 706 {"branch": "default"}
707 707 $ hg log -r0 -T '{date|json}\n'
708 708 [0, 0]
709 709
710 710 Test json filter applied to map result:
711 711
712 712 $ hg log -r0 -T '{json(extras % "{key}")}\n'
713 713 ["branch"]
714 714
715 715 Test localdate(date, tz) function:
716 716
717 717 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
718 718 1970-01-01 09:00 +0900
719 719 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
720 720 1970-01-01 00:00 +0000
721 721 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
722 722 hg: parse error: localdate expects a timezone
723 723 [255]
724 724 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
725 725 1970-01-01 02:00 +0200
726 726 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
727 727 1970-01-01 00:00 +0000
728 728 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
729 729 1970-01-01 00:00 +0000
730 730 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
731 731 hg: parse error: localdate expects a timezone
732 732 [255]
733 733 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
734 734 hg: parse error: localdate expects a timezone
735 735 [255]
736 736
737 737 Test shortest(node) function:
738 738
739 739 $ echo b > b
740 740 $ hg ci -qAm b
741 741 $ hg log --template '{shortest(node)}\n'
742 742 e777
743 743 bcc7
744 744 f776
745 745 $ hg log --template '{shortest(node, 10)}\n'
746 746 e777603221
747 747 bcc7ff960b
748 748 f7769ec2ab
749 749 $ hg log --template '{node|shortest}\n' -l1
750 750 e777
751 751
752 752 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
753 753 f7769ec2ab
754 754 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
755 755 hg: parse error: shortest() expects an integer minlength
756 756 [255]
757 757
758 758 $ hg log -r 'wdir()' -T '{node|shortest}\n'
759 759 ffff
760 760
761 761 $ hg log --template '{shortest("f")}\n' -l1
762 762 f
763 763
764 764 $ hg log --template '{shortest("0123456789012345678901234567890123456789")}\n' -l1
765 765 0123456789012345678901234567890123456789
766 766
767 767 $ hg log --template '{shortest("01234567890123456789012345678901234567890123456789")}\n' -l1
768 768 01234567890123456789012345678901234567890123456789
769 769
770 770 $ hg log --template '{shortest("not a hex string")}\n' -l1
771 771 not a hex string
772 772
773 773 $ hg log --template '{shortest("not a hex string, but it'\''s 40 bytes long")}\n' -l1
774 774 not a hex string, but it's 40 bytes long
775 775
776 776 $ hg log --template '{shortest("ffffffffffffffffffffffffffffffffffffffff")}\n' -l1
777 777 ffff
778 778
779 779 $ hg log --template '{shortest("fffffff")}\n' -l1
780 780 ffff
781 781
782 782 $ hg log --template '{shortest("ff")}\n' -l1
783 783 ffff
784 784
785 785 $ cd ..
786 786
787 787 Test shortest(node) with the repo having short hash collision:
788 788
789 789 $ hg init hashcollision
790 790 $ cd hashcollision
791 791 $ cat <<EOF >> .hg/hgrc
792 792 > [experimental]
793 793 > evolution.createmarkers=True
794 794 > EOF
795 795 $ echo 0 > a
796 796 $ hg ci -qAm 0
797 797 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
798 798 > hg up -q 0
799 799 > echo $i > a
800 800 > hg ci -qm $i
801 801 > done
802 802 $ hg up -q null
803 803 $ hg log -r0: -T '{rev}:{node}\n'
804 804 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
805 805 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
806 806 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
807 807 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
808 808 4:10776689e627b465361ad5c296a20a487e153ca4
809 809 5:a00be79088084cb3aff086ab799f8790e01a976b
810 810 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
811 811 7:a0457b3450b8e1b778f1163b31a435802987fe5d
812 812 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
813 813 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
814 814 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
815 815 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
816 816 obsoleted 1 changesets
817 817 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
818 818 obsoleted 1 changesets
819 819 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
820 820 obsoleted 1 changesets
821 821
822 822 nodes starting with '11' (we don't have the revision number '11' though)
823 823
824 824 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
825 825 1:1142
826 826 2:1140
827 827 3:11d
828 828
829 829 '5:a00' is hidden, but still we have two nodes starting with 'a0'
830 830
831 831 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
832 832 6:a0b
833 833 7:a04
834 834
835 835 node '10' conflicts with the revision number '10' even if it is hidden
836 836 (we could exclude hidden revision numbers, but currently we don't)
837 837
838 838 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
839 839 4:107
840 840 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
841 841 4:107
842 842
843 843 node 'c562' should be unique if the other 'c562' nodes are hidden
844 844 (but we don't try the slow path to filter out hidden nodes for now)
845 845
846 846 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
847 847 8:c5625
848 848 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
849 849 8:c5625
850 850 9:c5623
851 851 10:c562d
852 852
853 853 $ cd ..
854 854
855 855 Test pad function
856 856
857 857 $ cd r
858 858
859 859 $ hg log --template '{pad(rev, 20)} {author|user}\n'
860 860 2 test
861 861 1 {node|short}
862 862 0 test
863 863
864 864 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
865 865 2 test
866 866 1 {node|short}
867 867 0 test
868 868
869 869 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
870 870 2------------------- test
871 871 1------------------- {node|short}
872 872 0------------------- test
873 873
874 874 Test template string in pad function
875 875
876 876 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
877 877 {0} test
878 878
879 879 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
880 880 \{rev} test
881 881
882 882 Test width argument passed to pad function
883 883
884 884 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
885 885 0 test
886 886 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
887 887 hg: parse error: pad() expects an integer width
888 888 [255]
889 889
890 890 Test invalid fillchar passed to pad function
891 891
892 892 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
893 893 hg: parse error: pad() expects a single fill character
894 894 [255]
895 895 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
896 896 hg: parse error: pad() expects a single fill character
897 897 [255]
898 898
899 899 Test boolean argument passed to pad function
900 900
901 901 no crash
902 902
903 903 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
904 904 ---------0
905 905
906 906 string/literal
907 907
908 908 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
909 909 ---------0
910 910 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
911 911 0---------
912 912 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
913 913 0---------
914 914
915 915 unknown keyword is evaluated to ''
916 916
917 917 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
918 918 0---------
919 919
920 920 Test separate function
921 921
922 922 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
923 923 a-b-c
924 924 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
925 925 0:f7769ec2ab97 test default
926 926 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
927 927 a \x1b[0;31mb\x1b[0m c d (esc)
928 928
929 929 Test boolean expression/literal passed to if function
930 930
931 931 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
932 932 rev 0 is True
933 933 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
934 934 literal 0 is True as well
935 $ hg log -r 0 -T '{if(min(revset(r"0")), "0 of hybriditem is also True")}\n'
936 0 of hybriditem is also True
935 937 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
936 938 empty string is False
937 939 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
938 940 empty list is False
939 941 $ hg log -r 0 -T '{if(revset(r"0"), "non-empty list is True")}\n'
940 942 non-empty list is True
941 943 $ hg log -r 0 -T '{if(revset(r"0") % "", "list of empty strings is True")}\n'
942 944 list of empty strings is True
943 945 $ hg log -r 0 -T '{if(true, "true is True")}\n'
944 946 true is True
945 947 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
946 948 false is False
947 949 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
948 950 non-empty string is True
949 951
950 952 Test ifcontains function
951 953
952 954 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
953 955 2 is in the string
954 956 1 is not
955 957 0 is in the string
956 958
957 959 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
958 960 2 is in the string
959 961 1 is not
960 962 0 is in the string
961 963
962 964 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
963 965 2 did not add a
964 966 1 did not add a
965 967 0 added a
966 968
967 969 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
968 970 2 is parent of 1
969 971 1
970 972 0
971 973
972 974 $ hg log -l1 -T '{ifcontains("branch", extras, "t", "f")}\n'
973 975 t
974 976 $ hg log -l1 -T '{ifcontains("branch", extras % "{key}", "t", "f")}\n'
975 977 t
976 978 $ hg log -l1 -T '{ifcontains("branc", extras % "{key}", "t", "f")}\n'
977 979 f
978 980 $ hg log -l1 -T '{ifcontains("branc", stringify(extras % "{key}"), "t", "f")}\n'
979 981 t
980 982
981 983 Test revset function
982 984
983 985 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
984 986 2 current rev
985 987 1 not current rev
986 988 0 not current rev
987 989
988 990 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
989 991 2 match rev
990 992 1 match rev
991 993 0 not match rev
992 994
993 995 $ hg log -T '{ifcontains(desc, revset(":"), "", "type not match")}\n' -l1
994 996 type not match
995 997
996 998 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
997 999 2 Parents: 1
998 1000 1 Parents: 0
999 1001 0 Parents:
1000 1002
1001 1003 $ cat >> .hg/hgrc <<EOF
1002 1004 > [revsetalias]
1003 1005 > myparents(\$1) = parents(\$1)
1004 1006 > EOF
1005 1007 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
1006 1008 2 Parents: 1
1007 1009 1 Parents: 0
1008 1010 0 Parents:
1009 1011
1010 1012 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
1011 1013 Rev: 2
1012 1014 Ancestor: 0
1013 1015 Ancestor: 1
1014 1016 Ancestor: 2
1015 1017
1016 1018 Rev: 1
1017 1019 Ancestor: 0
1018 1020 Ancestor: 1
1019 1021
1020 1022 Rev: 0
1021 1023 Ancestor: 0
1022 1024
1023 1025 $ hg log --template '{revset("TIP"|lower)}\n' -l1
1024 1026 2
1025 1027
1026 1028 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
1027 1029 2
1028 1030
1029 1031 a list template is evaluated for each item of revset/parents
1030 1032
1031 1033 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
1032 1034 2 p: 1:bcc7ff960b8e
1033 1035 1 p: 0:f7769ec2ab97
1034 1036 0 p:
1035 1037
1036 1038 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
1037 1039 2 p: 1:bcc7ff960b8e -1:000000000000
1038 1040 1 p: 0:f7769ec2ab97 -1:000000000000
1039 1041 0 p: -1:000000000000 -1:000000000000
1040 1042
1041 1043 therefore, 'revcache' should be recreated for each rev
1042 1044
1043 1045 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
1044 1046 2 aa b
1045 1047 p
1046 1048 1
1047 1049 p a
1048 1050 0 a
1049 1051 p
1050 1052
1051 1053 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
1052 1054 2 aa b
1053 1055 p
1054 1056 1
1055 1057 p a
1056 1058 0 a
1057 1059 p
1058 1060
1059 1061 a revset item must be evaluated as an integer revision, not an offset from tip
1060 1062
1061 1063 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
1062 1064 -1:000000000000
1063 1065 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
1064 1066 -1:000000000000
1065 1067
1066 1068 join() should pick '{rev}' from revset items:
1067 1069
1068 1070 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
1069 1071 4, 5
1070 1072
1071 1073 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
1072 1074 default. join() should agree with the default formatting:
1073 1075
1074 1076 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
1075 1077 5:13207e5a10d9, 4:bbe44766e73d
1076 1078
1077 1079 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
1078 1080 5:13207e5a10d9fd28ec424934298e176197f2c67f,
1079 1081 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1080 1082
1081 1083 Invalid arguments passed to revset()
1082 1084
1083 1085 $ hg log -T '{revset("%whatever", 0)}\n'
1084 1086 hg: parse error: unexpected revspec format character w
1085 1087 [255]
1086 1088 $ hg log -T '{revset("%lwhatever", files)}\n'
1087 1089 hg: parse error: unexpected revspec format character w
1088 1090 [255]
1089 1091 $ hg log -T '{revset("%s %s", 0)}\n'
1090 1092 hg: parse error: missing argument for revspec
1091 1093 [255]
1092 1094 $ hg log -T '{revset("", 0)}\n'
1093 1095 hg: parse error: too many revspec arguments specified
1094 1096 [255]
1095 1097 $ hg log -T '{revset("%s", 0, 1)}\n'
1096 1098 hg: parse error: too many revspec arguments specified
1097 1099 [255]
1098 1100 $ hg log -T '{revset("%", 0)}\n'
1099 1101 hg: parse error: incomplete revspec format character
1100 1102 [255]
1101 1103 $ hg log -T '{revset("%l", 0)}\n'
1102 1104 hg: parse error: incomplete revspec format character
1103 1105 [255]
1104 1106 $ hg log -T '{revset("%d", 'foo')}\n'
1105 1107 hg: parse error: invalid argument for revspec
1106 1108 [255]
1107 1109 $ hg log -T '{revset("%ld", files)}\n'
1108 1110 hg: parse error: invalid argument for revspec
1109 1111 [255]
1110 1112 $ hg log -T '{revset("%ls", 0)}\n'
1111 1113 hg: parse error: invalid argument for revspec
1112 1114 [255]
1113 1115 $ hg log -T '{revset("%b", 'foo')}\n'
1114 1116 hg: parse error: invalid argument for revspec
1115 1117 [255]
1116 1118 $ hg log -T '{revset("%lb", files)}\n'
1117 1119 hg: parse error: invalid argument for revspec
1118 1120 [255]
1119 1121 $ hg log -T '{revset("%r", 0)}\n'
1120 1122 hg: parse error: invalid argument for revspec
1121 1123 [255]
1122 1124
1123 1125 Test files function
1124 1126
1125 1127 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
1126 1128 2
1127 1129 a
1128 1130 aa
1129 1131 b
1130 1132 1
1131 1133 a
1132 1134 0
1133 1135 a
1134 1136
1135 1137 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
1136 1138 2
1137 1139 aa
1138 1140 1
1139 1141
1140 1142 0
1141 1143
1142 1144 $ hg rm a
1143 1145 $ hg log -r "wdir()" -T "{rev}\n{join(files('*'), '\n')}\n"
1144 1146 2147483647
1145 1147 aa
1146 1148 b
1147 1149 $ hg revert a
1148 1150
1149 1151 Test relpath function
1150 1152
1151 1153 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
1152 1154 a
1153 1155 $ cd ..
1154 1156 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
1155 1157 r/a
1156 1158
1157 1159 Test stringify on sub expressions
1158 1160
1159 1161 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
1160 1162 fourth, second, third
1161 1163 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
1162 1164 abc
1163 1165
1164 1166 Test splitlines
1165 1167
1166 1168 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
1167 1169 @ foo Modify, add, remove, rename
1168 1170 |
1169 1171 o foo future
1170 1172 |
1171 1173 o foo third
1172 1174 |
1173 1175 o foo second
1174 1176
1175 1177 o foo merge
1176 1178 |\
1177 1179 | o foo new head
1178 1180 | |
1179 1181 o | foo new branch
1180 1182 |/
1181 1183 o foo no user, no domain
1182 1184 |
1183 1185 o foo no person
1184 1186 |
1185 1187 o foo other 1
1186 1188 | foo other 2
1187 1189 | foo
1188 1190 | foo other 3
1189 1191 o foo line 1
1190 1192 foo line 2
1191 1193
1192 1194 $ hg log -R a -r0 -T '{desc|splitlines}\n'
1193 1195 line 1 line 2
1194 1196 $ hg log -R a -r0 -T '{join(desc|splitlines, "|")}\n'
1195 1197 line 1|line 2
1196 1198
1197 1199 Test startswith
1198 1200 $ hg log -Gv -R a --template "{startswith(desc)}"
1199 1201 hg: parse error: startswith expects two arguments
1200 1202 [255]
1201 1203
1202 1204 $ hg log -Gv -R a --template "{startswith('line', desc)}"
1203 1205 @
1204 1206 |
1205 1207 o
1206 1208 |
1207 1209 o
1208 1210 |
1209 1211 o
1210 1212
1211 1213 o
1212 1214 |\
1213 1215 | o
1214 1216 | |
1215 1217 o |
1216 1218 |/
1217 1219 o
1218 1220 |
1219 1221 o
1220 1222 |
1221 1223 o
1222 1224 |
1223 1225 o line 1
1224 1226 line 2
1225 1227
1226 1228 Test word function (including index out of bounds graceful failure)
1227 1229
1228 1230 $ hg log -Gv -R a --template "{word('1', desc)}"
1229 1231 @ add,
1230 1232 |
1231 1233 o
1232 1234 |
1233 1235 o
1234 1236 |
1235 1237 o
1236 1238
1237 1239 o
1238 1240 |\
1239 1241 | o head
1240 1242 | |
1241 1243 o | branch
1242 1244 |/
1243 1245 o user,
1244 1246 |
1245 1247 o person
1246 1248 |
1247 1249 o 1
1248 1250 |
1249 1251 o 1
1250 1252
1251 1253
1252 1254 Test word third parameter used as splitter
1253 1255
1254 1256 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
1255 1257 @ M
1256 1258 |
1257 1259 o future
1258 1260 |
1259 1261 o third
1260 1262 |
1261 1263 o sec
1262 1264
1263 1265 o merge
1264 1266 |\
1265 1267 | o new head
1266 1268 | |
1267 1269 o | new branch
1268 1270 |/
1269 1271 o n
1270 1272 |
1271 1273 o n
1272 1274 |
1273 1275 o
1274 1276 |
1275 1277 o line 1
1276 1278 line 2
1277 1279
1278 1280 Test word error messages for not enough and too many arguments
1279 1281
1280 1282 $ hg log -Gv -R a --template "{word('0')}"
1281 1283 hg: parse error: word expects two or three arguments, got 1
1282 1284 [255]
1283 1285
1284 1286 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
1285 1287 hg: parse error: word expects two or three arguments, got 7
1286 1288 [255]
1287 1289
1288 1290 Test word for integer literal
1289 1291
1290 1292 $ hg log -R a --template "{word(2, desc)}\n" -r0
1291 1293 line
1292 1294
1293 1295 Test word for invalid numbers
1294 1296
1295 1297 $ hg log -Gv -R a --template "{word('a', desc)}"
1296 1298 hg: parse error: word expects an integer index
1297 1299 [255]
1298 1300
1299 1301 Test word for out of range
1300 1302
1301 1303 $ hg log -R a --template "{word(10000, desc)}"
1302 1304 $ hg log -R a --template "{word(-10000, desc)}"
1303 1305
1304 1306 Test indent and not adding to empty lines
1305 1307
1306 1308 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
1307 1309 -----
1308 1310 > line 1
1309 1311 >> line 2
1310 1312 -----
1311 1313 > other 1
1312 1314 >> other 2
1313 1315
1314 1316 >> other 3
1315 1317
1316 1318 Test with non-strings like dates
1317 1319
1318 1320 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
1319 1321 1200000.00
1320 1322 1300000.00
1321 1323
1322 1324 json filter should escape HTML tags so that the output can be embedded in hgweb:
1323 1325
1324 1326 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
1325 1327 "\u003cfoo@example.org\u003e"
1326 1328
1327 1329 Set up repository for non-ascii encoding tests:
1328 1330
1329 1331 $ hg init nonascii
1330 1332 $ cd nonascii
1331 1333 $ $PYTHON <<EOF
1332 1334 > open('latin1', 'wb').write(b'\xe9')
1333 1335 > open('utf-8', 'wb').write(b'\xc3\xa9')
1334 1336 > EOF
1335 1337 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
1336 1338 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
1337 1339
1338 1340 json filter should try round-trip conversion to utf-8:
1339 1341
1340 1342 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
1341 1343 "\u00e9"
1342 1344 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
1343 1345 "non-ascii branch: \u00e9"
1344 1346
1345 1347 json filter should take input as utf-8 if it was converted from utf-8:
1346 1348
1347 1349 $ HGENCODING=latin-1 hg log -T "{branch|json}\n" -r0
1348 1350 "\u00e9"
1349 1351 $ HGENCODING=latin-1 hg log -T "{desc|json}\n" -r0
1350 1352 "non-ascii branch: \u00e9"
1351 1353
1352 1354 json filter takes input as utf-8b:
1353 1355
1354 1356 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
1355 1357 "\u00e9"
1356 1358 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
1357 1359 "\udce9"
1358 1360
1359 1361 utf8 filter:
1360 1362
1361 1363 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
1362 1364 round-trip: c3a9
1363 1365 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
1364 1366 decoded: c3a9
1365 1367 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
1366 1368 abort: decoding near * (glob)
1367 1369 [255]
1368 1370 $ hg log -T "coerced to string: {rev|utf8}\n" -r0
1369 1371 coerced to string: 0
1370 1372
1371 1373 pad width:
1372 1374
1373 1375 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
1374 1376 \xc3\xa9- (esc)
1375 1377
1376 1378 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now