1# templatefuncs.py - common template functions 2# 3# Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8from __future__ import absolute_import 9 10import re 11 12from .i18n import _ 13from .node import bin 14from . import ( 15 color, 16 dagop, 17 diffutil, 18 encoding, 19 error, 20 minirst, 21 obsutil, 22 pycompat, 23 registrar, 24 revset as revsetmod, 25 revsetlang, 26 scmutil, 27 templatefilters, 28 templatekw, 29 templateutil, 30 util, 31) 32from .utils import ( 33 dateutil, 34 stringutil, 35) 36 37evalrawexp = templateutil.evalrawexp 38evalwrapped = templateutil.evalwrapped 39evalfuncarg = templateutil.evalfuncarg 40evalboolean = templateutil.evalboolean 41evaldate = templateutil.evaldate 42evalinteger = templateutil.evalinteger 43evalstring = templateutil.evalstring 44evalstringliteral = templateutil.evalstringliteral 45 46# dict of template built-in functions 47funcs = {} 48templatefunc = registrar.templatefunc(funcs) 49 50 51@templatefunc(b'date(date[, fmt])') 52def date(context, mapping, args): 53 """Format a date. See :hg:`help dates` for formatting 54 strings. The default is a Unix date format, including the timezone: 55 "Mon Sep 04 15:13:13 2006 0700".""" 56 if not (1 <= len(args) <= 2): 57 # i18n: "date" is a keyword 58 raise error.ParseError(_(b"date expects one or two arguments")) 59 60 date = evaldate( 61 context, 62 mapping, 63 args[0], 64 # i18n: "date" is a keyword 65 _(b"date expects a date information"), 66 ) 67 fmt = None 68 if len(args) == 2: 69 fmt = evalstring(context, mapping, args[1]) 70 if fmt is None: 71 return dateutil.datestr(date) 72 else: 73 return dateutil.datestr(date, fmt) 74 75 76@templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs') 77def dict_(context, mapping, args): 78 """Construct a dict from key-value pairs. A key may be omitted if 79 a value expression can provide an unambiguous name.""" 80 data = util.sortdict() 81 82 for v in args[b'args']: 83 k = templateutil.findsymbolicname(v) 84 if not k: 85 raise error.ParseError(_(b'dict key cannot be inferred')) 86 if k in data or k in args[b'kwargs']: 87 raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k) 88 data[k] = evalfuncarg(context, mapping, v) 89 90 data.update( 91 (k, evalfuncarg(context, mapping, v)) 92 for k, v in pycompat.iteritems(args[b'kwargs']) 93 ) 94 return templateutil.hybriddict(data) 95 96 97@templatefunc( 98 b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'} 99) 100def diff(context, mapping, args): 101 """Show a diff, optionally 102 specifying files to include or exclude.""" 103 if len(args) > 2: 104 # i18n: "diff" is a keyword 105 raise error.ParseError(_(b"diff expects zero, one, or two arguments")) 106 107 def getpatterns(i): 108 if i < len(args): 109 s = evalstring(context, mapping, args[i]).strip() 110 if s: 111 return [s] 112 return [] 113 114 ctx = context.resource(mapping, b'ctx') 115 ui = context.resource(mapping, b'ui') 116 diffopts = diffutil.diffallopts(ui) 117 chunks = ctx.diff( 118 match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts 119 ) 120 121 return b''.join(chunks) 122 123 124@templatefunc( 125 b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'} 126) 127def extdata(context, mapping, args): 128 """Show a text read from the specified extdata source. (EXPERIMENTAL)""" 129 if b'source' not in args: 130 # i18n: "extdata" is a keyword 131 raise error.ParseError(_(b'extdata expects one argument')) 132 133 source = evalstring(context, mapping, args[b'source']) 134 if not source: 135 sym = templateutil.findsymbolicname(args[b'source']) 136 if sym: 137 raise error.ParseError( 138 _(b'empty data source specified'), 139 hint=_(b"did you mean extdata('%s')?") % sym, 140 ) 141 else: 142 raise error.ParseError(_(b'empty data source specified')) 143 cache = context.resource(mapping, b'cache').setdefault(b'extdata', {}) 144 ctx = context.resource(mapping, b'ctx') 145 if source in cache: 146 data = cache[source] 147 else: 148 data = cache[source] = scmutil.extdatasource(ctx.repo(), source) 149 return data.get(ctx.rev(), b'') 150 151 152@templatefunc(b'files(pattern)', requires={b'ctx'}) 153def files(context, mapping, args): 154 """All files of the current changeset matching the pattern. See 155 :hg:`help patterns`.""" 156 if not len(args) == 1: 157 # i18n: "files" is a keyword 158 raise error.ParseError(_(b"files expects one argument")) 159 160 raw = evalstring(context, mapping, args[0]) 161 ctx = context.resource(mapping, b'ctx') 162 m = ctx.match([raw]) 163 files = list(ctx.matches(m)) 164 return templateutil.compatfileslist(context, mapping, b"file", files) 165 166 167@templatefunc(b'fill(text[, width[, initialident[, hangindent]]])') 168def fill(context, mapping, args): 169 """Fill many 170 paragraphs with optional indentation. See the "fill" filter.""" 171 if not (1 <= len(args) <= 4): 172 # i18n: "fill" is a keyword 173 raise error.ParseError(_(b"fill expects one to four arguments")) 174 175 text = evalstring(context, mapping, args[0]) 176 width = 76 177 initindent = b'' 178 hangindent = b'' 179 if 2 <= len(args) <= 4: 180 width = evalinteger( 181 context, 182 mapping, 183 args[1], 184 # i18n: "fill" is a keyword 185 _(b"fill expects an integer width"), 186 ) 187 try: 188 initindent = evalstring(context, mapping, args[2]) 189 hangindent = evalstring(context, mapping, args[3]) 190 except IndexError: 191 pass 192 193 return templatefilters.fill(text, width, initindent, hangindent) 194 195 196@templatefunc(b'filter(iterable[, expr])') 197def filter_(context, mapping, args): 198 """Remove empty elements from a list or a dict. If expr specified, it's 199 applied to each element to test emptiness.""" 200 if not (1 <= len(args) <= 2): 201 # i18n: "filter" is a keyword 202 raise error.ParseError(_(b"filter expects one or two arguments")) 203 iterable = evalwrapped(context, mapping, args[0]) 204 if len(args) == 1: 205 206 def select(w): 207 return w.tobool(context, mapping) 208 209 else: 210 211 def select(w): 212 if not isinstance(w, templateutil.mappable): 213 raise error.ParseError(_(b"not filterable by expression")) 214 lm = context.overlaymap(mapping, w.tomap(context)) 215 return evalboolean(context, lm, args[1]) 216 217 return iterable.filter(context, mapping, select) 218 219 220@templatefunc(b'formatnode(node)', requires={b'ui'}) 221def formatnode(context, mapping, args): 222 """Obtain the preferred form of a changeset hash. (DEPRECATED)""" 223 if len(args) != 1: 224 # i18n: "formatnode" is a keyword 225 raise error.ParseError(_(b"formatnode expects one argument")) 226 227 ui = context.resource(mapping, b'ui') 228 node = evalstring(context, mapping, args[0]) 229 if ui.debugflag: 230 return node 231 return templatefilters.short(node) 232 233 234@templatefunc(b'mailmap(author)', requires={b'repo', b'cache'}) 235def mailmap(context, mapping, args): 236 """Return the author, updated according to the value 237 set in the .mailmap file""" 238 if len(args) != 1: 239 raise error.ParseError(_(b"mailmap expects one argument")) 240 241 author = evalstring(context, mapping, args[0]) 242 243 cache = context.resource(mapping, b'cache') 244 repo = context.resource(mapping, b'repo') 245 246 if b'mailmap' not in cache: 247 data = repo.wvfs.tryread(b'.mailmap') 248 cache[b'mailmap'] = stringutil.parsemailmap(data) 249 250 return stringutil.mapname(cache[b'mailmap'], author) 251 252 253@templatefunc( 254 b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])', 255 argspec=b'text width fillchar left truncate', 256) 257def pad(context, mapping, args): 258 """Pad text with a 259 fill character.""" 260 if b'text' not in args or b'width' not in args: 261 # i18n: "pad" is a keyword 262 raise error.ParseError(_(b"pad() expects two to four arguments")) 263 264 width = evalinteger( 265 context, 266 mapping, 267 args[b'width'], 268 # i18n: "pad" is a keyword 269 _(b"pad() expects an integer width"), 270 ) 271 272 text = evalstring(context, mapping, args[b'text']) 273 274 truncate = False 275 left = False 276 fillchar = b' ' 277 if b'fillchar' in args: 278 fillchar = evalstring(context, mapping, args[b'fillchar']) 279 if len(color.stripeffects(fillchar)) != 1: 280 # i18n: "pad" is a keyword 281 raise error.ParseError(_(b"pad() expects a single fill character")) 282 if b'left' in args: 283 left = evalboolean(context, mapping, args[b'left']) 284 if b'truncate' in args: 285 truncate = evalboolean(context, mapping, args[b'truncate']) 286 287 fillwidth = width - encoding.colwidth(color.stripeffects(text)) 288 if fillwidth < 0 and truncate: 289 return encoding.trim(color.stripeffects(text), width, leftside=left) 290 if fillwidth <= 0: 291 return text 292 if left: 293 return fillchar * fillwidth + text 294 else: 295 return text + fillchar * fillwidth 296 297 298@templatefunc(b'indent(text, indentchars[, firstline])') 299def indent(context, mapping, args): 300 """Indents all non-empty lines 301 with the characters given in the indentchars string. An optional 302 third parameter will override the indent for the first line only 303 if present.""" 304 if not (2 <= len(args) <= 3): 305 # i18n: "indent" is a keyword 306 raise error.ParseError(_(b"indent() expects two or three arguments")) 307 308 text = evalstring(context, mapping, args[0]) 309 indent = evalstring(context, mapping, args[1]) 310 311 firstline = indent 312 if len(args) == 3: 313 firstline = evalstring(context, mapping, args[2]) 314 315 return templatefilters.indent(text, indent, firstline=firstline) 316 317 318@templatefunc(b'get(dict, key)') 319def get(context, mapping, args): 320 """Get an attribute/key from an object. Some keywords 321 are complex types. This function allows you to obtain the value of an 322 attribute on these types.""" 323 if len(args) != 2: 324 # i18n: "get" is a keyword 325 raise error.ParseError(_(b"get() expects two arguments")) 326 327 dictarg = evalwrapped(context, mapping, args[0]) 328 key = evalrawexp(context, mapping, args[1]) 329 try: 330 return dictarg.getmember(context, mapping, key) 331 except error.ParseError as err: 332 # i18n: "get" is a keyword 333 hint = _(b"get() expects a dict as first argument") 334 raise error.ParseError(bytes(err), hint=hint) 335 336 337@templatefunc(b'config(section, name[, default])', requires={b'ui'}) 338def config(context, mapping, args): 339 """Returns the requested hgrc config option as a string.""" 340 fn = context.resource(mapping, b'ui').config 341 return _config(context, mapping, args, fn, evalstring) 342 343 344@templatefunc(b'configbool(section, name[, default])', requires={b'ui'}) 345def configbool(context, mapping, args): 346 """Returns the requested hgrc config option as a boolean.""" 347 fn = context.resource(mapping, b'ui').configbool 348 return _config(context, mapping, args, fn, evalboolean) 349 350 351@templatefunc(b'configint(section, name[, default])', requires={b'ui'}) 352def configint(context, mapping, args): 353 """Returns the requested hgrc config option as an integer.""" 354 fn = context.resource(mapping, b'ui').configint 355 return _config(context, mapping, args, fn, evalinteger) 356 357 358def _config(context, mapping, args, configfn, defaultfn): 359 if not (2 <= len(args) <= 3): 360 raise error.ParseError(_(b"config expects two or three arguments")) 361 362 # The config option can come from any section, though we specifically 363 # reserve the [templateconfig] section for dynamically defining options 364 # for this function without also requiring an extension. 365 section = evalstringliteral(context, mapping, args[0]) 366 name = evalstringliteral(context, mapping, args[1]) 367 if len(args) == 3: 368 default = defaultfn(context, mapping, args[2]) 369 return configfn(section, name, default) 370 else: 371 return configfn(section, name) 372 373 374@templatefunc(b'if(expr, then[, else])') 375def if_(context, mapping, args): 376 """Conditionally execute based on the result of 377 an expression.""" 378 if not (2 <= len(args) <= 3): 379 # i18n: "if" is a keyword 380 raise error.ParseError(_(b"if expects two or three arguments")) 381 382 test = evalboolean(context, mapping, args[0]) 383 if test: 384 return evalrawexp(context, mapping, args[1]) 385 elif len(args) == 3: 386 return evalrawexp(context, mapping, args[2]) 387 388 389@templatefunc(b'ifcontains(needle, haystack, then[, else])') 390def ifcontains(context, mapping, args): 391 """Conditionally execute based 392 on whether the item "needle" is in "haystack".""" 393 if not (3 <= len(args) <= 4): 394 # i18n: "ifcontains" is a keyword 395 raise error.ParseError(_(b"ifcontains expects three or four arguments")) 396 397 haystack = evalwrapped(context, mapping, args[1]) 398 try: 399 needle = evalrawexp(context, mapping, args[0]) 400 found = haystack.contains(context, mapping, needle) 401 except error.ParseError: 402 found = False 403 404 if found: 405 return evalrawexp(context, mapping, args[2]) 406 elif len(args) == 4: 407 return evalrawexp(context, mapping, args[3]) 408 409 410@templatefunc(b'ifeq(expr1, expr2, then[, else])') 411def ifeq(context, mapping, args): 412 """Conditionally execute based on 413 whether 2 items are equivalent.""" 414 if not (3 <= len(args) <= 4): 415 # i18n: "ifeq" is a keyword 416 raise error.ParseError(_(b"ifeq expects three or four arguments")) 417 418 test = evalstring(context, mapping, args[0]) 419 match = evalstring(context, mapping, args[1]) 420 if test == match: 421 return evalrawexp(context, mapping, args[2]) 422 elif len(args) == 4: 423 return evalrawexp(context, mapping, args[3]) 424 425 426@templatefunc(b'join(list, sep)') 427def join(context, mapping, args): 428 """Join items in a list with a delimiter.""" 429 if not (1 <= len(args) <= 2): 430 # i18n: "join" is a keyword 431 raise error.ParseError(_(b"join expects one or two arguments")) 432 433 joinset = evalwrapped(context, mapping, args[0]) 434 joiner = b" " 435 if len(args) > 1: 436 joiner = evalstring(context, mapping, args[1]) 437 return joinset.join(context, mapping, joiner) 438 439 440@templatefunc(b'label(label, expr)', requires={b'ui'}) 441def label(context, mapping, args): 442 """Apply a label to generated content. Content with 443 a label applied can result in additional post-processing, such as 444 automatic colorization.""" 445 if len(args) != 2: 446 # i18n: "label" is a keyword 447 raise error.ParseError(_(b"label expects two arguments")) 448 449 ui = context.resource(mapping, b'ui') 450 thing = evalstring(context, mapping, args[1]) 451 # preserve unknown symbol as literal so effects like 'red', 'bold', 452 # etc. don't need to be quoted 453 label = evalstringliteral(context, mapping, args[0]) 454 455 return ui.label(thing, label) 456 457 458@templatefunc(b'latesttag([pattern])') 459def latesttag(context, mapping, args): 460 """The global tags matching the given pattern on the 461 most recent globally tagged ancestor of this changeset. 462 If no such tags exist, the "{tag}" template resolves to 463 the string "null". See :hg:`help revisions.patterns` for the pattern 464 syntax. 465 """ 466 if len(args) > 1: 467 # i18n: "latesttag" is a keyword 468 raise error.ParseError(_(b"latesttag expects at most one argument")) 469 470 pattern = None 471 if len(args) == 1: 472 pattern = evalstring(context, mapping, args[0]) 473 return templatekw.showlatesttags(context, mapping, pattern) 474 475 476@templatefunc(b'localdate(date[, tz])') 477def localdate(context, mapping, args): 478 """Converts a date to the specified timezone. 479 The default is local date.""" 480 if not (1 <= len(args) <= 2): 481 # i18n: "localdate" is a keyword 482 raise error.ParseError(_(b"localdate expects one or two arguments")) 483 484 date = evaldate( 485 context, 486 mapping, 487 args[0], 488 # i18n: "localdate" is a keyword 489 _(b"localdate expects a date information"), 490 ) 491 if len(args) >= 2: 492 tzoffset = None 493 tz = evalfuncarg(context, mapping, args[1]) 494 if isinstance(tz, bytes): 495 tzoffset, remainder = dateutil.parsetimezone(tz) 496 if remainder: 497 tzoffset = None 498 if tzoffset is None: 499 try: 500 tzoffset = int(tz) 501 except (TypeError, ValueError): 502 # i18n: "localdate" is a keyword 503 raise error.ParseError(_(b"localdate expects a timezone")) 504 else: 505 tzoffset = dateutil.makedate()[1] 506 return templateutil.date((date[0], tzoffset)) 507 508 509@templatefunc(b'max(iterable)') 510def max_(context, mapping, args, **kwargs): 511 """Return the max of an iterable""" 512 if len(args) != 1: 513 # i18n: "max" is a keyword 514 raise error.ParseError(_(b"max expects one argument")) 515 516 iterable = evalwrapped(context, mapping, args[0]) 517 try: 518 return iterable.getmax(context, mapping) 519 except error.ParseError as err: 520 # i18n: "max" is a keyword 521 hint = _(b"max first argument should be an iterable") 522 raise error.ParseError(bytes(err), hint=hint) 523 524 525@templatefunc(b'min(iterable)') 526def min_(context, mapping, args, **kwargs): 527 """Return the min of an iterable""" 528 if len(args) != 1: 529 # i18n: "min" is a keyword 530 raise error.ParseError(_(b"min expects one argument")) 531 532 iterable = evalwrapped(context, mapping, args[0]) 533 try: 534 return iterable.getmin(context, mapping) 535 except error.ParseError as err: 536 # i18n: "min" is a keyword 537 hint = _(b"min first argument should be an iterable") 538 raise error.ParseError(bytes(err), hint=hint) 539 540 541@templatefunc(b'mod(a, b)') 542def mod(context, mapping, args): 543 """Calculate a mod b such that a / b + a mod b == a""" 544 if not len(args) == 2: 545 # i18n: "mod" is a keyword 546 raise error.ParseError(_(b"mod expects two arguments")) 547 548 func = lambda a, b: a % b 549 return templateutil.runarithmetic( 550 context, mapping, (func, args[0], args[1]) 551 ) 552 553 554@templatefunc(b'obsfateoperations(markers)') 555def obsfateoperations(context, mapping, args): 556 """Compute obsfate related information based on markers (EXPERIMENTAL)""" 557 if len(args) != 1: 558 # i18n: "obsfateoperations" is a keyword 559 raise error.ParseError(_(b"obsfateoperations expects one argument")) 560 561 markers = evalfuncarg(context, mapping, args[0]) 562 563 try: 564 data = obsutil.markersoperations(markers) 565 return templateutil.hybridlist(data, name=b'operation') 566 except (TypeError, KeyError): 567 # i18n: "obsfateoperations" is a keyword 568 errmsg = _(b"obsfateoperations first argument should be an iterable") 569 raise error.ParseError(errmsg) 570 571 572@templatefunc(b'obsfatedate(markers)') 573def obsfatedate(context, mapping, args): 574 """Compute obsfate related information based on markers (EXPERIMENTAL)""" 575 if len(args) != 1: 576 # i18n: "obsfatedate" is a keyword 577 raise error.ParseError(_(b"obsfatedate expects one argument")) 578 579 markers = evalfuncarg(context, mapping, args[0]) 580 581 try: 582 # TODO: maybe this has to be a wrapped list of date wrappers? 583 data = obsutil.markersdates(markers) 584 return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d') 585 except (TypeError, KeyError): 586 # i18n: "obsfatedate" is a keyword 587 errmsg = _(b"obsfatedate first argument should be an iterable") 588 raise error.ParseError(errmsg) 589 590 591@templatefunc(b'obsfateusers(markers)') 592def obsfateusers(context, mapping, args): 593 """Compute obsfate related information based on markers (EXPERIMENTAL)""" 594 if len(args) != 1: 595 # i18n: "obsfateusers" is a keyword 596 raise error.ParseError(_(b"obsfateusers expects one argument")) 597 598 markers = evalfuncarg(context, mapping, args[0]) 599 600 try: 601 data = obsutil.markersusers(markers) 602 return templateutil.hybridlist(data, name=b'user') 603 except (TypeError, KeyError, ValueError): 604 # i18n: "obsfateusers" is a keyword 605 msg = _( 606 b"obsfateusers first argument should be an iterable of " 607 b"obsmakers" 608 ) 609 raise error.ParseError(msg) 610 611 612@templatefunc(b'obsfateverb(successors, markers)') 613def obsfateverb(context, mapping, args): 614 """Compute obsfate related information based on successors (EXPERIMENTAL)""" 615 if len(args) != 2: 616 # i18n: "obsfateverb" is a keyword 617 raise error.ParseError(_(b"obsfateverb expects two arguments")) 618 619 successors = evalfuncarg(context, mapping, args[0]) 620 markers = evalfuncarg(context, mapping, args[1]) 621 622 try: 623 return obsutil.obsfateverb(successors, markers) 624 except TypeError: 625 # i18n: "obsfateverb" is a keyword 626 errmsg = _(b"obsfateverb first argument should be countable") 627 raise error.ParseError(errmsg) 628 629 630@templatefunc(b'relpath(path)', requires={b'repo'}) 631def relpath(context, mapping, args): 632 """Convert a repository-absolute path into a filesystem path relative to 633 the current working directory.""" 634 if len(args) != 1: 635 # i18n: "relpath" is a keyword 636 raise error.ParseError(_(b"relpath expects one argument")) 637 638 repo = context.resource(mapping, b'repo') 639 path = evalstring(context, mapping, args[0]) 640 return repo.pathto(path) 641 642 643@templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'}) 644def revset(context, mapping, args): 645 """Execute a revision set query. See 646 :hg:`help revset`.""" 647 if not len(args) > 0: 648 # i18n: "revset" is a keyword 649 raise error.ParseError(_(b"revset expects one or more arguments")) 650 651 raw = evalstring(context, mapping, args[0]) 652 repo = context.resource(mapping, b'repo') 653 654 def query(expr): 655 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo)) 656 return m(repo) 657 658 if len(args) > 1: 659 key = None # dynamically-created revs shouldn't be cached 660 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]] 661 revs = query(revsetlang.formatspec(raw, *formatargs)) 662 else: 663 cache = context.resource(mapping, b'cache') 664 revsetcache = cache.setdefault(b"revsetcache", {}) 665 key = raw 666 if key in revsetcache: 667 revs = revsetcache[key] 668 else: 669 revs = query(raw) 670 revsetcache[key] = revs 671 return templateutil.revslist(repo, revs, name=b'revision', cachekey=key) 672 673 674@templatefunc(b'rstdoc(text, style)') 675def rstdoc(context, mapping, args): 676 """Format reStructuredText.""" 677 if len(args) != 2: 678 # i18n: "rstdoc" is a keyword 679 raise error.ParseError(_(b"rstdoc expects two arguments")) 680 681 text = evalstring(context, mapping, args[0]) 682 style = evalstring(context, mapping, args[1]) 683 684 return minirst.format(text, style=style, keep=[b'verbose']) 685 686 687@templatefunc(b'search(pattern, text)') 688def search(context, mapping, args): 689 """Look for the first text matching the regular expression pattern. 690 Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template.""" 691 if len(args) != 2: 692 # i18n: "search" is a keyword 693 raise error.ParseError(_(b'search expects two arguments')) 694 695 pat = evalstring(context, mapping, args[0]) 696 src = evalstring(context, mapping, args[1]) 697 try: 698 patre = re.compile(pat) 699 except re.error: 700 # i18n: "search" is a keyword 701 raise error.ParseError(_(b'search got an invalid pattern: %s') % pat) 702 # named groups shouldn't shadow *reserved* resource keywords 703 badgroups = context.knownresourcekeys() & set( 704 pycompat.byteskwargs(patre.groupindex) 705 ) 706 if badgroups: 707 raise error.ParseError( 708 # i18n: "search" is a keyword 709 _(b'invalid group %(group)s in search pattern: %(pat)s') 710 % { 711 b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)), 712 b'pat': pat, 713 } 714 ) 715 716 match = patre.search(src) 717 if not match: 718 return templateutil.mappingnone() 719 720 lm = {b'0': match.group(0)} 721 lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1)) 722 lm.update(pycompat.byteskwargs(match.groupdict())) 723 return templateutil.mappingdict(lm, tmpl=b'{0}') 724 725 726@templatefunc(b'separate(sep, args...)', argspec=b'sep *args') 727def separate(context, mapping, args): 728 """Add a separator between non-empty arguments.""" 729 if b'sep' not in args: 730 # i18n: "separate" is a keyword 731 raise error.ParseError(_(b"separate expects at least one argument")) 732 733 sep = evalstring(context, mapping, args[b'sep']) 734 first = True 735 for arg in args[b'args']: 736 argstr = evalstring(context, mapping, arg) 737 if not argstr: 738 continue 739 if first: 740 first = False 741 else: 742 yield sep 743 yield argstr 744 745 746@templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'}) 747def shortest(context, mapping, args): 748 """Obtain the shortest representation of 749 a node.""" 750 if not (1 <= len(args) <= 2): 751 # i18n: "shortest" is a keyword 752 raise error.ParseError(_(b"shortest() expects one or two arguments")) 753 754 hexnode = evalstring(context, mapping, args[0]) 755 756 minlength = 4 757 if len(args) > 1: 758 minlength = evalinteger( 759 context, 760 mapping, 761 args[1], 762 # i18n: "shortest" is a keyword 763 _(b"shortest() expects an integer minlength"), 764 ) 765 766 repo = context.resource(mapping, b'repo') 767 hexnodelen = 2 * repo.nodeconstants.nodelen 768 if len(hexnode) > hexnodelen: 769 return hexnode 770 elif len(hexnode) == hexnodelen: 771 try: 772 node = bin(hexnode) 773 except TypeError: 774 return hexnode 775 else: 776 try: 777 node = scmutil.resolvehexnodeidprefix(repo, hexnode) 778 except error.WdirUnsupported: 779 node = repo.nodeconstants.wdirid 780 except error.LookupError: 781 return hexnode 782 if not node: 783 return hexnode 784 cache = context.resource(mapping, b'cache') 785 try: 786 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache) 787 except error.RepoLookupError: 788 return hexnode 789 790 791@templatefunc(b'strip(text[, chars])') 792def strip(context, mapping, args): 793 """Strip characters from a string. By default, 794 strips all leading and trailing whitespace.""" 795 if not (1 <= len(args) <= 2): 796 # i18n: "strip" is a keyword 797 raise error.ParseError(_(b"strip expects one or two arguments")) 798 799 text = evalstring(context, mapping, args[0]) 800 if len(args) == 2: 801 chars = evalstring(context, mapping, args[1]) 802 return text.strip(chars) 803 return text.strip() 804 805 806@templatefunc(b'sub(pattern, replacement, expression)') 807def sub(context, mapping, args): 808 """Perform text substitution 809 using regular expressions.""" 810 if len(args) != 3: 811 # i18n: "sub" is a keyword 812 raise error.ParseError(_(b"sub expects three arguments")) 813 814 pat = evalstring(context, mapping, args[0]) 815 rpl = evalstring(context, mapping, args[1]) 816 src = evalstring(context, mapping, args[2]) 817 try: 818 patre = re.compile(pat) 819 except re.error: 820 # i18n: "sub" is a keyword 821 raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat) 822 try: 823 yield patre.sub(rpl, src) 824 except re.error: 825 # i18n: "sub" is a keyword 826 raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl) 827 828 829@templatefunc(b'startswith(pattern, text)') 830def startswith(context, mapping, args): 831 """Returns the value from the "text" argument 832 if it begins with the content from the "pattern" argument.""" 833 if len(args) != 2: 834 # i18n: "startswith" is a keyword 835 raise error.ParseError(_(b"startswith expects two arguments")) 836 837 patn = evalstring(context, mapping, args[0]) 838 text = evalstring(context, mapping, args[1]) 839 if text.startswith(patn): 840 return text 841 return b'' 842 843 844@templatefunc( 845 b'subsetparents(rev, revset)', 846 argspec=b'rev revset', 847 requires={b'repo', b'cache'}, 848) 849def subsetparents(context, mapping, args): 850 """Look up parents of the rev in the sub graph given by the revset.""" 851 if b'rev' not in args or b'revset' not in args: 852 # i18n: "subsetparents" is a keyword 853 raise error.ParseError(_(b"subsetparents expects two arguments")) 854 855 repo = context.resource(mapping, b'repo') 856 857 rev = templateutil.evalinteger(context, mapping, args[b'rev']) 858 859 # TODO: maybe subsetparents(rev) should be allowed. the default revset 860 # will be the revisions specified by -rREV argument. 861 q = templateutil.evalwrapped(context, mapping, args[b'revset']) 862 if not isinstance(q, templateutil.revslist): 863 # i18n: "subsetparents" is a keyword 864 raise error.ParseError(_(b"subsetparents expects a queried revset")) 865 subset = q.tovalue(context, mapping) 866 key = q.cachekey 867 868 if key: 869 # cache only if revset query isn't dynamic 870 cache = context.resource(mapping, b'cache') 871 walkercache = cache.setdefault(b'subsetparentswalker', {}) 872 if key in walkercache: 873 walker = walkercache[key] 874 else: 875 walker = dagop.subsetparentswalker(repo, subset) 876 walkercache[key] = walker 877 else: 878 # for one-shot use, specify startrev to limit the search space 879 walker = dagop.subsetparentswalker(repo, subset, startrev=rev) 880 return templateutil.revslist(repo, walker.parentsset(rev)) 881 882 883@templatefunc(b'word(number, text[, separator])') 884def word(context, mapping, args): 885 """Return the nth word from a string.""" 886 if not (2 <= len(args) <= 3): 887 # i18n: "word" is a keyword 888 raise error.ParseError( 889 _(b"word expects two or three arguments, got %d") % len(args) 890 ) 891 892 num = evalinteger( 893 context, 894 mapping, 895 args[0], 896 # i18n: "word" is a keyword 897 _(b"word expects an integer index"), 898 ) 899 text = evalstring(context, mapping, args[1]) 900 if len(args) == 3: 901 splitter = evalstring(context, mapping, args[2]) 902 else: 903 splitter = None 904 905 tokens = text.split(splitter) 906 if num >= len(tokens) or num < -len(tokens): 907 return b'' 908 else: 909 return tokens[num] 910 911 912def loadfunction(ui, extname, registrarobj): 913 """Load template function from specified registrarobj""" 914 for name, func in pycompat.iteritems(registrarobj._table): 915 funcs[name] = func 916 917 918# tell hggettext to extract docstrings from these functions: 919i18nfunctions = funcs.values() 920