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