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