templatefuncs.py
921 lines
| 30.1 KiB
| text/x-python
|
PythonLexer
/ mercurial / templatefuncs.py
|
r36940 | # templatefuncs.py - common template functions | ||
# | ||||
|
r47575 | # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com> | ||
|
r36940 | # | ||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
import re | ||||
from .i18n import _ | ||||
|
r37727 | from .node import ( | ||
bin, | ||||
|
r37876 | wdirid, | ||
|
r37727 | ) | ||
|
r36940 | from . import ( | ||
color, | ||||
|
r45083 | dagop, | ||
|
r43321 | diffutil, | ||
|
r36940 | encoding, | ||
error, | ||||
minirst, | ||||
obsutil, | ||||
|
r40970 | pycompat, | ||
|
r36940 | registrar, | ||
revset as revsetmod, | ||||
revsetlang, | ||||
scmutil, | ||||
templatefilters, | ||||
templatekw, | ||||
templateutil, | ||||
util, | ||||
) | ||||
|
r37227 | from .utils import ( | ||
dateutil, | ||||
stringutil, | ||||
) | ||||
|
r36940 | |||
evalrawexp = templateutil.evalrawexp | ||||
|
r38229 | evalwrapped = templateutil.evalwrapped | ||
|
r36940 | evalfuncarg = templateutil.evalfuncarg | ||
evalboolean = templateutil.evalboolean | ||||
|
r37241 | evaldate = templateutil.evaldate | ||
|
r36940 | evalinteger = templateutil.evalinteger | ||
evalstring = templateutil.evalstring | ||||
evalstringliteral = templateutil.evalstringliteral | ||||
# dict of template built-in functions | ||||
funcs = {} | ||||
templatefunc = registrar.templatefunc(funcs) | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'date(date[, fmt])') | ||
|
r36940 | def date(context, mapping, args): | ||
"""Format a date. See :hg:`help dates` for formatting | ||||
strings. The default is a Unix date format, including the timezone: | ||||
"Mon Sep 04 15:13:13 2006 0700".""" | ||||
if not (1 <= len(args) <= 2): | ||||
# i18n: "date" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"date expects one or two arguments")) | ||
|
r36940 | |||
|
r43346 | date = evaldate( | ||
context, | ||||
mapping, | ||||
args[0], | ||||
# i18n: "date" is a keyword | ||||
|
r43347 | _(b"date expects a date information"), | ||
|
r43346 | ) | ||
|
r36940 | fmt = None | ||
if len(args) == 2: | ||||
fmt = evalstring(context, mapping, args[1]) | ||||
|
r37242 | if fmt is None: | ||
return dateutil.datestr(date) | ||||
else: | ||||
return dateutil.datestr(date, fmt) | ||||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs') | ||
|
r36940 | def dict_(context, mapping, args): | ||
"""Construct a dict from key-value pairs. A key may be omitted if | ||||
a value expression can provide an unambiguous name.""" | ||||
data = util.sortdict() | ||||
|
r43347 | for v in args[b'args']: | ||
|
r36940 | k = templateutil.findsymbolicname(v) | ||
if not k: | ||||
|
r43347 | raise error.ParseError(_(b'dict key cannot be inferred')) | ||
if k in data or k in args[b'kwargs']: | ||||
raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k) | ||||
|
r36940 | data[k] = evalfuncarg(context, mapping, v) | ||
|
r43346 | data.update( | ||
(k, evalfuncarg(context, mapping, v)) | ||||
|
r43376 | for k, v in pycompat.iteritems(args[b'kwargs']) | ||
|
r43346 | ) | ||
|
r36940 | return templateutil.hybriddict(data) | ||
|
r43346 | |||
@templatefunc( | ||||
|
r43347 | b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'} | ||
|
r43346 | ) | ||
|
r36940 | def diff(context, mapping, args): | ||
"""Show a diff, optionally | ||||
specifying files to include or exclude.""" | ||||
if len(args) > 2: | ||||
# i18n: "diff" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"diff expects zero, one, or two arguments")) | ||
|
r36940 | |||
def getpatterns(i): | ||||
if i < len(args): | ||||
s = evalstring(context, mapping, args[i]).strip() | ||||
if s: | ||||
return [s] | ||||
return [] | ||||
|
r43347 | ctx = context.resource(mapping, b'ctx') | ||
ui = context.resource(mapping, b'ui') | ||||
|
r43321 | diffopts = diffutil.diffallopts(ui) | ||
|
r43346 | chunks = ctx.diff( | ||
match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts | ||||
) | ||||
|
r36940 | |||
|
r43347 | return b''.join(chunks) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc( | ||
b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'} | ||||
) | ||||
|
r36940 | def extdata(context, mapping, args): | ||
"""Show a text read from the specified extdata source. (EXPERIMENTAL)""" | ||||
|
r43347 | if b'source' not in args: | ||
|
r36940 | # i18n: "extdata" is a keyword | ||
|
r43347 | raise error.ParseError(_(b'extdata expects one argument')) | ||
|
r36940 | |||
|
r43347 | source = evalstring(context, mapping, args[b'source']) | ||
|
r37950 | if not source: | ||
|
r43347 | sym = templateutil.findsymbolicname(args[b'source']) | ||
|
r37950 | if sym: | ||
|
r43346 | raise error.ParseError( | ||
|
r43347 | _(b'empty data source specified'), | ||
hint=_(b"did you mean extdata('%s')?") % sym, | ||||
|
r43346 | ) | ||
|
r37950 | else: | ||
|
r43347 | raise error.ParseError(_(b'empty data source specified')) | ||
cache = context.resource(mapping, b'cache').setdefault(b'extdata', {}) | ||||
ctx = context.resource(mapping, b'ctx') | ||||
|
r36940 | if source in cache: | ||
data = cache[source] | ||||
else: | ||||
data = cache[source] = scmutil.extdatasource(ctx.repo(), source) | ||||
|
r43347 | return data.get(ctx.rev(), b'') | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'files(pattern)', requires={b'ctx'}) | ||
|
r36940 | def files(context, mapping, args): | ||
"""All files of the current changeset matching the pattern. See | ||||
:hg:`help patterns`.""" | ||||
if not len(args) == 1: | ||||
# i18n: "files" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"files expects one argument")) | ||
|
r36940 | |||
raw = evalstring(context, mapping, args[0]) | ||||
|
r43347 | ctx = context.resource(mapping, b'ctx') | ||
|
r36940 | m = ctx.match([raw]) | ||
files = list(ctx.matches(m)) | ||||
|
r43347 | return templateutil.compatfileslist(context, mapping, b"file", files) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])') | ||
|
r36940 | def fill(context, mapping, args): | ||
"""Fill many | ||||
paragraphs with optional indentation. See the "fill" filter.""" | ||||
if not (1 <= len(args) <= 4): | ||||
# i18n: "fill" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"fill expects one to four arguments")) | ||
|
r36940 | |||
text = evalstring(context, mapping, args[0]) | ||||
width = 76 | ||||
|
r43347 | initindent = b'' | ||
hangindent = b'' | ||||
|
r36940 | if 2 <= len(args) <= 4: | ||
|
r43346 | width = evalinteger( | ||
context, | ||||
mapping, | ||||
args[1], | ||||
# i18n: "fill" is a keyword | ||||
|
r43347 | _(b"fill expects an integer width"), | ||
|
r43346 | ) | ||
|
r36940 | try: | ||
initindent = evalstring(context, mapping, args[2]) | ||||
hangindent = evalstring(context, mapping, args[3]) | ||||
except IndexError: | ||||
pass | ||||
return templatefilters.fill(text, width, initindent, hangindent) | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'filter(iterable[, expr])') | ||
|
r38467 | def filter_(context, mapping, args): | ||
|
r38468 | """Remove empty elements from a list or a dict. If expr specified, it's | ||
applied to each element to test emptiness.""" | ||||
if not (1 <= len(args) <= 2): | ||||
|
r38467 | # i18n: "filter" is a keyword | ||
|
r43347 | raise error.ParseError(_(b"filter expects one or two arguments")) | ||
|
r38467 | iterable = evalwrapped(context, mapping, args[0]) | ||
|
r38468 | if len(args) == 1: | ||
|
r43346 | |||
|
r38468 | def select(w): | ||
return w.tobool(context, mapping) | ||||
|
r43346 | |||
|
r38468 | else: | ||
|
r43346 | |||
|
r38468 | def select(w): | ||
if not isinstance(w, templateutil.mappable): | ||||
|
r43347 | raise error.ParseError(_(b"not filterable by expression")) | ||
|
r38468 | lm = context.overlaymap(mapping, w.tomap(context)) | ||
return evalboolean(context, lm, args[1]) | ||||
|
r43346 | |||
|
r38467 | return iterable.filter(context, mapping, select) | ||
|
r43346 | |||
|
r43347 | @templatefunc(b'formatnode(node)', requires={b'ui'}) | ||
|
r36940 | def formatnode(context, mapping, args): | ||
"""Obtain the preferred form of a changeset hash. (DEPRECATED)""" | ||||
if len(args) != 1: | ||||
# i18n: "formatnode" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"formatnode expects one argument")) | ||
|
r36940 | |||
|
r43347 | ui = context.resource(mapping, b'ui') | ||
|
r36940 | node = evalstring(context, mapping, args[0]) | ||
if ui.debugflag: | ||||
return node | ||||
return templatefilters.short(node) | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'}) | ||
|
r37227 | def mailmap(context, mapping, args): | ||
"""Return the author, updated according to the value | ||||
set in the .mailmap file""" | ||||
if len(args) != 1: | ||||
|
r43347 | raise error.ParseError(_(b"mailmap expects one argument")) | ||
|
r37227 | |||
|
r37277 | author = evalstring(context, mapping, args[0]) | ||
|
r37227 | |||
|
r43347 | cache = context.resource(mapping, b'cache') | ||
repo = context.resource(mapping, b'repo') | ||||
|
r37227 | |||
|
r43347 | if b'mailmap' not in cache: | ||
data = repo.wvfs.tryread(b'.mailmap') | ||||
cache[b'mailmap'] = stringutil.parsemailmap(data) | ||||
|
r37227 | |||
|
r43347 | return stringutil.mapname(cache[b'mailmap'], author) | ||
|
r37227 | |||
|
r43346 | |||
|
r40225 | @templatefunc( | ||
|
r43347 | b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])', | ||
argspec=b'text width fillchar left truncate', | ||||
|
r43346 | ) | ||
|
r36940 | def pad(context, mapping, args): | ||
"""Pad text with a | ||||
fill character.""" | ||||
|
r43347 | if b'text' not in args or b'width' not in args: | ||
|
r36940 | # i18n: "pad" is a keyword | ||
|
r43347 | raise error.ParseError(_(b"pad() expects two to four arguments")) | ||
|
r36940 | |||
|
r43346 | width = evalinteger( | ||
context, | ||||
mapping, | ||||
|
r43347 | args[b'width'], | ||
|
r43346 | # i18n: "pad" is a keyword | ||
|
r43347 | _(b"pad() expects an integer width"), | ||
|
r43346 | ) | ||
|
r36940 | |||
|
r43347 | text = evalstring(context, mapping, args[b'text']) | ||
|
r36940 | |||
|
r40225 | truncate = False | ||
|
r36940 | left = False | ||
|
r43347 | fillchar = b' ' | ||
if b'fillchar' in args: | ||||
fillchar = evalstring(context, mapping, args[b'fillchar']) | ||||
|
r36940 | if len(color.stripeffects(fillchar)) != 1: | ||
# i18n: "pad" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"pad() expects a single fill character")) | ||
if b'left' in args: | ||||
left = evalboolean(context, mapping, args[b'left']) | ||||
if b'truncate' in args: | ||||
truncate = evalboolean(context, mapping, args[b'truncate']) | ||||
|
r36940 | |||
fillwidth = width - encoding.colwidth(color.stripeffects(text)) | ||||
|
r40225 | if fillwidth < 0 and truncate: | ||
return encoding.trim(color.stripeffects(text), width, leftside=left) | ||||
|
r36940 | if fillwidth <= 0: | ||
return text | ||||
if left: | ||||
return fillchar * fillwidth + text | ||||
else: | ||||
return text + fillchar * fillwidth | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'indent(text, indentchars[, firstline])') | ||
|
r36940 | def indent(context, mapping, args): | ||
"""Indents all non-empty lines | ||||
with the characters given in the indentchars string. An optional | ||||
third parameter will override the indent for the first line only | ||||
if present.""" | ||||
if not (2 <= len(args) <= 3): | ||||
# i18n: "indent" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"indent() expects two or three arguments")) | ||
|
r36940 | |||
text = evalstring(context, mapping, args[0]) | ||||
indent = evalstring(context, mapping, args[1]) | ||||
|
r44093 | firstline = indent | ||
|
r36940 | if len(args) == 3: | ||
firstline = evalstring(context, mapping, args[2]) | ||||
|
r44093 | return templatefilters.indent(text, indent, firstline=firstline) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'get(dict, key)') | ||
|
r36940 | def get(context, mapping, args): | ||
"""Get an attribute/key from an object. Some keywords | ||||
are complex types. This function allows you to obtain the value of an | ||||
attribute on these types.""" | ||||
if len(args) != 2: | ||||
# i18n: "get" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"get() expects two arguments")) | ||
|
r36940 | |||
|
r38258 | dictarg = evalwrapped(context, mapping, args[0]) | ||
|
r38262 | key = evalrawexp(context, mapping, args[1]) | ||
|
r38261 | try: | ||
return dictarg.getmember(context, mapping, key) | ||||
except error.ParseError as err: | ||||
|
r36940 | # i18n: "get" is a keyword | ||
|
r43347 | hint = _(b"get() expects a dict as first argument") | ||
|
r38261 | raise error.ParseError(bytes(err), hint=hint) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'config(section, name[, default])', requires={b'ui'}) | ||
|
r41869 | def config(context, mapping, args): | ||
"""Returns the requested hgrc config option as a string.""" | ||||
|
r43347 | fn = context.resource(mapping, b'ui').config | ||
|
r41869 | return _config(context, mapping, args, fn, evalstring) | ||
|
r43346 | |||
|
r43347 | @templatefunc(b'configbool(section, name[, default])', requires={b'ui'}) | ||
|
r41869 | def configbool(context, mapping, args): | ||
"""Returns the requested hgrc config option as a boolean.""" | ||||
|
r43347 | fn = context.resource(mapping, b'ui').configbool | ||
|
r41869 | return _config(context, mapping, args, fn, evalboolean) | ||
|
r43346 | |||
|
r43347 | @templatefunc(b'configint(section, name[, default])', requires={b'ui'}) | ||
|
r41869 | def configint(context, mapping, args): | ||
"""Returns the requested hgrc config option as an integer.""" | ||||
|
r43347 | fn = context.resource(mapping, b'ui').configint | ||
|
r41869 | return _config(context, mapping, args, fn, evalinteger) | ||
|
r43346 | |||
|
r41869 | def _config(context, mapping, args, configfn, defaultfn): | ||
if not (2 <= len(args) <= 3): | ||||
|
r43347 | raise error.ParseError(_(b"config expects two or three arguments")) | ||
|
r41869 | |||
# The config option can come from any section, though we specifically | ||||
# reserve the [templateconfig] section for dynamically defining options | ||||
# for this function without also requiring an extension. | ||||
section = evalstringliteral(context, mapping, args[0]) | ||||
name = evalstringliteral(context, mapping, args[1]) | ||||
if len(args) == 3: | ||||
default = defaultfn(context, mapping, args[2]) | ||||
return configfn(section, name, default) | ||||
else: | ||||
return configfn(section, name) | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'if(expr, then[, else])') | ||
|
r36940 | def if_(context, mapping, args): | ||
"""Conditionally execute based on the result of | ||||
an expression.""" | ||||
if not (2 <= len(args) <= 3): | ||||
# i18n: "if" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"if expects two or three arguments")) | ||
|
r36940 | |||
test = evalboolean(context, mapping, args[0]) | ||||
if test: | ||||
|
r37033 | return evalrawexp(context, mapping, args[1]) | ||
|
r36940 | elif len(args) == 3: | ||
|
r37033 | return evalrawexp(context, mapping, args[2]) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'ifcontains(needle, haystack, then[, else])') | ||
|
r36940 | def ifcontains(context, mapping, args): | ||
"""Conditionally execute based | ||||
on whether the item "needle" is in "haystack".""" | ||||
if not (3 <= len(args) <= 4): | ||||
# i18n: "ifcontains" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"ifcontains expects three or four arguments")) | ||
|
r36940 | |||
|
r38286 | haystack = evalwrapped(context, mapping, args[1]) | ||
|
r36940 | try: | ||
|
r37180 | needle = evalrawexp(context, mapping, args[0]) | ||
|
r38286 | found = haystack.contains(context, mapping, needle) | ||
|
r36940 | except error.ParseError: | ||
found = False | ||||
if found: | ||||
|
r37033 | return evalrawexp(context, mapping, args[2]) | ||
|
r36940 | elif len(args) == 4: | ||
|
r37033 | return evalrawexp(context, mapping, args[3]) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'ifeq(expr1, expr2, then[, else])') | ||
|
r36940 | def ifeq(context, mapping, args): | ||
"""Conditionally execute based on | ||||
whether 2 items are equivalent.""" | ||||
if not (3 <= len(args) <= 4): | ||||
# i18n: "ifeq" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"ifeq expects three or four arguments")) | ||
|
r36940 | |||
test = evalstring(context, mapping, args[0]) | ||||
match = evalstring(context, mapping, args[1]) | ||||
if test == match: | ||||
|
r37033 | return evalrawexp(context, mapping, args[2]) | ||
|
r36940 | elif len(args) == 4: | ||
|
r37033 | return evalrawexp(context, mapping, args[3]) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'join(list, sep)') | ||
|
r36940 | def join(context, mapping, args): | ||
"""Join items in a list with a delimiter.""" | ||||
if not (1 <= len(args) <= 2): | ||||
# i18n: "join" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"join expects one or two arguments")) | ||
|
r36940 | |||
|
r38229 | joinset = evalwrapped(context, mapping, args[0]) | ||
|
r43347 | joiner = b" " | ||
|
r36940 | if len(args) > 1: | ||
joiner = evalstring(context, mapping, args[1]) | ||||
|
r38229 | return joinset.join(context, mapping, joiner) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'label(label, expr)', requires={b'ui'}) | ||
|
r36940 | def label(context, mapping, args): | ||
"""Apply a label to generated content. Content with | ||||
a label applied can result in additional post-processing, such as | ||||
automatic colorization.""" | ||||
if len(args) != 2: | ||||
# i18n: "label" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"label expects two arguments")) | ||
|
r36940 | |||
|
r43347 | ui = context.resource(mapping, b'ui') | ||
|
r36940 | thing = evalstring(context, mapping, args[1]) | ||
# preserve unknown symbol as literal so effects like 'red', 'bold', | ||||
# etc. don't need to be quoted | ||||
label = evalstringliteral(context, mapping, args[0]) | ||||
return ui.label(thing, label) | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'latesttag([pattern])') | ||
|
r36940 | def latesttag(context, mapping, args): | ||
"""The global tags matching the given pattern on the | ||||
most recent globally tagged ancestor of this changeset. | ||||
If no such tags exist, the "{tag}" template resolves to | ||||
|
r38161 | the string "null". See :hg:`help revisions.patterns` for the pattern | ||
syntax. | ||||
""" | ||||
|
r36940 | if len(args) > 1: | ||
# i18n: "latesttag" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"latesttag expects at most one argument")) | ||
|
r36940 | |||
pattern = None | ||||
if len(args) == 1: | ||||
pattern = evalstring(context, mapping, args[0]) | ||||
return templatekw.showlatesttags(context, mapping, pattern) | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'localdate(date[, tz])') | ||
|
r36940 | def localdate(context, mapping, args): | ||
"""Converts a date to the specified timezone. | ||||
The default is local date.""" | ||||
if not (1 <= len(args) <= 2): | ||||
# i18n: "localdate" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"localdate expects one or two arguments")) | ||
|
r36940 | |||
|
r43346 | date = evaldate( | ||
context, | ||||
mapping, | ||||
args[0], | ||||
# i18n: "localdate" is a keyword | ||||
|
r43347 | _(b"localdate expects a date information"), | ||
|
r43346 | ) | ||
|
r36940 | if len(args) >= 2: | ||
tzoffset = None | ||||
tz = evalfuncarg(context, mapping, args[1]) | ||||
if isinstance(tz, bytes): | ||||
tzoffset, remainder = dateutil.parsetimezone(tz) | ||||
if remainder: | ||||
tzoffset = None | ||||
if tzoffset is None: | ||||
try: | ||||
tzoffset = int(tz) | ||||
except (TypeError, ValueError): | ||||
# i18n: "localdate" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"localdate expects a timezone")) | ||
|
r36940 | else: | ||
tzoffset = dateutil.makedate()[1] | ||||
|
r38304 | return templateutil.date((date[0], tzoffset)) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'max(iterable)') | ||
|
r36940 | def max_(context, mapping, args, **kwargs): | ||
"""Return the max of an iterable""" | ||||
if len(args) != 1: | ||||
# i18n: "max" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"max expects one argument")) | ||
|
r36940 | |||
|
r38284 | iterable = evalwrapped(context, mapping, args[0]) | ||
|
r36940 | try: | ||
|
r38284 | return iterable.getmax(context, mapping) | ||
except error.ParseError as err: | ||||
|
r36940 | # i18n: "max" is a keyword | ||
|
r43347 | hint = _(b"max first argument should be an iterable") | ||
|
r38284 | raise error.ParseError(bytes(err), hint=hint) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'min(iterable)') | ||
|
r36940 | def min_(context, mapping, args, **kwargs): | ||
"""Return the min of an iterable""" | ||||
if len(args) != 1: | ||||
# i18n: "min" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"min expects one argument")) | ||
|
r36940 | |||
|
r38284 | iterable = evalwrapped(context, mapping, args[0]) | ||
|
r36940 | try: | ||
|
r38284 | return iterable.getmin(context, mapping) | ||
except error.ParseError as err: | ||||
|
r36940 | # i18n: "min" is a keyword | ||
|
r43347 | hint = _(b"min first argument should be an iterable") | ||
|
r38284 | raise error.ParseError(bytes(err), hint=hint) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'mod(a, b)') | ||
|
r36940 | def mod(context, mapping, args): | ||
"""Calculate a mod b such that a / b + a mod b == a""" | ||||
if not len(args) == 2: | ||||
# i18n: "mod" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"mod expects two arguments")) | ||
|
r36940 | |||
func = lambda a, b: a % b | ||||
|
r43346 | return templateutil.runarithmetic( | ||
context, mapping, (func, args[0], args[1]) | ||||
) | ||||
|
r36940 | |||
|
r43347 | @templatefunc(b'obsfateoperations(markers)') | ||
|
r36940 | def obsfateoperations(context, mapping, args): | ||
"""Compute obsfate related information based on markers (EXPERIMENTAL)""" | ||||
if len(args) != 1: | ||||
# i18n: "obsfateoperations" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"obsfateoperations expects one argument")) | ||
|
r36940 | |||
markers = evalfuncarg(context, mapping, args[0]) | ||||
try: | ||||
data = obsutil.markersoperations(markers) | ||||
|
r43347 | return templateutil.hybridlist(data, name=b'operation') | ||
|
r36940 | except (TypeError, KeyError): | ||
# i18n: "obsfateoperations" is a keyword | ||||
|
r43347 | errmsg = _(b"obsfateoperations first argument should be an iterable") | ||
|
r36940 | raise error.ParseError(errmsg) | ||
|
r43346 | |||
|
r43347 | @templatefunc(b'obsfatedate(markers)') | ||
|
r36940 | def obsfatedate(context, mapping, args): | ||
"""Compute obsfate related information based on markers (EXPERIMENTAL)""" | ||||
if len(args) != 1: | ||||
# i18n: "obsfatedate" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"obsfatedate expects one argument")) | ||
|
r36940 | |||
markers = evalfuncarg(context, mapping, args[0]) | ||||
try: | ||||
|
r38304 | # TODO: maybe this has to be a wrapped list of date wrappers? | ||
|
r36940 | data = obsutil.markersdates(markers) | ||
|
r43347 | return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d') | ||
|
r36940 | except (TypeError, KeyError): | ||
# i18n: "obsfatedate" is a keyword | ||||
|
r43347 | errmsg = _(b"obsfatedate first argument should be an iterable") | ||
|
r36940 | raise error.ParseError(errmsg) | ||
|
r43346 | |||
|
r43347 | @templatefunc(b'obsfateusers(markers)') | ||
|
r36940 | def obsfateusers(context, mapping, args): | ||
"""Compute obsfate related information based on markers (EXPERIMENTAL)""" | ||||
if len(args) != 1: | ||||
# i18n: "obsfateusers" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"obsfateusers expects one argument")) | ||
|
r36940 | |||
markers = evalfuncarg(context, mapping, args[0]) | ||||
try: | ||||
data = obsutil.markersusers(markers) | ||||
|
r43347 | return templateutil.hybridlist(data, name=b'user') | ||
|
r36940 | except (TypeError, KeyError, ValueError): | ||
# i18n: "obsfateusers" is a keyword | ||||
|
r43346 | msg = _( | ||
|
r43347 | b"obsfateusers first argument should be an iterable of " | ||
b"obsmakers" | ||||
|
r43346 | ) | ||
|
r36940 | raise error.ParseError(msg) | ||
|
r43346 | |||
|
r43347 | @templatefunc(b'obsfateverb(successors, markers)') | ||
|
r36940 | def obsfateverb(context, mapping, args): | ||
"""Compute obsfate related information based on successors (EXPERIMENTAL)""" | ||||
if len(args) != 2: | ||||
# i18n: "obsfateverb" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"obsfateverb expects two arguments")) | ||
|
r36940 | |||
successors = evalfuncarg(context, mapping, args[0]) | ||||
markers = evalfuncarg(context, mapping, args[1]) | ||||
try: | ||||
return obsutil.obsfateverb(successors, markers) | ||||
except TypeError: | ||||
# i18n: "obsfateverb" is a keyword | ||||
|
r43347 | errmsg = _(b"obsfateverb first argument should be countable") | ||
|
r36940 | raise error.ParseError(errmsg) | ||
|
r43346 | |||
|
r43347 | @templatefunc(b'relpath(path)', requires={b'repo'}) | ||
|
r36940 | def relpath(context, mapping, args): | ||
"""Convert a repository-absolute path into a filesystem path relative to | ||||
the current working directory.""" | ||||
if len(args) != 1: | ||||
# i18n: "relpath" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"relpath expects one argument")) | ||
|
r36940 | |||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
|
r36940 | path = evalstring(context, mapping, args[0]) | ||
return repo.pathto(path) | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'}) | ||
|
r36940 | def revset(context, mapping, args): | ||
"""Execute a revision set query. See | ||||
:hg:`help revset`.""" | ||||
if not len(args) > 0: | ||||
# i18n: "revset" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"revset expects one or more arguments")) | ||
|
r36940 | |||
raw = evalstring(context, mapping, args[0]) | ||||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
|
r36940 | |||
def query(expr): | ||||
|
r37692 | m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo)) | ||
|
r36940 | return m(repo) | ||
if len(args) > 1: | ||||
|
r45082 | key = None # dynamically-created revs shouldn't be cached | ||
|
r36940 | formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]] | ||
revs = query(revsetlang.formatspec(raw, *formatargs)) | ||||
else: | ||||
|
r43347 | cache = context.resource(mapping, b'cache') | ||
revsetcache = cache.setdefault(b"revsetcache", {}) | ||||
|
r45082 | key = raw | ||
if key in revsetcache: | ||||
revs = revsetcache[key] | ||||
|
r36940 | else: | ||
revs = query(raw) | ||||
|
r45082 | revsetcache[key] = revs | ||
return templateutil.revslist(repo, revs, name=b'revision', cachekey=key) | ||||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'rstdoc(text, style)') | ||
|
r36940 | def rstdoc(context, mapping, args): | ||
"""Format reStructuredText.""" | ||||
if len(args) != 2: | ||||
# i18n: "rstdoc" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"rstdoc expects two arguments")) | ||
|
r36940 | |||
text = evalstring(context, mapping, args[0]) | ||||
style = evalstring(context, mapping, args[1]) | ||||
|
r43347 | return minirst.format(text, style=style, keep=[b'verbose']) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'search(pattern, text)') | ||
|
r40970 | def search(context, mapping, args): | ||
"""Look for the first text matching the regular expression pattern. | ||||
Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template.""" | ||||
if len(args) != 2: | ||||
# i18n: "search" is a keyword | ||||
raise error.ParseError(_(b'search expects two arguments')) | ||||
pat = evalstring(context, mapping, args[0]) | ||||
src = evalstring(context, mapping, args[1]) | ||||
try: | ||||
patre = re.compile(pat) | ||||
except re.error: | ||||
# i18n: "search" is a keyword | ||||
raise error.ParseError(_(b'search got an invalid pattern: %s') % pat) | ||||
# named groups shouldn't shadow *reserved* resource keywords | ||||
|
r43346 | badgroups = context.knownresourcekeys() & set( | ||
pycompat.byteskwargs(patre.groupindex) | ||||
) | ||||
|
r40970 | if badgroups: | ||
raise error.ParseError( | ||||
# i18n: "search" is a keyword | ||||
_(b'invalid group %(group)s in search pattern: %(pat)s') | ||||
|
r43346 | % { | ||
|
r43347 | b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)), | ||
|
r43346 | b'pat': pat, | ||
} | ||||
) | ||||
|
r40970 | |||
match = patre.search(src) | ||||
if not match: | ||||
|
r40971 | return templateutil.mappingnone() | ||
|
r40970 | |||
lm = {b'0': match.group(0)} | ||||
lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1)) | ||||
lm.update(pycompat.byteskwargs(match.groupdict())) | ||||
return templateutil.mappingdict(lm, tmpl=b'{0}') | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'separate(sep, args...)', argspec=b'sep *args') | ||
|
r36940 | def separate(context, mapping, args): | ||
"""Add a separator between non-empty arguments.""" | ||||
|
r43347 | if b'sep' not in args: | ||
|
r36940 | # i18n: "separate" is a keyword | ||
|
r43347 | raise error.ParseError(_(b"separate expects at least one argument")) | ||
|
r36940 | |||
|
r43347 | sep = evalstring(context, mapping, args[b'sep']) | ||
|
r36940 | first = True | ||
|
r43347 | for arg in args[b'args']: | ||
|
r36940 | argstr = evalstring(context, mapping, arg) | ||
if not argstr: | ||||
continue | ||||
if first: | ||||
first = False | ||||
else: | ||||
yield sep | ||||
yield argstr | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'}) | ||
|
r36940 | def shortest(context, mapping, args): | ||
"""Obtain the shortest representation of | ||||
a node.""" | ||||
if not (1 <= len(args) <= 2): | ||||
# i18n: "shortest" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"shortest() expects one or two arguments")) | ||
|
r36940 | |||
|
r37727 | hexnode = evalstring(context, mapping, args[0]) | ||
|
r36940 | |||
minlength = 4 | ||||
if len(args) > 1: | ||||
|
r43346 | minlength = evalinteger( | ||
context, | ||||
mapping, | ||||
args[1], | ||||
# i18n: "shortest" is a keyword | ||||
|
r43347 | _(b"shortest() expects an integer minlength"), | ||
|
r43346 | ) | ||
|
r36940 | |||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
|
r37727 | if len(hexnode) > 40: | ||
return hexnode | ||||
elif len(hexnode) == 40: | ||||
try: | ||||
node = bin(hexnode) | ||||
except TypeError: | ||||
return hexnode | ||||
else: | ||||
try: | ||||
node = scmutil.resolvehexnodeidprefix(repo, hexnode) | ||||
|
r37876 | except error.WdirUnsupported: | ||
node = wdirid | ||||
except error.LookupError: | ||||
|
r37727 | return hexnode | ||
if not node: | ||||
return hexnode | ||||
|
r43347 | cache = context.resource(mapping, b'cache') | ||
|
r37882 | try: | ||
|
r38889 | return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache) | ||
|
r37882 | except error.RepoLookupError: | ||
return hexnode | ||||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'strip(text[, chars])') | ||
|
r36940 | def strip(context, mapping, args): | ||
"""Strip characters from a string. By default, | ||||
strips all leading and trailing whitespace.""" | ||||
if not (1 <= len(args) <= 2): | ||||
# i18n: "strip" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"strip expects one or two arguments")) | ||
|
r36940 | |||
text = evalstring(context, mapping, args[0]) | ||||
if len(args) == 2: | ||||
chars = evalstring(context, mapping, args[1]) | ||||
return text.strip(chars) | ||||
return text.strip() | ||||
|
r43346 | |||
|
r43347 | @templatefunc(b'sub(pattern, replacement, expression)') | ||
|
r36940 | def sub(context, mapping, args): | ||
"""Perform text substitution | ||||
using regular expressions.""" | ||||
if len(args) != 3: | ||||
# i18n: "sub" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"sub expects three arguments")) | ||
|
r36940 | |||
pat = evalstring(context, mapping, args[0]) | ||||
rpl = evalstring(context, mapping, args[1]) | ||||
src = evalstring(context, mapping, args[2]) | ||||
try: | ||||
patre = re.compile(pat) | ||||
except re.error: | ||||
# i18n: "sub" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat) | ||
|
r36940 | try: | ||
yield patre.sub(rpl, src) | ||||
except re.error: | ||||
# i18n: "sub" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl) | ||
|
r36940 | |||
|
r43346 | |||
|
r43347 | @templatefunc(b'startswith(pattern, text)') | ||
|
r36940 | def startswith(context, mapping, args): | ||
"""Returns the value from the "text" argument | ||||
if it begins with the content from the "pattern" argument.""" | ||||
if len(args) != 2: | ||||
# i18n: "startswith" is a keyword | ||||
|
r43347 | raise error.ParseError(_(b"startswith expects two arguments")) | ||
|
r36940 | |||
patn = evalstring(context, mapping, args[0]) | ||||
text = evalstring(context, mapping, args[1]) | ||||
if text.startswith(patn): | ||||
return text | ||||
|
r43347 | return b'' | ||
|
r36940 | |||
|
r43346 | |||
|
r45083 | @templatefunc( | ||
b'subsetparents(rev, revset)', | ||||
argspec=b'rev revset', | ||||
requires={b'repo', b'cache'}, | ||||
) | ||||
def subsetparents(context, mapping, args): | ||||
"""Look up parents of the rev in the sub graph given by the revset.""" | ||||
if b'rev' not in args or b'revset' not in args: | ||||
# i18n: "subsetparents" is a keyword | ||||
raise error.ParseError(_(b"subsetparents expects two arguments")) | ||||
repo = context.resource(mapping, b'repo') | ||||
rev = templateutil.evalinteger(context, mapping, args[b'rev']) | ||||
# TODO: maybe subsetparents(rev) should be allowed. the default revset | ||||
# will be the revisions specified by -rREV argument. | ||||
q = templateutil.evalwrapped(context, mapping, args[b'revset']) | ||||
if not isinstance(q, templateutil.revslist): | ||||
# i18n: "subsetparents" is a keyword | ||||
raise error.ParseError(_(b"subsetparents expects a queried revset")) | ||||
subset = q.tovalue(context, mapping) | ||||
key = q.cachekey | ||||
if key: | ||||
# cache only if revset query isn't dynamic | ||||
cache = context.resource(mapping, b'cache') | ||||
walkercache = cache.setdefault(b'subsetparentswalker', {}) | ||||
if key in walkercache: | ||||
walker = walkercache[key] | ||||
else: | ||||
walker = dagop.subsetparentswalker(repo, subset) | ||||
walkercache[key] = walker | ||||
else: | ||||
# for one-shot use, specify startrev to limit the search space | ||||
walker = dagop.subsetparentswalker(repo, subset, startrev=rev) | ||||
return templateutil.revslist(repo, walker.parentsset(rev)) | ||||
|
r43347 | @templatefunc(b'word(number, text[, separator])') | ||
|
r36940 | def word(context, mapping, args): | ||
"""Return the nth word from a string.""" | ||||
if not (2 <= len(args) <= 3): | ||||
# i18n: "word" is a keyword | ||||
|
r43346 | raise error.ParseError( | ||
|
r43347 | _(b"word expects two or three arguments, got %d") % len(args) | ||
|
r43346 | ) | ||
|
r36940 | |||
|
r43346 | num = evalinteger( | ||
context, | ||||
mapping, | ||||
args[0], | ||||
# i18n: "word" is a keyword | ||||
|
r43347 | _(b"word expects an integer index"), | ||
|
r43346 | ) | ||
|
r36940 | text = evalstring(context, mapping, args[1]) | ||
if len(args) == 3: | ||||
splitter = evalstring(context, mapping, args[2]) | ||||
else: | ||||
splitter = None | ||||
tokens = text.split(splitter) | ||||
if num >= len(tokens) or num < -len(tokens): | ||||
|
r43347 | return b'' | ||
|
r36940 | else: | ||
return tokens[num] | ||||
|
r43346 | |||
|
r36940 | def loadfunction(ui, extname, registrarobj): | ||
|
r46554 | """Load template function from specified registrarobj""" | ||
|
r43376 | for name, func in pycompat.iteritems(registrarobj._table): | ||
|
r36940 | funcs[name] = func | ||
|
r43346 | |||
|
r36940 | # tell hggettext to extract docstrings from these functions: | ||
i18nfunctions = funcs.values() | ||||