##// END OF EJS Templates
templatefuncs: do not crash because of invalid value fed to mailmap()
Yuya Nishihara -
r37277:8e57c3b0 default
parent child Browse files
Show More
@@ -1,681 +1,681 b''
1 1 # templatefuncs.py - common template functions
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 color,
15 15 encoding,
16 16 error,
17 17 minirst,
18 18 obsutil,
19 19 pycompat,
20 20 registrar,
21 21 revset as revsetmod,
22 22 revsetlang,
23 23 scmutil,
24 24 templatefilters,
25 25 templatekw,
26 26 templateutil,
27 27 util,
28 28 )
29 29 from .utils import (
30 30 dateutil,
31 31 stringutil,
32 32 )
33 33
34 34 evalrawexp = templateutil.evalrawexp
35 35 evalfuncarg = templateutil.evalfuncarg
36 36 evalboolean = templateutil.evalboolean
37 37 evaldate = templateutil.evaldate
38 38 evalinteger = templateutil.evalinteger
39 39 evalstring = templateutil.evalstring
40 40 evalstringliteral = templateutil.evalstringliteral
41 41
42 42 # dict of template built-in functions
43 43 funcs = {}
44 44 templatefunc = registrar.templatefunc(funcs)
45 45
46 46 @templatefunc('date(date[, fmt])')
47 47 def date(context, mapping, args):
48 48 """Format a date. See :hg:`help dates` for formatting
49 49 strings. The default is a Unix date format, including the timezone:
50 50 "Mon Sep 04 15:13:13 2006 0700"."""
51 51 if not (1 <= len(args) <= 2):
52 52 # i18n: "date" is a keyword
53 53 raise error.ParseError(_("date expects one or two arguments"))
54 54
55 55 date = evaldate(context, mapping, args[0],
56 56 # i18n: "date" is a keyword
57 57 _("date expects a date information"))
58 58 fmt = None
59 59 if len(args) == 2:
60 60 fmt = evalstring(context, mapping, args[1])
61 61 if fmt is None:
62 62 return dateutil.datestr(date)
63 63 else:
64 64 return dateutil.datestr(date, fmt)
65 65
66 66 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
67 67 def dict_(context, mapping, args):
68 68 """Construct a dict from key-value pairs. A key may be omitted if
69 69 a value expression can provide an unambiguous name."""
70 70 data = util.sortdict()
71 71
72 72 for v in args['args']:
73 73 k = templateutil.findsymbolicname(v)
74 74 if not k:
75 75 raise error.ParseError(_('dict key cannot be inferred'))
76 76 if k in data or k in args['kwargs']:
77 77 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
78 78 data[k] = evalfuncarg(context, mapping, v)
79 79
80 80 data.update((k, evalfuncarg(context, mapping, v))
81 81 for k, v in args['kwargs'].iteritems())
82 82 return templateutil.hybriddict(data)
83 83
84 84 @templatefunc('diff([includepattern [, excludepattern]])')
85 85 def diff(context, mapping, args):
86 86 """Show a diff, optionally
87 87 specifying files to include or exclude."""
88 88 if len(args) > 2:
89 89 # i18n: "diff" is a keyword
90 90 raise error.ParseError(_("diff expects zero, one, or two arguments"))
91 91
92 92 def getpatterns(i):
93 93 if i < len(args):
94 94 s = evalstring(context, mapping, args[i]).strip()
95 95 if s:
96 96 return [s]
97 97 return []
98 98
99 99 ctx = context.resource(mapping, 'ctx')
100 100 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
101 101
102 102 return ''.join(chunks)
103 103
104 104 @templatefunc('extdata(source)', argspec='source')
105 105 def extdata(context, mapping, args):
106 106 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
107 107 if 'source' not in args:
108 108 # i18n: "extdata" is a keyword
109 109 raise error.ParseError(_('extdata expects one argument'))
110 110
111 111 source = evalstring(context, mapping, args['source'])
112 112 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
113 113 ctx = context.resource(mapping, 'ctx')
114 114 if source in cache:
115 115 data = cache[source]
116 116 else:
117 117 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
118 118 return data.get(ctx.rev(), '')
119 119
120 120 @templatefunc('files(pattern)')
121 121 def files(context, mapping, args):
122 122 """All files of the current changeset matching the pattern. See
123 123 :hg:`help patterns`."""
124 124 if not len(args) == 1:
125 125 # i18n: "files" is a keyword
126 126 raise error.ParseError(_("files expects one argument"))
127 127
128 128 raw = evalstring(context, mapping, args[0])
129 129 ctx = context.resource(mapping, 'ctx')
130 130 m = ctx.match([raw])
131 131 files = list(ctx.matches(m))
132 132 return templateutil.compatlist(context, mapping, "file", files)
133 133
134 134 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
135 135 def fill(context, mapping, args):
136 136 """Fill many
137 137 paragraphs with optional indentation. See the "fill" filter."""
138 138 if not (1 <= len(args) <= 4):
139 139 # i18n: "fill" is a keyword
140 140 raise error.ParseError(_("fill expects one to four arguments"))
141 141
142 142 text = evalstring(context, mapping, args[0])
143 143 width = 76
144 144 initindent = ''
145 145 hangindent = ''
146 146 if 2 <= len(args) <= 4:
147 147 width = evalinteger(context, mapping, args[1],
148 148 # i18n: "fill" is a keyword
149 149 _("fill expects an integer width"))
150 150 try:
151 151 initindent = evalstring(context, mapping, args[2])
152 152 hangindent = evalstring(context, mapping, args[3])
153 153 except IndexError:
154 154 pass
155 155
156 156 return templatefilters.fill(text, width, initindent, hangindent)
157 157
158 158 @templatefunc('formatnode(node)')
159 159 def formatnode(context, mapping, args):
160 160 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
161 161 if len(args) != 1:
162 162 # i18n: "formatnode" is a keyword
163 163 raise error.ParseError(_("formatnode expects one argument"))
164 164
165 165 ui = context.resource(mapping, 'ui')
166 166 node = evalstring(context, mapping, args[0])
167 167 if ui.debugflag:
168 168 return node
169 169 return templatefilters.short(node)
170 170
171 171 @templatefunc('mailmap(author)')
172 172 def mailmap(context, mapping, args):
173 173 """Return the author, updated according to the value
174 174 set in the .mailmap file"""
175 175 if len(args) != 1:
176 176 raise error.ParseError(_("mailmap expects one argument"))
177 177
178 author = evalfuncarg(context, mapping, args[0])
178 author = evalstring(context, mapping, args[0])
179 179
180 180 cache = context.resource(mapping, 'cache')
181 181 repo = context.resource(mapping, 'repo')
182 182
183 183 if 'mailmap' not in cache:
184 184 data = repo.wvfs.tryread('.mailmap')
185 185 cache['mailmap'] = stringutil.parsemailmap(data)
186 186
187 187 return stringutil.mapname(cache['mailmap'], author)
188 188
189 189 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
190 190 argspec='text width fillchar left')
191 191 def pad(context, mapping, args):
192 192 """Pad text with a
193 193 fill character."""
194 194 if 'text' not in args or 'width' not in args:
195 195 # i18n: "pad" is a keyword
196 196 raise error.ParseError(_("pad() expects two to four arguments"))
197 197
198 198 width = evalinteger(context, mapping, args['width'],
199 199 # i18n: "pad" is a keyword
200 200 _("pad() expects an integer width"))
201 201
202 202 text = evalstring(context, mapping, args['text'])
203 203
204 204 left = False
205 205 fillchar = ' '
206 206 if 'fillchar' in args:
207 207 fillchar = evalstring(context, mapping, args['fillchar'])
208 208 if len(color.stripeffects(fillchar)) != 1:
209 209 # i18n: "pad" is a keyword
210 210 raise error.ParseError(_("pad() expects a single fill character"))
211 211 if 'left' in args:
212 212 left = evalboolean(context, mapping, args['left'])
213 213
214 214 fillwidth = width - encoding.colwidth(color.stripeffects(text))
215 215 if fillwidth <= 0:
216 216 return text
217 217 if left:
218 218 return fillchar * fillwidth + text
219 219 else:
220 220 return text + fillchar * fillwidth
221 221
222 222 @templatefunc('indent(text, indentchars[, firstline])')
223 223 def indent(context, mapping, args):
224 224 """Indents all non-empty lines
225 225 with the characters given in the indentchars string. An optional
226 226 third parameter will override the indent for the first line only
227 227 if present."""
228 228 if not (2 <= len(args) <= 3):
229 229 # i18n: "indent" is a keyword
230 230 raise error.ParseError(_("indent() expects two or three arguments"))
231 231
232 232 text = evalstring(context, mapping, args[0])
233 233 indent = evalstring(context, mapping, args[1])
234 234
235 235 if len(args) == 3:
236 236 firstline = evalstring(context, mapping, args[2])
237 237 else:
238 238 firstline = indent
239 239
240 240 # the indent function doesn't indent the first line, so we do it here
241 241 return templatefilters.indent(firstline + text, indent)
242 242
243 243 @templatefunc('get(dict, key)')
244 244 def get(context, mapping, args):
245 245 """Get an attribute/key from an object. Some keywords
246 246 are complex types. This function allows you to obtain the value of an
247 247 attribute on these types."""
248 248 if len(args) != 2:
249 249 # i18n: "get" is a keyword
250 250 raise error.ParseError(_("get() expects two arguments"))
251 251
252 252 dictarg = evalfuncarg(context, mapping, args[0])
253 253 if not util.safehasattr(dictarg, 'get'):
254 254 # i18n: "get" is a keyword
255 255 raise error.ParseError(_("get() expects a dict as first argument"))
256 256
257 257 key = evalfuncarg(context, mapping, args[1])
258 258 return templateutil.getdictitem(dictarg, key)
259 259
260 260 @templatefunc('if(expr, then[, else])')
261 261 def if_(context, mapping, args):
262 262 """Conditionally execute based on the result of
263 263 an expression."""
264 264 if not (2 <= len(args) <= 3):
265 265 # i18n: "if" is a keyword
266 266 raise error.ParseError(_("if expects two or three arguments"))
267 267
268 268 test = evalboolean(context, mapping, args[0])
269 269 if test:
270 270 return evalrawexp(context, mapping, args[1])
271 271 elif len(args) == 3:
272 272 return evalrawexp(context, mapping, args[2])
273 273
274 274 @templatefunc('ifcontains(needle, haystack, then[, else])')
275 275 def ifcontains(context, mapping, args):
276 276 """Conditionally execute based
277 277 on whether the item "needle" is in "haystack"."""
278 278 if not (3 <= len(args) <= 4):
279 279 # i18n: "ifcontains" is a keyword
280 280 raise error.ParseError(_("ifcontains expects three or four arguments"))
281 281
282 282 haystack = evalfuncarg(context, mapping, args[1])
283 283 keytype = getattr(haystack, 'keytype', None)
284 284 try:
285 285 needle = evalrawexp(context, mapping, args[0])
286 286 needle = templateutil.unwrapastype(needle, keytype or bytes)
287 287 found = (needle in haystack)
288 288 except error.ParseError:
289 289 found = False
290 290
291 291 if found:
292 292 return evalrawexp(context, mapping, args[2])
293 293 elif len(args) == 4:
294 294 return evalrawexp(context, mapping, args[3])
295 295
296 296 @templatefunc('ifeq(expr1, expr2, then[, else])')
297 297 def ifeq(context, mapping, args):
298 298 """Conditionally execute based on
299 299 whether 2 items are equivalent."""
300 300 if not (3 <= len(args) <= 4):
301 301 # i18n: "ifeq" is a keyword
302 302 raise error.ParseError(_("ifeq expects three or four arguments"))
303 303
304 304 test = evalstring(context, mapping, args[0])
305 305 match = evalstring(context, mapping, args[1])
306 306 if test == match:
307 307 return evalrawexp(context, mapping, args[2])
308 308 elif len(args) == 4:
309 309 return evalrawexp(context, mapping, args[3])
310 310
311 311 @templatefunc('join(list, sep)')
312 312 def join(context, mapping, args):
313 313 """Join items in a list with a delimiter."""
314 314 if not (1 <= len(args) <= 2):
315 315 # i18n: "join" is a keyword
316 316 raise error.ParseError(_("join expects one or two arguments"))
317 317
318 318 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
319 319 # abuses generator as a keyword that returns a list of dicts.
320 320 joinset = evalrawexp(context, mapping, args[0])
321 321 joinset = templateutil.unwrapvalue(joinset)
322 322 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
323 323 joiner = " "
324 324 if len(args) > 1:
325 325 joiner = evalstring(context, mapping, args[1])
326 326
327 327 first = True
328 328 for x in pycompat.maybebytestr(joinset):
329 329 if first:
330 330 first = False
331 331 else:
332 332 yield joiner
333 333 yield joinfmt(x)
334 334
335 335 @templatefunc('label(label, expr)')
336 336 def label(context, mapping, args):
337 337 """Apply a label to generated content. Content with
338 338 a label applied can result in additional post-processing, such as
339 339 automatic colorization."""
340 340 if len(args) != 2:
341 341 # i18n: "label" is a keyword
342 342 raise error.ParseError(_("label expects two arguments"))
343 343
344 344 ui = context.resource(mapping, 'ui')
345 345 thing = evalstring(context, mapping, args[1])
346 346 # preserve unknown symbol as literal so effects like 'red', 'bold',
347 347 # etc. don't need to be quoted
348 348 label = evalstringliteral(context, mapping, args[0])
349 349
350 350 return ui.label(thing, label)
351 351
352 352 @templatefunc('latesttag([pattern])')
353 353 def latesttag(context, mapping, args):
354 354 """The global tags matching the given pattern on the
355 355 most recent globally tagged ancestor of this changeset.
356 356 If no such tags exist, the "{tag}" template resolves to
357 357 the string "null"."""
358 358 if len(args) > 1:
359 359 # i18n: "latesttag" is a keyword
360 360 raise error.ParseError(_("latesttag expects at most one argument"))
361 361
362 362 pattern = None
363 363 if len(args) == 1:
364 364 pattern = evalstring(context, mapping, args[0])
365 365 return templatekw.showlatesttags(context, mapping, pattern)
366 366
367 367 @templatefunc('localdate(date[, tz])')
368 368 def localdate(context, mapping, args):
369 369 """Converts a date to the specified timezone.
370 370 The default is local date."""
371 371 if not (1 <= len(args) <= 2):
372 372 # i18n: "localdate" is a keyword
373 373 raise error.ParseError(_("localdate expects one or two arguments"))
374 374
375 375 date = evaldate(context, mapping, args[0],
376 376 # i18n: "localdate" is a keyword
377 377 _("localdate expects a date information"))
378 378 if len(args) >= 2:
379 379 tzoffset = None
380 380 tz = evalfuncarg(context, mapping, args[1])
381 381 if isinstance(tz, bytes):
382 382 tzoffset, remainder = dateutil.parsetimezone(tz)
383 383 if remainder:
384 384 tzoffset = None
385 385 if tzoffset is None:
386 386 try:
387 387 tzoffset = int(tz)
388 388 except (TypeError, ValueError):
389 389 # i18n: "localdate" is a keyword
390 390 raise error.ParseError(_("localdate expects a timezone"))
391 391 else:
392 392 tzoffset = dateutil.makedate()[1]
393 393 return (date[0], tzoffset)
394 394
395 395 @templatefunc('max(iterable)')
396 396 def max_(context, mapping, args, **kwargs):
397 397 """Return the max of an iterable"""
398 398 if len(args) != 1:
399 399 # i18n: "max" is a keyword
400 400 raise error.ParseError(_("max expects one argument"))
401 401
402 402 iterable = evalfuncarg(context, mapping, args[0])
403 403 try:
404 404 x = max(pycompat.maybebytestr(iterable))
405 405 except (TypeError, ValueError):
406 406 # i18n: "max" is a keyword
407 407 raise error.ParseError(_("max first argument should be an iterable"))
408 408 return templateutil.wraphybridvalue(iterable, x, x)
409 409
410 410 @templatefunc('min(iterable)')
411 411 def min_(context, mapping, args, **kwargs):
412 412 """Return the min of an iterable"""
413 413 if len(args) != 1:
414 414 # i18n: "min" is a keyword
415 415 raise error.ParseError(_("min expects one argument"))
416 416
417 417 iterable = evalfuncarg(context, mapping, args[0])
418 418 try:
419 419 x = min(pycompat.maybebytestr(iterable))
420 420 except (TypeError, ValueError):
421 421 # i18n: "min" is a keyword
422 422 raise error.ParseError(_("min first argument should be an iterable"))
423 423 return templateutil.wraphybridvalue(iterable, x, x)
424 424
425 425 @templatefunc('mod(a, b)')
426 426 def mod(context, mapping, args):
427 427 """Calculate a mod b such that a / b + a mod b == a"""
428 428 if not len(args) == 2:
429 429 # i18n: "mod" is a keyword
430 430 raise error.ParseError(_("mod expects two arguments"))
431 431
432 432 func = lambda a, b: a % b
433 433 return templateutil.runarithmetic(context, mapping,
434 434 (func, args[0], args[1]))
435 435
436 436 @templatefunc('obsfateoperations(markers)')
437 437 def obsfateoperations(context, mapping, args):
438 438 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
439 439 if len(args) != 1:
440 440 # i18n: "obsfateoperations" is a keyword
441 441 raise error.ParseError(_("obsfateoperations expects one argument"))
442 442
443 443 markers = evalfuncarg(context, mapping, args[0])
444 444
445 445 try:
446 446 data = obsutil.markersoperations(markers)
447 447 return templateutil.hybridlist(data, name='operation')
448 448 except (TypeError, KeyError):
449 449 # i18n: "obsfateoperations" is a keyword
450 450 errmsg = _("obsfateoperations first argument should be an iterable")
451 451 raise error.ParseError(errmsg)
452 452
453 453 @templatefunc('obsfatedate(markers)')
454 454 def obsfatedate(context, mapping, args):
455 455 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
456 456 if len(args) != 1:
457 457 # i18n: "obsfatedate" is a keyword
458 458 raise error.ParseError(_("obsfatedate expects one argument"))
459 459
460 460 markers = evalfuncarg(context, mapping, args[0])
461 461
462 462 try:
463 463 data = obsutil.markersdates(markers)
464 464 return templateutil.hybridlist(data, name='date', fmt='%d %d')
465 465 except (TypeError, KeyError):
466 466 # i18n: "obsfatedate" is a keyword
467 467 errmsg = _("obsfatedate first argument should be an iterable")
468 468 raise error.ParseError(errmsg)
469 469
470 470 @templatefunc('obsfateusers(markers)')
471 471 def obsfateusers(context, mapping, args):
472 472 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
473 473 if len(args) != 1:
474 474 # i18n: "obsfateusers" is a keyword
475 475 raise error.ParseError(_("obsfateusers expects one argument"))
476 476
477 477 markers = evalfuncarg(context, mapping, args[0])
478 478
479 479 try:
480 480 data = obsutil.markersusers(markers)
481 481 return templateutil.hybridlist(data, name='user')
482 482 except (TypeError, KeyError, ValueError):
483 483 # i18n: "obsfateusers" is a keyword
484 484 msg = _("obsfateusers first argument should be an iterable of "
485 485 "obsmakers")
486 486 raise error.ParseError(msg)
487 487
488 488 @templatefunc('obsfateverb(successors, markers)')
489 489 def obsfateverb(context, mapping, args):
490 490 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
491 491 if len(args) != 2:
492 492 # i18n: "obsfateverb" is a keyword
493 493 raise error.ParseError(_("obsfateverb expects two arguments"))
494 494
495 495 successors = evalfuncarg(context, mapping, args[0])
496 496 markers = evalfuncarg(context, mapping, args[1])
497 497
498 498 try:
499 499 return obsutil.obsfateverb(successors, markers)
500 500 except TypeError:
501 501 # i18n: "obsfateverb" is a keyword
502 502 errmsg = _("obsfateverb first argument should be countable")
503 503 raise error.ParseError(errmsg)
504 504
505 505 @templatefunc('relpath(path)')
506 506 def relpath(context, mapping, args):
507 507 """Convert a repository-absolute path into a filesystem path relative to
508 508 the current working directory."""
509 509 if len(args) != 1:
510 510 # i18n: "relpath" is a keyword
511 511 raise error.ParseError(_("relpath expects one argument"))
512 512
513 513 repo = context.resource(mapping, 'ctx').repo()
514 514 path = evalstring(context, mapping, args[0])
515 515 return repo.pathto(path)
516 516
517 517 @templatefunc('revset(query[, formatargs...])')
518 518 def revset(context, mapping, args):
519 519 """Execute a revision set query. See
520 520 :hg:`help revset`."""
521 521 if not len(args) > 0:
522 522 # i18n: "revset" is a keyword
523 523 raise error.ParseError(_("revset expects one or more arguments"))
524 524
525 525 raw = evalstring(context, mapping, args[0])
526 526 ctx = context.resource(mapping, 'ctx')
527 527 repo = ctx.repo()
528 528
529 529 def query(expr):
530 530 m = revsetmod.match(repo.ui, expr, repo=repo)
531 531 return m(repo)
532 532
533 533 if len(args) > 1:
534 534 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
535 535 revs = query(revsetlang.formatspec(raw, *formatargs))
536 536 revs = list(revs)
537 537 else:
538 538 cache = context.resource(mapping, 'cache')
539 539 revsetcache = cache.setdefault("revsetcache", {})
540 540 if raw in revsetcache:
541 541 revs = revsetcache[raw]
542 542 else:
543 543 revs = query(raw)
544 544 revs = list(revs)
545 545 revsetcache[raw] = revs
546 546 return templatekw.showrevslist(context, mapping, "revision", revs)
547 547
548 548 @templatefunc('rstdoc(text, style)')
549 549 def rstdoc(context, mapping, args):
550 550 """Format reStructuredText."""
551 551 if len(args) != 2:
552 552 # i18n: "rstdoc" is a keyword
553 553 raise error.ParseError(_("rstdoc expects two arguments"))
554 554
555 555 text = evalstring(context, mapping, args[0])
556 556 style = evalstring(context, mapping, args[1])
557 557
558 558 return minirst.format(text, style=style, keep=['verbose'])
559 559
560 560 @templatefunc('separate(sep, args)', argspec='sep *args')
561 561 def separate(context, mapping, args):
562 562 """Add a separator between non-empty arguments."""
563 563 if 'sep' not in args:
564 564 # i18n: "separate" is a keyword
565 565 raise error.ParseError(_("separate expects at least one argument"))
566 566
567 567 sep = evalstring(context, mapping, args['sep'])
568 568 first = True
569 569 for arg in args['args']:
570 570 argstr = evalstring(context, mapping, arg)
571 571 if not argstr:
572 572 continue
573 573 if first:
574 574 first = False
575 575 else:
576 576 yield sep
577 577 yield argstr
578 578
579 579 @templatefunc('shortest(node, minlength=4)')
580 580 def shortest(context, mapping, args):
581 581 """Obtain the shortest representation of
582 582 a node."""
583 583 if not (1 <= len(args) <= 2):
584 584 # i18n: "shortest" is a keyword
585 585 raise error.ParseError(_("shortest() expects one or two arguments"))
586 586
587 587 node = evalstring(context, mapping, args[0])
588 588
589 589 minlength = 4
590 590 if len(args) > 1:
591 591 minlength = evalinteger(context, mapping, args[1],
592 592 # i18n: "shortest" is a keyword
593 593 _("shortest() expects an integer minlength"))
594 594
595 595 # _partialmatch() of filtered changelog could take O(len(repo)) time,
596 596 # which would be unacceptably slow. so we look for hash collision in
597 597 # unfiltered space, which means some hashes may be slightly longer.
598 598 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
599 599 return cl.shortest(node, minlength)
600 600
601 601 @templatefunc('strip(text[, chars])')
602 602 def strip(context, mapping, args):
603 603 """Strip characters from a string. By default,
604 604 strips all leading and trailing whitespace."""
605 605 if not (1 <= len(args) <= 2):
606 606 # i18n: "strip" is a keyword
607 607 raise error.ParseError(_("strip expects one or two arguments"))
608 608
609 609 text = evalstring(context, mapping, args[0])
610 610 if len(args) == 2:
611 611 chars = evalstring(context, mapping, args[1])
612 612 return text.strip(chars)
613 613 return text.strip()
614 614
615 615 @templatefunc('sub(pattern, replacement, expression)')
616 616 def sub(context, mapping, args):
617 617 """Perform text substitution
618 618 using regular expressions."""
619 619 if len(args) != 3:
620 620 # i18n: "sub" is a keyword
621 621 raise error.ParseError(_("sub expects three arguments"))
622 622
623 623 pat = evalstring(context, mapping, args[0])
624 624 rpl = evalstring(context, mapping, args[1])
625 625 src = evalstring(context, mapping, args[2])
626 626 try:
627 627 patre = re.compile(pat)
628 628 except re.error:
629 629 # i18n: "sub" is a keyword
630 630 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
631 631 try:
632 632 yield patre.sub(rpl, src)
633 633 except re.error:
634 634 # i18n: "sub" is a keyword
635 635 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
636 636
637 637 @templatefunc('startswith(pattern, text)')
638 638 def startswith(context, mapping, args):
639 639 """Returns the value from the "text" argument
640 640 if it begins with the content from the "pattern" argument."""
641 641 if len(args) != 2:
642 642 # i18n: "startswith" is a keyword
643 643 raise error.ParseError(_("startswith expects two arguments"))
644 644
645 645 patn = evalstring(context, mapping, args[0])
646 646 text = evalstring(context, mapping, args[1])
647 647 if text.startswith(patn):
648 648 return text
649 649 return ''
650 650
651 651 @templatefunc('word(number, text[, separator])')
652 652 def word(context, mapping, args):
653 653 """Return the nth word from a string."""
654 654 if not (2 <= len(args) <= 3):
655 655 # i18n: "word" is a keyword
656 656 raise error.ParseError(_("word expects two or three arguments, got %d")
657 657 % len(args))
658 658
659 659 num = evalinteger(context, mapping, args[0],
660 660 # i18n: "word" is a keyword
661 661 _("word expects an integer index"))
662 662 text = evalstring(context, mapping, args[1])
663 663 if len(args) == 3:
664 664 splitter = evalstring(context, mapping, args[2])
665 665 else:
666 666 splitter = None
667 667
668 668 tokens = text.split(splitter)
669 669 if num >= len(tokens) or num < -len(tokens):
670 670 return ''
671 671 else:
672 672 return tokens[num]
673 673
674 674 def loadfunction(ui, extname, registrarobj):
675 675 """Load template function from specified registrarobj
676 676 """
677 677 for name, func in registrarobj._table.iteritems():
678 678 funcs[name] = func
679 679
680 680 # tell hggettext to extract docstrings from these functions:
681 681 i18nfunctions = funcs.values()
@@ -1,67 +1,72 b''
1 1 Create a repo and add some commits
2 2
3 3 $ hg init mm
4 4 $ cd mm
5 5 $ echo "Test content" > testfile1
6 6 $ hg add testfile1
7 7 $ hg commit -m "First commit" -u "Proper <commit@m.c>"
8 8 $ echo "Test content 2" > testfile2
9 9 $ hg add testfile2
10 10 $ hg commit -m "Second commit" -u "Commit Name 2 <commit2@m.c>"
11 11 $ echo "Test content 3" > testfile3
12 12 $ hg add testfile3
13 13 $ hg commit -m "Third commit" -u "Commit Name 3 <commit3@m.c>"
14 14 $ echo "Test content 4" > testfile4
15 15 $ hg add testfile4
16 16 $ hg commit -m "Fourth commit" -u "Commit Name 4 <commit4@m.c>"
17 17
18 18 Add a .mailmap file with each possible entry type plus comments
19 19 $ cat > .mailmap << EOF
20 20 > # Comment shouldn't break anything
21 21 > <proper@m.c> <commit@m.c> # Should update email only
22 22 > Proper Name 2 <commit2@m.c> # Should update name only
23 23 > Proper Name 3 <proper@m.c> <commit3@m.c> # Should update name, email due to email
24 24 > Proper Name 4 <proper@m.c> Commit Name 4 <commit4@m.c> # Should update name, email due to name, email
25 25 > EOF
26 26 $ hg add .mailmap
27 27 $ hg commit -m "Add mailmap file" -u "Testuser <test123@m.c>"
28 28
29 29 Output of commits should be normal without filter
30 30 $ hg log -T "{author}\n" -r "all()"
31 31 Proper <commit@m.c>
32 32 Commit Name 2 <commit2@m.c>
33 33 Commit Name 3 <commit3@m.c>
34 34 Commit Name 4 <commit4@m.c>
35 35 Testuser <test123@m.c>
36 36
37 37 Output of commits with filter shows their mailmap values
38 38 $ hg log -T "{mailmap(author)}\n" -r "all()"
39 39 Proper <proper@m.c>
40 40 Proper Name 2 <commit2@m.c>
41 41 Proper Name 3 <proper@m.c>
42 42 Proper Name 4 <proper@m.c>
43 43 Testuser <test123@m.c>
44 44
45 45 Add new mailmap entry for testuser
46 46 $ cat >> .mailmap << EOF
47 47 > <newmmentry@m.c> <test123@m.c>
48 48 > EOF
49 49
50 50 Output of commits with filter shows their updated mailmap values
51 51 $ hg log -T "{mailmap(author)}\n" -r "all()"
52 52 Proper <proper@m.c>
53 53 Proper Name 2 <commit2@m.c>
54 54 Proper Name 3 <proper@m.c>
55 55 Proper Name 4 <proper@m.c>
56 56 Testuser <newmmentry@m.c>
57 57
58 58 A commit with improperly formatted user field should not break the filter
59 59 $ echo "some more test content" > testfile1
60 60 $ hg commit -m "Commit with improper user field" -u "Improper user"
61 61 $ hg log -T "{mailmap(author)}\n" -r "all()"
62 62 Proper <proper@m.c>
63 63 Proper Name 2 <commit2@m.c>
64 64 Proper Name 3 <proper@m.c>
65 65 Proper Name 4 <proper@m.c>
66 66 Testuser <newmmentry@m.c>
67 67 Improper user
68
69 No TypeError beacause of invalid input
70
71 $ hg log -T '{mailmap(termwidth)}\n' -r0
72 80
General Comments 0
You need to be logged in to leave comments. Login now