##// END OF EJS Templates
templatekw: alias {file} of files list to {path}...
Yuya Nishihara -
r39403:83f8f7b9 default
parent child Browse files
Show More
@@ -1,718 +1,718
1 1 # templatefuncs.py - common template functions
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 bin,
15 15 wdirid,
16 16 )
17 17 from . import (
18 18 color,
19 19 encoding,
20 20 error,
21 21 minirst,
22 22 obsutil,
23 23 registrar,
24 24 revset as revsetmod,
25 25 revsetlang,
26 26 scmutil,
27 27 templatefilters,
28 28 templatekw,
29 29 templateutil,
30 30 util,
31 31 )
32 32 from .utils import (
33 33 dateutil,
34 34 stringutil,
35 35 )
36 36
37 37 evalrawexp = templateutil.evalrawexp
38 38 evalwrapped = templateutil.evalwrapped
39 39 evalfuncarg = templateutil.evalfuncarg
40 40 evalboolean = templateutil.evalboolean
41 41 evaldate = templateutil.evaldate
42 42 evalinteger = templateutil.evalinteger
43 43 evalstring = templateutil.evalstring
44 44 evalstringliteral = templateutil.evalstringliteral
45 45
46 46 # dict of template built-in functions
47 47 funcs = {}
48 48 templatefunc = registrar.templatefunc(funcs)
49 49
50 50 @templatefunc('date(date[, fmt])')
51 51 def date(context, mapping, args):
52 52 """Format a date. See :hg:`help dates` for formatting
53 53 strings. The default is a Unix date format, including the timezone:
54 54 "Mon Sep 04 15:13:13 2006 0700"."""
55 55 if not (1 <= len(args) <= 2):
56 56 # i18n: "date" is a keyword
57 57 raise error.ParseError(_("date expects one or two arguments"))
58 58
59 59 date = evaldate(context, mapping, args[0],
60 60 # i18n: "date" is a keyword
61 61 _("date expects a date information"))
62 62 fmt = None
63 63 if len(args) == 2:
64 64 fmt = evalstring(context, mapping, args[1])
65 65 if fmt is None:
66 66 return dateutil.datestr(date)
67 67 else:
68 68 return dateutil.datestr(date, fmt)
69 69
70 70 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
71 71 def dict_(context, mapping, args):
72 72 """Construct a dict from key-value pairs. A key may be omitted if
73 73 a value expression can provide an unambiguous name."""
74 74 data = util.sortdict()
75 75
76 76 for v in args['args']:
77 77 k = templateutil.findsymbolicname(v)
78 78 if not k:
79 79 raise error.ParseError(_('dict key cannot be inferred'))
80 80 if k in data or k in args['kwargs']:
81 81 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
82 82 data[k] = evalfuncarg(context, mapping, v)
83 83
84 84 data.update((k, evalfuncarg(context, mapping, v))
85 85 for k, v in args['kwargs'].iteritems())
86 86 return templateutil.hybriddict(data)
87 87
88 88 @templatefunc('diff([includepattern [, excludepattern]])', requires={'ctx'})
89 89 def diff(context, mapping, args):
90 90 """Show a diff, optionally
91 91 specifying files to include or exclude."""
92 92 if len(args) > 2:
93 93 # i18n: "diff" is a keyword
94 94 raise error.ParseError(_("diff expects zero, one, or two arguments"))
95 95
96 96 def getpatterns(i):
97 97 if i < len(args):
98 98 s = evalstring(context, mapping, args[i]).strip()
99 99 if s:
100 100 return [s]
101 101 return []
102 102
103 103 ctx = context.resource(mapping, 'ctx')
104 104 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
105 105
106 106 return ''.join(chunks)
107 107
108 108 @templatefunc('extdata(source)', argspec='source', requires={'ctx', 'cache'})
109 109 def extdata(context, mapping, args):
110 110 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
111 111 if 'source' not in args:
112 112 # i18n: "extdata" is a keyword
113 113 raise error.ParseError(_('extdata expects one argument'))
114 114
115 115 source = evalstring(context, mapping, args['source'])
116 116 if not source:
117 117 sym = templateutil.findsymbolicname(args['source'])
118 118 if sym:
119 119 raise error.ParseError(_('empty data source specified'),
120 120 hint=_("did you mean extdata('%s')?") % sym)
121 121 else:
122 122 raise error.ParseError(_('empty data source specified'))
123 123 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
124 124 ctx = context.resource(mapping, 'ctx')
125 125 if source in cache:
126 126 data = cache[source]
127 127 else:
128 128 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
129 129 return data.get(ctx.rev(), '')
130 130
131 131 @templatefunc('files(pattern)', requires={'ctx'})
132 132 def files(context, mapping, args):
133 133 """All files of the current changeset matching the pattern. See
134 134 :hg:`help patterns`."""
135 135 if not len(args) == 1:
136 136 # i18n: "files" is a keyword
137 137 raise error.ParseError(_("files expects one argument"))
138 138
139 139 raw = evalstring(context, mapping, args[0])
140 140 ctx = context.resource(mapping, 'ctx')
141 141 m = ctx.match([raw])
142 142 files = list(ctx.matches(m))
143 return templateutil.compatlist(context, mapping, "file", files)
143 return templateutil.compatfileslist(context, mapping, "file", files)
144 144
145 145 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
146 146 def fill(context, mapping, args):
147 147 """Fill many
148 148 paragraphs with optional indentation. See the "fill" filter."""
149 149 if not (1 <= len(args) <= 4):
150 150 # i18n: "fill" is a keyword
151 151 raise error.ParseError(_("fill expects one to four arguments"))
152 152
153 153 text = evalstring(context, mapping, args[0])
154 154 width = 76
155 155 initindent = ''
156 156 hangindent = ''
157 157 if 2 <= len(args) <= 4:
158 158 width = evalinteger(context, mapping, args[1],
159 159 # i18n: "fill" is a keyword
160 160 _("fill expects an integer width"))
161 161 try:
162 162 initindent = evalstring(context, mapping, args[2])
163 163 hangindent = evalstring(context, mapping, args[3])
164 164 except IndexError:
165 165 pass
166 166
167 167 return templatefilters.fill(text, width, initindent, hangindent)
168 168
169 169 @templatefunc('filter(iterable[, expr])')
170 170 def filter_(context, mapping, args):
171 171 """Remove empty elements from a list or a dict. If expr specified, it's
172 172 applied to each element to test emptiness."""
173 173 if not (1 <= len(args) <= 2):
174 174 # i18n: "filter" is a keyword
175 175 raise error.ParseError(_("filter expects one or two arguments"))
176 176 iterable = evalwrapped(context, mapping, args[0])
177 177 if len(args) == 1:
178 178 def select(w):
179 179 return w.tobool(context, mapping)
180 180 else:
181 181 def select(w):
182 182 if not isinstance(w, templateutil.mappable):
183 183 raise error.ParseError(_("not filterable by expression"))
184 184 lm = context.overlaymap(mapping, w.tomap(context))
185 185 return evalboolean(context, lm, args[1])
186 186 return iterable.filter(context, mapping, select)
187 187
188 188 @templatefunc('formatnode(node)', requires={'ui'})
189 189 def formatnode(context, mapping, args):
190 190 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
191 191 if len(args) != 1:
192 192 # i18n: "formatnode" is a keyword
193 193 raise error.ParseError(_("formatnode expects one argument"))
194 194
195 195 ui = context.resource(mapping, 'ui')
196 196 node = evalstring(context, mapping, args[0])
197 197 if ui.debugflag:
198 198 return node
199 199 return templatefilters.short(node)
200 200
201 201 @templatefunc('mailmap(author)', requires={'repo', 'cache'})
202 202 def mailmap(context, mapping, args):
203 203 """Return the author, updated according to the value
204 204 set in the .mailmap file"""
205 205 if len(args) != 1:
206 206 raise error.ParseError(_("mailmap expects one argument"))
207 207
208 208 author = evalstring(context, mapping, args[0])
209 209
210 210 cache = context.resource(mapping, 'cache')
211 211 repo = context.resource(mapping, 'repo')
212 212
213 213 if 'mailmap' not in cache:
214 214 data = repo.wvfs.tryread('.mailmap')
215 215 cache['mailmap'] = stringutil.parsemailmap(data)
216 216
217 217 return stringutil.mapname(cache['mailmap'], author)
218 218
219 219 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
220 220 argspec='text width fillchar left')
221 221 def pad(context, mapping, args):
222 222 """Pad text with a
223 223 fill character."""
224 224 if 'text' not in args or 'width' not in args:
225 225 # i18n: "pad" is a keyword
226 226 raise error.ParseError(_("pad() expects two to four arguments"))
227 227
228 228 width = evalinteger(context, mapping, args['width'],
229 229 # i18n: "pad" is a keyword
230 230 _("pad() expects an integer width"))
231 231
232 232 text = evalstring(context, mapping, args['text'])
233 233
234 234 left = False
235 235 fillchar = ' '
236 236 if 'fillchar' in args:
237 237 fillchar = evalstring(context, mapping, args['fillchar'])
238 238 if len(color.stripeffects(fillchar)) != 1:
239 239 # i18n: "pad" is a keyword
240 240 raise error.ParseError(_("pad() expects a single fill character"))
241 241 if 'left' in args:
242 242 left = evalboolean(context, mapping, args['left'])
243 243
244 244 fillwidth = width - encoding.colwidth(color.stripeffects(text))
245 245 if fillwidth <= 0:
246 246 return text
247 247 if left:
248 248 return fillchar * fillwidth + text
249 249 else:
250 250 return text + fillchar * fillwidth
251 251
252 252 @templatefunc('indent(text, indentchars[, firstline])')
253 253 def indent(context, mapping, args):
254 254 """Indents all non-empty lines
255 255 with the characters given in the indentchars string. An optional
256 256 third parameter will override the indent for the first line only
257 257 if present."""
258 258 if not (2 <= len(args) <= 3):
259 259 # i18n: "indent" is a keyword
260 260 raise error.ParseError(_("indent() expects two or three arguments"))
261 261
262 262 text = evalstring(context, mapping, args[0])
263 263 indent = evalstring(context, mapping, args[1])
264 264
265 265 if len(args) == 3:
266 266 firstline = evalstring(context, mapping, args[2])
267 267 else:
268 268 firstline = indent
269 269
270 270 # the indent function doesn't indent the first line, so we do it here
271 271 return templatefilters.indent(firstline + text, indent)
272 272
273 273 @templatefunc('get(dict, key)')
274 274 def get(context, mapping, args):
275 275 """Get an attribute/key from an object. Some keywords
276 276 are complex types. This function allows you to obtain the value of an
277 277 attribute on these types."""
278 278 if len(args) != 2:
279 279 # i18n: "get" is a keyword
280 280 raise error.ParseError(_("get() expects two arguments"))
281 281
282 282 dictarg = evalwrapped(context, mapping, args[0])
283 283 key = evalrawexp(context, mapping, args[1])
284 284 try:
285 285 return dictarg.getmember(context, mapping, key)
286 286 except error.ParseError as err:
287 287 # i18n: "get" is a keyword
288 288 hint = _("get() expects a dict as first argument")
289 289 raise error.ParseError(bytes(err), hint=hint)
290 290
291 291 @templatefunc('if(expr, then[, else])')
292 292 def if_(context, mapping, args):
293 293 """Conditionally execute based on the result of
294 294 an expression."""
295 295 if not (2 <= len(args) <= 3):
296 296 # i18n: "if" is a keyword
297 297 raise error.ParseError(_("if expects two or three arguments"))
298 298
299 299 test = evalboolean(context, mapping, args[0])
300 300 if test:
301 301 return evalrawexp(context, mapping, args[1])
302 302 elif len(args) == 3:
303 303 return evalrawexp(context, mapping, args[2])
304 304
305 305 @templatefunc('ifcontains(needle, haystack, then[, else])')
306 306 def ifcontains(context, mapping, args):
307 307 """Conditionally execute based
308 308 on whether the item "needle" is in "haystack"."""
309 309 if not (3 <= len(args) <= 4):
310 310 # i18n: "ifcontains" is a keyword
311 311 raise error.ParseError(_("ifcontains expects three or four arguments"))
312 312
313 313 haystack = evalwrapped(context, mapping, args[1])
314 314 try:
315 315 needle = evalrawexp(context, mapping, args[0])
316 316 found = haystack.contains(context, mapping, needle)
317 317 except error.ParseError:
318 318 found = False
319 319
320 320 if found:
321 321 return evalrawexp(context, mapping, args[2])
322 322 elif len(args) == 4:
323 323 return evalrawexp(context, mapping, args[3])
324 324
325 325 @templatefunc('ifeq(expr1, expr2, then[, else])')
326 326 def ifeq(context, mapping, args):
327 327 """Conditionally execute based on
328 328 whether 2 items are equivalent."""
329 329 if not (3 <= len(args) <= 4):
330 330 # i18n: "ifeq" is a keyword
331 331 raise error.ParseError(_("ifeq expects three or four arguments"))
332 332
333 333 test = evalstring(context, mapping, args[0])
334 334 match = evalstring(context, mapping, args[1])
335 335 if test == match:
336 336 return evalrawexp(context, mapping, args[2])
337 337 elif len(args) == 4:
338 338 return evalrawexp(context, mapping, args[3])
339 339
340 340 @templatefunc('join(list, sep)')
341 341 def join(context, mapping, args):
342 342 """Join items in a list with a delimiter."""
343 343 if not (1 <= len(args) <= 2):
344 344 # i18n: "join" is a keyword
345 345 raise error.ParseError(_("join expects one or two arguments"))
346 346
347 347 joinset = evalwrapped(context, mapping, args[0])
348 348 joiner = " "
349 349 if len(args) > 1:
350 350 joiner = evalstring(context, mapping, args[1])
351 351 return joinset.join(context, mapping, joiner)
352 352
353 353 @templatefunc('label(label, expr)', requires={'ui'})
354 354 def label(context, mapping, args):
355 355 """Apply a label to generated content. Content with
356 356 a label applied can result in additional post-processing, such as
357 357 automatic colorization."""
358 358 if len(args) != 2:
359 359 # i18n: "label" is a keyword
360 360 raise error.ParseError(_("label expects two arguments"))
361 361
362 362 ui = context.resource(mapping, 'ui')
363 363 thing = evalstring(context, mapping, args[1])
364 364 # preserve unknown symbol as literal so effects like 'red', 'bold',
365 365 # etc. don't need to be quoted
366 366 label = evalstringliteral(context, mapping, args[0])
367 367
368 368 return ui.label(thing, label)
369 369
370 370 @templatefunc('latesttag([pattern])')
371 371 def latesttag(context, mapping, args):
372 372 """The global tags matching the given pattern on the
373 373 most recent globally tagged ancestor of this changeset.
374 374 If no such tags exist, the "{tag}" template resolves to
375 375 the string "null". See :hg:`help revisions.patterns` for the pattern
376 376 syntax.
377 377 """
378 378 if len(args) > 1:
379 379 # i18n: "latesttag" is a keyword
380 380 raise error.ParseError(_("latesttag expects at most one argument"))
381 381
382 382 pattern = None
383 383 if len(args) == 1:
384 384 pattern = evalstring(context, mapping, args[0])
385 385 return templatekw.showlatesttags(context, mapping, pattern)
386 386
387 387 @templatefunc('localdate(date[, tz])')
388 388 def localdate(context, mapping, args):
389 389 """Converts a date to the specified timezone.
390 390 The default is local date."""
391 391 if not (1 <= len(args) <= 2):
392 392 # i18n: "localdate" is a keyword
393 393 raise error.ParseError(_("localdate expects one or two arguments"))
394 394
395 395 date = evaldate(context, mapping, args[0],
396 396 # i18n: "localdate" is a keyword
397 397 _("localdate expects a date information"))
398 398 if len(args) >= 2:
399 399 tzoffset = None
400 400 tz = evalfuncarg(context, mapping, args[1])
401 401 if isinstance(tz, bytes):
402 402 tzoffset, remainder = dateutil.parsetimezone(tz)
403 403 if remainder:
404 404 tzoffset = None
405 405 if tzoffset is None:
406 406 try:
407 407 tzoffset = int(tz)
408 408 except (TypeError, ValueError):
409 409 # i18n: "localdate" is a keyword
410 410 raise error.ParseError(_("localdate expects a timezone"))
411 411 else:
412 412 tzoffset = dateutil.makedate()[1]
413 413 return templateutil.date((date[0], tzoffset))
414 414
415 415 @templatefunc('max(iterable)')
416 416 def max_(context, mapping, args, **kwargs):
417 417 """Return the max of an iterable"""
418 418 if len(args) != 1:
419 419 # i18n: "max" is a keyword
420 420 raise error.ParseError(_("max expects one argument"))
421 421
422 422 iterable = evalwrapped(context, mapping, args[0])
423 423 try:
424 424 return iterable.getmax(context, mapping)
425 425 except error.ParseError as err:
426 426 # i18n: "max" is a keyword
427 427 hint = _("max first argument should be an iterable")
428 428 raise error.ParseError(bytes(err), hint=hint)
429 429
430 430 @templatefunc('min(iterable)')
431 431 def min_(context, mapping, args, **kwargs):
432 432 """Return the min of an iterable"""
433 433 if len(args) != 1:
434 434 # i18n: "min" is a keyword
435 435 raise error.ParseError(_("min expects one argument"))
436 436
437 437 iterable = evalwrapped(context, mapping, args[0])
438 438 try:
439 439 return iterable.getmin(context, mapping)
440 440 except error.ParseError as err:
441 441 # i18n: "min" is a keyword
442 442 hint = _("min first argument should be an iterable")
443 443 raise error.ParseError(bytes(err), hint=hint)
444 444
445 445 @templatefunc('mod(a, b)')
446 446 def mod(context, mapping, args):
447 447 """Calculate a mod b such that a / b + a mod b == a"""
448 448 if not len(args) == 2:
449 449 # i18n: "mod" is a keyword
450 450 raise error.ParseError(_("mod expects two arguments"))
451 451
452 452 func = lambda a, b: a % b
453 453 return templateutil.runarithmetic(context, mapping,
454 454 (func, args[0], args[1]))
455 455
456 456 @templatefunc('obsfateoperations(markers)')
457 457 def obsfateoperations(context, mapping, args):
458 458 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
459 459 if len(args) != 1:
460 460 # i18n: "obsfateoperations" is a keyword
461 461 raise error.ParseError(_("obsfateoperations expects one argument"))
462 462
463 463 markers = evalfuncarg(context, mapping, args[0])
464 464
465 465 try:
466 466 data = obsutil.markersoperations(markers)
467 467 return templateutil.hybridlist(data, name='operation')
468 468 except (TypeError, KeyError):
469 469 # i18n: "obsfateoperations" is a keyword
470 470 errmsg = _("obsfateoperations first argument should be an iterable")
471 471 raise error.ParseError(errmsg)
472 472
473 473 @templatefunc('obsfatedate(markers)')
474 474 def obsfatedate(context, mapping, args):
475 475 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
476 476 if len(args) != 1:
477 477 # i18n: "obsfatedate" is a keyword
478 478 raise error.ParseError(_("obsfatedate expects one argument"))
479 479
480 480 markers = evalfuncarg(context, mapping, args[0])
481 481
482 482 try:
483 483 # TODO: maybe this has to be a wrapped list of date wrappers?
484 484 data = obsutil.markersdates(markers)
485 485 return templateutil.hybridlist(data, name='date', fmt='%d %d')
486 486 except (TypeError, KeyError):
487 487 # i18n: "obsfatedate" is a keyword
488 488 errmsg = _("obsfatedate first argument should be an iterable")
489 489 raise error.ParseError(errmsg)
490 490
491 491 @templatefunc('obsfateusers(markers)')
492 492 def obsfateusers(context, mapping, args):
493 493 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
494 494 if len(args) != 1:
495 495 # i18n: "obsfateusers" is a keyword
496 496 raise error.ParseError(_("obsfateusers expects one argument"))
497 497
498 498 markers = evalfuncarg(context, mapping, args[0])
499 499
500 500 try:
501 501 data = obsutil.markersusers(markers)
502 502 return templateutil.hybridlist(data, name='user')
503 503 except (TypeError, KeyError, ValueError):
504 504 # i18n: "obsfateusers" is a keyword
505 505 msg = _("obsfateusers first argument should be an iterable of "
506 506 "obsmakers")
507 507 raise error.ParseError(msg)
508 508
509 509 @templatefunc('obsfateverb(successors, markers)')
510 510 def obsfateverb(context, mapping, args):
511 511 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
512 512 if len(args) != 2:
513 513 # i18n: "obsfateverb" is a keyword
514 514 raise error.ParseError(_("obsfateverb expects two arguments"))
515 515
516 516 successors = evalfuncarg(context, mapping, args[0])
517 517 markers = evalfuncarg(context, mapping, args[1])
518 518
519 519 try:
520 520 return obsutil.obsfateverb(successors, markers)
521 521 except TypeError:
522 522 # i18n: "obsfateverb" is a keyword
523 523 errmsg = _("obsfateverb first argument should be countable")
524 524 raise error.ParseError(errmsg)
525 525
526 526 @templatefunc('relpath(path)', requires={'repo'})
527 527 def relpath(context, mapping, args):
528 528 """Convert a repository-absolute path into a filesystem path relative to
529 529 the current working directory."""
530 530 if len(args) != 1:
531 531 # i18n: "relpath" is a keyword
532 532 raise error.ParseError(_("relpath expects one argument"))
533 533
534 534 repo = context.resource(mapping, 'repo')
535 535 path = evalstring(context, mapping, args[0])
536 536 return repo.pathto(path)
537 537
538 538 @templatefunc('revset(query[, formatargs...])', requires={'repo', 'cache'})
539 539 def revset(context, mapping, args):
540 540 """Execute a revision set query. See
541 541 :hg:`help revset`."""
542 542 if not len(args) > 0:
543 543 # i18n: "revset" is a keyword
544 544 raise error.ParseError(_("revset expects one or more arguments"))
545 545
546 546 raw = evalstring(context, mapping, args[0])
547 547 repo = context.resource(mapping, 'repo')
548 548
549 549 def query(expr):
550 550 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
551 551 return m(repo)
552 552
553 553 if len(args) > 1:
554 554 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
555 555 revs = query(revsetlang.formatspec(raw, *formatargs))
556 556 revs = list(revs)
557 557 else:
558 558 cache = context.resource(mapping, 'cache')
559 559 revsetcache = cache.setdefault("revsetcache", {})
560 560 if raw in revsetcache:
561 561 revs = revsetcache[raw]
562 562 else:
563 563 revs = query(raw)
564 564 revs = list(revs)
565 565 revsetcache[raw] = revs
566 566 return templatekw.showrevslist(context, mapping, "revision", revs)
567 567
568 568 @templatefunc('rstdoc(text, style)')
569 569 def rstdoc(context, mapping, args):
570 570 """Format reStructuredText."""
571 571 if len(args) != 2:
572 572 # i18n: "rstdoc" is a keyword
573 573 raise error.ParseError(_("rstdoc expects two arguments"))
574 574
575 575 text = evalstring(context, mapping, args[0])
576 576 style = evalstring(context, mapping, args[1])
577 577
578 578 return minirst.format(text, style=style, keep=['verbose'])
579 579
580 580 @templatefunc('separate(sep, args...)', argspec='sep *args')
581 581 def separate(context, mapping, args):
582 582 """Add a separator between non-empty arguments."""
583 583 if 'sep' not in args:
584 584 # i18n: "separate" is a keyword
585 585 raise error.ParseError(_("separate expects at least one argument"))
586 586
587 587 sep = evalstring(context, mapping, args['sep'])
588 588 first = True
589 589 for arg in args['args']:
590 590 argstr = evalstring(context, mapping, arg)
591 591 if not argstr:
592 592 continue
593 593 if first:
594 594 first = False
595 595 else:
596 596 yield sep
597 597 yield argstr
598 598
599 599 @templatefunc('shortest(node, minlength=4)', requires={'repo', 'cache'})
600 600 def shortest(context, mapping, args):
601 601 """Obtain the shortest representation of
602 602 a node."""
603 603 if not (1 <= len(args) <= 2):
604 604 # i18n: "shortest" is a keyword
605 605 raise error.ParseError(_("shortest() expects one or two arguments"))
606 606
607 607 hexnode = evalstring(context, mapping, args[0])
608 608
609 609 minlength = 4
610 610 if len(args) > 1:
611 611 minlength = evalinteger(context, mapping, args[1],
612 612 # i18n: "shortest" is a keyword
613 613 _("shortest() expects an integer minlength"))
614 614
615 615 repo = context.resource(mapping, 'repo')
616 616 if len(hexnode) > 40:
617 617 return hexnode
618 618 elif len(hexnode) == 40:
619 619 try:
620 620 node = bin(hexnode)
621 621 except TypeError:
622 622 return hexnode
623 623 else:
624 624 try:
625 625 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
626 626 except error.WdirUnsupported:
627 627 node = wdirid
628 628 except error.LookupError:
629 629 return hexnode
630 630 if not node:
631 631 return hexnode
632 632 cache = context.resource(mapping, 'cache')
633 633 try:
634 634 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
635 635 except error.RepoLookupError:
636 636 return hexnode
637 637
638 638 @templatefunc('strip(text[, chars])')
639 639 def strip(context, mapping, args):
640 640 """Strip characters from a string. By default,
641 641 strips all leading and trailing whitespace."""
642 642 if not (1 <= len(args) <= 2):
643 643 # i18n: "strip" is a keyword
644 644 raise error.ParseError(_("strip expects one or two arguments"))
645 645
646 646 text = evalstring(context, mapping, args[0])
647 647 if len(args) == 2:
648 648 chars = evalstring(context, mapping, args[1])
649 649 return text.strip(chars)
650 650 return text.strip()
651 651
652 652 @templatefunc('sub(pattern, replacement, expression)')
653 653 def sub(context, mapping, args):
654 654 """Perform text substitution
655 655 using regular expressions."""
656 656 if len(args) != 3:
657 657 # i18n: "sub" is a keyword
658 658 raise error.ParseError(_("sub expects three arguments"))
659 659
660 660 pat = evalstring(context, mapping, args[0])
661 661 rpl = evalstring(context, mapping, args[1])
662 662 src = evalstring(context, mapping, args[2])
663 663 try:
664 664 patre = re.compile(pat)
665 665 except re.error:
666 666 # i18n: "sub" is a keyword
667 667 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
668 668 try:
669 669 yield patre.sub(rpl, src)
670 670 except re.error:
671 671 # i18n: "sub" is a keyword
672 672 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
673 673
674 674 @templatefunc('startswith(pattern, text)')
675 675 def startswith(context, mapping, args):
676 676 """Returns the value from the "text" argument
677 677 if it begins with the content from the "pattern" argument."""
678 678 if len(args) != 2:
679 679 # i18n: "startswith" is a keyword
680 680 raise error.ParseError(_("startswith expects two arguments"))
681 681
682 682 patn = evalstring(context, mapping, args[0])
683 683 text = evalstring(context, mapping, args[1])
684 684 if text.startswith(patn):
685 685 return text
686 686 return ''
687 687
688 688 @templatefunc('word(number, text[, separator])')
689 689 def word(context, mapping, args):
690 690 """Return the nth word from a string."""
691 691 if not (2 <= len(args) <= 3):
692 692 # i18n: "word" is a keyword
693 693 raise error.ParseError(_("word expects two or three arguments, got %d")
694 694 % len(args))
695 695
696 696 num = evalinteger(context, mapping, args[0],
697 697 # i18n: "word" is a keyword
698 698 _("word expects an integer index"))
699 699 text = evalstring(context, mapping, args[1])
700 700 if len(args) == 3:
701 701 splitter = evalstring(context, mapping, args[2])
702 702 else:
703 703 splitter = None
704 704
705 705 tokens = text.split(splitter)
706 706 if num >= len(tokens) or num < -len(tokens):
707 707 return ''
708 708 else:
709 709 return tokens[num]
710 710
711 711 def loadfunction(ui, extname, registrarobj):
712 712 """Load template function from specified registrarobj
713 713 """
714 714 for name, func in registrarobj._table.iteritems():
715 715 funcs[name] = func
716 716
717 717 # tell hggettext to extract docstrings from these functions:
718 718 i18nfunctions = funcs.values()
@@ -1,820 +1,820
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 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 from .i18n import _
11 11 from .node import (
12 12 hex,
13 13 nullid,
14 14 )
15 15
16 16 from . import (
17 17 diffutil,
18 18 encoding,
19 19 error,
20 20 hbisect,
21 21 i18n,
22 22 obsutil,
23 23 patch,
24 24 pycompat,
25 25 registrar,
26 26 scmutil,
27 27 templateutil,
28 28 util,
29 29 )
30 30 from .utils import (
31 31 stringutil,
32 32 )
33 33
34 34 _hybrid = templateutil.hybrid
35 35 hybriddict = templateutil.hybriddict
36 36 hybridlist = templateutil.hybridlist
37 37 compatdict = templateutil.compatdict
38 38 compatlist = templateutil.compatlist
39 39 _showcompatlist = templateutil._showcompatlist
40 40
41 41 def getlatesttags(context, mapping, pattern=None):
42 42 '''return date, distance and name for the latest tag of rev'''
43 43 repo = context.resource(mapping, 'repo')
44 44 ctx = context.resource(mapping, 'ctx')
45 45 cache = context.resource(mapping, 'cache')
46 46
47 47 cachename = 'latesttags'
48 48 if pattern is not None:
49 49 cachename += '-' + pattern
50 50 match = stringutil.stringmatcher(pattern)[2]
51 51 else:
52 52 match = util.always
53 53
54 54 if cachename not in cache:
55 55 # Cache mapping from rev to a tuple with tag date, tag
56 56 # distance and tag name
57 57 cache[cachename] = {-1: (0, 0, ['null'])}
58 58 latesttags = cache[cachename]
59 59
60 60 rev = ctx.rev()
61 61 todo = [rev]
62 62 while todo:
63 63 rev = todo.pop()
64 64 if rev in latesttags:
65 65 continue
66 66 ctx = repo[rev]
67 67 tags = [t for t in ctx.tags()
68 68 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
69 69 and match(t))]
70 70 if tags:
71 71 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
72 72 continue
73 73 try:
74 74 ptags = [latesttags[p.rev()] for p in ctx.parents()]
75 75 if len(ptags) > 1:
76 76 if ptags[0][2] == ptags[1][2]:
77 77 # The tuples are laid out so the right one can be found by
78 78 # comparison in this case.
79 79 pdate, pdist, ptag = max(ptags)
80 80 else:
81 81 def key(x):
82 82 changessincetag = len(repo.revs('only(%d, %s)',
83 83 ctx.rev(), x[2][0]))
84 84 # Smallest number of changes since tag wins. Date is
85 85 # used as tiebreaker.
86 86 return [-changessincetag, x[0]]
87 87 pdate, pdist, ptag = max(ptags, key=key)
88 88 else:
89 89 pdate, pdist, ptag = ptags[0]
90 90 except KeyError:
91 91 # Cache miss - recurse
92 92 todo.append(rev)
93 93 todo.extend(p.rev() for p in ctx.parents())
94 94 continue
95 95 latesttags[rev] = pdate, pdist + 1, ptag
96 96 return latesttags[rev]
97 97
98 98 def getrenamedfn(repo, endrev=None):
99 99 rcache = {}
100 100 if endrev is None:
101 101 endrev = len(repo)
102 102
103 103 def getrenamed(fn, rev):
104 104 '''looks up all renames for a file (up to endrev) the first
105 105 time the file is given. It indexes on the changerev and only
106 106 parses the manifest if linkrev != changerev.
107 107 Returns rename info for fn at changerev rev.'''
108 108 if fn not in rcache:
109 109 rcache[fn] = {}
110 110 fl = repo.file(fn)
111 111 for i in fl:
112 112 lr = fl.linkrev(i)
113 113 renamed = fl.renamed(fl.node(i))
114 114 rcache[fn][lr] = renamed and renamed[0]
115 115 if lr >= endrev:
116 116 break
117 117 if rev in rcache[fn]:
118 118 return rcache[fn][rev]
119 119
120 120 # If linkrev != rev (i.e. rev not found in rcache) fallback to
121 121 # filectx logic.
122 122 try:
123 123 renamed = repo[rev][fn].renamed()
124 124 return renamed and renamed[0]
125 125 except error.LookupError:
126 126 return None
127 127
128 128 return getrenamed
129 129
130 130 def getlogcolumns():
131 131 """Return a dict of log column labels"""
132 132 _ = pycompat.identity # temporarily disable gettext
133 133 # i18n: column positioning for "hg log"
134 134 columns = _('bookmark: %s\n'
135 135 'branch: %s\n'
136 136 'changeset: %s\n'
137 137 'copies: %s\n'
138 138 'date: %s\n'
139 139 'extra: %s=%s\n'
140 140 'files+: %s\n'
141 141 'files-: %s\n'
142 142 'files: %s\n'
143 143 'instability: %s\n'
144 144 'manifest: %s\n'
145 145 'obsolete: %s\n'
146 146 'parent: %s\n'
147 147 'phase: %s\n'
148 148 'summary: %s\n'
149 149 'tag: %s\n'
150 150 'user: %s\n')
151 151 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
152 152 i18n._(columns).splitlines(True)))
153 153
154 154 # default templates internally used for rendering of lists
155 155 defaulttempl = {
156 156 'parent': '{rev}:{node|formatnode} ',
157 157 'manifest': '{rev}:{node|formatnode}',
158 158 'file_copy': '{name} ({source})',
159 159 'envvar': '{key}={value}',
160 160 'extra': '{key}={value|stringescape}'
161 161 }
162 162 # filecopy is preserved for compatibility reasons
163 163 defaulttempl['filecopy'] = defaulttempl['file_copy']
164 164
165 165 # keywords are callables (see registrar.templatekeyword for details)
166 166 keywords = {}
167 167 templatekeyword = registrar.templatekeyword(keywords)
168 168
169 169 @templatekeyword('author', requires={'ctx'})
170 170 def showauthor(context, mapping):
171 171 """Alias for ``{user}``"""
172 172 return showuser(context, mapping)
173 173
174 174 @templatekeyword('bisect', requires={'repo', 'ctx'})
175 175 def showbisect(context, mapping):
176 176 """String. The changeset bisection status."""
177 177 repo = context.resource(mapping, 'repo')
178 178 ctx = context.resource(mapping, 'ctx')
179 179 return hbisect.label(repo, ctx.node())
180 180
181 181 @templatekeyword('branch', requires={'ctx'})
182 182 def showbranch(context, mapping):
183 183 """String. The name of the branch on which the changeset was
184 184 committed.
185 185 """
186 186 ctx = context.resource(mapping, 'ctx')
187 187 return ctx.branch()
188 188
189 189 @templatekeyword('branches', requires={'ctx'})
190 190 def showbranches(context, mapping):
191 191 """List of strings. The name of the branch on which the
192 192 changeset was committed. Will be empty if the branch name was
193 193 default. (DEPRECATED)
194 194 """
195 195 ctx = context.resource(mapping, 'ctx')
196 196 branch = ctx.branch()
197 197 if branch != 'default':
198 198 return compatlist(context, mapping, 'branch', [branch],
199 199 plural='branches')
200 200 return compatlist(context, mapping, 'branch', [], plural='branches')
201 201
202 202 @templatekeyword('bookmarks', requires={'repo', 'ctx'})
203 203 def showbookmarks(context, mapping):
204 204 """List of strings. Any bookmarks associated with the
205 205 changeset. Also sets 'active', the name of the active bookmark.
206 206 """
207 207 repo = context.resource(mapping, 'repo')
208 208 ctx = context.resource(mapping, 'ctx')
209 209 bookmarks = ctx.bookmarks()
210 210 active = repo._activebookmark
211 211 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
212 212 f = _showcompatlist(context, mapping, 'bookmark', bookmarks)
213 213 return _hybrid(f, bookmarks, makemap, pycompat.identity)
214 214
215 215 @templatekeyword('children', requires={'ctx'})
216 216 def showchildren(context, mapping):
217 217 """List of strings. The children of the changeset."""
218 218 ctx = context.resource(mapping, 'ctx')
219 219 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
220 220 return compatlist(context, mapping, 'children', childrevs, element='child')
221 221
222 222 # Deprecated, but kept alive for help generation a purpose.
223 223 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
224 224 def showcurrentbookmark(context, mapping):
225 225 """String. The active bookmark, if it is associated with the changeset.
226 226 (DEPRECATED)"""
227 227 return showactivebookmark(context, mapping)
228 228
229 229 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
230 230 def showactivebookmark(context, mapping):
231 231 """String. The active bookmark, if it is associated with the changeset."""
232 232 repo = context.resource(mapping, 'repo')
233 233 ctx = context.resource(mapping, 'ctx')
234 234 active = repo._activebookmark
235 235 if active and active in ctx.bookmarks():
236 236 return active
237 237 return ''
238 238
239 239 @templatekeyword('date', requires={'ctx'})
240 240 def showdate(context, mapping):
241 241 """Date information. The date when the changeset was committed."""
242 242 ctx = context.resource(mapping, 'ctx')
243 243 # the default string format is '<float(unixtime)><tzoffset>' because
244 244 # python-hglib splits date at decimal separator.
245 245 return templateutil.date(ctx.date(), showfmt='%d.0%d')
246 246
247 247 @templatekeyword('desc', requires={'ctx'})
248 248 def showdescription(context, mapping):
249 249 """String. The text of the changeset description."""
250 250 ctx = context.resource(mapping, 'ctx')
251 251 s = ctx.description()
252 252 if isinstance(s, encoding.localstr):
253 253 # try hard to preserve utf-8 bytes
254 254 return encoding.tolocal(encoding.fromlocal(s).strip())
255 255 elif isinstance(s, encoding.safelocalstr):
256 256 return encoding.safelocalstr(s.strip())
257 257 else:
258 258 return s.strip()
259 259
260 260 @templatekeyword('diffstat', requires={'ui', 'ctx'})
261 261 def showdiffstat(context, mapping):
262 262 """String. Statistics of changes with the following format:
263 263 "modified files: +added/-removed lines"
264 264 """
265 265 ui = context.resource(mapping, 'ui')
266 266 ctx = context.resource(mapping, 'ctx')
267 267 diffopts = diffutil.diffallopts(ui, {'noprefix': False})
268 268 diff = ctx.diff(opts=diffopts)
269 269 stats = patch.diffstatdata(util.iterlines(diff))
270 270 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
271 271 return '%d: +%d/-%d' % (len(stats), adds, removes)
272 272
273 273 @templatekeyword('envvars', requires={'ui'})
274 274 def showenvvars(context, mapping):
275 275 """A dictionary of environment variables. (EXPERIMENTAL)"""
276 276 ui = context.resource(mapping, 'ui')
277 277 env = ui.exportableenviron()
278 278 env = util.sortdict((k, env[k]) for k in sorted(env))
279 279 return compatdict(context, mapping, 'envvar', env, plural='envvars')
280 280
281 281 @templatekeyword('extras', requires={'ctx'})
282 282 def showextras(context, mapping):
283 283 """List of dicts with key, value entries of the 'extras'
284 284 field of this changeset."""
285 285 ctx = context.resource(mapping, 'ctx')
286 286 extras = ctx.extra()
287 287 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
288 288 makemap = lambda k: {'key': k, 'value': extras[k]}
289 289 c = [makemap(k) for k in extras]
290 290 f = _showcompatlist(context, mapping, 'extra', c, plural='extras')
291 291 return _hybrid(f, extras, makemap,
292 292 lambda k: '%s=%s' % (k, stringutil.escapestr(extras[k])))
293 293
294 294 def _showfilesbystat(context, mapping, name, index):
295 295 ctx = context.resource(mapping, 'ctx')
296 296 revcache = context.resource(mapping, 'revcache')
297 297 if 'files' not in revcache:
298 298 revcache['files'] = ctx.p1().status(ctx)[:3]
299 299 files = revcache['files'][index]
300 return compatlist(context, mapping, name, files, element='file')
300 return templateutil.compatfileslist(context, mapping, name, files)
301 301
302 302 @templatekeyword('file_adds', requires={'ctx', 'revcache'})
303 303 def showfileadds(context, mapping):
304 304 """List of strings. Files added by this changeset."""
305 305 return _showfilesbystat(context, mapping, 'file_add', 1)
306 306
307 307 @templatekeyword('file_copies',
308 308 requires={'repo', 'ctx', 'cache', 'revcache'})
309 309 def showfilecopies(context, mapping):
310 310 """List of strings. Files copied in this changeset with
311 311 their sources.
312 312 """
313 313 repo = context.resource(mapping, 'repo')
314 314 ctx = context.resource(mapping, 'ctx')
315 315 cache = context.resource(mapping, 'cache')
316 316 copies = context.resource(mapping, 'revcache').get('copies')
317 317 if copies is None:
318 318 if 'getrenamed' not in cache:
319 319 cache['getrenamed'] = getrenamedfn(repo)
320 320 copies = []
321 321 getrenamed = cache['getrenamed']
322 322 for fn in ctx.files():
323 323 rename = getrenamed(fn, ctx.rev())
324 324 if rename:
325 325 copies.append((fn, rename))
326 326
327 327 copies = util.sortdict(copies)
328 328 return compatdict(context, mapping, 'file_copy', copies,
329 329 key='name', value='source', fmt='%s (%s)',
330 330 plural='file_copies')
331 331
332 332 # showfilecopiesswitch() displays file copies only if copy records are
333 333 # provided before calling the templater, usually with a --copies
334 334 # command line switch.
335 335 @templatekeyword('file_copies_switch', requires={'revcache'})
336 336 def showfilecopiesswitch(context, mapping):
337 337 """List of strings. Like "file_copies" but displayed
338 338 only if the --copied switch is set.
339 339 """
340 340 copies = context.resource(mapping, 'revcache').get('copies') or []
341 341 copies = util.sortdict(copies)
342 342 return compatdict(context, mapping, 'file_copy', copies,
343 343 key='name', value='source', fmt='%s (%s)',
344 344 plural='file_copies')
345 345
346 346 @templatekeyword('file_dels', requires={'ctx', 'revcache'})
347 347 def showfiledels(context, mapping):
348 348 """List of strings. Files removed by this changeset."""
349 349 return _showfilesbystat(context, mapping, 'file_del', 2)
350 350
351 351 @templatekeyword('file_mods', requires={'ctx', 'revcache'})
352 352 def showfilemods(context, mapping):
353 353 """List of strings. Files modified by this changeset."""
354 354 return _showfilesbystat(context, mapping, 'file_mod', 0)
355 355
356 356 @templatekeyword('files', requires={'ctx'})
357 357 def showfiles(context, mapping):
358 358 """List of strings. All files modified, added, or removed by this
359 359 changeset.
360 360 """
361 361 ctx = context.resource(mapping, 'ctx')
362 return compatlist(context, mapping, 'file', ctx.files())
362 return templateutil.compatfileslist(context, mapping, 'file', ctx.files())
363 363
364 364 @templatekeyword('graphnode', requires={'repo', 'ctx'})
365 365 def showgraphnode(context, mapping):
366 366 """String. The character representing the changeset node in an ASCII
367 367 revision graph."""
368 368 repo = context.resource(mapping, 'repo')
369 369 ctx = context.resource(mapping, 'ctx')
370 370 return getgraphnode(repo, ctx)
371 371
372 372 def getgraphnode(repo, ctx):
373 373 return getgraphnodecurrent(repo, ctx) or getgraphnodesymbol(ctx)
374 374
375 375 def getgraphnodecurrent(repo, ctx):
376 376 wpnodes = repo.dirstate.parents()
377 377 if wpnodes[1] == nullid:
378 378 wpnodes = wpnodes[:1]
379 379 if ctx.node() in wpnodes:
380 380 return '@'
381 381 else:
382 382 return ''
383 383
384 384 def getgraphnodesymbol(ctx):
385 385 if ctx.obsolete():
386 386 return 'x'
387 387 elif ctx.isunstable():
388 388 return '*'
389 389 elif ctx.closesbranch():
390 390 return '_'
391 391 else:
392 392 return 'o'
393 393
394 394 @templatekeyword('graphwidth', requires=())
395 395 def showgraphwidth(context, mapping):
396 396 """Integer. The width of the graph drawn by 'log --graph' or zero."""
397 397 # just hosts documentation; should be overridden by template mapping
398 398 return 0
399 399
400 400 @templatekeyword('index', requires=())
401 401 def showindex(context, mapping):
402 402 """Integer. The current iteration of the loop. (0 indexed)"""
403 403 # just hosts documentation; should be overridden by template mapping
404 404 raise error.Abort(_("can't use index in this context"))
405 405
406 406 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache'})
407 407 def showlatesttag(context, mapping):
408 408 """List of strings. The global tags on the most recent globally
409 409 tagged ancestor of this changeset. If no such tags exist, the list
410 410 consists of the single string "null".
411 411 """
412 412 return showlatesttags(context, mapping, None)
413 413
414 414 def showlatesttags(context, mapping, pattern):
415 415 """helper method for the latesttag keyword and function"""
416 416 latesttags = getlatesttags(context, mapping, pattern)
417 417
418 418 # latesttag[0] is an implementation detail for sorting csets on different
419 419 # branches in a stable manner- it is the date the tagged cset was created,
420 420 # not the date the tag was created. Therefore it isn't made visible here.
421 421 makemap = lambda v: {
422 422 'changes': _showchangessincetag,
423 423 'distance': latesttags[1],
424 424 'latesttag': v, # BC with {latesttag % '{latesttag}'}
425 425 'tag': v
426 426 }
427 427
428 428 tags = latesttags[2]
429 429 f = _showcompatlist(context, mapping, 'latesttag', tags, separator=':')
430 430 return _hybrid(f, tags, makemap, pycompat.identity)
431 431
432 432 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
433 433 def showlatesttagdistance(context, mapping):
434 434 """Integer. Longest path to the latest tag."""
435 435 return getlatesttags(context, mapping)[1]
436 436
437 437 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
438 438 def showchangessincelatesttag(context, mapping):
439 439 """Integer. All ancestors not in the latest tag."""
440 440 tag = getlatesttags(context, mapping)[2][0]
441 441 mapping = context.overlaymap(mapping, {'tag': tag})
442 442 return _showchangessincetag(context, mapping)
443 443
444 444 def _showchangessincetag(context, mapping):
445 445 repo = context.resource(mapping, 'repo')
446 446 ctx = context.resource(mapping, 'ctx')
447 447 offset = 0
448 448 revs = [ctx.rev()]
449 449 tag = context.symbol(mapping, 'tag')
450 450
451 451 # The only() revset doesn't currently support wdir()
452 452 if ctx.rev() is None:
453 453 offset = 1
454 454 revs = [p.rev() for p in ctx.parents()]
455 455
456 456 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
457 457
458 458 # teach templater latesttags.changes is switched to (context, mapping) API
459 459 _showchangessincetag._requires = {'repo', 'ctx'}
460 460
461 461 @templatekeyword('manifest', requires={'repo', 'ctx'})
462 462 def showmanifest(context, mapping):
463 463 repo = context.resource(mapping, 'repo')
464 464 ctx = context.resource(mapping, 'ctx')
465 465 mnode = ctx.manifestnode()
466 466 if mnode is None:
467 467 # just avoid crash, we might want to use the 'ff...' hash in future
468 468 return
469 469 mrev = repo.manifestlog.rev(mnode)
470 470 mhex = hex(mnode)
471 471 mapping = context.overlaymap(mapping, {'rev': mrev, 'node': mhex})
472 472 f = context.process('manifest', mapping)
473 473 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
474 474 # rev and node are completely different from changeset's.
475 475 return templateutil.hybriditem(f, None, f,
476 476 lambda x: {'rev': mrev, 'node': mhex})
477 477
478 478 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx'})
479 479 def showobsfate(context, mapping):
480 480 # this function returns a list containing pre-formatted obsfate strings.
481 481 #
482 482 # This function will be replaced by templates fragments when we will have
483 483 # the verbosity templatekw available.
484 484 succsandmarkers = showsuccsandmarkers(context, mapping)
485 485
486 486 ui = context.resource(mapping, 'ui')
487 487 repo = context.resource(mapping, 'repo')
488 488 values = []
489 489
490 490 for x in succsandmarkers.tovalue(context, mapping):
491 491 v = obsutil.obsfateprinter(ui, repo, x['successors'], x['markers'],
492 492 scmutil.formatchangeid)
493 493 values.append(v)
494 494
495 495 return compatlist(context, mapping, "fate", values)
496 496
497 497 def shownames(context, mapping, namespace):
498 498 """helper method to generate a template keyword for a namespace"""
499 499 repo = context.resource(mapping, 'repo')
500 500 ctx = context.resource(mapping, 'ctx')
501 501 ns = repo.names[namespace]
502 502 names = ns.names(repo, ctx.node())
503 503 return compatlist(context, mapping, ns.templatename, names,
504 504 plural=namespace)
505 505
506 506 @templatekeyword('namespaces', requires={'repo', 'ctx'})
507 507 def shownamespaces(context, mapping):
508 508 """Dict of lists. Names attached to this changeset per
509 509 namespace."""
510 510 repo = context.resource(mapping, 'repo')
511 511 ctx = context.resource(mapping, 'ctx')
512 512
513 513 namespaces = util.sortdict()
514 514 def makensmapfn(ns):
515 515 # 'name' for iterating over namespaces, templatename for local reference
516 516 return lambda v: {'name': v, ns.templatename: v}
517 517
518 518 for k, ns in repo.names.iteritems():
519 519 names = ns.names(repo, ctx.node())
520 520 f = _showcompatlist(context, mapping, 'name', names)
521 521 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
522 522
523 523 f = _showcompatlist(context, mapping, 'namespace', list(namespaces))
524 524
525 525 def makemap(ns):
526 526 return {
527 527 'namespace': ns,
528 528 'names': namespaces[ns],
529 529 'builtin': repo.names[ns].builtin,
530 530 'colorname': repo.names[ns].colorname,
531 531 }
532 532
533 533 return _hybrid(f, namespaces, makemap, pycompat.identity)
534 534
535 535 @templatekeyword('node', requires={'ctx'})
536 536 def shownode(context, mapping):
537 537 """String. The changeset identification hash, as a 40 hexadecimal
538 538 digit string.
539 539 """
540 540 ctx = context.resource(mapping, 'ctx')
541 541 return ctx.hex()
542 542
543 543 @templatekeyword('obsolete', requires={'ctx'})
544 544 def showobsolete(context, mapping):
545 545 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
546 546 ctx = context.resource(mapping, 'ctx')
547 547 if ctx.obsolete():
548 548 return 'obsolete'
549 549 return ''
550 550
551 551 @templatekeyword('peerurls', requires={'repo'})
552 552 def showpeerurls(context, mapping):
553 553 """A dictionary of repository locations defined in the [paths] section
554 554 of your configuration file."""
555 555 repo = context.resource(mapping, 'repo')
556 556 # see commands.paths() for naming of dictionary keys
557 557 paths = repo.ui.paths
558 558 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
559 559 def makemap(k):
560 560 p = paths[k]
561 561 d = {'name': k, 'url': p.rawloc}
562 562 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
563 563 return d
564 564 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
565 565
566 566 @templatekeyword("predecessors", requires={'repo', 'ctx'})
567 567 def showpredecessors(context, mapping):
568 568 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
569 569 repo = context.resource(mapping, 'repo')
570 570 ctx = context.resource(mapping, 'ctx')
571 571 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
572 572 predecessors = pycompat.maplist(hex, predecessors)
573 573
574 574 return _hybrid(None, predecessors,
575 575 lambda x: {'ctx': repo[x]},
576 576 lambda x: scmutil.formatchangeid(repo[x]))
577 577
578 578 @templatekeyword('reporoot', requires={'repo'})
579 579 def showreporoot(context, mapping):
580 580 """String. The root directory of the current repository."""
581 581 repo = context.resource(mapping, 'repo')
582 582 return repo.root
583 583
584 584 @templatekeyword("successorssets", requires={'repo', 'ctx'})
585 585 def showsuccessorssets(context, mapping):
586 586 """Returns a string of sets of successors for a changectx. Format used
587 587 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
588 588 while also diverged into ctx3. (EXPERIMENTAL)"""
589 589 repo = context.resource(mapping, 'repo')
590 590 ctx = context.resource(mapping, 'ctx')
591 591 if not ctx.obsolete():
592 592 return ''
593 593
594 594 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
595 595 ssets = [[hex(n) for n in ss] for ss in ssets]
596 596
597 597 data = []
598 598 for ss in ssets:
599 599 h = _hybrid(None, ss, lambda x: {'ctx': repo[x]},
600 600 lambda x: scmutil.formatchangeid(repo[x]))
601 601 data.append(h)
602 602
603 603 # Format the successorssets
604 604 def render(d):
605 605 return templateutil.stringify(context, mapping, d)
606 606
607 607 def gen(data):
608 608 yield "; ".join(render(d) for d in data)
609 609
610 610 return _hybrid(gen(data), data, lambda x: {'successorset': x},
611 611 pycompat.identity)
612 612
613 613 @templatekeyword("succsandmarkers", requires={'repo', 'ctx'})
614 614 def showsuccsandmarkers(context, mapping):
615 615 """Returns a list of dict for each final successor of ctx. The dict
616 616 contains successors node id in "successors" keys and the list of
617 617 obs-markers from ctx to the set of successors in "markers".
618 618 (EXPERIMENTAL)
619 619 """
620 620 repo = context.resource(mapping, 'repo')
621 621 ctx = context.resource(mapping, 'ctx')
622 622
623 623 values = obsutil.successorsandmarkers(repo, ctx)
624 624
625 625 if values is None:
626 626 values = []
627 627
628 628 # Format successors and markers to avoid exposing binary to templates
629 629 data = []
630 630 for i in values:
631 631 # Format successors
632 632 successors = i['successors']
633 633
634 634 successors = [hex(n) for n in successors]
635 635 successors = _hybrid(None, successors,
636 636 lambda x: {'ctx': repo[x]},
637 637 lambda x: scmutil.formatchangeid(repo[x]))
638 638
639 639 # Format markers
640 640 finalmarkers = []
641 641 for m in i['markers']:
642 642 hexprec = hex(m[0])
643 643 hexsucs = tuple(hex(n) for n in m[1])
644 644 hexparents = None
645 645 if m[5] is not None:
646 646 hexparents = tuple(hex(n) for n in m[5])
647 647 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
648 648 finalmarkers.append(newmarker)
649 649
650 650 data.append({'successors': successors, 'markers': finalmarkers})
651 651
652 652 return templateutil.mappinglist(data)
653 653
654 654 @templatekeyword('p1rev', requires={'ctx'})
655 655 def showp1rev(context, mapping):
656 656 """Integer. The repository-local revision number of the changeset's
657 657 first parent, or -1 if the changeset has no parents."""
658 658 ctx = context.resource(mapping, 'ctx')
659 659 return ctx.p1().rev()
660 660
661 661 @templatekeyword('p2rev', requires={'ctx'})
662 662 def showp2rev(context, mapping):
663 663 """Integer. The repository-local revision number of the changeset's
664 664 second parent, or -1 if the changeset has no second parent."""
665 665 ctx = context.resource(mapping, 'ctx')
666 666 return ctx.p2().rev()
667 667
668 668 @templatekeyword('p1node', requires={'ctx'})
669 669 def showp1node(context, mapping):
670 670 """String. The identification hash of the changeset's first parent,
671 671 as a 40 digit hexadecimal string. If the changeset has no parents, all
672 672 digits are 0."""
673 673 ctx = context.resource(mapping, 'ctx')
674 674 return ctx.p1().hex()
675 675
676 676 @templatekeyword('p2node', requires={'ctx'})
677 677 def showp2node(context, mapping):
678 678 """String. The identification hash of the changeset's second
679 679 parent, as a 40 digit hexadecimal string. If the changeset has no second
680 680 parent, all digits are 0."""
681 681 ctx = context.resource(mapping, 'ctx')
682 682 return ctx.p2().hex()
683 683
684 684 @templatekeyword('parents', requires={'repo', 'ctx'})
685 685 def showparents(context, mapping):
686 686 """List of strings. The parents of the changeset in "rev:node"
687 687 format. If the changeset has only one "natural" parent (the predecessor
688 688 revision) nothing is shown."""
689 689 repo = context.resource(mapping, 'repo')
690 690 ctx = context.resource(mapping, 'ctx')
691 691 pctxs = scmutil.meaningfulparents(repo, ctx)
692 692 prevs = [p.rev() for p in pctxs]
693 693 parents = [[('rev', p.rev()),
694 694 ('node', p.hex()),
695 695 ('phase', p.phasestr())]
696 696 for p in pctxs]
697 697 f = _showcompatlist(context, mapping, 'parent', parents)
698 698 return _hybrid(f, prevs, lambda x: {'ctx': repo[x]},
699 699 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
700 700
701 701 @templatekeyword('phase', requires={'ctx'})
702 702 def showphase(context, mapping):
703 703 """String. The changeset phase name."""
704 704 ctx = context.resource(mapping, 'ctx')
705 705 return ctx.phasestr()
706 706
707 707 @templatekeyword('phaseidx', requires={'ctx'})
708 708 def showphaseidx(context, mapping):
709 709 """Integer. The changeset phase index. (ADVANCED)"""
710 710 ctx = context.resource(mapping, 'ctx')
711 711 return ctx.phase()
712 712
713 713 @templatekeyword('rev', requires={'ctx'})
714 714 def showrev(context, mapping):
715 715 """Integer. The repository-local changeset revision number."""
716 716 ctx = context.resource(mapping, 'ctx')
717 717 return scmutil.intrev(ctx)
718 718
719 719 def showrevslist(context, mapping, name, revs):
720 720 """helper to generate a list of revisions in which a mapped template will
721 721 be evaluated"""
722 722 repo = context.resource(mapping, 'repo')
723 723 f = _showcompatlist(context, mapping, name, ['%d' % r for r in revs])
724 724 return _hybrid(f, revs,
725 725 lambda x: {name: x, 'ctx': repo[x]},
726 726 pycompat.identity, keytype=int)
727 727
728 728 @templatekeyword('subrepos', requires={'ctx'})
729 729 def showsubrepos(context, mapping):
730 730 """List of strings. Updated subrepositories in the changeset."""
731 731 ctx = context.resource(mapping, 'ctx')
732 732 substate = ctx.substate
733 733 if not substate:
734 734 return compatlist(context, mapping, 'subrepo', [])
735 735 psubstate = ctx.parents()[0].substate or {}
736 736 subrepos = []
737 737 for sub in substate:
738 738 if sub not in psubstate or substate[sub] != psubstate[sub]:
739 739 subrepos.append(sub) # modified or newly added in ctx
740 740 for sub in psubstate:
741 741 if sub not in substate:
742 742 subrepos.append(sub) # removed in ctx
743 743 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
744 744
745 745 # don't remove "showtags" definition, even though namespaces will put
746 746 # a helper function for "tags" keyword into "keywords" map automatically,
747 747 # because online help text is built without namespaces initialization
748 748 @templatekeyword('tags', requires={'repo', 'ctx'})
749 749 def showtags(context, mapping):
750 750 """List of strings. Any tags associated with the changeset."""
751 751 return shownames(context, mapping, 'tags')
752 752
753 753 @templatekeyword('termwidth', requires={'ui'})
754 754 def showtermwidth(context, mapping):
755 755 """Integer. The width of the current terminal."""
756 756 ui = context.resource(mapping, 'ui')
757 757 return ui.termwidth()
758 758
759 759 @templatekeyword('user', requires={'ctx'})
760 760 def showuser(context, mapping):
761 761 """String. The unmodified author of the changeset."""
762 762 ctx = context.resource(mapping, 'ctx')
763 763 return ctx.user()
764 764
765 765 @templatekeyword('instabilities', requires={'ctx'})
766 766 def showinstabilities(context, mapping):
767 767 """List of strings. Evolution instabilities affecting the changeset.
768 768 (EXPERIMENTAL)
769 769 """
770 770 ctx = context.resource(mapping, 'ctx')
771 771 return compatlist(context, mapping, 'instability', ctx.instabilities(),
772 772 plural='instabilities')
773 773
774 774 @templatekeyword('verbosity', requires={'ui'})
775 775 def showverbosity(context, mapping):
776 776 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
777 777 or ''."""
778 778 ui = context.resource(mapping, 'ui')
779 779 # see logcmdutil.changesettemplater for priority of these flags
780 780 if ui.debugflag:
781 781 return 'debug'
782 782 elif ui.quiet:
783 783 return 'quiet'
784 784 elif ui.verbose:
785 785 return 'verbose'
786 786 return ''
787 787
788 788 @templatekeyword('whyunstable', requires={'repo', 'ctx'})
789 789 def showwhyunstable(context, mapping):
790 790 """List of dicts explaining all instabilities of a changeset.
791 791 (EXPERIMENTAL)
792 792 """
793 793 repo = context.resource(mapping, 'repo')
794 794 ctx = context.resource(mapping, 'ctx')
795 795
796 796 def formatnode(ctx):
797 797 return '%s (%s)' % (scmutil.formatchangeid(ctx), ctx.phasestr())
798 798
799 799 entries = obsutil.whyunstable(repo, ctx)
800 800
801 801 for entry in entries:
802 802 if entry.get('divergentnodes'):
803 803 dnodes = entry['divergentnodes']
804 804 dnhybrid = _hybrid(None, [dnode.hex() for dnode in dnodes],
805 805 lambda x: {'ctx': repo[x]},
806 806 lambda x: formatnode(repo[x]))
807 807 entry['divergentnodes'] = dnhybrid
808 808
809 809 tmpl = ('{instability}:{if(divergentnodes, " ")}{divergentnodes} '
810 810 '{reason} {node|short}')
811 811 return templateutil.mappinglist(entries, tmpl=tmpl, sep='\n')
812 812
813 813 def loadkeyword(ui, extname, registrarobj):
814 814 """Load template keyword from specified registrarobj
815 815 """
816 816 for name, func in registrarobj._table.iteritems():
817 817 keywords[name] = func
818 818
819 819 # tell hggettext to extract docstrings from these functions:
820 820 i18nfunctions = keywords.values()
@@ -1,937 +1,948
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 filter(self, context, mapping, select):
67 67 """Return new container of the same type which includes only the
68 68 selected elements
69 69
70 70 select() takes each item as a wrapped object and returns True/False.
71 71 """
72 72
73 73 @abc.abstractmethod
74 74 def itermaps(self, context):
75 75 """Yield each template mapping"""
76 76
77 77 @abc.abstractmethod
78 78 def join(self, context, mapping, sep):
79 79 """Join items with the separator; Returns a bytes or (possibly nested)
80 80 generator of bytes
81 81
82 82 A pre-configured template may be rendered per item if this container
83 83 holds unprintable items.
84 84 """
85 85
86 86 @abc.abstractmethod
87 87 def show(self, context, mapping):
88 88 """Return a bytes or (possibly nested) generator of bytes representing
89 89 the underlying object
90 90
91 91 A pre-configured template may be rendered if the underlying object is
92 92 not printable.
93 93 """
94 94
95 95 @abc.abstractmethod
96 96 def tobool(self, context, mapping):
97 97 """Return a boolean representation of the inner value"""
98 98
99 99 @abc.abstractmethod
100 100 def tovalue(self, context, mapping):
101 101 """Move the inner value object out or create a value representation
102 102
103 103 A returned value must be serializable by templaterfilters.json().
104 104 """
105 105
106 106 class mappable(object):
107 107 """Object which can be converted to a single template mapping"""
108 108
109 109 def itermaps(self, context):
110 110 yield self.tomap(context)
111 111
112 112 @abc.abstractmethod
113 113 def tomap(self, context):
114 114 """Create a single template mapping representing this"""
115 115
116 116 class wrappedbytes(wrapped):
117 117 """Wrapper for byte string"""
118 118
119 119 def __init__(self, value):
120 120 self._value = value
121 121
122 122 def contains(self, context, mapping, item):
123 123 item = stringify(context, mapping, item)
124 124 return item in self._value
125 125
126 126 def getmember(self, context, mapping, key):
127 127 raise error.ParseError(_('%r is not a dictionary')
128 128 % pycompat.bytestr(self._value))
129 129
130 130 def getmin(self, context, mapping):
131 131 return self._getby(context, mapping, min)
132 132
133 133 def getmax(self, context, mapping):
134 134 return self._getby(context, mapping, max)
135 135
136 136 def _getby(self, context, mapping, func):
137 137 if not self._value:
138 138 raise error.ParseError(_('empty string'))
139 139 return func(pycompat.iterbytestr(self._value))
140 140
141 141 def filter(self, context, mapping, select):
142 142 raise error.ParseError(_('%r is not filterable')
143 143 % pycompat.bytestr(self._value))
144 144
145 145 def itermaps(self, context):
146 146 raise error.ParseError(_('%r is not iterable of mappings')
147 147 % pycompat.bytestr(self._value))
148 148
149 149 def join(self, context, mapping, sep):
150 150 return joinitems(pycompat.iterbytestr(self._value), sep)
151 151
152 152 def show(self, context, mapping):
153 153 return self._value
154 154
155 155 def tobool(self, context, mapping):
156 156 return bool(self._value)
157 157
158 158 def tovalue(self, context, mapping):
159 159 return self._value
160 160
161 161 class wrappedvalue(wrapped):
162 162 """Generic wrapper for pure non-list/dict/bytes value"""
163 163
164 164 def __init__(self, value):
165 165 self._value = value
166 166
167 167 def contains(self, context, mapping, item):
168 168 raise error.ParseError(_("%r is not iterable") % self._value)
169 169
170 170 def getmember(self, context, mapping, key):
171 171 raise error.ParseError(_('%r is not a dictionary') % self._value)
172 172
173 173 def getmin(self, context, mapping):
174 174 raise error.ParseError(_("%r is not iterable") % self._value)
175 175
176 176 def getmax(self, context, mapping):
177 177 raise error.ParseError(_("%r is not iterable") % self._value)
178 178
179 179 def filter(self, context, mapping, select):
180 180 raise error.ParseError(_("%r is not iterable") % self._value)
181 181
182 182 def itermaps(self, context):
183 183 raise error.ParseError(_('%r is not iterable of mappings')
184 184 % self._value)
185 185
186 186 def join(self, context, mapping, sep):
187 187 raise error.ParseError(_('%r is not iterable') % self._value)
188 188
189 189 def show(self, context, mapping):
190 190 if self._value is None:
191 191 return b''
192 192 return pycompat.bytestr(self._value)
193 193
194 194 def tobool(self, context, mapping):
195 195 if self._value is None:
196 196 return False
197 197 if isinstance(self._value, bool):
198 198 return self._value
199 199 # otherwise evaluate as string, which means 0 is True
200 200 return bool(pycompat.bytestr(self._value))
201 201
202 202 def tovalue(self, context, mapping):
203 203 return self._value
204 204
205 205 class date(mappable, wrapped):
206 206 """Wrapper for date tuple"""
207 207
208 208 def __init__(self, value, showfmt='%d %d'):
209 209 # value may be (float, int), but public interface shouldn't support
210 210 # floating-point timestamp
211 211 self._unixtime, self._tzoffset = map(int, value)
212 212 self._showfmt = showfmt
213 213
214 214 def contains(self, context, mapping, item):
215 215 raise error.ParseError(_('date is not iterable'))
216 216
217 217 def getmember(self, context, mapping, key):
218 218 raise error.ParseError(_('date is not a dictionary'))
219 219
220 220 def getmin(self, context, mapping):
221 221 raise error.ParseError(_('date is not iterable'))
222 222
223 223 def getmax(self, context, mapping):
224 224 raise error.ParseError(_('date is not iterable'))
225 225
226 226 def filter(self, context, mapping, select):
227 227 raise error.ParseError(_('date is not iterable'))
228 228
229 229 def join(self, context, mapping, sep):
230 230 raise error.ParseError(_("date is not iterable"))
231 231
232 232 def show(self, context, mapping):
233 233 return self._showfmt % (self._unixtime, self._tzoffset)
234 234
235 235 def tomap(self, context):
236 236 return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
237 237
238 238 def tobool(self, context, mapping):
239 239 return True
240 240
241 241 def tovalue(self, context, mapping):
242 242 return (self._unixtime, self._tzoffset)
243 243
244 244 class hybrid(wrapped):
245 245 """Wrapper for list or dict to support legacy template
246 246
247 247 This class allows us to handle both:
248 248 - "{files}" (legacy command-line-specific list hack) and
249 249 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
250 250 and to access raw values:
251 251 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
252 252 - "{get(extras, key)}"
253 253 - "{files|json}"
254 254 """
255 255
256 256 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
257 257 self._gen = gen # generator or function returning generator
258 258 self._values = values
259 259 self._makemap = makemap
260 260 self._joinfmt = joinfmt
261 261 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
262 262
263 263 def contains(self, context, mapping, item):
264 264 item = unwrapastype(context, mapping, item, self._keytype)
265 265 return item in self._values
266 266
267 267 def getmember(self, context, mapping, key):
268 268 # TODO: maybe split hybrid list/dict types?
269 269 if not util.safehasattr(self._values, 'get'):
270 270 raise error.ParseError(_('not a dictionary'))
271 271 key = unwrapastype(context, mapping, key, self._keytype)
272 272 return self._wrapvalue(key, self._values.get(key))
273 273
274 274 def getmin(self, context, mapping):
275 275 return self._getby(context, mapping, min)
276 276
277 277 def getmax(self, context, mapping):
278 278 return self._getby(context, mapping, max)
279 279
280 280 def _getby(self, context, mapping, func):
281 281 if not self._values:
282 282 raise error.ParseError(_('empty sequence'))
283 283 val = func(self._values)
284 284 return self._wrapvalue(val, val)
285 285
286 286 def _wrapvalue(self, key, val):
287 287 if val is None:
288 288 return
289 289 if util.safehasattr(val, '_makemap'):
290 290 # a nested hybrid list/dict, which has its own way of map operation
291 291 return val
292 292 return hybriditem(None, key, val, self._makemap)
293 293
294 294 def filter(self, context, mapping, select):
295 295 if util.safehasattr(self._values, 'get'):
296 296 values = {k: v for k, v in self._values.iteritems()
297 297 if select(self._wrapvalue(k, v))}
298 298 else:
299 299 values = [v for v in self._values if select(self._wrapvalue(v, v))]
300 300 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
301 301
302 302 def itermaps(self, context):
303 303 makemap = self._makemap
304 304 for x in self._values:
305 305 yield makemap(x)
306 306
307 307 def join(self, context, mapping, sep):
308 308 # TODO: switch gen to (context, mapping) API?
309 309 return joinitems((self._joinfmt(x) for x in self._values), sep)
310 310
311 311 def show(self, context, mapping):
312 312 # TODO: switch gen to (context, mapping) API?
313 313 gen = self._gen
314 314 if gen is None:
315 315 return self.join(context, mapping, ' ')
316 316 if callable(gen):
317 317 return gen()
318 318 return gen
319 319
320 320 def tobool(self, context, mapping):
321 321 return bool(self._values)
322 322
323 323 def tovalue(self, context, mapping):
324 324 # TODO: make it non-recursive for trivial lists/dicts
325 325 xs = self._values
326 326 if util.safehasattr(xs, 'get'):
327 327 return {k: unwrapvalue(context, mapping, v)
328 328 for k, v in xs.iteritems()}
329 329 return [unwrapvalue(context, mapping, x) for x in xs]
330 330
331 331 class hybriditem(mappable, wrapped):
332 332 """Wrapper for non-list/dict object to support map operation
333 333
334 334 This class allows us to handle both:
335 335 - "{manifest}"
336 336 - "{manifest % '{rev}:{node}'}"
337 337 - "{manifest.rev}"
338 338 """
339 339
340 340 def __init__(self, gen, key, value, makemap):
341 341 self._gen = gen # generator or function returning generator
342 342 self._key = key
343 343 self._value = value # may be generator of strings
344 344 self._makemap = makemap
345 345
346 346 def tomap(self, context):
347 347 return self._makemap(self._key)
348 348
349 349 def contains(self, context, mapping, item):
350 350 w = makewrapped(context, mapping, self._value)
351 351 return w.contains(context, mapping, item)
352 352
353 353 def getmember(self, context, mapping, key):
354 354 w = makewrapped(context, mapping, self._value)
355 355 return w.getmember(context, mapping, key)
356 356
357 357 def getmin(self, context, mapping):
358 358 w = makewrapped(context, mapping, self._value)
359 359 return w.getmin(context, mapping)
360 360
361 361 def getmax(self, context, mapping):
362 362 w = makewrapped(context, mapping, self._value)
363 363 return w.getmax(context, mapping)
364 364
365 365 def filter(self, context, mapping, select):
366 366 w = makewrapped(context, mapping, self._value)
367 367 return w.filter(context, mapping, select)
368 368
369 369 def join(self, context, mapping, sep):
370 370 w = makewrapped(context, mapping, self._value)
371 371 return w.join(context, mapping, sep)
372 372
373 373 def show(self, context, mapping):
374 374 # TODO: switch gen to (context, mapping) API?
375 375 gen = self._gen
376 376 if gen is None:
377 377 return pycompat.bytestr(self._value)
378 378 if callable(gen):
379 379 return gen()
380 380 return gen
381 381
382 382 def tobool(self, context, mapping):
383 383 w = makewrapped(context, mapping, self._value)
384 384 return w.tobool(context, mapping)
385 385
386 386 def tovalue(self, context, mapping):
387 387 return _unthunk(context, mapping, self._value)
388 388
389 389 class _mappingsequence(wrapped):
390 390 """Wrapper for sequence of template mappings
391 391
392 392 This represents an inner template structure (i.e. a list of dicts),
393 393 which can also be rendered by the specified named/literal template.
394 394
395 395 Template mappings may be nested.
396 396 """
397 397
398 398 def __init__(self, name=None, tmpl=None, sep=''):
399 399 if name is not None and tmpl is not None:
400 400 raise error.ProgrammingError('name and tmpl are mutually exclusive')
401 401 self._name = name
402 402 self._tmpl = tmpl
403 403 self._defaultsep = sep
404 404
405 405 def contains(self, context, mapping, item):
406 406 raise error.ParseError(_('not comparable'))
407 407
408 408 def getmember(self, context, mapping, key):
409 409 raise error.ParseError(_('not a dictionary'))
410 410
411 411 def getmin(self, context, mapping):
412 412 raise error.ParseError(_('not comparable'))
413 413
414 414 def getmax(self, context, mapping):
415 415 raise error.ParseError(_('not comparable'))
416 416
417 417 def filter(self, context, mapping, select):
418 418 # implement if necessary; we'll need a wrapped type for a mapping dict
419 419 raise error.ParseError(_('not filterable without template'))
420 420
421 421 def join(self, context, mapping, sep):
422 422 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
423 423 if self._name:
424 424 itemiter = (context.process(self._name, m) for m in mapsiter)
425 425 elif self._tmpl:
426 426 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
427 427 else:
428 428 raise error.ParseError(_('not displayable without template'))
429 429 return joinitems(itemiter, sep)
430 430
431 431 def show(self, context, mapping):
432 432 return self.join(context, mapping, self._defaultsep)
433 433
434 434 def tovalue(self, context, mapping):
435 435 knownres = context.knownresourcekeys()
436 436 items = []
437 437 for nm in self.itermaps(context):
438 438 # drop internal resources (recursively) which shouldn't be displayed
439 439 lm = context.overlaymap(mapping, nm)
440 440 items.append({k: unwrapvalue(context, lm, v)
441 441 for k, v in nm.iteritems() if k not in knownres})
442 442 return items
443 443
444 444 class mappinggenerator(_mappingsequence):
445 445 """Wrapper for generator of template mappings
446 446
447 447 The function ``make(context, *args)`` should return a generator of
448 448 mapping dicts.
449 449 """
450 450
451 451 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
452 452 super(mappinggenerator, self).__init__(name, tmpl, sep)
453 453 self._make = make
454 454 self._args = args
455 455
456 456 def itermaps(self, context):
457 457 return self._make(context, *self._args)
458 458
459 459 def tobool(self, context, mapping):
460 460 return _nonempty(self.itermaps(context))
461 461
462 462 class mappinglist(_mappingsequence):
463 463 """Wrapper for list of template mappings"""
464 464
465 465 def __init__(self, mappings, name=None, tmpl=None, sep=''):
466 466 super(mappinglist, self).__init__(name, tmpl, sep)
467 467 self._mappings = mappings
468 468
469 469 def itermaps(self, context):
470 470 return iter(self._mappings)
471 471
472 472 def tobool(self, context, mapping):
473 473 return bool(self._mappings)
474 474
475 475 class mappedgenerator(wrapped):
476 476 """Wrapper for generator of strings which acts as a list
477 477
478 478 The function ``make(context, *args)`` should return a generator of
479 479 byte strings, or a generator of (possibly nested) generators of byte
480 480 strings (i.e. a generator for a list of byte strings.)
481 481 """
482 482
483 483 def __init__(self, make, args=()):
484 484 self._make = make
485 485 self._args = args
486 486
487 487 def contains(self, context, mapping, item):
488 488 item = stringify(context, mapping, item)
489 489 return item in self.tovalue(context, mapping)
490 490
491 491 def _gen(self, context):
492 492 return self._make(context, *self._args)
493 493
494 494 def getmember(self, context, mapping, key):
495 495 raise error.ParseError(_('not a dictionary'))
496 496
497 497 def getmin(self, context, mapping):
498 498 return self._getby(context, mapping, min)
499 499
500 500 def getmax(self, context, mapping):
501 501 return self._getby(context, mapping, max)
502 502
503 503 def _getby(self, context, mapping, func):
504 504 xs = self.tovalue(context, mapping)
505 505 if not xs:
506 506 raise error.ParseError(_('empty sequence'))
507 507 return func(xs)
508 508
509 509 @staticmethod
510 510 def _filteredgen(context, mapping, make, args, select):
511 511 for x in make(context, *args):
512 512 s = stringify(context, mapping, x)
513 513 if select(wrappedbytes(s)):
514 514 yield s
515 515
516 516 def filter(self, context, mapping, select):
517 517 args = (mapping, self._make, self._args, select)
518 518 return mappedgenerator(self._filteredgen, args)
519 519
520 520 def itermaps(self, context):
521 521 raise error.ParseError(_('list of strings is not mappable'))
522 522
523 523 def join(self, context, mapping, sep):
524 524 return joinitems(self._gen(context), sep)
525 525
526 526 def show(self, context, mapping):
527 527 return self.join(context, mapping, '')
528 528
529 529 def tobool(self, context, mapping):
530 530 return _nonempty(self._gen(context))
531 531
532 532 def tovalue(self, context, mapping):
533 533 return [stringify(context, mapping, x) for x in self._gen(context)]
534 534
535 535 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
536 536 """Wrap data to support both dict-like and string-like operations"""
537 537 prefmt = pycompat.identity
538 538 if fmt is None:
539 539 fmt = '%s=%s'
540 540 prefmt = pycompat.bytestr
541 541 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
542 542 lambda k: fmt % (prefmt(k), prefmt(data[k])))
543 543
544 544 def hybridlist(data, name, fmt=None, gen=None):
545 545 """Wrap data to support both list-like and string-like operations"""
546 546 prefmt = pycompat.identity
547 547 if fmt is None:
548 548 fmt = '%s'
549 549 prefmt = pycompat.bytestr
550 550 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
551 551
552 552 def compatdict(context, mapping, name, data, key='key', value='value',
553 553 fmt=None, plural=None, separator=' '):
554 554 """Wrap data like hybriddict(), but also supports old-style list template
555 555
556 556 This exists for backward compatibility with the old-style template. Use
557 557 hybriddict() for new template keywords.
558 558 """
559 559 c = [{key: k, value: v} for k, v in data.iteritems()]
560 560 f = _showcompatlist(context, mapping, name, c, plural, separator)
561 561 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
562 562
563 563 def compatlist(context, mapping, name, data, element=None, fmt=None,
564 564 plural=None, separator=' '):
565 565 """Wrap data like hybridlist(), but also supports old-style list template
566 566
567 567 This exists for backward compatibility with the old-style template. Use
568 568 hybridlist() for new template keywords.
569 569 """
570 570 f = _showcompatlist(context, mapping, name, data, plural, separator)
571 571 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
572 572
573 def compatfileslist(context, mapping, name, files):
574 """Wrap list of file names to support old-style list template and field
575 names
576
577 This exists for backward compatibility. Use hybridlist for new template
578 keywords.
579 """
580 f = _showcompatlist(context, mapping, name, files)
581 return hybrid(f, files, lambda x: {'file': x, 'path': x},
582 pycompat.identity)
583
573 584 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
574 585 """Return a generator that renders old-style list template
575 586
576 587 name is name of key in template map.
577 588 values is list of strings or dicts.
578 589 plural is plural of name, if not simply name + 's'.
579 590 separator is used to join values as a string
580 591
581 592 expansion works like this, given name 'foo'.
582 593
583 594 if values is empty, expand 'no_foos'.
584 595
585 596 if 'foo' not in template map, return values as a string,
586 597 joined by 'separator'.
587 598
588 599 expand 'start_foos'.
589 600
590 601 for each value, expand 'foo'. if 'last_foo' in template
591 602 map, expand it instead of 'foo' for last key.
592 603
593 604 expand 'end_foos'.
594 605 """
595 606 if not plural:
596 607 plural = name + 's'
597 608 if not values:
598 609 noname = 'no_' + plural
599 610 if context.preload(noname):
600 611 yield context.process(noname, mapping)
601 612 return
602 613 if not context.preload(name):
603 614 if isinstance(values[0], bytes):
604 615 yield separator.join(values)
605 616 else:
606 617 for v in values:
607 618 r = dict(v)
608 619 r.update(mapping)
609 620 yield r
610 621 return
611 622 startname = 'start_' + plural
612 623 if context.preload(startname):
613 624 yield context.process(startname, mapping)
614 625 def one(v, tag=name):
615 626 vmapping = {}
616 627 try:
617 628 vmapping.update(v)
618 629 # Python 2 raises ValueError if the type of v is wrong. Python
619 630 # 3 raises TypeError.
620 631 except (AttributeError, TypeError, ValueError):
621 632 try:
622 633 # Python 2 raises ValueError trying to destructure an e.g.
623 634 # bytes. Python 3 raises TypeError.
624 635 for a, b in v:
625 636 vmapping[a] = b
626 637 except (TypeError, ValueError):
627 638 vmapping[name] = v
628 639 vmapping = context.overlaymap(mapping, vmapping)
629 640 return context.process(tag, vmapping)
630 641 lastname = 'last_' + name
631 642 if context.preload(lastname):
632 643 last = values.pop()
633 644 else:
634 645 last = None
635 646 for v in values:
636 647 yield one(v)
637 648 if last is not None:
638 649 yield one(last, tag=lastname)
639 650 endname = 'end_' + plural
640 651 if context.preload(endname):
641 652 yield context.process(endname, mapping)
642 653
643 654 def flatten(context, mapping, thing):
644 655 """Yield a single stream from a possibly nested set of iterators"""
645 656 if isinstance(thing, wrapped):
646 657 thing = thing.show(context, mapping)
647 658 if isinstance(thing, bytes):
648 659 yield thing
649 660 elif isinstance(thing, str):
650 661 # We can only hit this on Python 3, and it's here to guard
651 662 # against infinite recursion.
652 663 raise error.ProgrammingError('Mercurial IO including templates is done'
653 664 ' with bytes, not strings, got %r' % thing)
654 665 elif thing is None:
655 666 pass
656 667 elif not util.safehasattr(thing, '__iter__'):
657 668 yield pycompat.bytestr(thing)
658 669 else:
659 670 for i in thing:
660 671 if isinstance(i, wrapped):
661 672 i = i.show(context, mapping)
662 673 if isinstance(i, bytes):
663 674 yield i
664 675 elif i is None:
665 676 pass
666 677 elif not util.safehasattr(i, '__iter__'):
667 678 yield pycompat.bytestr(i)
668 679 else:
669 680 for j in flatten(context, mapping, i):
670 681 yield j
671 682
672 683 def stringify(context, mapping, thing):
673 684 """Turn values into bytes by converting into text and concatenating them"""
674 685 if isinstance(thing, bytes):
675 686 return thing # retain localstr to be round-tripped
676 687 return b''.join(flatten(context, mapping, thing))
677 688
678 689 def findsymbolicname(arg):
679 690 """Find symbolic name for the given compiled expression; returns None
680 691 if nothing found reliably"""
681 692 while True:
682 693 func, data = arg
683 694 if func is runsymbol:
684 695 return data
685 696 elif func is runfilter:
686 697 arg = data[0]
687 698 else:
688 699 return None
689 700
690 701 def _nonempty(xiter):
691 702 try:
692 703 next(xiter)
693 704 return True
694 705 except StopIteration:
695 706 return False
696 707
697 708 def _unthunk(context, mapping, thing):
698 709 """Evaluate a lazy byte string into value"""
699 710 if not isinstance(thing, types.GeneratorType):
700 711 return thing
701 712 return stringify(context, mapping, thing)
702 713
703 714 def evalrawexp(context, mapping, arg):
704 715 """Evaluate given argument as a bare template object which may require
705 716 further processing (such as folding generator of strings)"""
706 717 func, data = arg
707 718 return func(context, mapping, data)
708 719
709 720 def evalwrapped(context, mapping, arg):
710 721 """Evaluate given argument to wrapped object"""
711 722 thing = evalrawexp(context, mapping, arg)
712 723 return makewrapped(context, mapping, thing)
713 724
714 725 def makewrapped(context, mapping, thing):
715 726 """Lift object to a wrapped type"""
716 727 if isinstance(thing, wrapped):
717 728 return thing
718 729 thing = _unthunk(context, mapping, thing)
719 730 if isinstance(thing, bytes):
720 731 return wrappedbytes(thing)
721 732 return wrappedvalue(thing)
722 733
723 734 def evalfuncarg(context, mapping, arg):
724 735 """Evaluate given argument as value type"""
725 736 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
726 737
727 738 def unwrapvalue(context, mapping, thing):
728 739 """Move the inner value object out of the wrapper"""
729 740 if isinstance(thing, wrapped):
730 741 return thing.tovalue(context, mapping)
731 742 # evalrawexp() may return string, generator of strings or arbitrary object
732 743 # such as date tuple, but filter does not want generator.
733 744 return _unthunk(context, mapping, thing)
734 745
735 746 def evalboolean(context, mapping, arg):
736 747 """Evaluate given argument as boolean, but also takes boolean literals"""
737 748 func, data = arg
738 749 if func is runsymbol:
739 750 thing = func(context, mapping, data, default=None)
740 751 if thing is None:
741 752 # not a template keyword, takes as a boolean literal
742 753 thing = stringutil.parsebool(data)
743 754 else:
744 755 thing = func(context, mapping, data)
745 756 return makewrapped(context, mapping, thing).tobool(context, mapping)
746 757
747 758 def evaldate(context, mapping, arg, err=None):
748 759 """Evaluate given argument as a date tuple or a date string; returns
749 760 a (unixtime, offset) tuple"""
750 761 thing = evalrawexp(context, mapping, arg)
751 762 return unwrapdate(context, mapping, thing, err)
752 763
753 764 def unwrapdate(context, mapping, thing, err=None):
754 765 if isinstance(thing, date):
755 766 return thing.tovalue(context, mapping)
756 767 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
757 768 thing = unwrapvalue(context, mapping, thing)
758 769 try:
759 770 return dateutil.parsedate(thing)
760 771 except AttributeError:
761 772 raise error.ParseError(err or _('not a date tuple nor a string'))
762 773 except error.ParseError:
763 774 if not err:
764 775 raise
765 776 raise error.ParseError(err)
766 777
767 778 def evalinteger(context, mapping, arg, err=None):
768 779 thing = evalrawexp(context, mapping, arg)
769 780 return unwrapinteger(context, mapping, thing, err)
770 781
771 782 def unwrapinteger(context, mapping, thing, err=None):
772 783 thing = unwrapvalue(context, mapping, thing)
773 784 try:
774 785 return int(thing)
775 786 except (TypeError, ValueError):
776 787 raise error.ParseError(err or _('not an integer'))
777 788
778 789 def evalstring(context, mapping, arg):
779 790 return stringify(context, mapping, evalrawexp(context, mapping, arg))
780 791
781 792 def evalstringliteral(context, mapping, arg):
782 793 """Evaluate given argument as string template, but returns symbol name
783 794 if it is unknown"""
784 795 func, data = arg
785 796 if func is runsymbol:
786 797 thing = func(context, mapping, data, default=data)
787 798 else:
788 799 thing = func(context, mapping, data)
789 800 return stringify(context, mapping, thing)
790 801
791 802 _unwrapfuncbytype = {
792 803 None: unwrapvalue,
793 804 bytes: stringify,
794 805 date: unwrapdate,
795 806 int: unwrapinteger,
796 807 }
797 808
798 809 def unwrapastype(context, mapping, thing, typ):
799 810 """Move the inner value object out of the wrapper and coerce its type"""
800 811 try:
801 812 f = _unwrapfuncbytype[typ]
802 813 except KeyError:
803 814 raise error.ProgrammingError('invalid type specified: %r' % typ)
804 815 return f(context, mapping, thing)
805 816
806 817 def runinteger(context, mapping, data):
807 818 return int(data)
808 819
809 820 def runstring(context, mapping, data):
810 821 return data
811 822
812 823 def _recursivesymbolblocker(key):
813 824 def showrecursion(context, mapping):
814 825 raise error.Abort(_("recursive reference '%s' in template") % key)
815 826 showrecursion._requires = () # mark as new-style templatekw
816 827 return showrecursion
817 828
818 829 def runsymbol(context, mapping, key, default=''):
819 830 v = context.symbol(mapping, key)
820 831 if v is None:
821 832 # put poison to cut recursion. we can't move this to parsing phase
822 833 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
823 834 safemapping = mapping.copy()
824 835 safemapping[key] = _recursivesymbolblocker(key)
825 836 try:
826 837 v = context.process(key, safemapping)
827 838 except TemplateNotFound:
828 839 v = default
829 840 if callable(v) and getattr(v, '_requires', None) is None:
830 841 # old templatekw: expand all keywords and resources
831 842 # (TODO: drop support for old-style functions. 'f._requires = ()'
832 843 # can be removed.)
833 844 props = {k: context._resources.lookup(context, mapping, k)
834 845 for k in context._resources.knownkeys()}
835 846 # pass context to _showcompatlist() through templatekw._showlist()
836 847 props['templ'] = context
837 848 props.update(mapping)
838 849 ui = props.get('ui')
839 850 if ui:
840 851 ui.deprecwarn("old-style template keyword '%s'" % key, '4.8')
841 852 return v(**pycompat.strkwargs(props))
842 853 if callable(v):
843 854 # new templatekw
844 855 try:
845 856 return v(context, mapping)
846 857 except ResourceUnavailable:
847 858 # unsupported keyword is mapped to empty just like unknown keyword
848 859 return None
849 860 return v
850 861
851 862 def runtemplate(context, mapping, template):
852 863 for arg in template:
853 864 yield evalrawexp(context, mapping, arg)
854 865
855 866 def runfilter(context, mapping, data):
856 867 arg, filt = data
857 868 thing = evalrawexp(context, mapping, arg)
858 869 intype = getattr(filt, '_intype', None)
859 870 try:
860 871 thing = unwrapastype(context, mapping, thing, intype)
861 872 return filt(thing)
862 873 except error.ParseError as e:
863 874 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
864 875
865 876 def _formatfiltererror(arg, filt):
866 877 fn = pycompat.sysbytes(filt.__name__)
867 878 sym = findsymbolicname(arg)
868 879 if not sym:
869 880 return _("incompatible use of template filter '%s'") % fn
870 881 return (_("template filter '%s' is not compatible with keyword '%s'")
871 882 % (fn, sym))
872 883
873 884 def _iteroverlaymaps(context, origmapping, newmappings):
874 885 """Generate combined mappings from the original mapping and an iterable
875 886 of partial mappings to override the original"""
876 887 for i, nm in enumerate(newmappings):
877 888 lm = context.overlaymap(origmapping, nm)
878 889 lm['index'] = i
879 890 yield lm
880 891
881 892 def _applymap(context, mapping, d, darg, targ):
882 893 try:
883 894 diter = d.itermaps(context)
884 895 except error.ParseError as err:
885 896 sym = findsymbolicname(darg)
886 897 if not sym:
887 898 raise
888 899 hint = _("keyword '%s' does not support map operation") % sym
889 900 raise error.ParseError(bytes(err), hint=hint)
890 901 for lm in _iteroverlaymaps(context, mapping, diter):
891 902 yield evalrawexp(context, lm, targ)
892 903
893 904 def runmap(context, mapping, data):
894 905 darg, targ = data
895 906 d = evalwrapped(context, mapping, darg)
896 907 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
897 908
898 909 def runmember(context, mapping, data):
899 910 darg, memb = data
900 911 d = evalwrapped(context, mapping, darg)
901 912 if isinstance(d, mappable):
902 913 lm = context.overlaymap(mapping, d.tomap(context))
903 914 return runsymbol(context, lm, memb)
904 915 try:
905 916 return d.getmember(context, mapping, memb)
906 917 except error.ParseError as err:
907 918 sym = findsymbolicname(darg)
908 919 if not sym:
909 920 raise
910 921 hint = _("keyword '%s' does not support member operation") % sym
911 922 raise error.ParseError(bytes(err), hint=hint)
912 923
913 924 def runnegate(context, mapping, data):
914 925 data = evalinteger(context, mapping, data,
915 926 _('negation needs an integer argument'))
916 927 return -data
917 928
918 929 def runarithmetic(context, mapping, data):
919 930 func, left, right = data
920 931 left = evalinteger(context, mapping, left,
921 932 _('arithmetic only defined on integers'))
922 933 right = evalinteger(context, mapping, right,
923 934 _('arithmetic only defined on integers'))
924 935 try:
925 936 return func(left, right)
926 937 except ZeroDivisionError:
927 938 raise error.Abort(_('division by zero is not defined'))
928 939
929 940 def joinitems(itemiter, sep):
930 941 """Join items with the separator; Returns generator of bytes"""
931 942 first = True
932 943 for x in itemiter:
933 944 if first:
934 945 first = False
935 946 elif sep:
936 947 yield sep
937 948 yield x
@@ -1,1435 +1,1441
1 1 Test template filters and functions
2 2 ===================================
3 3
4 4 $ hg init a
5 5 $ cd a
6 6 $ echo a > a
7 7 $ hg add a
8 8 $ echo line 1 > b
9 9 $ echo line 2 >> b
10 10 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
11 11
12 12 $ hg add b
13 13 $ echo other 1 > c
14 14 $ echo other 2 >> c
15 15 $ echo >> c
16 16 $ echo other 3 >> c
17 17 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
18 18
19 19 $ hg add c
20 20 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
21 21 $ echo c >> c
22 22 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
23 23
24 24 $ echo foo > .hg/branch
25 25 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
26 26
27 27 $ hg co -q 3
28 28 $ echo other 4 >> d
29 29 $ hg add d
30 30 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
31 31
32 32 $ hg merge -q foo
33 33 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
34 34
35 35 Second branch starting at nullrev:
36 36
37 37 $ hg update null
38 38 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
39 39 $ echo second > second
40 40 $ hg add second
41 41 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
42 42 created new head
43 43
44 44 $ echo third > third
45 45 $ hg add third
46 46 $ hg mv second fourth
47 47 $ hg commit -m third -d "2020-01-01 10:01"
48 48
49 49 $ hg phase -r 5 --public
50 50 $ hg phase -r 7 --secret --force
51 51
52 52 Filters work:
53 53
54 54 $ hg log --template '{author|domain}\n'
55 55
56 56 hostname
57 57
58 58
59 59
60 60
61 61 place
62 62 place
63 63 hostname
64 64
65 65 $ hg log --template '{author|person}\n'
66 66 test
67 67 User Name
68 68 person
69 69 person
70 70 person
71 71 person
72 72 other
73 73 A. N. Other
74 74 User Name
75 75
76 76 $ hg log --template '{author|user}\n'
77 77 test
78 78 user
79 79 person
80 80 person
81 81 person
82 82 person
83 83 other
84 84 other
85 85 user
86 86
87 87 $ hg log --template '{date|date}\n'
88 88 Wed Jan 01 10:01:00 2020 +0000
89 89 Mon Jan 12 13:46:40 1970 +0000
90 90 Sun Jan 18 08:40:01 1970 +0000
91 91 Sun Jan 18 08:40:00 1970 +0000
92 92 Sat Jan 17 04:53:20 1970 +0000
93 93 Fri Jan 16 01:06:40 1970 +0000
94 94 Wed Jan 14 21:20:00 1970 +0000
95 95 Tue Jan 13 17:33:20 1970 +0000
96 96 Mon Jan 12 13:46:40 1970 +0000
97 97
98 98 $ hg log --template '{date|isodate}\n'
99 99 2020-01-01 10:01 +0000
100 100 1970-01-12 13:46 +0000
101 101 1970-01-18 08:40 +0000
102 102 1970-01-18 08:40 +0000
103 103 1970-01-17 04:53 +0000
104 104 1970-01-16 01:06 +0000
105 105 1970-01-14 21:20 +0000
106 106 1970-01-13 17:33 +0000
107 107 1970-01-12 13:46 +0000
108 108
109 109 $ hg log --template '{date|isodatesec}\n'
110 110 2020-01-01 10:01:00 +0000
111 111 1970-01-12 13:46:40 +0000
112 112 1970-01-18 08:40:01 +0000
113 113 1970-01-18 08:40:00 +0000
114 114 1970-01-17 04:53:20 +0000
115 115 1970-01-16 01:06:40 +0000
116 116 1970-01-14 21:20:00 +0000
117 117 1970-01-13 17:33:20 +0000
118 118 1970-01-12 13:46:40 +0000
119 119
120 120 $ hg log --template '{date|rfc822date}\n'
121 121 Wed, 01 Jan 2020 10:01:00 +0000
122 122 Mon, 12 Jan 1970 13:46:40 +0000
123 123 Sun, 18 Jan 1970 08:40:01 +0000
124 124 Sun, 18 Jan 1970 08:40:00 +0000
125 125 Sat, 17 Jan 1970 04:53:20 +0000
126 126 Fri, 16 Jan 1970 01:06:40 +0000
127 127 Wed, 14 Jan 1970 21:20:00 +0000
128 128 Tue, 13 Jan 1970 17:33:20 +0000
129 129 Mon, 12 Jan 1970 13:46:40 +0000
130 130
131 131 $ hg log --template '{desc|firstline}\n'
132 132 third
133 133 second
134 134 merge
135 135 new head
136 136 new branch
137 137 no user, no domain
138 138 no person
139 139 other 1
140 140 line 1
141 141
142 142 $ hg log --template '{node|short}\n'
143 143 95c24699272e
144 144 29114dbae42b
145 145 d41e714fe50d
146 146 13207e5a10d9
147 147 bbe44766e73d
148 148 10e46f2dcbf4
149 149 97054abb4ab8
150 150 b608e9d1a3f0
151 151 1e4e1b8f71e0
152 152
153 153 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
154 154 <changeset author="test"/>
155 155 <changeset author="User Name &lt;user@hostname&gt;"/>
156 156 <changeset author="person"/>
157 157 <changeset author="person"/>
158 158 <changeset author="person"/>
159 159 <changeset author="person"/>
160 160 <changeset author="other@place"/>
161 161 <changeset author="A. N. Other &lt;other@place&gt;"/>
162 162 <changeset author="User Name &lt;user@hostname&gt;"/>
163 163
164 164 $ hg log --template '{rev}: {children}\n'
165 165 8:
166 166 7: 8:95c24699272e
167 167 6:
168 168 5: 6:d41e714fe50d
169 169 4: 6:d41e714fe50d
170 170 3: 4:bbe44766e73d 5:13207e5a10d9
171 171 2: 3:10e46f2dcbf4
172 172 1: 2:97054abb4ab8
173 173 0: 1:b608e9d1a3f0
174 174
175 175 Formatnode filter works:
176 176
177 177 $ hg -q log -r 0 --template '{node|formatnode}\n'
178 178 1e4e1b8f71e0
179 179
180 180 $ hg log -r 0 --template '{node|formatnode}\n'
181 181 1e4e1b8f71e0
182 182
183 183 $ hg -v log -r 0 --template '{node|formatnode}\n'
184 184 1e4e1b8f71e0
185 185
186 186 $ hg --debug log -r 0 --template '{node|formatnode}\n'
187 187 1e4e1b8f71e05681d422154f5421e385fec3454f
188 188
189 189 Age filter:
190 190
191 191 $ hg init unstable-hash
192 192 $ cd unstable-hash
193 193 $ hg log --template '{date|age}\n' > /dev/null || exit 1
194 194
195 195 >>> from __future__ import absolute_import
196 196 >>> import datetime
197 197 >>> fp = open('a', 'wb')
198 198 >>> n = datetime.datetime.now() + datetime.timedelta(366 * 7)
199 199 >>> fp.write(b'%d-%d-%d 00:00' % (n.year, n.month, n.day)) and None
200 200 >>> fp.close()
201 201 $ hg add a
202 202 $ hg commit -m future -d "`cat a`"
203 203
204 204 $ hg log -l1 --template '{date|age}\n'
205 205 7 years from now
206 206
207 207 $ cd ..
208 208 $ rm -rf unstable-hash
209 209
210 210 Filename filters:
211 211
212 212 $ hg debugtemplate '{"foo/bar"|basename}|{"foo/"|basename}|{"foo"|basename}|\n'
213 213 bar||foo|
214 214 $ hg debugtemplate '{"foo/bar"|dirname}|{"foo/"|dirname}|{"foo"|dirname}|\n'
215 215 foo|foo||
216 216 $ hg debugtemplate '{"foo/bar"|stripdir}|{"foo/"|stripdir}|{"foo"|stripdir}|\n'
217 217 foo|foo|foo|
218 218
219 219 commondir() filter:
220 220
221 221 $ hg debugtemplate '{""|splitlines|commondir}\n'
222 222
223 223 $ hg debugtemplate '{"foo/bar\nfoo/baz\nfoo/foobar\n"|splitlines|commondir}\n'
224 224 foo
225 225 $ hg debugtemplate '{"foo/bar\nfoo/bar\n"|splitlines|commondir}\n'
226 226 foo
227 227 $ hg debugtemplate '{"/foo/bar\n/foo/bar\n"|splitlines|commondir}\n'
228 228 foo
229 229 $ hg debugtemplate '{"/foo\n/foo\n"|splitlines|commondir}\n'
230 230
231 231 $ hg debugtemplate '{"foo/bar\nbar/baz"|splitlines|commondir}\n'
232 232
233 233 $ hg debugtemplate '{"foo/bar\nbar/baz\nbar/foo\n"|splitlines|commondir}\n'
234 234
235 235 $ hg debugtemplate '{"foo/../bar\nfoo/bar"|splitlines|commondir}\n'
236 236 foo
237 237 $ hg debugtemplate '{"foo\n/foo"|splitlines|commondir}\n'
238 238
239 239
240 240 $ hg log -r null -T '{rev|commondir}'
241 241 hg: parse error: argument is not a list of text
242 242 (template filter 'commondir' is not compatible with keyword 'rev')
243 243 [255]
244 244
245 245 Add a dummy commit to make up for the instability of the above:
246 246
247 247 $ echo a > a
248 248 $ hg add a
249 249 $ hg ci -m future
250 250
251 251 Count filter:
252 252
253 253 $ hg log -l1 --template '{node|count} {node|short|count}\n'
254 254 40 12
255 255
256 256 $ hg log -l1 --template '{revset("null^")|count} {revset(".")|count} {revset("0::3")|count}\n'
257 257 0 1 4
258 258
259 259 $ hg log -G --template '{rev}: children: {children|count}, \
260 260 > tags: {tags|count}, file_adds: {file_adds|count}, \
261 261 > ancestors: {revset("ancestors(%s)", rev)|count}'
262 262 @ 9: children: 0, tags: 1, file_adds: 1, ancestors: 3
263 263 |
264 264 o 8: children: 1, tags: 0, file_adds: 2, ancestors: 2
265 265 |
266 266 o 7: children: 1, tags: 0, file_adds: 1, ancestors: 1
267 267
268 268 o 6: children: 0, tags: 0, file_adds: 0, ancestors: 7
269 269 |\
270 270 | o 5: children: 1, tags: 0, file_adds: 1, ancestors: 5
271 271 | |
272 272 o | 4: children: 1, tags: 0, file_adds: 0, ancestors: 5
273 273 |/
274 274 o 3: children: 2, tags: 0, file_adds: 0, ancestors: 4
275 275 |
276 276 o 2: children: 1, tags: 0, file_adds: 1, ancestors: 3
277 277 |
278 278 o 1: children: 1, tags: 0, file_adds: 1, ancestors: 2
279 279 |
280 280 o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1
281 281
282 282
283 283 $ hg log -l1 -T '{termwidth|count}\n'
284 284 hg: parse error: not countable
285 285 (template filter 'count' is not compatible with keyword 'termwidth')
286 286 [255]
287 287
288 288 Upper/lower filters:
289 289
290 290 $ hg log -r0 --template '{branch|upper}\n'
291 291 DEFAULT
292 292 $ hg log -r0 --template '{author|lower}\n'
293 293 user name <user@hostname>
294 294 $ hg log -r0 --template '{date|upper}\n'
295 295 1000000.00
296 296
297 297 Add a commit that does all possible modifications at once
298 298
299 299 $ echo modify >> third
300 300 $ touch b
301 301 $ hg add b
302 302 $ hg mv fourth fifth
303 303 $ hg rm a
304 304 $ hg ci -m "Modify, add, remove, rename"
305 305
306 306 Pass generator object created by template function to filter
307 307
308 308 $ hg log -l 1 --template '{if(author, author)|user}\n'
309 309 test
310 310
311 311 Test diff function:
312 312
313 313 $ hg diff -c 8
314 314 diff -r 29114dbae42b -r 95c24699272e fourth
315 315 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
316 316 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
317 317 @@ -0,0 +1,1 @@
318 318 +second
319 319 diff -r 29114dbae42b -r 95c24699272e second
320 320 --- a/second Mon Jan 12 13:46:40 1970 +0000
321 321 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
322 322 @@ -1,1 +0,0 @@
323 323 -second
324 324 diff -r 29114dbae42b -r 95c24699272e third
325 325 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
326 326 +++ b/third Wed Jan 01 10:01:00 2020 +0000
327 327 @@ -0,0 +1,1 @@
328 328 +third
329 329
330 330 $ hg log -r 8 -T "{diff()}"
331 331 diff -r 29114dbae42b -r 95c24699272e fourth
332 332 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
333 333 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
334 334 @@ -0,0 +1,1 @@
335 335 +second
336 336 diff -r 29114dbae42b -r 95c24699272e second
337 337 --- a/second Mon Jan 12 13:46:40 1970 +0000
338 338 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
339 339 @@ -1,1 +0,0 @@
340 340 -second
341 341 diff -r 29114dbae42b -r 95c24699272e third
342 342 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
343 343 +++ b/third Wed Jan 01 10:01:00 2020 +0000
344 344 @@ -0,0 +1,1 @@
345 345 +third
346 346
347 347 $ hg log -r 8 -T "{diff('glob:f*')}"
348 348 diff -r 29114dbae42b -r 95c24699272e fourth
349 349 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
350 350 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
351 351 @@ -0,0 +1,1 @@
352 352 +second
353 353
354 354 $ hg log -r 8 -T "{diff('', 'glob:f*')}"
355 355 diff -r 29114dbae42b -r 95c24699272e second
356 356 --- a/second Mon Jan 12 13:46:40 1970 +0000
357 357 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
358 358 @@ -1,1 +0,0 @@
359 359 -second
360 360 diff -r 29114dbae42b -r 95c24699272e third
361 361 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
362 362 +++ b/third Wed Jan 01 10:01:00 2020 +0000
363 363 @@ -0,0 +1,1 @@
364 364 +third
365 365
366 366 $ hg log -r 8 -T "{diff('FOURTH'|lower)}"
367 367 diff -r 29114dbae42b -r 95c24699272e fourth
368 368 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
369 369 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
370 370 @@ -0,0 +1,1 @@
371 371 +second
372 372
373 373 $ cd ..
374 374
375 375 latesttag() function:
376 376
377 377 $ hg init latesttag
378 378 $ cd latesttag
379 379
380 380 $ echo a > file
381 381 $ hg ci -Am a -d '0 0'
382 382 adding file
383 383
384 384 $ echo b >> file
385 385 $ hg ci -m b -d '1 0'
386 386
387 387 $ echo c >> head1
388 388 $ hg ci -Am h1c -d '2 0'
389 389 adding head1
390 390
391 391 $ hg update -q 1
392 392 $ echo d >> head2
393 393 $ hg ci -Am h2d -d '3 0'
394 394 adding head2
395 395 created new head
396 396
397 397 $ echo e >> head2
398 398 $ hg ci -m h2e -d '4 0'
399 399
400 400 $ hg merge -q
401 401 $ hg ci -m merge -d '5 -3600'
402 402
403 403 $ hg tag -r 1 -m t1 -d '6 0' t1
404 404 $ hg tag -r 2 -m t2 -d '7 0' t2
405 405 $ hg tag -r 3 -m t3 -d '8 0' t3
406 406 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
407 407 $ hg tag -r 5 -m t5 -d '9 0' t5
408 408 $ hg tag -r 3 -m at3 -d '10 0' at3
409 409
410 410 $ hg log -G --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
411 411 @ 11: t3, C: 9, D: 8
412 412 |
413 413 o 10: t3, C: 8, D: 7
414 414 |
415 415 o 9: t3, C: 7, D: 6
416 416 |
417 417 o 8: t3, C: 6, D: 5
418 418 |
419 419 o 7: t3, C: 5, D: 4
420 420 |
421 421 o 6: t3, C: 4, D: 3
422 422 |
423 423 o 5: t3, C: 3, D: 2
424 424 |\
425 425 | o 4: t3, C: 1, D: 1
426 426 | |
427 427 | o 3: t3, C: 0, D: 0
428 428 | |
429 429 o | 2: t1, C: 1, D: 1
430 430 |/
431 431 o 1: t1, C: 0, D: 0
432 432 |
433 433 o 0: null, C: 1, D: 1
434 434
435 435
436 436 $ cd ..
437 437
438 438 Test filter() empty values:
439 439
440 440 $ hg log -R a -r 1 -T '{filter(desc|splitlines) % "{line}\n"}'
441 441 other 1
442 442 other 2
443 443 other 3
444 444 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1) % "{ifeq(key, "a", "{value}\n")}")}'
445 445 0
446 446
447 447 0 should not be falsy
448 448
449 449 $ hg log -R a -r 0 -T '{filter(revset("0:2"))}\n'
450 450 0 1 2
451 451
452 452 Test filter() by expression:
453 453
454 454 $ hg log -R a -r 1 -T '{filter(desc|splitlines, ifcontains("1", line, "t"))}\n'
455 455 other 1
456 456 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1), ifeq(key, "b", "t"))}\n'
457 457 b=1
458 458
459 459 Test filter() shouldn't crash:
460 460
461 461 $ hg log -R a -r 0 -T '{filter(extras)}\n'
462 462 branch=default
463 463 $ hg log -R a -r 0 -T '{filter(files)}\n'
464 464 a
465 465
466 466 Test filter() unsupported arguments:
467 467
468 468 $ hg log -R a -r 0 -T '{filter()}\n'
469 469 hg: parse error: filter expects one or two arguments
470 470 [255]
471 471 $ hg log -R a -r 0 -T '{filter(date)}\n'
472 472 hg: parse error: date is not iterable
473 473 [255]
474 474 $ hg log -R a -r 0 -T '{filter(rev)}\n'
475 475 hg: parse error: 0 is not iterable
476 476 [255]
477 477 $ hg log -R a -r 0 -T '{filter(desc|firstline)}\n'
478 478 hg: parse error: 'line 1' is not filterable
479 479 [255]
480 480 $ hg log -R a -r 0 -T '{filter(manifest)}\n'
481 481 hg: parse error: '0:a0c8bcbbb45c' is not filterable
482 482 [255]
483 483 $ hg log -R a -r 0 -T '{filter(succsandmarkers)}\n'
484 484 hg: parse error: not filterable without template
485 485 [255]
486 486 $ hg log -R a -r 0 -T '{filter(desc|splitlines % "{line}", "")}\n'
487 487 hg: parse error: not filterable by expression
488 488 [255]
489 489
490 490 Test manifest/get() can be join()-ed as string, though it's silly:
491 491
492 492 $ hg log -R latesttag -r tip -T '{join(manifest, ".")}\n'
493 493 1.1.:.2.b.c.6.e.9.0.0.6.c.e.2
494 494 $ hg log -R latesttag -r tip -T '{join(get(extras, "branch"), ".")}\n'
495 495 d.e.f.a.u.l.t
496 496
497 497 Test join() over string
498 498
499 499 $ hg log -R latesttag -r tip -T '{join(rev|stringify, ".")}\n'
500 500 1.1
501 501
502 502 Test join() over uniterable
503 503
504 504 $ hg log -R latesttag -r tip -T '{join(rev, "")}\n'
505 505 hg: parse error: 11 is not iterable
506 506 [255]
507 507
508 508 Test min/max of integers
509 509
510 510 $ hg log -R latesttag -l1 -T '{min(revset("9:10"))}\n'
511 511 9
512 512 $ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
513 513 10
514 514
515 515 Test min/max over map operation:
516 516
517 517 $ hg log -R latesttag -r3 -T '{min(tags % "{tag}")}\n'
518 518 at3
519 519 $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
520 520 t3
521 521
522 522 Test min/max of strings:
523 523
524 524 $ hg log -R latesttag -l1 -T '{min(desc)}\n'
525 525 3
526 526 $ hg log -R latesttag -l1 -T '{max(desc)}\n'
527 527 t
528 528
529 529 Test min/max of non-iterable:
530 530
531 531 $ hg debugtemplate '{min(1)}'
532 532 hg: parse error: 1 is not iterable
533 533 (min first argument should be an iterable)
534 534 [255]
535 535 $ hg debugtemplate '{max(2)}'
536 536 hg: parse error: 2 is not iterable
537 537 (max first argument should be an iterable)
538 538 [255]
539 539
540 540 $ hg log -R latesttag -l1 -T '{min(date)}'
541 541 hg: parse error: date is not iterable
542 542 (min first argument should be an iterable)
543 543 [255]
544 544 $ hg log -R latesttag -l1 -T '{max(date)}'
545 545 hg: parse error: date is not iterable
546 546 (max first argument should be an iterable)
547 547 [255]
548 548
549 549 Test min/max of empty sequence:
550 550
551 551 $ hg debugtemplate '{min("")}'
552 552 hg: parse error: empty string
553 553 (min first argument should be an iterable)
554 554 [255]
555 555 $ hg debugtemplate '{max("")}'
556 556 hg: parse error: empty string
557 557 (max first argument should be an iterable)
558 558 [255]
559 559 $ hg debugtemplate '{min(dict())}'
560 560 hg: parse error: empty sequence
561 561 (min first argument should be an iterable)
562 562 [255]
563 563 $ hg debugtemplate '{max(dict())}'
564 564 hg: parse error: empty sequence
565 565 (max first argument should be an iterable)
566 566 [255]
567 567 $ hg debugtemplate '{min(dict() % "")}'
568 568 hg: parse error: empty sequence
569 569 (min first argument should be an iterable)
570 570 [255]
571 571 $ hg debugtemplate '{max(dict() % "")}'
572 572 hg: parse error: empty sequence
573 573 (max first argument should be an iterable)
574 574 [255]
575 575
576 576 Test min/max of if() result
577 577
578 578 $ cd latesttag
579 579 $ hg log -l1 -T '{min(if(true, revset("9:10"), ""))}\n'
580 580 9
581 581 $ hg log -l1 -T '{max(if(false, "", revset("9:10")))}\n'
582 582 10
583 583 $ hg log -l1 -T '{min(ifcontains("a", "aa", revset("9:10"), ""))}\n'
584 584 9
585 585 $ hg log -l1 -T '{max(ifcontains("a", "bb", "", revset("9:10")))}\n'
586 586 10
587 587 $ hg log -l1 -T '{min(ifeq(0, 0, revset("9:10"), ""))}\n'
588 588 9
589 589 $ hg log -l1 -T '{max(ifeq(0, 1, "", revset("9:10")))}\n'
590 590 10
591 591 $ cd ..
592 592
593 593 Test laziness of if() then/else clause
594 594
595 595 $ hg debugtemplate '{count(0)}'
596 596 hg: parse error: not countable
597 597 (incompatible use of template filter 'count')
598 598 [255]
599 599 $ hg debugtemplate '{if(true, "", count(0))}'
600 600 $ hg debugtemplate '{if(false, count(0), "")}'
601 601 $ hg debugtemplate '{ifcontains("a", "aa", "", count(0))}'
602 602 $ hg debugtemplate '{ifcontains("a", "bb", count(0), "")}'
603 603 $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
604 604 $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
605 605
606 606 Test the sub function of templating for expansion:
607 607
608 608 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
609 609 xx
610 610
611 611 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
612 612 hg: parse error: sub got an invalid pattern: [
613 613 [255]
614 614 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
615 615 hg: parse error: sub got an invalid replacement: \1
616 616 [255]
617 617
618 618 Test the strip function with chars specified:
619 619
620 620 $ hg log -R latesttag --template '{desc}\n'
621 621 at3
622 622 t5
623 623 t4
624 624 t3
625 625 t2
626 626 t1
627 627 merge
628 628 h2e
629 629 h2d
630 630 h1c
631 631 b
632 632 a
633 633
634 634 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
635 635 at3
636 636 5
637 637 4
638 638 3
639 639 2
640 640 1
641 641 merg
642 642 h2
643 643 h2d
644 644 h1c
645 645 b
646 646 a
647 647
648 648 Test date format:
649 649
650 650 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
651 651 date: 70 01 01 10 +0000
652 652 date: 70 01 01 09 +0000
653 653 date: 70 01 01 04 +0000
654 654 date: 70 01 01 08 +0000
655 655 date: 70 01 01 07 +0000
656 656 date: 70 01 01 06 +0000
657 657 date: 70 01 01 05 +0100
658 658 date: 70 01 01 04 +0000
659 659 date: 70 01 01 03 +0000
660 660 date: 70 01 01 02 +0000
661 661 date: 70 01 01 01 +0000
662 662 date: 70 01 01 00 +0000
663 663
664 664 Test invalid date:
665 665
666 666 $ hg log -R latesttag -T '{date(rev)}\n'
667 667 hg: parse error: date expects a date information
668 668 [255]
669 669
670 670 Set up repository containing template fragments in commit metadata:
671 671
672 672 $ hg init r
673 673 $ cd r
674 674 $ echo a > a
675 675 $ hg ci -Am '{rev}'
676 676 adding a
677 677
678 678 $ hg branch -q 'text.{rev}'
679 679 $ echo aa >> aa
680 680 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
681 681
682 682 color effect can be specified without quoting:
683 683
684 684 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
685 685 \x1b[0;31mtext\x1b[0m (esc)
686 686
687 687 color effects can be nested (issue5413)
688 688
689 689 $ hg debugtemplate --color=always \
690 690 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
691 691 \x1b[0;31mred\x1b[0;35mma\x1b[0;36mcyan\x1b[0m\x1b[0;31m\x1b[0;35m\x1b[0;33myellow\x1b[0m\x1b[0;31m\x1b[0;35mgenta\x1b[0m (esc)
692 692
693 693 pad() should interact well with color codes (issue5416)
694 694
695 695 $ hg debugtemplate --color=always \
696 696 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
697 697 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
698 698
699 699 label should be no-op if color is disabled:
700 700
701 701 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
702 702 text
703 703 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
704 704 text
705 705
706 706 Test branches inside if statement:
707 707
708 708 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
709 709 no
710 710
711 711 Test dict constructor:
712 712
713 713 $ hg log -r 0 -T '{dict(y=node|short, x=rev)}\n'
714 714 y=f7769ec2ab97 x=0
715 715 $ hg log -r 0 -T '{dict(x=rev, y=node|short) % "{key}={value}\n"}'
716 716 x=0
717 717 y=f7769ec2ab97
718 718 $ hg log -r 0 -T '{dict(x=rev, y=node|short)|json}\n'
719 719 {"x": 0, "y": "f7769ec2ab97"}
720 720 $ hg log -r 0 -T '{dict()|json}\n'
721 721 {}
722 722
723 723 $ hg log -r 0 -T '{dict(rev, node=node|short)}\n'
724 724 rev=0 node=f7769ec2ab97
725 725 $ hg log -r 0 -T '{dict(rev, node|short)}\n'
726 726 rev=0 node=f7769ec2ab97
727 727
728 728 $ hg log -r 0 -T '{dict(rev, rev=rev)}\n'
729 729 hg: parse error: duplicated dict key 'rev' inferred
730 730 [255]
731 731 $ hg log -r 0 -T '{dict(node, node|short)}\n'
732 732 hg: parse error: duplicated dict key 'node' inferred
733 733 [255]
734 734 $ hg log -r 0 -T '{dict(1 + 2)}'
735 735 hg: parse error: dict key cannot be inferred
736 736 [255]
737 737
738 738 $ hg log -r 0 -T '{dict(x=rev, x=node)}'
739 739 hg: parse error: dict got multiple values for keyword argument 'x'
740 740 [255]
741 741
742 742 Test get function:
743 743
744 744 $ hg log -r 0 --template '{get(extras, "branch")}\n'
745 745 default
746 746 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
747 747 default
748 748 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
749 749 hg: parse error: not a dictionary
750 750 (get() expects a dict as first argument)
751 751 [255]
752 752
753 753 Test json filter applied to wrapped object:
754 754
755 755 $ hg log -r0 -T '{files|json}\n'
756 756 ["a"]
757 757 $ hg log -r0 -T '{extras|json}\n'
758 758 {"branch": "default"}
759 759 $ hg log -r0 -T '{date|json}\n'
760 760 [0, 0]
761 761
762 762 Test json filter applied to map result:
763 763
764 764 $ hg log -r0 -T '{json(extras % "{key}")}\n'
765 765 ["branch"]
766 766
767 767 Test localdate(date, tz) function:
768 768
769 769 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
770 770 1970-01-01 09:00 +0900
771 771 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
772 772 1970-01-01 00:00 +0000
773 773 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
774 774 hg: parse error: localdate expects a timezone
775 775 [255]
776 776 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
777 777 1970-01-01 02:00 +0200
778 778 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
779 779 1970-01-01 00:00 +0000
780 780 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
781 781 1970-01-01 00:00 +0000
782 782 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
783 783 hg: parse error: localdate expects a timezone
784 784 [255]
785 785 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
786 786 hg: parse error: localdate expects a timezone
787 787 [255]
788 788
789 789 Test shortest(node) function:
790 790
791 791 $ echo b > b
792 792 $ hg ci -qAm b
793 793 $ hg log --template '{shortest(node)}\n'
794 794 e777
795 795 bcc7
796 796 f776
797 797 $ hg log --template '{shortest(node, 10)}\n'
798 798 e777603221
799 799 bcc7ff960b
800 800 f7769ec2ab
801 801 $ hg log --template '{node|shortest}\n' -l1
802 802 e777
803 803
804 804 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
805 805 f7769ec2ab
806 806 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
807 807 hg: parse error: shortest() expects an integer minlength
808 808 [255]
809 809
810 810 $ hg log -r 'wdir()' -T '{node|shortest}\n'
811 811 ffff
812 812
813 813 $ hg log --template '{shortest("f")}\n' -l1
814 814 f
815 815
816 816 $ hg log --template '{shortest("0123456789012345678901234567890123456789")}\n' -l1
817 817 0123456789012345678901234567890123456789
818 818
819 819 $ hg log --template '{shortest("01234567890123456789012345678901234567890123456789")}\n' -l1
820 820 01234567890123456789012345678901234567890123456789
821 821
822 822 $ hg log --template '{shortest("not a hex string")}\n' -l1
823 823 not a hex string
824 824
825 825 $ hg log --template '{shortest("not a hex string, but it'\''s 40 bytes long")}\n' -l1
826 826 not a hex string, but it's 40 bytes long
827 827
828 828 $ hg log --template '{shortest("ffffffffffffffffffffffffffffffffffffffff")}\n' -l1
829 829 ffff
830 830
831 831 $ hg log --template '{shortest("fffffff")}\n' -l1
832 832 ffff
833 833
834 834 $ hg log --template '{shortest("ff")}\n' -l1
835 835 ffff
836 836
837 837 $ cd ..
838 838
839 839 Test shortest(node) with the repo having short hash collision:
840 840
841 841 $ hg init hashcollision
842 842 $ cd hashcollision
843 843 $ cat <<EOF >> .hg/hgrc
844 844 > [experimental]
845 845 > evolution.createmarkers=True
846 846 > EOF
847 847 $ echo 0 > a
848 848 $ hg ci -qAm 0
849 849 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
850 850 > hg up -q 0
851 851 > echo $i > a
852 852 > hg ci -qm $i
853 853 > done
854 854 $ hg up -q null
855 855 $ hg log -r0: -T '{rev}:{node}\n'
856 856 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
857 857 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
858 858 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
859 859 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
860 860 4:10776689e627b465361ad5c296a20a487e153ca4
861 861 5:a00be79088084cb3aff086ab799f8790e01a976b
862 862 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
863 863 7:a0457b3450b8e1b778f1163b31a435802987fe5d
864 864 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
865 865 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
866 866 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
867 867 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
868 868 obsoleted 1 changesets
869 869 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
870 870 obsoleted 1 changesets
871 871 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
872 872 obsoleted 1 changesets
873 873
874 874 nodes starting with '11' (we don't have the revision number '11' though)
875 875
876 876 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
877 877 1:1142
878 878 2:1140
879 879 3:11d
880 880
881 881 '5:a00' is hidden, but still we have two nodes starting with 'a0'
882 882
883 883 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
884 884 6:a0b
885 885 7:a04
886 886
887 887 node '10' conflicts with the revision number '10' even if it is hidden
888 888 (we could exclude hidden revision numbers, but currently we don't)
889 889
890 890 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
891 891 4:107
892 892 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
893 893 4:107
894 894
895 895 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n'
896 896 4:x10
897 897 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
898 898 4:x10
899 899
900 900 node 'c562' should be unique if the other 'c562' nodes are hidden
901 901 (but we don't try the slow path to filter out hidden nodes for now)
902 902
903 903 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
904 904 8:c5625
905 905 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
906 906 8:c5625
907 907 9:c5623
908 908 10:c562d
909 909
910 910 $ cd ..
911 911
912 912 Test pad function
913 913
914 914 $ cd r
915 915
916 916 $ hg log --template '{pad(rev, 20)} {author|user}\n'
917 917 2 test
918 918 1 {node|short}
919 919 0 test
920 920
921 921 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
922 922 2 test
923 923 1 {node|short}
924 924 0 test
925 925
926 926 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
927 927 2------------------- test
928 928 1------------------- {node|short}
929 929 0------------------- test
930 930
931 931 Test template string in pad function
932 932
933 933 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
934 934 {0} test
935 935
936 936 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
937 937 \{rev} test
938 938
939 939 Test width argument passed to pad function
940 940
941 941 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
942 942 0 test
943 943 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
944 944 hg: parse error: pad() expects an integer width
945 945 [255]
946 946
947 947 Test invalid fillchar passed to pad function
948 948
949 949 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
950 950 hg: parse error: pad() expects a single fill character
951 951 [255]
952 952 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
953 953 hg: parse error: pad() expects a single fill character
954 954 [255]
955 955
956 956 Test boolean argument passed to pad function
957 957
958 958 no crash
959 959
960 960 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
961 961 ---------0
962 962
963 963 string/literal
964 964
965 965 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
966 966 ---------0
967 967 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
968 968 0---------
969 969 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
970 970 0---------
971 971
972 972 unknown keyword is evaluated to ''
973 973
974 974 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
975 975 0---------
976 976
977 977 Test separate function
978 978
979 979 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
980 980 a-b-c
981 981 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
982 982 0:f7769ec2ab97 test default
983 983 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
984 984 a \x1b[0;31mb\x1b[0m c d (esc)
985 985
986 986 Test boolean expression/literal passed to if function
987 987
988 988 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
989 989 rev 0 is True
990 990 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
991 991 literal 0 is True as well
992 992 $ hg log -r 0 -T '{if(min(revset(r"0")), "0 of hybriditem is also True")}\n'
993 993 0 of hybriditem is also True
994 994 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
995 995 empty string is False
996 996 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
997 997 empty list is False
998 998 $ hg log -r 0 -T '{if(revset(r"0"), "non-empty list is True")}\n'
999 999 non-empty list is True
1000 1000 $ hg log -r 0 -T '{if(revset(r"0") % "", "list of empty strings is True")}\n'
1001 1001 list of empty strings is True
1002 1002 $ hg log -r 0 -T '{if(true, "true is True")}\n'
1003 1003 true is True
1004 1004 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
1005 1005 false is False
1006 1006 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
1007 1007 non-empty string is True
1008 1008
1009 1009 Test ifcontains function
1010 1010
1011 1011 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
1012 1012 2 is in the string
1013 1013 1 is not
1014 1014 0 is in the string
1015 1015
1016 1016 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
1017 1017 2 is in the string
1018 1018 1 is not
1019 1019 0 is in the string
1020 1020
1021 1021 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
1022 1022 2 did not add a
1023 1023 1 did not add a
1024 1024 0 added a
1025 1025
1026 1026 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
1027 1027 2 is parent of 1
1028 1028 1
1029 1029 0
1030 1030
1031 1031 $ hg log -l1 -T '{ifcontains("branch", extras, "t", "f")}\n'
1032 1032 t
1033 1033 $ hg log -l1 -T '{ifcontains("branch", extras % "{key}", "t", "f")}\n'
1034 1034 t
1035 1035 $ hg log -l1 -T '{ifcontains("branc", extras % "{key}", "t", "f")}\n'
1036 1036 f
1037 1037 $ hg log -l1 -T '{ifcontains("branc", stringify(extras % "{key}"), "t", "f")}\n'
1038 1038 t
1039 1039
1040 1040 Test revset function
1041 1041
1042 1042 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
1043 1043 2 current rev
1044 1044 1 not current rev
1045 1045 0 not current rev
1046 1046
1047 1047 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
1048 1048 2 match rev
1049 1049 1 match rev
1050 1050 0 not match rev
1051 1051
1052 1052 $ hg log -T '{ifcontains(desc, revset(":"), "", "type not match")}\n' -l1
1053 1053 type not match
1054 1054
1055 1055 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
1056 1056 2 Parents: 1
1057 1057 1 Parents: 0
1058 1058 0 Parents:
1059 1059
1060 1060 $ cat >> .hg/hgrc <<EOF
1061 1061 > [revsetalias]
1062 1062 > myparents(\$1) = parents(\$1)
1063 1063 > EOF
1064 1064 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
1065 1065 2 Parents: 1
1066 1066 1 Parents: 0
1067 1067 0 Parents:
1068 1068
1069 1069 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
1070 1070 Rev: 2
1071 1071 Ancestor: 0
1072 1072 Ancestor: 1
1073 1073 Ancestor: 2
1074 1074
1075 1075 Rev: 1
1076 1076 Ancestor: 0
1077 1077 Ancestor: 1
1078 1078
1079 1079 Rev: 0
1080 1080 Ancestor: 0
1081 1081
1082 1082 $ hg log --template '{revset("TIP"|lower)}\n' -l1
1083 1083 2
1084 1084
1085 1085 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
1086 1086 2
1087 1087
1088 1088 a list template is evaluated for each item of revset/parents
1089 1089
1090 1090 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
1091 1091 2 p: 1:bcc7ff960b8e
1092 1092 1 p: 0:f7769ec2ab97
1093 1093 0 p:
1094 1094
1095 1095 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
1096 1096 2 p: 1:bcc7ff960b8e -1:000000000000
1097 1097 1 p: 0:f7769ec2ab97 -1:000000000000
1098 1098 0 p: -1:000000000000 -1:000000000000
1099 1099
1100 1100 therefore, 'revcache' should be recreated for each rev
1101 1101
1102 1102 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
1103 1103 2 aa b
1104 1104 p
1105 1105 1
1106 1106 p a
1107 1107 0 a
1108 1108 p
1109 1109
1110 1110 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
1111 1111 2 aa b
1112 1112 p
1113 1113 1
1114 1114 p a
1115 1115 0 a
1116 1116 p
1117 1117
1118 1118 a revset item must be evaluated as an integer revision, not an offset from tip
1119 1119
1120 1120 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
1121 1121 -1:000000000000
1122 1122 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
1123 1123 -1:000000000000
1124 1124
1125 1125 join() should pick '{rev}' from revset items:
1126 1126
1127 1127 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
1128 1128 4, 5
1129 1129
1130 1130 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
1131 1131 default. join() should agree with the default formatting:
1132 1132
1133 1133 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
1134 1134 5:13207e5a10d9, 4:bbe44766e73d
1135 1135
1136 1136 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
1137 1137 5:13207e5a10d9fd28ec424934298e176197f2c67f,
1138 1138 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1139 1139
1140 1140 Invalid arguments passed to revset()
1141 1141
1142 1142 $ hg log -T '{revset("%whatever", 0)}\n'
1143 1143 hg: parse error: unexpected revspec format character w
1144 1144 [255]
1145 1145 $ hg log -T '{revset("%lwhatever", files)}\n'
1146 1146 hg: parse error: unexpected revspec format character w
1147 1147 [255]
1148 1148 $ hg log -T '{revset("%s %s", 0)}\n'
1149 1149 hg: parse error: missing argument for revspec
1150 1150 [255]
1151 1151 $ hg log -T '{revset("", 0)}\n'
1152 1152 hg: parse error: too many revspec arguments specified
1153 1153 [255]
1154 1154 $ hg log -T '{revset("%s", 0, 1)}\n'
1155 1155 hg: parse error: too many revspec arguments specified
1156 1156 [255]
1157 1157 $ hg log -T '{revset("%", 0)}\n'
1158 1158 hg: parse error: incomplete revspec format character
1159 1159 [255]
1160 1160 $ hg log -T '{revset("%l", 0)}\n'
1161 1161 hg: parse error: incomplete revspec format character
1162 1162 [255]
1163 1163 $ hg log -T '{revset("%d", 'foo')}\n'
1164 1164 hg: parse error: invalid argument for revspec
1165 1165 [255]
1166 1166 $ hg log -T '{revset("%ld", files)}\n'
1167 1167 hg: parse error: invalid argument for revspec
1168 1168 [255]
1169 1169 $ hg log -T '{revset("%ls", 0)}\n'
1170 1170 hg: parse error: invalid argument for revspec
1171 1171 [255]
1172 1172 $ hg log -T '{revset("%b", 'foo')}\n'
1173 1173 hg: parse error: invalid argument for revspec
1174 1174 [255]
1175 1175 $ hg log -T '{revset("%lb", files)}\n'
1176 1176 hg: parse error: invalid argument for revspec
1177 1177 [255]
1178 1178 $ hg log -T '{revset("%r", 0)}\n'
1179 1179 hg: parse error: invalid argument for revspec
1180 1180 [255]
1181 1181
1182 1182 Test files function
1183 1183
1184 1184 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
1185 1185 2
1186 1186 a
1187 1187 aa
1188 1188 b
1189 1189 1
1190 1190 a
1191 1191 0
1192 1192 a
1193 1193
1194 1194 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
1195 1195 2
1196 1196 aa
1197 1197 1
1198 1198
1199 1199 0
1200 1200
1201
1202 $ hg log -l1 -T "{files('aa') % '{file}\n'}"
1203 aa
1204 $ hg log -l1 -T "{files('aa') % '{path}\n'}"
1205 aa
1206
1201 1207 $ hg rm a
1202 1208 $ hg log -r "wdir()" -T "{rev}\n{join(files('*'), '\n')}\n"
1203 1209 2147483647
1204 1210 aa
1205 1211 b
1206 1212 $ hg revert a
1207 1213
1208 1214 Test relpath function
1209 1215
1210 1216 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
1211 1217 a
1212 1218 $ cd ..
1213 1219 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
1214 1220 r/a
1215 1221
1216 1222 Test stringify on sub expressions
1217 1223
1218 1224 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
1219 1225 fourth, second, third
1220 1226 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
1221 1227 abc
1222 1228
1223 1229 Test splitlines
1224 1230
1225 1231 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
1226 1232 @ foo Modify, add, remove, rename
1227 1233 |
1228 1234 o foo future
1229 1235 |
1230 1236 o foo third
1231 1237 |
1232 1238 o foo second
1233 1239
1234 1240 o foo merge
1235 1241 |\
1236 1242 | o foo new head
1237 1243 | |
1238 1244 o | foo new branch
1239 1245 |/
1240 1246 o foo no user, no domain
1241 1247 |
1242 1248 o foo no person
1243 1249 |
1244 1250 o foo other 1
1245 1251 | foo other 2
1246 1252 | foo
1247 1253 | foo other 3
1248 1254 o foo line 1
1249 1255 foo line 2
1250 1256
1251 1257 $ hg log -R a -r0 -T '{desc|splitlines}\n'
1252 1258 line 1 line 2
1253 1259 $ hg log -R a -r0 -T '{join(desc|splitlines, "|")}\n'
1254 1260 line 1|line 2
1255 1261
1256 1262 Test startswith
1257 1263 $ hg log -Gv -R a --template "{startswith(desc)}"
1258 1264 hg: parse error: startswith expects two arguments
1259 1265 [255]
1260 1266
1261 1267 $ hg log -Gv -R a --template "{startswith('line', desc)}"
1262 1268 @
1263 1269 |
1264 1270 o
1265 1271 |
1266 1272 o
1267 1273 |
1268 1274 o
1269 1275
1270 1276 o
1271 1277 |\
1272 1278 | o
1273 1279 | |
1274 1280 o |
1275 1281 |/
1276 1282 o
1277 1283 |
1278 1284 o
1279 1285 |
1280 1286 o
1281 1287 |
1282 1288 o line 1
1283 1289 line 2
1284 1290
1285 1291 Test word function (including index out of bounds graceful failure)
1286 1292
1287 1293 $ hg log -Gv -R a --template "{word('1', desc)}"
1288 1294 @ add,
1289 1295 |
1290 1296 o
1291 1297 |
1292 1298 o
1293 1299 |
1294 1300 o
1295 1301
1296 1302 o
1297 1303 |\
1298 1304 | o head
1299 1305 | |
1300 1306 o | branch
1301 1307 |/
1302 1308 o user,
1303 1309 |
1304 1310 o person
1305 1311 |
1306 1312 o 1
1307 1313 |
1308 1314 o 1
1309 1315
1310 1316
1311 1317 Test word third parameter used as splitter
1312 1318
1313 1319 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
1314 1320 @ M
1315 1321 |
1316 1322 o future
1317 1323 |
1318 1324 o third
1319 1325 |
1320 1326 o sec
1321 1327
1322 1328 o merge
1323 1329 |\
1324 1330 | o new head
1325 1331 | |
1326 1332 o | new branch
1327 1333 |/
1328 1334 o n
1329 1335 |
1330 1336 o n
1331 1337 |
1332 1338 o
1333 1339 |
1334 1340 o line 1
1335 1341 line 2
1336 1342
1337 1343 Test word error messages for not enough and too many arguments
1338 1344
1339 1345 $ hg log -Gv -R a --template "{word('0')}"
1340 1346 hg: parse error: word expects two or three arguments, got 1
1341 1347 [255]
1342 1348
1343 1349 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
1344 1350 hg: parse error: word expects two or three arguments, got 7
1345 1351 [255]
1346 1352
1347 1353 Test word for integer literal
1348 1354
1349 1355 $ hg log -R a --template "{word(2, desc)}\n" -r0
1350 1356 line
1351 1357
1352 1358 Test word for invalid numbers
1353 1359
1354 1360 $ hg log -Gv -R a --template "{word('a', desc)}"
1355 1361 hg: parse error: word expects an integer index
1356 1362 [255]
1357 1363
1358 1364 Test word for out of range
1359 1365
1360 1366 $ hg log -R a --template "{word(10000, desc)}"
1361 1367 $ hg log -R a --template "{word(-10000, desc)}"
1362 1368
1363 1369 Test indent and not adding to empty lines
1364 1370
1365 1371 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
1366 1372 -----
1367 1373 > line 1
1368 1374 >> line 2
1369 1375 -----
1370 1376 > other 1
1371 1377 >> other 2
1372 1378
1373 1379 >> other 3
1374 1380
1375 1381 Test with non-strings like dates
1376 1382
1377 1383 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
1378 1384 1200000.00
1379 1385 1300000.00
1380 1386
1381 1387 json filter should escape HTML tags so that the output can be embedded in hgweb:
1382 1388
1383 1389 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
1384 1390 "\u003cfoo@example.org\u003e"
1385 1391
1386 1392 Set up repository for non-ascii encoding tests:
1387 1393
1388 1394 $ hg init nonascii
1389 1395 $ cd nonascii
1390 1396 $ $PYTHON <<EOF
1391 1397 > open('latin1', 'wb').write(b'\xe9')
1392 1398 > open('utf-8', 'wb').write(b'\xc3\xa9')
1393 1399 > EOF
1394 1400 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
1395 1401 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
1396 1402
1397 1403 json filter should try round-trip conversion to utf-8:
1398 1404
1399 1405 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
1400 1406 "\u00e9"
1401 1407 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
1402 1408 "non-ascii branch: \u00e9"
1403 1409
1404 1410 json filter should take input as utf-8 if it was converted from utf-8:
1405 1411
1406 1412 $ HGENCODING=latin-1 hg log -T "{branch|json}\n" -r0
1407 1413 "\u00e9"
1408 1414 $ HGENCODING=latin-1 hg log -T "{desc|json}\n" -r0
1409 1415 "non-ascii branch: \u00e9"
1410 1416
1411 1417 json filter takes input as utf-8b:
1412 1418
1413 1419 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
1414 1420 "\u00e9"
1415 1421 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
1416 1422 "\udce9"
1417 1423
1418 1424 utf8 filter:
1419 1425
1420 1426 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
1421 1427 round-trip: c3a9
1422 1428 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
1423 1429 decoded: c3a9
1424 1430 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
1425 1431 abort: decoding near * (glob)
1426 1432 [255]
1427 1433 $ hg log -T "coerced to string: {rev|utf8}\n" -r0
1428 1434 coerced to string: 0
1429 1435
1430 1436 pad width:
1431 1437
1432 1438 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
1433 1439 \xc3\xa9- (esc)
1434 1440
1435 1441 $ cd ..
@@ -1,1222 +1,1246
1 1 Test template keywords
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 Working-directory revision has special identifiers, though they are still
50 50 experimental:
51 51
52 52 $ hg log -r 'wdir()' -T '{rev}:{node}\n'
53 53 2147483647:ffffffffffffffffffffffffffffffffffffffff
54 54
55 55 Some keywords are invalid for working-directory revision, but they should
56 56 never cause crash:
57 57
58 58 $ hg log -r 'wdir()' -T '{manifest}\n'
59 59
60 60
61 61 Check that {phase} works correctly on parents:
62 62
63 63 $ cat << EOF > parentphase
64 64 > changeset_debug = '{rev} ({phase}):{parents}\n'
65 65 > parent = ' {rev} ({phase})'
66 66 > EOF
67 67 $ hg phase -r 5 --public
68 68 $ hg phase -r 7 --secret --force
69 69 $ hg log --debug -G --style ./parentphase
70 70 @ 8 (secret): 7 (secret) -1 (public)
71 71 |
72 72 o 7 (secret): -1 (public) -1 (public)
73 73
74 74 o 6 (draft): 5 (public) 4 (draft)
75 75 |\
76 76 | o 5 (public): 3 (public) -1 (public)
77 77 | |
78 78 o | 4 (draft): 3 (public) -1 (public)
79 79 |/
80 80 o 3 (public): 2 (public) -1 (public)
81 81 |
82 82 o 2 (public): 1 (public) -1 (public)
83 83 |
84 84 o 1 (public): 0 (public) -1 (public)
85 85 |
86 86 o 0 (public): -1 (public) -1 (public)
87 87
88 88
89 89 Keys work:
90 90
91 91 $ for key in author branch branches date desc file_adds file_dels file_mods \
92 92 > file_copies file_copies_switch files \
93 93 > manifest node parents rev tags diffstat extras \
94 94 > p1rev p2rev p1node p2node user; do
95 95 > for mode in '' --verbose --debug; do
96 96 > hg log $mode --template "$key$mode: {$key}\n"
97 97 > done
98 98 > done
99 99 author: test
100 100 author: User Name <user@hostname>
101 101 author: person
102 102 author: person
103 103 author: person
104 104 author: person
105 105 author: other@place
106 106 author: A. N. Other <other@place>
107 107 author: User Name <user@hostname>
108 108 author--verbose: test
109 109 author--verbose: User Name <user@hostname>
110 110 author--verbose: person
111 111 author--verbose: person
112 112 author--verbose: person
113 113 author--verbose: person
114 114 author--verbose: other@place
115 115 author--verbose: A. N. Other <other@place>
116 116 author--verbose: User Name <user@hostname>
117 117 author--debug: test
118 118 author--debug: User Name <user@hostname>
119 119 author--debug: person
120 120 author--debug: person
121 121 author--debug: person
122 122 author--debug: person
123 123 author--debug: other@place
124 124 author--debug: A. N. Other <other@place>
125 125 author--debug: User Name <user@hostname>
126 126 branch: default
127 127 branch: default
128 128 branch: default
129 129 branch: default
130 130 branch: foo
131 131 branch: default
132 132 branch: default
133 133 branch: default
134 134 branch: default
135 135 branch--verbose: default
136 136 branch--verbose: default
137 137 branch--verbose: default
138 138 branch--verbose: default
139 139 branch--verbose: foo
140 140 branch--verbose: default
141 141 branch--verbose: default
142 142 branch--verbose: default
143 143 branch--verbose: default
144 144 branch--debug: default
145 145 branch--debug: default
146 146 branch--debug: default
147 147 branch--debug: default
148 148 branch--debug: foo
149 149 branch--debug: default
150 150 branch--debug: default
151 151 branch--debug: default
152 152 branch--debug: default
153 153 branches:
154 154 branches:
155 155 branches:
156 156 branches:
157 157 branches: foo
158 158 branches:
159 159 branches:
160 160 branches:
161 161 branches:
162 162 branches--verbose:
163 163 branches--verbose:
164 164 branches--verbose:
165 165 branches--verbose:
166 166 branches--verbose: foo
167 167 branches--verbose:
168 168 branches--verbose:
169 169 branches--verbose:
170 170 branches--verbose:
171 171 branches--debug:
172 172 branches--debug:
173 173 branches--debug:
174 174 branches--debug:
175 175 branches--debug: foo
176 176 branches--debug:
177 177 branches--debug:
178 178 branches--debug:
179 179 branches--debug:
180 180 date: 1577872860.00
181 181 date: 1000000.00
182 182 date: 1500001.00
183 183 date: 1500000.00
184 184 date: 1400000.00
185 185 date: 1300000.00
186 186 date: 1200000.00
187 187 date: 1100000.00
188 188 date: 1000000.00
189 189 date--verbose: 1577872860.00
190 190 date--verbose: 1000000.00
191 191 date--verbose: 1500001.00
192 192 date--verbose: 1500000.00
193 193 date--verbose: 1400000.00
194 194 date--verbose: 1300000.00
195 195 date--verbose: 1200000.00
196 196 date--verbose: 1100000.00
197 197 date--verbose: 1000000.00
198 198 date--debug: 1577872860.00
199 199 date--debug: 1000000.00
200 200 date--debug: 1500001.00
201 201 date--debug: 1500000.00
202 202 date--debug: 1400000.00
203 203 date--debug: 1300000.00
204 204 date--debug: 1200000.00
205 205 date--debug: 1100000.00
206 206 date--debug: 1000000.00
207 207 desc: third
208 208 desc: second
209 209 desc: merge
210 210 desc: new head
211 211 desc: new branch
212 212 desc: no user, no domain
213 213 desc: no person
214 214 desc: other 1
215 215 other 2
216 216
217 217 other 3
218 218 desc: line 1
219 219 line 2
220 220 desc--verbose: third
221 221 desc--verbose: second
222 222 desc--verbose: merge
223 223 desc--verbose: new head
224 224 desc--verbose: new branch
225 225 desc--verbose: no user, no domain
226 226 desc--verbose: no person
227 227 desc--verbose: other 1
228 228 other 2
229 229
230 230 other 3
231 231 desc--verbose: line 1
232 232 line 2
233 233 desc--debug: third
234 234 desc--debug: second
235 235 desc--debug: merge
236 236 desc--debug: new head
237 237 desc--debug: new branch
238 238 desc--debug: no user, no domain
239 239 desc--debug: no person
240 240 desc--debug: other 1
241 241 other 2
242 242
243 243 other 3
244 244 desc--debug: line 1
245 245 line 2
246 246 file_adds: fourth third
247 247 file_adds: second
248 248 file_adds:
249 249 file_adds: d
250 250 file_adds:
251 251 file_adds:
252 252 file_adds: c
253 253 file_adds: b
254 254 file_adds: a
255 255 file_adds--verbose: fourth third
256 256 file_adds--verbose: second
257 257 file_adds--verbose:
258 258 file_adds--verbose: d
259 259 file_adds--verbose:
260 260 file_adds--verbose:
261 261 file_adds--verbose: c
262 262 file_adds--verbose: b
263 263 file_adds--verbose: a
264 264 file_adds--debug: fourth third
265 265 file_adds--debug: second
266 266 file_adds--debug:
267 267 file_adds--debug: d
268 268 file_adds--debug:
269 269 file_adds--debug:
270 270 file_adds--debug: c
271 271 file_adds--debug: b
272 272 file_adds--debug: a
273 273 file_dels: second
274 274 file_dels:
275 275 file_dels:
276 276 file_dels:
277 277 file_dels:
278 278 file_dels:
279 279 file_dels:
280 280 file_dels:
281 281 file_dels:
282 282 file_dels--verbose: second
283 283 file_dels--verbose:
284 284 file_dels--verbose:
285 285 file_dels--verbose:
286 286 file_dels--verbose:
287 287 file_dels--verbose:
288 288 file_dels--verbose:
289 289 file_dels--verbose:
290 290 file_dels--verbose:
291 291 file_dels--debug: second
292 292 file_dels--debug:
293 293 file_dels--debug:
294 294 file_dels--debug:
295 295 file_dels--debug:
296 296 file_dels--debug:
297 297 file_dels--debug:
298 298 file_dels--debug:
299 299 file_dels--debug:
300 300 file_mods:
301 301 file_mods:
302 302 file_mods:
303 303 file_mods:
304 304 file_mods:
305 305 file_mods: c
306 306 file_mods:
307 307 file_mods:
308 308 file_mods:
309 309 file_mods--verbose:
310 310 file_mods--verbose:
311 311 file_mods--verbose:
312 312 file_mods--verbose:
313 313 file_mods--verbose:
314 314 file_mods--verbose: c
315 315 file_mods--verbose:
316 316 file_mods--verbose:
317 317 file_mods--verbose:
318 318 file_mods--debug:
319 319 file_mods--debug:
320 320 file_mods--debug:
321 321 file_mods--debug:
322 322 file_mods--debug:
323 323 file_mods--debug: c
324 324 file_mods--debug:
325 325 file_mods--debug:
326 326 file_mods--debug:
327 327 file_copies: fourth (second)
328 328 file_copies:
329 329 file_copies:
330 330 file_copies:
331 331 file_copies:
332 332 file_copies:
333 333 file_copies:
334 334 file_copies:
335 335 file_copies:
336 336 file_copies--verbose: fourth (second)
337 337 file_copies--verbose:
338 338 file_copies--verbose:
339 339 file_copies--verbose:
340 340 file_copies--verbose:
341 341 file_copies--verbose:
342 342 file_copies--verbose:
343 343 file_copies--verbose:
344 344 file_copies--verbose:
345 345 file_copies--debug: fourth (second)
346 346 file_copies--debug:
347 347 file_copies--debug:
348 348 file_copies--debug:
349 349 file_copies--debug:
350 350 file_copies--debug:
351 351 file_copies--debug:
352 352 file_copies--debug:
353 353 file_copies--debug:
354 354 file_copies_switch:
355 355 file_copies_switch:
356 356 file_copies_switch:
357 357 file_copies_switch:
358 358 file_copies_switch:
359 359 file_copies_switch:
360 360 file_copies_switch:
361 361 file_copies_switch:
362 362 file_copies_switch:
363 363 file_copies_switch--verbose:
364 364 file_copies_switch--verbose:
365 365 file_copies_switch--verbose:
366 366 file_copies_switch--verbose:
367 367 file_copies_switch--verbose:
368 368 file_copies_switch--verbose:
369 369 file_copies_switch--verbose:
370 370 file_copies_switch--verbose:
371 371 file_copies_switch--verbose:
372 372 file_copies_switch--debug:
373 373 file_copies_switch--debug:
374 374 file_copies_switch--debug:
375 375 file_copies_switch--debug:
376 376 file_copies_switch--debug:
377 377 file_copies_switch--debug:
378 378 file_copies_switch--debug:
379 379 file_copies_switch--debug:
380 380 file_copies_switch--debug:
381 381 files: fourth second third
382 382 files: second
383 383 files:
384 384 files: d
385 385 files:
386 386 files: c
387 387 files: c
388 388 files: b
389 389 files: a
390 390 files--verbose: fourth second third
391 391 files--verbose: second
392 392 files--verbose:
393 393 files--verbose: d
394 394 files--verbose:
395 395 files--verbose: c
396 396 files--verbose: c
397 397 files--verbose: b
398 398 files--verbose: a
399 399 files--debug: fourth second third
400 400 files--debug: second
401 401 files--debug:
402 402 files--debug: d
403 403 files--debug:
404 404 files--debug: c
405 405 files--debug: c
406 406 files--debug: b
407 407 files--debug: a
408 408 manifest: 6:94961b75a2da
409 409 manifest: 5:f2dbc354b94e
410 410 manifest: 4:4dc3def4f9b4
411 411 manifest: 4:4dc3def4f9b4
412 412 manifest: 3:cb5a1327723b
413 413 manifest: 3:cb5a1327723b
414 414 manifest: 2:6e0e82995c35
415 415 manifest: 1:4e8d705b1e53
416 416 manifest: 0:a0c8bcbbb45c
417 417 manifest--verbose: 6:94961b75a2da
418 418 manifest--verbose: 5:f2dbc354b94e
419 419 manifest--verbose: 4:4dc3def4f9b4
420 420 manifest--verbose: 4:4dc3def4f9b4
421 421 manifest--verbose: 3:cb5a1327723b
422 422 manifest--verbose: 3:cb5a1327723b
423 423 manifest--verbose: 2:6e0e82995c35
424 424 manifest--verbose: 1:4e8d705b1e53
425 425 manifest--verbose: 0:a0c8bcbbb45c
426 426 manifest--debug: 6:94961b75a2da554b4df6fb599e5bfc7d48de0c64
427 427 manifest--debug: 5:f2dbc354b94e5ec0b4f10680ee0cee816101d0bf
428 428 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
429 429 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
430 430 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
431 431 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
432 432 manifest--debug: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
433 433 manifest--debug: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
434 434 manifest--debug: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
435 435 node: 95c24699272ef57d062b8bccc32c878bf841784a
436 436 node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
437 437 node: d41e714fe50d9e4a5f11b4d595d543481b5f980b
438 438 node: 13207e5a10d9fd28ec424934298e176197f2c67f
439 439 node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
440 440 node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
441 441 node: 97054abb4ab824450e9164180baf491ae0078465
442 442 node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
443 443 node: 1e4e1b8f71e05681d422154f5421e385fec3454f
444 444 node--verbose: 95c24699272ef57d062b8bccc32c878bf841784a
445 445 node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
446 446 node--verbose: d41e714fe50d9e4a5f11b4d595d543481b5f980b
447 447 node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
448 448 node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
449 449 node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
450 450 node--verbose: 97054abb4ab824450e9164180baf491ae0078465
451 451 node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
452 452 node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
453 453 node--debug: 95c24699272ef57d062b8bccc32c878bf841784a
454 454 node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
455 455 node--debug: d41e714fe50d9e4a5f11b4d595d543481b5f980b
456 456 node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
457 457 node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
458 458 node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
459 459 node--debug: 97054abb4ab824450e9164180baf491ae0078465
460 460 node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
461 461 node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
462 462 parents:
463 463 parents: -1:000000000000
464 464 parents: 5:13207e5a10d9 4:bbe44766e73d
465 465 parents: 3:10e46f2dcbf4
466 466 parents:
467 467 parents:
468 468 parents:
469 469 parents:
470 470 parents:
471 471 parents--verbose:
472 472 parents--verbose: -1:000000000000
473 473 parents--verbose: 5:13207e5a10d9 4:bbe44766e73d
474 474 parents--verbose: 3:10e46f2dcbf4
475 475 parents--verbose:
476 476 parents--verbose:
477 477 parents--verbose:
478 478 parents--verbose:
479 479 parents--verbose:
480 480 parents--debug: 7:29114dbae42b9f078cf2714dbe3a86bba8ec7453 -1:0000000000000000000000000000000000000000
481 481 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
482 482 parents--debug: 5:13207e5a10d9fd28ec424934298e176197f2c67f 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
483 483 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
484 484 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
485 485 parents--debug: 2:97054abb4ab824450e9164180baf491ae0078465 -1:0000000000000000000000000000000000000000
486 486 parents--debug: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965 -1:0000000000000000000000000000000000000000
487 487 parents--debug: 0:1e4e1b8f71e05681d422154f5421e385fec3454f -1:0000000000000000000000000000000000000000
488 488 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
489 489 rev: 8
490 490 rev: 7
491 491 rev: 6
492 492 rev: 5
493 493 rev: 4
494 494 rev: 3
495 495 rev: 2
496 496 rev: 1
497 497 rev: 0
498 498 rev--verbose: 8
499 499 rev--verbose: 7
500 500 rev--verbose: 6
501 501 rev--verbose: 5
502 502 rev--verbose: 4
503 503 rev--verbose: 3
504 504 rev--verbose: 2
505 505 rev--verbose: 1
506 506 rev--verbose: 0
507 507 rev--debug: 8
508 508 rev--debug: 7
509 509 rev--debug: 6
510 510 rev--debug: 5
511 511 rev--debug: 4
512 512 rev--debug: 3
513 513 rev--debug: 2
514 514 rev--debug: 1
515 515 rev--debug: 0
516 516 tags: tip
517 517 tags:
518 518 tags:
519 519 tags:
520 520 tags:
521 521 tags:
522 522 tags:
523 523 tags:
524 524 tags:
525 525 tags--verbose: tip
526 526 tags--verbose:
527 527 tags--verbose:
528 528 tags--verbose:
529 529 tags--verbose:
530 530 tags--verbose:
531 531 tags--verbose:
532 532 tags--verbose:
533 533 tags--verbose:
534 534 tags--debug: tip
535 535 tags--debug:
536 536 tags--debug:
537 537 tags--debug:
538 538 tags--debug:
539 539 tags--debug:
540 540 tags--debug:
541 541 tags--debug:
542 542 tags--debug:
543 543 diffstat: 3: +2/-1
544 544 diffstat: 1: +1/-0
545 545 diffstat: 0: +0/-0
546 546 diffstat: 1: +1/-0
547 547 diffstat: 0: +0/-0
548 548 diffstat: 1: +1/-0
549 549 diffstat: 1: +4/-0
550 550 diffstat: 1: +2/-0
551 551 diffstat: 1: +1/-0
552 552 diffstat--verbose: 3: +2/-1
553 553 diffstat--verbose: 1: +1/-0
554 554 diffstat--verbose: 0: +0/-0
555 555 diffstat--verbose: 1: +1/-0
556 556 diffstat--verbose: 0: +0/-0
557 557 diffstat--verbose: 1: +1/-0
558 558 diffstat--verbose: 1: +4/-0
559 559 diffstat--verbose: 1: +2/-0
560 560 diffstat--verbose: 1: +1/-0
561 561 diffstat--debug: 3: +2/-1
562 562 diffstat--debug: 1: +1/-0
563 563 diffstat--debug: 0: +0/-0
564 564 diffstat--debug: 1: +1/-0
565 565 diffstat--debug: 0: +0/-0
566 566 diffstat--debug: 1: +1/-0
567 567 diffstat--debug: 1: +4/-0
568 568 diffstat--debug: 1: +2/-0
569 569 diffstat--debug: 1: +1/-0
570 570 extras: branch=default
571 571 extras: branch=default
572 572 extras: branch=default
573 573 extras: branch=default
574 574 extras: branch=foo
575 575 extras: branch=default
576 576 extras: branch=default
577 577 extras: branch=default
578 578 extras: branch=default
579 579 extras--verbose: branch=default
580 580 extras--verbose: branch=default
581 581 extras--verbose: branch=default
582 582 extras--verbose: branch=default
583 583 extras--verbose: branch=foo
584 584 extras--verbose: branch=default
585 585 extras--verbose: branch=default
586 586 extras--verbose: branch=default
587 587 extras--verbose: branch=default
588 588 extras--debug: branch=default
589 589 extras--debug: branch=default
590 590 extras--debug: branch=default
591 591 extras--debug: branch=default
592 592 extras--debug: branch=foo
593 593 extras--debug: branch=default
594 594 extras--debug: branch=default
595 595 extras--debug: branch=default
596 596 extras--debug: branch=default
597 597 p1rev: 7
598 598 p1rev: -1
599 599 p1rev: 5
600 600 p1rev: 3
601 601 p1rev: 3
602 602 p1rev: 2
603 603 p1rev: 1
604 604 p1rev: 0
605 605 p1rev: -1
606 606 p1rev--verbose: 7
607 607 p1rev--verbose: -1
608 608 p1rev--verbose: 5
609 609 p1rev--verbose: 3
610 610 p1rev--verbose: 3
611 611 p1rev--verbose: 2
612 612 p1rev--verbose: 1
613 613 p1rev--verbose: 0
614 614 p1rev--verbose: -1
615 615 p1rev--debug: 7
616 616 p1rev--debug: -1
617 617 p1rev--debug: 5
618 618 p1rev--debug: 3
619 619 p1rev--debug: 3
620 620 p1rev--debug: 2
621 621 p1rev--debug: 1
622 622 p1rev--debug: 0
623 623 p1rev--debug: -1
624 624 p2rev: -1
625 625 p2rev: -1
626 626 p2rev: 4
627 627 p2rev: -1
628 628 p2rev: -1
629 629 p2rev: -1
630 630 p2rev: -1
631 631 p2rev: -1
632 632 p2rev: -1
633 633 p2rev--verbose: -1
634 634 p2rev--verbose: -1
635 635 p2rev--verbose: 4
636 636 p2rev--verbose: -1
637 637 p2rev--verbose: -1
638 638 p2rev--verbose: -1
639 639 p2rev--verbose: -1
640 640 p2rev--verbose: -1
641 641 p2rev--verbose: -1
642 642 p2rev--debug: -1
643 643 p2rev--debug: -1
644 644 p2rev--debug: 4
645 645 p2rev--debug: -1
646 646 p2rev--debug: -1
647 647 p2rev--debug: -1
648 648 p2rev--debug: -1
649 649 p2rev--debug: -1
650 650 p2rev--debug: -1
651 651 p1node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
652 652 p1node: 0000000000000000000000000000000000000000
653 653 p1node: 13207e5a10d9fd28ec424934298e176197f2c67f
654 654 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
655 655 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
656 656 p1node: 97054abb4ab824450e9164180baf491ae0078465
657 657 p1node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
658 658 p1node: 1e4e1b8f71e05681d422154f5421e385fec3454f
659 659 p1node: 0000000000000000000000000000000000000000
660 660 p1node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
661 661 p1node--verbose: 0000000000000000000000000000000000000000
662 662 p1node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
663 663 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
664 664 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
665 665 p1node--verbose: 97054abb4ab824450e9164180baf491ae0078465
666 666 p1node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
667 667 p1node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
668 668 p1node--verbose: 0000000000000000000000000000000000000000
669 669 p1node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
670 670 p1node--debug: 0000000000000000000000000000000000000000
671 671 p1node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
672 672 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
673 673 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
674 674 p1node--debug: 97054abb4ab824450e9164180baf491ae0078465
675 675 p1node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
676 676 p1node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
677 677 p1node--debug: 0000000000000000000000000000000000000000
678 678 p2node: 0000000000000000000000000000000000000000
679 679 p2node: 0000000000000000000000000000000000000000
680 680 p2node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
681 681 p2node: 0000000000000000000000000000000000000000
682 682 p2node: 0000000000000000000000000000000000000000
683 683 p2node: 0000000000000000000000000000000000000000
684 684 p2node: 0000000000000000000000000000000000000000
685 685 p2node: 0000000000000000000000000000000000000000
686 686 p2node: 0000000000000000000000000000000000000000
687 687 p2node--verbose: 0000000000000000000000000000000000000000
688 688 p2node--verbose: 0000000000000000000000000000000000000000
689 689 p2node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
690 690 p2node--verbose: 0000000000000000000000000000000000000000
691 691 p2node--verbose: 0000000000000000000000000000000000000000
692 692 p2node--verbose: 0000000000000000000000000000000000000000
693 693 p2node--verbose: 0000000000000000000000000000000000000000
694 694 p2node--verbose: 0000000000000000000000000000000000000000
695 695 p2node--verbose: 0000000000000000000000000000000000000000
696 696 p2node--debug: 0000000000000000000000000000000000000000
697 697 p2node--debug: 0000000000000000000000000000000000000000
698 698 p2node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
699 699 p2node--debug: 0000000000000000000000000000000000000000
700 700 p2node--debug: 0000000000000000000000000000000000000000
701 701 p2node--debug: 0000000000000000000000000000000000000000
702 702 p2node--debug: 0000000000000000000000000000000000000000
703 703 p2node--debug: 0000000000000000000000000000000000000000
704 704 p2node--debug: 0000000000000000000000000000000000000000
705 705 user: test
706 706 user: User Name <user@hostname>
707 707 user: person
708 708 user: person
709 709 user: person
710 710 user: person
711 711 user: other@place
712 712 user: A. N. Other <other@place>
713 713 user: User Name <user@hostname>
714 714 user--verbose: test
715 715 user--verbose: User Name <user@hostname>
716 716 user--verbose: person
717 717 user--verbose: person
718 718 user--verbose: person
719 719 user--verbose: person
720 720 user--verbose: other@place
721 721 user--verbose: A. N. Other <other@place>
722 722 user--verbose: User Name <user@hostname>
723 723 user--debug: test
724 724 user--debug: User Name <user@hostname>
725 725 user--debug: person
726 726 user--debug: person
727 727 user--debug: person
728 728 user--debug: person
729 729 user--debug: other@place
730 730 user--debug: A. N. Other <other@place>
731 731 user--debug: User Name <user@hostname>
732 732
733 733 Add a dummy commit to make up for the instability of the above:
734 734
735 735 $ echo a > a
736 736 $ hg add a
737 737 $ hg ci -m future
738 738
739 739 Add a commit that does all possible modifications at once
740 740
741 741 $ echo modify >> third
742 742 $ touch b
743 743 $ hg add b
744 744 $ hg mv fourth fifth
745 745 $ hg rm a
746 746 $ hg ci -m "Modify, add, remove, rename"
747 747
748 Test files list:
749
750 $ hg log -l1 -T '{join(file_mods, " ")}\n'
751 third
752 $ hg log -l1 -T '{file_mods % "{file}\n"}'
753 third
754 $ hg log -l1 -T '{file_mods % "{path}\n"}'
755 third
756
757 $ hg log -l1 -T '{join(files, " ")}\n'
758 a b fifth fourth third
759 $ hg log -l1 -T '{files % "{file}\n"}'
760 a
761 b
762 fifth
763 fourth
764 third
765 $ hg log -l1 -T '{files % "{path}\n"}'
766 a
767 b
768 fifth
769 fourth
770 third
771
748 772 Test index keyword:
749 773
750 774 $ hg log -l 2 -T '{index + 10}{files % " {index}:{file}"}\n'
751 775 10 0:a 1:b 2:fifth 3:fourth 4:third
752 776 11 0:a
753 777
754 778 $ hg branches -T '{index} {branch}\n'
755 779 0 default
756 780 1 foo
757 781
758 782 ui verbosity:
759 783
760 784 $ hg log -l1 -T '{verbosity}\n'
761 785
762 786 $ hg log -l1 -T '{verbosity}\n' --debug
763 787 debug
764 788 $ hg log -l1 -T '{verbosity}\n' --quiet
765 789 quiet
766 790 $ hg log -l1 -T '{verbosity}\n' --verbose
767 791 verbose
768 792
769 793 $ cd ..
770 794
771 795 latesttag:
772 796
773 797 $ hg init latesttag
774 798 $ cd latesttag
775 799
776 800 $ echo a > file
777 801 $ hg ci -Am a -d '0 0'
778 802 adding file
779 803
780 804 $ echo b >> file
781 805 $ hg ci -m b -d '1 0'
782 806
783 807 $ echo c >> head1
784 808 $ hg ci -Am h1c -d '2 0'
785 809 adding head1
786 810
787 811 $ hg update -q 1
788 812 $ echo d >> head2
789 813 $ hg ci -Am h2d -d '3 0'
790 814 adding head2
791 815 created new head
792 816
793 817 $ echo e >> head2
794 818 $ hg ci -m h2e -d '4 0'
795 819
796 820 $ hg merge -q
797 821 $ hg ci -m merge -d '5 -3600'
798 822
799 823 No tag set:
800 824
801 825 $ hg log -G --template '{rev}: {latesttag}+{latesttagdistance}\n'
802 826 @ 5: null+5
803 827 |\
804 828 | o 4: null+4
805 829 | |
806 830 | o 3: null+3
807 831 | |
808 832 o | 2: null+3
809 833 |/
810 834 o 1: null+2
811 835 |
812 836 o 0: null+1
813 837
814 838
815 839 One common tag: longest path wins for {latesttagdistance}:
816 840
817 841 $ hg tag -r 1 -m t1 -d '6 0' t1
818 842 $ hg log -G --template '{rev}: {latesttag}+{latesttagdistance}\n'
819 843 @ 6: t1+4
820 844 |
821 845 o 5: t1+3
822 846 |\
823 847 | o 4: t1+2
824 848 | |
825 849 | o 3: t1+1
826 850 | |
827 851 o | 2: t1+1
828 852 |/
829 853 o 1: t1+0
830 854 |
831 855 o 0: null+1
832 856
833 857
834 858 One ancestor tag: closest wins:
835 859
836 860 $ hg tag -r 2 -m t2 -d '7 0' t2
837 861 $ hg log -G --template '{rev}: {latesttag}+{latesttagdistance}\n'
838 862 @ 7: t2+3
839 863 |
840 864 o 6: t2+2
841 865 |
842 866 o 5: t2+1
843 867 |\
844 868 | o 4: t1+2
845 869 | |
846 870 | o 3: t1+1
847 871 | |
848 872 o | 2: t2+0
849 873 |/
850 874 o 1: t1+0
851 875 |
852 876 o 0: null+1
853 877
854 878
855 879 Two branch tags: more recent wins if same number of changes:
856 880
857 881 $ hg tag -r 3 -m t3 -d '8 0' t3
858 882 $ hg log -G --template '{rev}: {latesttag}+{latesttagdistance}\n'
859 883 @ 8: t3+5
860 884 |
861 885 o 7: t3+4
862 886 |
863 887 o 6: t3+3
864 888 |
865 889 o 5: t3+2
866 890 |\
867 891 | o 4: t3+1
868 892 | |
869 893 | o 3: t3+0
870 894 | |
871 895 o | 2: t2+0
872 896 |/
873 897 o 1: t1+0
874 898 |
875 899 o 0: null+1
876 900
877 901
878 902 Two branch tags: fewest changes wins:
879 903
880 904 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
881 905 $ hg log -G --template "{rev}: {latesttag % '{tag}+{distance},{changes} '}\n"
882 906 @ 9: t4+5,6
883 907 |
884 908 o 8: t4+4,5
885 909 |
886 910 o 7: t4+3,4
887 911 |
888 912 o 6: t4+2,3
889 913 |
890 914 o 5: t4+1,2
891 915 |\
892 916 | o 4: t4+0,0
893 917 | |
894 918 | o 3: t3+0,0
895 919 | |
896 920 o | 2: t2+0,0
897 921 |/
898 922 o 1: t1+0,0
899 923 |
900 924 o 0: null+1,1
901 925
902 926
903 927 Merged tag overrides:
904 928
905 929 $ hg tag -r 5 -m t5 -d '9 0' t5
906 930 $ hg tag -r 3 -m at3 -d '10 0' at3
907 931 $ hg log -G --template '{rev}: {latesttag}+{latesttagdistance}\n'
908 932 @ 11: t5+6
909 933 |
910 934 o 10: t5+5
911 935 |
912 936 o 9: t5+4
913 937 |
914 938 o 8: t5+3
915 939 |
916 940 o 7: t5+2
917 941 |
918 942 o 6: t5+1
919 943 |
920 944 o 5: t5+0
921 945 |\
922 946 | o 4: t4+0
923 947 | |
924 948 | o 3: at3:t3+0
925 949 | |
926 950 o | 2: t2+0
927 951 |/
928 952 o 1: t1+0
929 953 |
930 954 o 0: null+1
931 955
932 956
933 957 $ hg log -G --template "{rev}: {latesttag % '{tag}+{distance},{changes} '}\n"
934 958 @ 11: t5+6,6
935 959 |
936 960 o 10: t5+5,5
937 961 |
938 962 o 9: t5+4,4
939 963 |
940 964 o 8: t5+3,3
941 965 |
942 966 o 7: t5+2,2
943 967 |
944 968 o 6: t5+1,1
945 969 |
946 970 o 5: t5+0,0
947 971 |\
948 972 | o 4: t4+0,0
949 973 | |
950 974 | o 3: at3+0,0 t3+0,0
951 975 | |
952 976 o | 2: t2+0,0
953 977 |/
954 978 o 1: t1+0,0
955 979 |
956 980 o 0: null+1,1
957 981
958 982
959 983 $ cd ..
960 984
961 985 Set up repository containing template fragments in commit metadata:
962 986
963 987 $ hg init r
964 988 $ cd r
965 989 $ echo a > a
966 990 $ hg ci -Am '{rev}'
967 991 adding a
968 992
969 993 $ hg branch -q 'text.{rev}'
970 994 $ echo aa >> aa
971 995 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
972 996
973 997 Test termwidth:
974 998
975 999 $ COLUMNS=25 hg log -l1 --template '{fill(desc, termwidth, "{node|short}:", "termwidth.{rev}:")}'
976 1000 bcc7ff960b8e:desc to be
977 1001 termwidth.1:wrapped desc
978 1002 termwidth.1:to be wrapped (no-eol)
979 1003
980 1004 Just one more commit:
981 1005
982 1006 $ echo b > b
983 1007 $ hg ci -qAm b
984 1008
985 1009 Test 'originalnode'
986 1010
987 1011 $ hg log -r 1 -T '{revset("null") % "{node|short} {originalnode|short}"}\n'
988 1012 000000000000 bcc7ff960b8e
989 1013 $ hg log -r 0 -T '{manifest % "{node} {originalnode}"}\n'
990 1014 a0c8bcbbb45c63b90b70ad007bf38961f64f2af0 f7769ec2ab975ad19684098ad1ffd9b81ecc71a1
991 1015
992 1016 Test active bookmark templating
993 1017
994 1018 $ hg book foo
995 1019 $ hg book bar
996 1020 $ hg log --template "{rev} {bookmarks % '{bookmark}{ifeq(bookmark, active, \"*\")} '}\n"
997 1021 2 bar* foo
998 1022 1
999 1023 0
1000 1024 $ hg log --template "{rev} {activebookmark}\n"
1001 1025 2 bar
1002 1026 1
1003 1027 0
1004 1028 $ hg bookmarks --inactive bar
1005 1029 $ hg log --template "{rev} {activebookmark}\n"
1006 1030 2
1007 1031 1
1008 1032 0
1009 1033 $ hg book -r1 baz
1010 1034 $ hg log --template "{rev} {join(bookmarks, ' ')}\n"
1011 1035 2 bar foo
1012 1036 1 baz
1013 1037 0
1014 1038 $ hg log --template "{rev} {ifcontains('foo', bookmarks, 't', 'f')}\n"
1015 1039 2 t
1016 1040 1 f
1017 1041 0 f
1018 1042
1019 1043 Test namespaces dict
1020 1044
1021 1045 $ hg --config extensions.revnamesext=$TESTDIR/revnamesext.py log -T '{rev}\n{namespaces % " {namespace} color={colorname} builtin={builtin}\n {join(names, ",")}\n"}\n'
1022 1046 2
1023 1047 bookmarks color=bookmark builtin=True
1024 1048 bar,foo
1025 1049 tags color=tag builtin=True
1026 1050 tip
1027 1051 branches color=branch builtin=True
1028 1052 text.{rev}
1029 1053 revnames color=revname builtin=False
1030 1054 r2
1031 1055
1032 1056 1
1033 1057 bookmarks color=bookmark builtin=True
1034 1058 baz
1035 1059 tags color=tag builtin=True
1036 1060
1037 1061 branches color=branch builtin=True
1038 1062 text.{rev}
1039 1063 revnames color=revname builtin=False
1040 1064 r1
1041 1065
1042 1066 0
1043 1067 bookmarks color=bookmark builtin=True
1044 1068
1045 1069 tags color=tag builtin=True
1046 1070
1047 1071 branches color=branch builtin=True
1048 1072 default
1049 1073 revnames color=revname builtin=False
1050 1074 r0
1051 1075
1052 1076 $ hg log -r2 -T '{namespaces % "{namespace}: {names}\n"}'
1053 1077 bookmarks: bar foo
1054 1078 tags: tip
1055 1079 branches: text.{rev}
1056 1080 $ hg log -r2 -T '{namespaces % "{namespace}:\n{names % " {name}\n"}"}'
1057 1081 bookmarks:
1058 1082 bar
1059 1083 foo
1060 1084 tags:
1061 1085 tip
1062 1086 branches:
1063 1087 text.{rev}
1064 1088 $ hg log -r2 -T '{get(namespaces, "bookmarks") % "{name}\n"}'
1065 1089 bar
1066 1090 foo
1067 1091 $ hg log -r2 -T '{namespaces.bookmarks % "{bookmark}\n"}'
1068 1092 bar
1069 1093 foo
1070 1094
1071 1095 $ cd ..
1072 1096
1073 1097 Test 'graphwidth' in 'hg log' on various topologies. The key here is that the
1074 1098 printed graphwidths 3, 5, 7, etc. should all line up in their respective
1075 1099 columns. We don't care about other aspects of the graph rendering here.
1076 1100
1077 1101 $ hg init graphwidth
1078 1102 $ cd graphwidth
1079 1103
1080 1104 $ wrappabletext="a a a a a a a a a a a a"
1081 1105
1082 1106 $ printf "first\n" > file
1083 1107 $ hg add file
1084 1108 $ hg commit -m "$wrappabletext"
1085 1109
1086 1110 $ printf "first\nsecond\n" > file
1087 1111 $ hg commit -m "$wrappabletext"
1088 1112
1089 1113 $ hg checkout 0
1090 1114 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1091 1115 $ printf "third\nfirst\n" > file
1092 1116 $ hg commit -m "$wrappabletext"
1093 1117 created new head
1094 1118
1095 1119 $ hg merge
1096 1120 merging file
1097 1121 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
1098 1122 (branch merge, don't forget to commit)
1099 1123
1100 1124 $ hg log --graph -T "{graphwidth}"
1101 1125 @ 3
1102 1126 |
1103 1127 | @ 5
1104 1128 |/
1105 1129 o 3
1106 1130
1107 1131 $ hg commit -m "$wrappabletext"
1108 1132
1109 1133 $ hg log --graph -T "{graphwidth}"
1110 1134 @ 5
1111 1135 |\
1112 1136 | o 5
1113 1137 | |
1114 1138 o | 5
1115 1139 |/
1116 1140 o 3
1117 1141
1118 1142
1119 1143 $ hg checkout 0
1120 1144 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1121 1145 $ printf "third\nfirst\nsecond\n" > file
1122 1146 $ hg commit -m "$wrappabletext"
1123 1147 created new head
1124 1148
1125 1149 $ hg log --graph -T "{graphwidth}"
1126 1150 @ 3
1127 1151 |
1128 1152 | o 7
1129 1153 | |\
1130 1154 +---o 7
1131 1155 | |
1132 1156 | o 5
1133 1157 |/
1134 1158 o 3
1135 1159
1136 1160
1137 1161 $ hg log --graph -T "{graphwidth}" -r 3
1138 1162 o 5
1139 1163 |\
1140 1164 ~ ~
1141 1165
1142 1166 $ hg log --graph -T "{graphwidth}" -r 1
1143 1167 o 3
1144 1168 |
1145 1169 ~
1146 1170
1147 1171 $ hg merge
1148 1172 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1149 1173 (branch merge, don't forget to commit)
1150 1174 $ hg commit -m "$wrappabletext"
1151 1175
1152 1176 $ printf "seventh\n" >> file
1153 1177 $ hg commit -m "$wrappabletext"
1154 1178
1155 1179 $ hg log --graph -T "{graphwidth}"
1156 1180 @ 3
1157 1181 |
1158 1182 o 5
1159 1183 |\
1160 1184 | o 5
1161 1185 | |
1162 1186 o | 7
1163 1187 |\ \
1164 1188 | o | 7
1165 1189 | |/
1166 1190 o / 5
1167 1191 |/
1168 1192 o 3
1169 1193
1170 1194
1171 1195 The point of graphwidth is to allow wrapping that accounts for the space taken
1172 1196 by the graph.
1173 1197
1174 1198 $ COLUMNS=10 hg log --graph -T "{fill(desc, termwidth - graphwidth)}"
1175 1199 @ a a a a
1176 1200 | a a a a
1177 1201 | a a a a
1178 1202 o a a a
1179 1203 |\ a a a
1180 1204 | | a a a
1181 1205 | | a a a
1182 1206 | o a a a
1183 1207 | | a a a
1184 1208 | | a a a
1185 1209 | | a a a
1186 1210 o | a a
1187 1211 |\ \ a a
1188 1212 | | | a a
1189 1213 | | | a a
1190 1214 | | | a a
1191 1215 | | | a a
1192 1216 | o | a a
1193 1217 | |/ a a
1194 1218 | | a a
1195 1219 | | a a
1196 1220 | | a a
1197 1221 | | a a
1198 1222 o | a a a
1199 1223 |/ a a a
1200 1224 | a a a
1201 1225 | a a a
1202 1226 o a a a a
1203 1227 a a a a
1204 1228 a a a a
1205 1229
1206 1230 Something tricky happens when there are elided nodes; the next drawn row of
1207 1231 edges can be more than one column wider, but the graph width only increases by
1208 1232 one column. The remaining columns are added in between the nodes.
1209 1233
1210 1234 $ hg log --graph -T "{graphwidth}" -r "0|2|4|5"
1211 1235 o 5
1212 1236 |\
1213 1237 | \
1214 1238 | :\
1215 1239 o : : 7
1216 1240 :/ /
1217 1241 : o 5
1218 1242 :/
1219 1243 o 3
1220 1244
1221 1245
1222 1246 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now