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