##// END OF EJS Templates
templater: remember cache key of evaluated revset...
Yuya Nishihara -
r45082:1f81f680 default
parent child Browse files
Show More
@@ -1,880 +1,882
1 # templatefuncs.py - common template functions
1 # templatefuncs.py - common template functions
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 bin,
14 bin,
15 wdirid,
15 wdirid,
16 )
16 )
17 from . import (
17 from . import (
18 color,
18 color,
19 diffutil,
19 diffutil,
20 encoding,
20 encoding,
21 error,
21 error,
22 minirst,
22 minirst,
23 obsutil,
23 obsutil,
24 pycompat,
24 pycompat,
25 registrar,
25 registrar,
26 revset as revsetmod,
26 revset as revsetmod,
27 revsetlang,
27 revsetlang,
28 scmutil,
28 scmutil,
29 templatefilters,
29 templatefilters,
30 templatekw,
30 templatekw,
31 templateutil,
31 templateutil,
32 util,
32 util,
33 )
33 )
34 from .utils import (
34 from .utils import (
35 dateutil,
35 dateutil,
36 stringutil,
36 stringutil,
37 )
37 )
38
38
39 evalrawexp = templateutil.evalrawexp
39 evalrawexp = templateutil.evalrawexp
40 evalwrapped = templateutil.evalwrapped
40 evalwrapped = templateutil.evalwrapped
41 evalfuncarg = templateutil.evalfuncarg
41 evalfuncarg = templateutil.evalfuncarg
42 evalboolean = templateutil.evalboolean
42 evalboolean = templateutil.evalboolean
43 evaldate = templateutil.evaldate
43 evaldate = templateutil.evaldate
44 evalinteger = templateutil.evalinteger
44 evalinteger = templateutil.evalinteger
45 evalstring = templateutil.evalstring
45 evalstring = templateutil.evalstring
46 evalstringliteral = templateutil.evalstringliteral
46 evalstringliteral = templateutil.evalstringliteral
47
47
48 # dict of template built-in functions
48 # dict of template built-in functions
49 funcs = {}
49 funcs = {}
50 templatefunc = registrar.templatefunc(funcs)
50 templatefunc = registrar.templatefunc(funcs)
51
51
52
52
53 @templatefunc(b'date(date[, fmt])')
53 @templatefunc(b'date(date[, fmt])')
54 def date(context, mapping, args):
54 def date(context, mapping, args):
55 """Format a date. See :hg:`help dates` for formatting
55 """Format a date. See :hg:`help dates` for formatting
56 strings. The default is a Unix date format, including the timezone:
56 strings. The default is a Unix date format, including the timezone:
57 "Mon Sep 04 15:13:13 2006 0700"."""
57 "Mon Sep 04 15:13:13 2006 0700"."""
58 if not (1 <= len(args) <= 2):
58 if not (1 <= len(args) <= 2):
59 # i18n: "date" is a keyword
59 # i18n: "date" is a keyword
60 raise error.ParseError(_(b"date expects one or two arguments"))
60 raise error.ParseError(_(b"date expects one or two arguments"))
61
61
62 date = evaldate(
62 date = evaldate(
63 context,
63 context,
64 mapping,
64 mapping,
65 args[0],
65 args[0],
66 # i18n: "date" is a keyword
66 # i18n: "date" is a keyword
67 _(b"date expects a date information"),
67 _(b"date expects a date information"),
68 )
68 )
69 fmt = None
69 fmt = None
70 if len(args) == 2:
70 if len(args) == 2:
71 fmt = evalstring(context, mapping, args[1])
71 fmt = evalstring(context, mapping, args[1])
72 if fmt is None:
72 if fmt is None:
73 return dateutil.datestr(date)
73 return dateutil.datestr(date)
74 else:
74 else:
75 return dateutil.datestr(date, fmt)
75 return dateutil.datestr(date, fmt)
76
76
77
77
78 @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs')
78 @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs')
79 def dict_(context, mapping, args):
79 def dict_(context, mapping, args):
80 """Construct a dict from key-value pairs. A key may be omitted if
80 """Construct a dict from key-value pairs. A key may be omitted if
81 a value expression can provide an unambiguous name."""
81 a value expression can provide an unambiguous name."""
82 data = util.sortdict()
82 data = util.sortdict()
83
83
84 for v in args[b'args']:
84 for v in args[b'args']:
85 k = templateutil.findsymbolicname(v)
85 k = templateutil.findsymbolicname(v)
86 if not k:
86 if not k:
87 raise error.ParseError(_(b'dict key cannot be inferred'))
87 raise error.ParseError(_(b'dict key cannot be inferred'))
88 if k in data or k in args[b'kwargs']:
88 if k in data or k in args[b'kwargs']:
89 raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k)
89 raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k)
90 data[k] = evalfuncarg(context, mapping, v)
90 data[k] = evalfuncarg(context, mapping, v)
91
91
92 data.update(
92 data.update(
93 (k, evalfuncarg(context, mapping, v))
93 (k, evalfuncarg(context, mapping, v))
94 for k, v in pycompat.iteritems(args[b'kwargs'])
94 for k, v in pycompat.iteritems(args[b'kwargs'])
95 )
95 )
96 return templateutil.hybriddict(data)
96 return templateutil.hybriddict(data)
97
97
98
98
99 @templatefunc(
99 @templatefunc(
100 b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'}
100 b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'}
101 )
101 )
102 def diff(context, mapping, args):
102 def diff(context, mapping, args):
103 """Show a diff, optionally
103 """Show a diff, optionally
104 specifying files to include or exclude."""
104 specifying files to include or exclude."""
105 if len(args) > 2:
105 if len(args) > 2:
106 # i18n: "diff" is a keyword
106 # i18n: "diff" is a keyword
107 raise error.ParseError(_(b"diff expects zero, one, or two arguments"))
107 raise error.ParseError(_(b"diff expects zero, one, or two arguments"))
108
108
109 def getpatterns(i):
109 def getpatterns(i):
110 if i < len(args):
110 if i < len(args):
111 s = evalstring(context, mapping, args[i]).strip()
111 s = evalstring(context, mapping, args[i]).strip()
112 if s:
112 if s:
113 return [s]
113 return [s]
114 return []
114 return []
115
115
116 ctx = context.resource(mapping, b'ctx')
116 ctx = context.resource(mapping, b'ctx')
117 ui = context.resource(mapping, b'ui')
117 ui = context.resource(mapping, b'ui')
118 diffopts = diffutil.diffallopts(ui)
118 diffopts = diffutil.diffallopts(ui)
119 chunks = ctx.diff(
119 chunks = ctx.diff(
120 match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts
120 match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts
121 )
121 )
122
122
123 return b''.join(chunks)
123 return b''.join(chunks)
124
124
125
125
126 @templatefunc(
126 @templatefunc(
127 b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'}
127 b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'}
128 )
128 )
129 def extdata(context, mapping, args):
129 def extdata(context, mapping, args):
130 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
130 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
131 if b'source' not in args:
131 if b'source' not in args:
132 # i18n: "extdata" is a keyword
132 # i18n: "extdata" is a keyword
133 raise error.ParseError(_(b'extdata expects one argument'))
133 raise error.ParseError(_(b'extdata expects one argument'))
134
134
135 source = evalstring(context, mapping, args[b'source'])
135 source = evalstring(context, mapping, args[b'source'])
136 if not source:
136 if not source:
137 sym = templateutil.findsymbolicname(args[b'source'])
137 sym = templateutil.findsymbolicname(args[b'source'])
138 if sym:
138 if sym:
139 raise error.ParseError(
139 raise error.ParseError(
140 _(b'empty data source specified'),
140 _(b'empty data source specified'),
141 hint=_(b"did you mean extdata('%s')?") % sym,
141 hint=_(b"did you mean extdata('%s')?") % sym,
142 )
142 )
143 else:
143 else:
144 raise error.ParseError(_(b'empty data source specified'))
144 raise error.ParseError(_(b'empty data source specified'))
145 cache = context.resource(mapping, b'cache').setdefault(b'extdata', {})
145 cache = context.resource(mapping, b'cache').setdefault(b'extdata', {})
146 ctx = context.resource(mapping, b'ctx')
146 ctx = context.resource(mapping, b'ctx')
147 if source in cache:
147 if source in cache:
148 data = cache[source]
148 data = cache[source]
149 else:
149 else:
150 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
150 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
151 return data.get(ctx.rev(), b'')
151 return data.get(ctx.rev(), b'')
152
152
153
153
154 @templatefunc(b'files(pattern)', requires={b'ctx'})
154 @templatefunc(b'files(pattern)', requires={b'ctx'})
155 def files(context, mapping, args):
155 def files(context, mapping, args):
156 """All files of the current changeset matching the pattern. See
156 """All files of the current changeset matching the pattern. See
157 :hg:`help patterns`."""
157 :hg:`help patterns`."""
158 if not len(args) == 1:
158 if not len(args) == 1:
159 # i18n: "files" is a keyword
159 # i18n: "files" is a keyword
160 raise error.ParseError(_(b"files expects one argument"))
160 raise error.ParseError(_(b"files expects one argument"))
161
161
162 raw = evalstring(context, mapping, args[0])
162 raw = evalstring(context, mapping, args[0])
163 ctx = context.resource(mapping, b'ctx')
163 ctx = context.resource(mapping, b'ctx')
164 m = ctx.match([raw])
164 m = ctx.match([raw])
165 files = list(ctx.matches(m))
165 files = list(ctx.matches(m))
166 return templateutil.compatfileslist(context, mapping, b"file", files)
166 return templateutil.compatfileslist(context, mapping, b"file", files)
167
167
168
168
169 @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])')
169 @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])')
170 def fill(context, mapping, args):
170 def fill(context, mapping, args):
171 """Fill many
171 """Fill many
172 paragraphs with optional indentation. See the "fill" filter."""
172 paragraphs with optional indentation. See the "fill" filter."""
173 if not (1 <= len(args) <= 4):
173 if not (1 <= len(args) <= 4):
174 # i18n: "fill" is a keyword
174 # i18n: "fill" is a keyword
175 raise error.ParseError(_(b"fill expects one to four arguments"))
175 raise error.ParseError(_(b"fill expects one to four arguments"))
176
176
177 text = evalstring(context, mapping, args[0])
177 text = evalstring(context, mapping, args[0])
178 width = 76
178 width = 76
179 initindent = b''
179 initindent = b''
180 hangindent = b''
180 hangindent = b''
181 if 2 <= len(args) <= 4:
181 if 2 <= len(args) <= 4:
182 width = evalinteger(
182 width = evalinteger(
183 context,
183 context,
184 mapping,
184 mapping,
185 args[1],
185 args[1],
186 # i18n: "fill" is a keyword
186 # i18n: "fill" is a keyword
187 _(b"fill expects an integer width"),
187 _(b"fill expects an integer width"),
188 )
188 )
189 try:
189 try:
190 initindent = evalstring(context, mapping, args[2])
190 initindent = evalstring(context, mapping, args[2])
191 hangindent = evalstring(context, mapping, args[3])
191 hangindent = evalstring(context, mapping, args[3])
192 except IndexError:
192 except IndexError:
193 pass
193 pass
194
194
195 return templatefilters.fill(text, width, initindent, hangindent)
195 return templatefilters.fill(text, width, initindent, hangindent)
196
196
197
197
198 @templatefunc(b'filter(iterable[, expr])')
198 @templatefunc(b'filter(iterable[, expr])')
199 def filter_(context, mapping, args):
199 def filter_(context, mapping, args):
200 """Remove empty elements from a list or a dict. If expr specified, it's
200 """Remove empty elements from a list or a dict. If expr specified, it's
201 applied to each element to test emptiness."""
201 applied to each element to test emptiness."""
202 if not (1 <= len(args) <= 2):
202 if not (1 <= len(args) <= 2):
203 # i18n: "filter" is a keyword
203 # i18n: "filter" is a keyword
204 raise error.ParseError(_(b"filter expects one or two arguments"))
204 raise error.ParseError(_(b"filter expects one or two arguments"))
205 iterable = evalwrapped(context, mapping, args[0])
205 iterable = evalwrapped(context, mapping, args[0])
206 if len(args) == 1:
206 if len(args) == 1:
207
207
208 def select(w):
208 def select(w):
209 return w.tobool(context, mapping)
209 return w.tobool(context, mapping)
210
210
211 else:
211 else:
212
212
213 def select(w):
213 def select(w):
214 if not isinstance(w, templateutil.mappable):
214 if not isinstance(w, templateutil.mappable):
215 raise error.ParseError(_(b"not filterable by expression"))
215 raise error.ParseError(_(b"not filterable by expression"))
216 lm = context.overlaymap(mapping, w.tomap(context))
216 lm = context.overlaymap(mapping, w.tomap(context))
217 return evalboolean(context, lm, args[1])
217 return evalboolean(context, lm, args[1])
218
218
219 return iterable.filter(context, mapping, select)
219 return iterable.filter(context, mapping, select)
220
220
221
221
222 @templatefunc(b'formatnode(node)', requires={b'ui'})
222 @templatefunc(b'formatnode(node)', requires={b'ui'})
223 def formatnode(context, mapping, args):
223 def formatnode(context, mapping, args):
224 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
224 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
225 if len(args) != 1:
225 if len(args) != 1:
226 # i18n: "formatnode" is a keyword
226 # i18n: "formatnode" is a keyword
227 raise error.ParseError(_(b"formatnode expects one argument"))
227 raise error.ParseError(_(b"formatnode expects one argument"))
228
228
229 ui = context.resource(mapping, b'ui')
229 ui = context.resource(mapping, b'ui')
230 node = evalstring(context, mapping, args[0])
230 node = evalstring(context, mapping, args[0])
231 if ui.debugflag:
231 if ui.debugflag:
232 return node
232 return node
233 return templatefilters.short(node)
233 return templatefilters.short(node)
234
234
235
235
236 @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'})
236 @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'})
237 def mailmap(context, mapping, args):
237 def mailmap(context, mapping, args):
238 """Return the author, updated according to the value
238 """Return the author, updated according to the value
239 set in the .mailmap file"""
239 set in the .mailmap file"""
240 if len(args) != 1:
240 if len(args) != 1:
241 raise error.ParseError(_(b"mailmap expects one argument"))
241 raise error.ParseError(_(b"mailmap expects one argument"))
242
242
243 author = evalstring(context, mapping, args[0])
243 author = evalstring(context, mapping, args[0])
244
244
245 cache = context.resource(mapping, b'cache')
245 cache = context.resource(mapping, b'cache')
246 repo = context.resource(mapping, b'repo')
246 repo = context.resource(mapping, b'repo')
247
247
248 if b'mailmap' not in cache:
248 if b'mailmap' not in cache:
249 data = repo.wvfs.tryread(b'.mailmap')
249 data = repo.wvfs.tryread(b'.mailmap')
250 cache[b'mailmap'] = stringutil.parsemailmap(data)
250 cache[b'mailmap'] = stringutil.parsemailmap(data)
251
251
252 return stringutil.mapname(cache[b'mailmap'], author)
252 return stringutil.mapname(cache[b'mailmap'], author)
253
253
254
254
255 @templatefunc(
255 @templatefunc(
256 b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])',
256 b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])',
257 argspec=b'text width fillchar left truncate',
257 argspec=b'text width fillchar left truncate',
258 )
258 )
259 def pad(context, mapping, args):
259 def pad(context, mapping, args):
260 """Pad text with a
260 """Pad text with a
261 fill character."""
261 fill character."""
262 if b'text' not in args or b'width' not in args:
262 if b'text' not in args or b'width' not in args:
263 # i18n: "pad" is a keyword
263 # i18n: "pad" is a keyword
264 raise error.ParseError(_(b"pad() expects two to four arguments"))
264 raise error.ParseError(_(b"pad() expects two to four arguments"))
265
265
266 width = evalinteger(
266 width = evalinteger(
267 context,
267 context,
268 mapping,
268 mapping,
269 args[b'width'],
269 args[b'width'],
270 # i18n: "pad" is a keyword
270 # i18n: "pad" is a keyword
271 _(b"pad() expects an integer width"),
271 _(b"pad() expects an integer width"),
272 )
272 )
273
273
274 text = evalstring(context, mapping, args[b'text'])
274 text = evalstring(context, mapping, args[b'text'])
275
275
276 truncate = False
276 truncate = False
277 left = False
277 left = False
278 fillchar = b' '
278 fillchar = b' '
279 if b'fillchar' in args:
279 if b'fillchar' in args:
280 fillchar = evalstring(context, mapping, args[b'fillchar'])
280 fillchar = evalstring(context, mapping, args[b'fillchar'])
281 if len(color.stripeffects(fillchar)) != 1:
281 if len(color.stripeffects(fillchar)) != 1:
282 # i18n: "pad" is a keyword
282 # i18n: "pad" is a keyword
283 raise error.ParseError(_(b"pad() expects a single fill character"))
283 raise error.ParseError(_(b"pad() expects a single fill character"))
284 if b'left' in args:
284 if b'left' in args:
285 left = evalboolean(context, mapping, args[b'left'])
285 left = evalboolean(context, mapping, args[b'left'])
286 if b'truncate' in args:
286 if b'truncate' in args:
287 truncate = evalboolean(context, mapping, args[b'truncate'])
287 truncate = evalboolean(context, mapping, args[b'truncate'])
288
288
289 fillwidth = width - encoding.colwidth(color.stripeffects(text))
289 fillwidth = width - encoding.colwidth(color.stripeffects(text))
290 if fillwidth < 0 and truncate:
290 if fillwidth < 0 and truncate:
291 return encoding.trim(color.stripeffects(text), width, leftside=left)
291 return encoding.trim(color.stripeffects(text), width, leftside=left)
292 if fillwidth <= 0:
292 if fillwidth <= 0:
293 return text
293 return text
294 if left:
294 if left:
295 return fillchar * fillwidth + text
295 return fillchar * fillwidth + text
296 else:
296 else:
297 return text + fillchar * fillwidth
297 return text + fillchar * fillwidth
298
298
299
299
300 @templatefunc(b'indent(text, indentchars[, firstline])')
300 @templatefunc(b'indent(text, indentchars[, firstline])')
301 def indent(context, mapping, args):
301 def indent(context, mapping, args):
302 """Indents all non-empty lines
302 """Indents all non-empty lines
303 with the characters given in the indentchars string. An optional
303 with the characters given in the indentchars string. An optional
304 third parameter will override the indent for the first line only
304 third parameter will override the indent for the first line only
305 if present."""
305 if present."""
306 if not (2 <= len(args) <= 3):
306 if not (2 <= len(args) <= 3):
307 # i18n: "indent" is a keyword
307 # i18n: "indent" is a keyword
308 raise error.ParseError(_(b"indent() expects two or three arguments"))
308 raise error.ParseError(_(b"indent() expects two or three arguments"))
309
309
310 text = evalstring(context, mapping, args[0])
310 text = evalstring(context, mapping, args[0])
311 indent = evalstring(context, mapping, args[1])
311 indent = evalstring(context, mapping, args[1])
312
312
313 firstline = indent
313 firstline = indent
314 if len(args) == 3:
314 if len(args) == 3:
315 firstline = evalstring(context, mapping, args[2])
315 firstline = evalstring(context, mapping, args[2])
316
316
317 return templatefilters.indent(text, indent, firstline=firstline)
317 return templatefilters.indent(text, indent, firstline=firstline)
318
318
319
319
320 @templatefunc(b'get(dict, key)')
320 @templatefunc(b'get(dict, key)')
321 def get(context, mapping, args):
321 def get(context, mapping, args):
322 """Get an attribute/key from an object. Some keywords
322 """Get an attribute/key from an object. Some keywords
323 are complex types. This function allows you to obtain the value of an
323 are complex types. This function allows you to obtain the value of an
324 attribute on these types."""
324 attribute on these types."""
325 if len(args) != 2:
325 if len(args) != 2:
326 # i18n: "get" is a keyword
326 # i18n: "get" is a keyword
327 raise error.ParseError(_(b"get() expects two arguments"))
327 raise error.ParseError(_(b"get() expects two arguments"))
328
328
329 dictarg = evalwrapped(context, mapping, args[0])
329 dictarg = evalwrapped(context, mapping, args[0])
330 key = evalrawexp(context, mapping, args[1])
330 key = evalrawexp(context, mapping, args[1])
331 try:
331 try:
332 return dictarg.getmember(context, mapping, key)
332 return dictarg.getmember(context, mapping, key)
333 except error.ParseError as err:
333 except error.ParseError as err:
334 # i18n: "get" is a keyword
334 # i18n: "get" is a keyword
335 hint = _(b"get() expects a dict as first argument")
335 hint = _(b"get() expects a dict as first argument")
336 raise error.ParseError(bytes(err), hint=hint)
336 raise error.ParseError(bytes(err), hint=hint)
337
337
338
338
339 @templatefunc(b'config(section, name[, default])', requires={b'ui'})
339 @templatefunc(b'config(section, name[, default])', requires={b'ui'})
340 def config(context, mapping, args):
340 def config(context, mapping, args):
341 """Returns the requested hgrc config option as a string."""
341 """Returns the requested hgrc config option as a string."""
342 fn = context.resource(mapping, b'ui').config
342 fn = context.resource(mapping, b'ui').config
343 return _config(context, mapping, args, fn, evalstring)
343 return _config(context, mapping, args, fn, evalstring)
344
344
345
345
346 @templatefunc(b'configbool(section, name[, default])', requires={b'ui'})
346 @templatefunc(b'configbool(section, name[, default])', requires={b'ui'})
347 def configbool(context, mapping, args):
347 def configbool(context, mapping, args):
348 """Returns the requested hgrc config option as a boolean."""
348 """Returns the requested hgrc config option as a boolean."""
349 fn = context.resource(mapping, b'ui').configbool
349 fn = context.resource(mapping, b'ui').configbool
350 return _config(context, mapping, args, fn, evalboolean)
350 return _config(context, mapping, args, fn, evalboolean)
351
351
352
352
353 @templatefunc(b'configint(section, name[, default])', requires={b'ui'})
353 @templatefunc(b'configint(section, name[, default])', requires={b'ui'})
354 def configint(context, mapping, args):
354 def configint(context, mapping, args):
355 """Returns the requested hgrc config option as an integer."""
355 """Returns the requested hgrc config option as an integer."""
356 fn = context.resource(mapping, b'ui').configint
356 fn = context.resource(mapping, b'ui').configint
357 return _config(context, mapping, args, fn, evalinteger)
357 return _config(context, mapping, args, fn, evalinteger)
358
358
359
359
360 def _config(context, mapping, args, configfn, defaultfn):
360 def _config(context, mapping, args, configfn, defaultfn):
361 if not (2 <= len(args) <= 3):
361 if not (2 <= len(args) <= 3):
362 raise error.ParseError(_(b"config expects two or three arguments"))
362 raise error.ParseError(_(b"config expects two or three arguments"))
363
363
364 # The config option can come from any section, though we specifically
364 # The config option can come from any section, though we specifically
365 # reserve the [templateconfig] section for dynamically defining options
365 # reserve the [templateconfig] section for dynamically defining options
366 # for this function without also requiring an extension.
366 # for this function without also requiring an extension.
367 section = evalstringliteral(context, mapping, args[0])
367 section = evalstringliteral(context, mapping, args[0])
368 name = evalstringliteral(context, mapping, args[1])
368 name = evalstringliteral(context, mapping, args[1])
369 if len(args) == 3:
369 if len(args) == 3:
370 default = defaultfn(context, mapping, args[2])
370 default = defaultfn(context, mapping, args[2])
371 return configfn(section, name, default)
371 return configfn(section, name, default)
372 else:
372 else:
373 return configfn(section, name)
373 return configfn(section, name)
374
374
375
375
376 @templatefunc(b'if(expr, then[, else])')
376 @templatefunc(b'if(expr, then[, else])')
377 def if_(context, mapping, args):
377 def if_(context, mapping, args):
378 """Conditionally execute based on the result of
378 """Conditionally execute based on the result of
379 an expression."""
379 an expression."""
380 if not (2 <= len(args) <= 3):
380 if not (2 <= len(args) <= 3):
381 # i18n: "if" is a keyword
381 # i18n: "if" is a keyword
382 raise error.ParseError(_(b"if expects two or three arguments"))
382 raise error.ParseError(_(b"if expects two or three arguments"))
383
383
384 test = evalboolean(context, mapping, args[0])
384 test = evalboolean(context, mapping, args[0])
385 if test:
385 if test:
386 return evalrawexp(context, mapping, args[1])
386 return evalrawexp(context, mapping, args[1])
387 elif len(args) == 3:
387 elif len(args) == 3:
388 return evalrawexp(context, mapping, args[2])
388 return evalrawexp(context, mapping, args[2])
389
389
390
390
391 @templatefunc(b'ifcontains(needle, haystack, then[, else])')
391 @templatefunc(b'ifcontains(needle, haystack, then[, else])')
392 def ifcontains(context, mapping, args):
392 def ifcontains(context, mapping, args):
393 """Conditionally execute based
393 """Conditionally execute based
394 on whether the item "needle" is in "haystack"."""
394 on whether the item "needle" is in "haystack"."""
395 if not (3 <= len(args) <= 4):
395 if not (3 <= len(args) <= 4):
396 # i18n: "ifcontains" is a keyword
396 # i18n: "ifcontains" is a keyword
397 raise error.ParseError(_(b"ifcontains expects three or four arguments"))
397 raise error.ParseError(_(b"ifcontains expects three or four arguments"))
398
398
399 haystack = evalwrapped(context, mapping, args[1])
399 haystack = evalwrapped(context, mapping, args[1])
400 try:
400 try:
401 needle = evalrawexp(context, mapping, args[0])
401 needle = evalrawexp(context, mapping, args[0])
402 found = haystack.contains(context, mapping, needle)
402 found = haystack.contains(context, mapping, needle)
403 except error.ParseError:
403 except error.ParseError:
404 found = False
404 found = False
405
405
406 if found:
406 if found:
407 return evalrawexp(context, mapping, args[2])
407 return evalrawexp(context, mapping, args[2])
408 elif len(args) == 4:
408 elif len(args) == 4:
409 return evalrawexp(context, mapping, args[3])
409 return evalrawexp(context, mapping, args[3])
410
410
411
411
412 @templatefunc(b'ifeq(expr1, expr2, then[, else])')
412 @templatefunc(b'ifeq(expr1, expr2, then[, else])')
413 def ifeq(context, mapping, args):
413 def ifeq(context, mapping, args):
414 """Conditionally execute based on
414 """Conditionally execute based on
415 whether 2 items are equivalent."""
415 whether 2 items are equivalent."""
416 if not (3 <= len(args) <= 4):
416 if not (3 <= len(args) <= 4):
417 # i18n: "ifeq" is a keyword
417 # i18n: "ifeq" is a keyword
418 raise error.ParseError(_(b"ifeq expects three or four arguments"))
418 raise error.ParseError(_(b"ifeq expects three or four arguments"))
419
419
420 test = evalstring(context, mapping, args[0])
420 test = evalstring(context, mapping, args[0])
421 match = evalstring(context, mapping, args[1])
421 match = evalstring(context, mapping, args[1])
422 if test == match:
422 if test == match:
423 return evalrawexp(context, mapping, args[2])
423 return evalrawexp(context, mapping, args[2])
424 elif len(args) == 4:
424 elif len(args) == 4:
425 return evalrawexp(context, mapping, args[3])
425 return evalrawexp(context, mapping, args[3])
426
426
427
427
428 @templatefunc(b'join(list, sep)')
428 @templatefunc(b'join(list, sep)')
429 def join(context, mapping, args):
429 def join(context, mapping, args):
430 """Join items in a list with a delimiter."""
430 """Join items in a list with a delimiter."""
431 if not (1 <= len(args) <= 2):
431 if not (1 <= len(args) <= 2):
432 # i18n: "join" is a keyword
432 # i18n: "join" is a keyword
433 raise error.ParseError(_(b"join expects one or two arguments"))
433 raise error.ParseError(_(b"join expects one or two arguments"))
434
434
435 joinset = evalwrapped(context, mapping, args[0])
435 joinset = evalwrapped(context, mapping, args[0])
436 joiner = b" "
436 joiner = b" "
437 if len(args) > 1:
437 if len(args) > 1:
438 joiner = evalstring(context, mapping, args[1])
438 joiner = evalstring(context, mapping, args[1])
439 return joinset.join(context, mapping, joiner)
439 return joinset.join(context, mapping, joiner)
440
440
441
441
442 @templatefunc(b'label(label, expr)', requires={b'ui'})
442 @templatefunc(b'label(label, expr)', requires={b'ui'})
443 def label(context, mapping, args):
443 def label(context, mapping, args):
444 """Apply a label to generated content. Content with
444 """Apply a label to generated content. Content with
445 a label applied can result in additional post-processing, such as
445 a label applied can result in additional post-processing, such as
446 automatic colorization."""
446 automatic colorization."""
447 if len(args) != 2:
447 if len(args) != 2:
448 # i18n: "label" is a keyword
448 # i18n: "label" is a keyword
449 raise error.ParseError(_(b"label expects two arguments"))
449 raise error.ParseError(_(b"label expects two arguments"))
450
450
451 ui = context.resource(mapping, b'ui')
451 ui = context.resource(mapping, b'ui')
452 thing = evalstring(context, mapping, args[1])
452 thing = evalstring(context, mapping, args[1])
453 # preserve unknown symbol as literal so effects like 'red', 'bold',
453 # preserve unknown symbol as literal so effects like 'red', 'bold',
454 # etc. don't need to be quoted
454 # etc. don't need to be quoted
455 label = evalstringliteral(context, mapping, args[0])
455 label = evalstringliteral(context, mapping, args[0])
456
456
457 return ui.label(thing, label)
457 return ui.label(thing, label)
458
458
459
459
460 @templatefunc(b'latesttag([pattern])')
460 @templatefunc(b'latesttag([pattern])')
461 def latesttag(context, mapping, args):
461 def latesttag(context, mapping, args):
462 """The global tags matching the given pattern on the
462 """The global tags matching the given pattern on the
463 most recent globally tagged ancestor of this changeset.
463 most recent globally tagged ancestor of this changeset.
464 If no such tags exist, the "{tag}" template resolves to
464 If no such tags exist, the "{tag}" template resolves to
465 the string "null". See :hg:`help revisions.patterns` for the pattern
465 the string "null". See :hg:`help revisions.patterns` for the pattern
466 syntax.
466 syntax.
467 """
467 """
468 if len(args) > 1:
468 if len(args) > 1:
469 # i18n: "latesttag" is a keyword
469 # i18n: "latesttag" is a keyword
470 raise error.ParseError(_(b"latesttag expects at most one argument"))
470 raise error.ParseError(_(b"latesttag expects at most one argument"))
471
471
472 pattern = None
472 pattern = None
473 if len(args) == 1:
473 if len(args) == 1:
474 pattern = evalstring(context, mapping, args[0])
474 pattern = evalstring(context, mapping, args[0])
475 return templatekw.showlatesttags(context, mapping, pattern)
475 return templatekw.showlatesttags(context, mapping, pattern)
476
476
477
477
478 @templatefunc(b'localdate(date[, tz])')
478 @templatefunc(b'localdate(date[, tz])')
479 def localdate(context, mapping, args):
479 def localdate(context, mapping, args):
480 """Converts a date to the specified timezone.
480 """Converts a date to the specified timezone.
481 The default is local date."""
481 The default is local date."""
482 if not (1 <= len(args) <= 2):
482 if not (1 <= len(args) <= 2):
483 # i18n: "localdate" is a keyword
483 # i18n: "localdate" is a keyword
484 raise error.ParseError(_(b"localdate expects one or two arguments"))
484 raise error.ParseError(_(b"localdate expects one or two arguments"))
485
485
486 date = evaldate(
486 date = evaldate(
487 context,
487 context,
488 mapping,
488 mapping,
489 args[0],
489 args[0],
490 # i18n: "localdate" is a keyword
490 # i18n: "localdate" is a keyword
491 _(b"localdate expects a date information"),
491 _(b"localdate expects a date information"),
492 )
492 )
493 if len(args) >= 2:
493 if len(args) >= 2:
494 tzoffset = None
494 tzoffset = None
495 tz = evalfuncarg(context, mapping, args[1])
495 tz = evalfuncarg(context, mapping, args[1])
496 if isinstance(tz, bytes):
496 if isinstance(tz, bytes):
497 tzoffset, remainder = dateutil.parsetimezone(tz)
497 tzoffset, remainder = dateutil.parsetimezone(tz)
498 if remainder:
498 if remainder:
499 tzoffset = None
499 tzoffset = None
500 if tzoffset is None:
500 if tzoffset is None:
501 try:
501 try:
502 tzoffset = int(tz)
502 tzoffset = int(tz)
503 except (TypeError, ValueError):
503 except (TypeError, ValueError):
504 # i18n: "localdate" is a keyword
504 # i18n: "localdate" is a keyword
505 raise error.ParseError(_(b"localdate expects a timezone"))
505 raise error.ParseError(_(b"localdate expects a timezone"))
506 else:
506 else:
507 tzoffset = dateutil.makedate()[1]
507 tzoffset = dateutil.makedate()[1]
508 return templateutil.date((date[0], tzoffset))
508 return templateutil.date((date[0], tzoffset))
509
509
510
510
511 @templatefunc(b'max(iterable)')
511 @templatefunc(b'max(iterable)')
512 def max_(context, mapping, args, **kwargs):
512 def max_(context, mapping, args, **kwargs):
513 """Return the max of an iterable"""
513 """Return the max of an iterable"""
514 if len(args) != 1:
514 if len(args) != 1:
515 # i18n: "max" is a keyword
515 # i18n: "max" is a keyword
516 raise error.ParseError(_(b"max expects one argument"))
516 raise error.ParseError(_(b"max expects one argument"))
517
517
518 iterable = evalwrapped(context, mapping, args[0])
518 iterable = evalwrapped(context, mapping, args[0])
519 try:
519 try:
520 return iterable.getmax(context, mapping)
520 return iterable.getmax(context, mapping)
521 except error.ParseError as err:
521 except error.ParseError as err:
522 # i18n: "max" is a keyword
522 # i18n: "max" is a keyword
523 hint = _(b"max first argument should be an iterable")
523 hint = _(b"max first argument should be an iterable")
524 raise error.ParseError(bytes(err), hint=hint)
524 raise error.ParseError(bytes(err), hint=hint)
525
525
526
526
527 @templatefunc(b'min(iterable)')
527 @templatefunc(b'min(iterable)')
528 def min_(context, mapping, args, **kwargs):
528 def min_(context, mapping, args, **kwargs):
529 """Return the min of an iterable"""
529 """Return the min of an iterable"""
530 if len(args) != 1:
530 if len(args) != 1:
531 # i18n: "min" is a keyword
531 # i18n: "min" is a keyword
532 raise error.ParseError(_(b"min expects one argument"))
532 raise error.ParseError(_(b"min expects one argument"))
533
533
534 iterable = evalwrapped(context, mapping, args[0])
534 iterable = evalwrapped(context, mapping, args[0])
535 try:
535 try:
536 return iterable.getmin(context, mapping)
536 return iterable.getmin(context, mapping)
537 except error.ParseError as err:
537 except error.ParseError as err:
538 # i18n: "min" is a keyword
538 # i18n: "min" is a keyword
539 hint = _(b"min first argument should be an iterable")
539 hint = _(b"min first argument should be an iterable")
540 raise error.ParseError(bytes(err), hint=hint)
540 raise error.ParseError(bytes(err), hint=hint)
541
541
542
542
543 @templatefunc(b'mod(a, b)')
543 @templatefunc(b'mod(a, b)')
544 def mod(context, mapping, args):
544 def mod(context, mapping, args):
545 """Calculate a mod b such that a / b + a mod b == a"""
545 """Calculate a mod b such that a / b + a mod b == a"""
546 if not len(args) == 2:
546 if not len(args) == 2:
547 # i18n: "mod" is a keyword
547 # i18n: "mod" is a keyword
548 raise error.ParseError(_(b"mod expects two arguments"))
548 raise error.ParseError(_(b"mod expects two arguments"))
549
549
550 func = lambda a, b: a % b
550 func = lambda a, b: a % b
551 return templateutil.runarithmetic(
551 return templateutil.runarithmetic(
552 context, mapping, (func, args[0], args[1])
552 context, mapping, (func, args[0], args[1])
553 )
553 )
554
554
555
555
556 @templatefunc(b'obsfateoperations(markers)')
556 @templatefunc(b'obsfateoperations(markers)')
557 def obsfateoperations(context, mapping, args):
557 def obsfateoperations(context, mapping, args):
558 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
558 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
559 if len(args) != 1:
559 if len(args) != 1:
560 # i18n: "obsfateoperations" is a keyword
560 # i18n: "obsfateoperations" is a keyword
561 raise error.ParseError(_(b"obsfateoperations expects one argument"))
561 raise error.ParseError(_(b"obsfateoperations expects one argument"))
562
562
563 markers = evalfuncarg(context, mapping, args[0])
563 markers = evalfuncarg(context, mapping, args[0])
564
564
565 try:
565 try:
566 data = obsutil.markersoperations(markers)
566 data = obsutil.markersoperations(markers)
567 return templateutil.hybridlist(data, name=b'operation')
567 return templateutil.hybridlist(data, name=b'operation')
568 except (TypeError, KeyError):
568 except (TypeError, KeyError):
569 # i18n: "obsfateoperations" is a keyword
569 # i18n: "obsfateoperations" is a keyword
570 errmsg = _(b"obsfateoperations first argument should be an iterable")
570 errmsg = _(b"obsfateoperations first argument should be an iterable")
571 raise error.ParseError(errmsg)
571 raise error.ParseError(errmsg)
572
572
573
573
574 @templatefunc(b'obsfatedate(markers)')
574 @templatefunc(b'obsfatedate(markers)')
575 def obsfatedate(context, mapping, args):
575 def obsfatedate(context, mapping, args):
576 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
576 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
577 if len(args) != 1:
577 if len(args) != 1:
578 # i18n: "obsfatedate" is a keyword
578 # i18n: "obsfatedate" is a keyword
579 raise error.ParseError(_(b"obsfatedate expects one argument"))
579 raise error.ParseError(_(b"obsfatedate expects one argument"))
580
580
581 markers = evalfuncarg(context, mapping, args[0])
581 markers = evalfuncarg(context, mapping, args[0])
582
582
583 try:
583 try:
584 # TODO: maybe this has to be a wrapped list of date wrappers?
584 # TODO: maybe this has to be a wrapped list of date wrappers?
585 data = obsutil.markersdates(markers)
585 data = obsutil.markersdates(markers)
586 return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d')
586 return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d')
587 except (TypeError, KeyError):
587 except (TypeError, KeyError):
588 # i18n: "obsfatedate" is a keyword
588 # i18n: "obsfatedate" is a keyword
589 errmsg = _(b"obsfatedate first argument should be an iterable")
589 errmsg = _(b"obsfatedate first argument should be an iterable")
590 raise error.ParseError(errmsg)
590 raise error.ParseError(errmsg)
591
591
592
592
593 @templatefunc(b'obsfateusers(markers)')
593 @templatefunc(b'obsfateusers(markers)')
594 def obsfateusers(context, mapping, args):
594 def obsfateusers(context, mapping, args):
595 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
595 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
596 if len(args) != 1:
596 if len(args) != 1:
597 # i18n: "obsfateusers" is a keyword
597 # i18n: "obsfateusers" is a keyword
598 raise error.ParseError(_(b"obsfateusers expects one argument"))
598 raise error.ParseError(_(b"obsfateusers expects one argument"))
599
599
600 markers = evalfuncarg(context, mapping, args[0])
600 markers = evalfuncarg(context, mapping, args[0])
601
601
602 try:
602 try:
603 data = obsutil.markersusers(markers)
603 data = obsutil.markersusers(markers)
604 return templateutil.hybridlist(data, name=b'user')
604 return templateutil.hybridlist(data, name=b'user')
605 except (TypeError, KeyError, ValueError):
605 except (TypeError, KeyError, ValueError):
606 # i18n: "obsfateusers" is a keyword
606 # i18n: "obsfateusers" is a keyword
607 msg = _(
607 msg = _(
608 b"obsfateusers first argument should be an iterable of "
608 b"obsfateusers first argument should be an iterable of "
609 b"obsmakers"
609 b"obsmakers"
610 )
610 )
611 raise error.ParseError(msg)
611 raise error.ParseError(msg)
612
612
613
613
614 @templatefunc(b'obsfateverb(successors, markers)')
614 @templatefunc(b'obsfateverb(successors, markers)')
615 def obsfateverb(context, mapping, args):
615 def obsfateverb(context, mapping, args):
616 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
616 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
617 if len(args) != 2:
617 if len(args) != 2:
618 # i18n: "obsfateverb" is a keyword
618 # i18n: "obsfateverb" is a keyword
619 raise error.ParseError(_(b"obsfateverb expects two arguments"))
619 raise error.ParseError(_(b"obsfateverb expects two arguments"))
620
620
621 successors = evalfuncarg(context, mapping, args[0])
621 successors = evalfuncarg(context, mapping, args[0])
622 markers = evalfuncarg(context, mapping, args[1])
622 markers = evalfuncarg(context, mapping, args[1])
623
623
624 try:
624 try:
625 return obsutil.obsfateverb(successors, markers)
625 return obsutil.obsfateverb(successors, markers)
626 except TypeError:
626 except TypeError:
627 # i18n: "obsfateverb" is a keyword
627 # i18n: "obsfateverb" is a keyword
628 errmsg = _(b"obsfateverb first argument should be countable")
628 errmsg = _(b"obsfateverb first argument should be countable")
629 raise error.ParseError(errmsg)
629 raise error.ParseError(errmsg)
630
630
631
631
632 @templatefunc(b'relpath(path)', requires={b'repo'})
632 @templatefunc(b'relpath(path)', requires={b'repo'})
633 def relpath(context, mapping, args):
633 def relpath(context, mapping, args):
634 """Convert a repository-absolute path into a filesystem path relative to
634 """Convert a repository-absolute path into a filesystem path relative to
635 the current working directory."""
635 the current working directory."""
636 if len(args) != 1:
636 if len(args) != 1:
637 # i18n: "relpath" is a keyword
637 # i18n: "relpath" is a keyword
638 raise error.ParseError(_(b"relpath expects one argument"))
638 raise error.ParseError(_(b"relpath expects one argument"))
639
639
640 repo = context.resource(mapping, b'repo')
640 repo = context.resource(mapping, b'repo')
641 path = evalstring(context, mapping, args[0])
641 path = evalstring(context, mapping, args[0])
642 return repo.pathto(path)
642 return repo.pathto(path)
643
643
644
644
645 @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'})
645 @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'})
646 def revset(context, mapping, args):
646 def revset(context, mapping, args):
647 """Execute a revision set query. See
647 """Execute a revision set query. See
648 :hg:`help revset`."""
648 :hg:`help revset`."""
649 if not len(args) > 0:
649 if not len(args) > 0:
650 # i18n: "revset" is a keyword
650 # i18n: "revset" is a keyword
651 raise error.ParseError(_(b"revset expects one or more arguments"))
651 raise error.ParseError(_(b"revset expects one or more arguments"))
652
652
653 raw = evalstring(context, mapping, args[0])
653 raw = evalstring(context, mapping, args[0])
654 repo = context.resource(mapping, b'repo')
654 repo = context.resource(mapping, b'repo')
655
655
656 def query(expr):
656 def query(expr):
657 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
657 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
658 return m(repo)
658 return m(repo)
659
659
660 if len(args) > 1:
660 if len(args) > 1:
661 key = None # dynamically-created revs shouldn't be cached
661 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
662 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
662 revs = query(revsetlang.formatspec(raw, *formatargs))
663 revs = query(revsetlang.formatspec(raw, *formatargs))
663 else:
664 else:
664 cache = context.resource(mapping, b'cache')
665 cache = context.resource(mapping, b'cache')
665 revsetcache = cache.setdefault(b"revsetcache", {})
666 revsetcache = cache.setdefault(b"revsetcache", {})
666 if raw in revsetcache:
667 key = raw
667 revs = revsetcache[raw]
668 if key in revsetcache:
669 revs = revsetcache[key]
668 else:
670 else:
669 revs = query(raw)
671 revs = query(raw)
670 revsetcache[raw] = revs
672 revsetcache[key] = revs
671 return templateutil.revslist(repo, revs, name=b'revision')
673 return templateutil.revslist(repo, revs, name=b'revision', cachekey=key)
672
674
673
675
674 @templatefunc(b'rstdoc(text, style)')
676 @templatefunc(b'rstdoc(text, style)')
675 def rstdoc(context, mapping, args):
677 def rstdoc(context, mapping, args):
676 """Format reStructuredText."""
678 """Format reStructuredText."""
677 if len(args) != 2:
679 if len(args) != 2:
678 # i18n: "rstdoc" is a keyword
680 # i18n: "rstdoc" is a keyword
679 raise error.ParseError(_(b"rstdoc expects two arguments"))
681 raise error.ParseError(_(b"rstdoc expects two arguments"))
680
682
681 text = evalstring(context, mapping, args[0])
683 text = evalstring(context, mapping, args[0])
682 style = evalstring(context, mapping, args[1])
684 style = evalstring(context, mapping, args[1])
683
685
684 return minirst.format(text, style=style, keep=[b'verbose'])
686 return minirst.format(text, style=style, keep=[b'verbose'])
685
687
686
688
687 @templatefunc(b'search(pattern, text)')
689 @templatefunc(b'search(pattern, text)')
688 def search(context, mapping, args):
690 def search(context, mapping, args):
689 """Look for the first text matching the regular expression pattern.
691 """Look for the first text matching the regular expression pattern.
690 Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template."""
692 Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template."""
691 if len(args) != 2:
693 if len(args) != 2:
692 # i18n: "search" is a keyword
694 # i18n: "search" is a keyword
693 raise error.ParseError(_(b'search expects two arguments'))
695 raise error.ParseError(_(b'search expects two arguments'))
694
696
695 pat = evalstring(context, mapping, args[0])
697 pat = evalstring(context, mapping, args[0])
696 src = evalstring(context, mapping, args[1])
698 src = evalstring(context, mapping, args[1])
697 try:
699 try:
698 patre = re.compile(pat)
700 patre = re.compile(pat)
699 except re.error:
701 except re.error:
700 # i18n: "search" is a keyword
702 # i18n: "search" is a keyword
701 raise error.ParseError(_(b'search got an invalid pattern: %s') % pat)
703 raise error.ParseError(_(b'search got an invalid pattern: %s') % pat)
702 # named groups shouldn't shadow *reserved* resource keywords
704 # named groups shouldn't shadow *reserved* resource keywords
703 badgroups = context.knownresourcekeys() & set(
705 badgroups = context.knownresourcekeys() & set(
704 pycompat.byteskwargs(patre.groupindex)
706 pycompat.byteskwargs(patre.groupindex)
705 )
707 )
706 if badgroups:
708 if badgroups:
707 raise error.ParseError(
709 raise error.ParseError(
708 # i18n: "search" is a keyword
710 # i18n: "search" is a keyword
709 _(b'invalid group %(group)s in search pattern: %(pat)s')
711 _(b'invalid group %(group)s in search pattern: %(pat)s')
710 % {
712 % {
711 b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)),
713 b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)),
712 b'pat': pat,
714 b'pat': pat,
713 }
715 }
714 )
716 )
715
717
716 match = patre.search(src)
718 match = patre.search(src)
717 if not match:
719 if not match:
718 return templateutil.mappingnone()
720 return templateutil.mappingnone()
719
721
720 lm = {b'0': match.group(0)}
722 lm = {b'0': match.group(0)}
721 lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
723 lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
722 lm.update(pycompat.byteskwargs(match.groupdict()))
724 lm.update(pycompat.byteskwargs(match.groupdict()))
723 return templateutil.mappingdict(lm, tmpl=b'{0}')
725 return templateutil.mappingdict(lm, tmpl=b'{0}')
724
726
725
727
726 @templatefunc(b'separate(sep, args...)', argspec=b'sep *args')
728 @templatefunc(b'separate(sep, args...)', argspec=b'sep *args')
727 def separate(context, mapping, args):
729 def separate(context, mapping, args):
728 """Add a separator between non-empty arguments."""
730 """Add a separator between non-empty arguments."""
729 if b'sep' not in args:
731 if b'sep' not in args:
730 # i18n: "separate" is a keyword
732 # i18n: "separate" is a keyword
731 raise error.ParseError(_(b"separate expects at least one argument"))
733 raise error.ParseError(_(b"separate expects at least one argument"))
732
734
733 sep = evalstring(context, mapping, args[b'sep'])
735 sep = evalstring(context, mapping, args[b'sep'])
734 first = True
736 first = True
735 for arg in args[b'args']:
737 for arg in args[b'args']:
736 argstr = evalstring(context, mapping, arg)
738 argstr = evalstring(context, mapping, arg)
737 if not argstr:
739 if not argstr:
738 continue
740 continue
739 if first:
741 if first:
740 first = False
742 first = False
741 else:
743 else:
742 yield sep
744 yield sep
743 yield argstr
745 yield argstr
744
746
745
747
746 @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'})
748 @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'})
747 def shortest(context, mapping, args):
749 def shortest(context, mapping, args):
748 """Obtain the shortest representation of
750 """Obtain the shortest representation of
749 a node."""
751 a node."""
750 if not (1 <= len(args) <= 2):
752 if not (1 <= len(args) <= 2):
751 # i18n: "shortest" is a keyword
753 # i18n: "shortest" is a keyword
752 raise error.ParseError(_(b"shortest() expects one or two arguments"))
754 raise error.ParseError(_(b"shortest() expects one or two arguments"))
753
755
754 hexnode = evalstring(context, mapping, args[0])
756 hexnode = evalstring(context, mapping, args[0])
755
757
756 minlength = 4
758 minlength = 4
757 if len(args) > 1:
759 if len(args) > 1:
758 minlength = evalinteger(
760 minlength = evalinteger(
759 context,
761 context,
760 mapping,
762 mapping,
761 args[1],
763 args[1],
762 # i18n: "shortest" is a keyword
764 # i18n: "shortest" is a keyword
763 _(b"shortest() expects an integer minlength"),
765 _(b"shortest() expects an integer minlength"),
764 )
766 )
765
767
766 repo = context.resource(mapping, b'repo')
768 repo = context.resource(mapping, b'repo')
767 if len(hexnode) > 40:
769 if len(hexnode) > 40:
768 return hexnode
770 return hexnode
769 elif len(hexnode) == 40:
771 elif len(hexnode) == 40:
770 try:
772 try:
771 node = bin(hexnode)
773 node = bin(hexnode)
772 except TypeError:
774 except TypeError:
773 return hexnode
775 return hexnode
774 else:
776 else:
775 try:
777 try:
776 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
778 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
777 except error.WdirUnsupported:
779 except error.WdirUnsupported:
778 node = wdirid
780 node = wdirid
779 except error.LookupError:
781 except error.LookupError:
780 return hexnode
782 return hexnode
781 if not node:
783 if not node:
782 return hexnode
784 return hexnode
783 cache = context.resource(mapping, b'cache')
785 cache = context.resource(mapping, b'cache')
784 try:
786 try:
785 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
787 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
786 except error.RepoLookupError:
788 except error.RepoLookupError:
787 return hexnode
789 return hexnode
788
790
789
791
790 @templatefunc(b'strip(text[, chars])')
792 @templatefunc(b'strip(text[, chars])')
791 def strip(context, mapping, args):
793 def strip(context, mapping, args):
792 """Strip characters from a string. By default,
794 """Strip characters from a string. By default,
793 strips all leading and trailing whitespace."""
795 strips all leading and trailing whitespace."""
794 if not (1 <= len(args) <= 2):
796 if not (1 <= len(args) <= 2):
795 # i18n: "strip" is a keyword
797 # i18n: "strip" is a keyword
796 raise error.ParseError(_(b"strip expects one or two arguments"))
798 raise error.ParseError(_(b"strip expects one or two arguments"))
797
799
798 text = evalstring(context, mapping, args[0])
800 text = evalstring(context, mapping, args[0])
799 if len(args) == 2:
801 if len(args) == 2:
800 chars = evalstring(context, mapping, args[1])
802 chars = evalstring(context, mapping, args[1])
801 return text.strip(chars)
803 return text.strip(chars)
802 return text.strip()
804 return text.strip()
803
805
804
806
805 @templatefunc(b'sub(pattern, replacement, expression)')
807 @templatefunc(b'sub(pattern, replacement, expression)')
806 def sub(context, mapping, args):
808 def sub(context, mapping, args):
807 """Perform text substitution
809 """Perform text substitution
808 using regular expressions."""
810 using regular expressions."""
809 if len(args) != 3:
811 if len(args) != 3:
810 # i18n: "sub" is a keyword
812 # i18n: "sub" is a keyword
811 raise error.ParseError(_(b"sub expects three arguments"))
813 raise error.ParseError(_(b"sub expects three arguments"))
812
814
813 pat = evalstring(context, mapping, args[0])
815 pat = evalstring(context, mapping, args[0])
814 rpl = evalstring(context, mapping, args[1])
816 rpl = evalstring(context, mapping, args[1])
815 src = evalstring(context, mapping, args[2])
817 src = evalstring(context, mapping, args[2])
816 try:
818 try:
817 patre = re.compile(pat)
819 patre = re.compile(pat)
818 except re.error:
820 except re.error:
819 # i18n: "sub" is a keyword
821 # i18n: "sub" is a keyword
820 raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat)
822 raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat)
821 try:
823 try:
822 yield patre.sub(rpl, src)
824 yield patre.sub(rpl, src)
823 except re.error:
825 except re.error:
824 # i18n: "sub" is a keyword
826 # i18n: "sub" is a keyword
825 raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl)
827 raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl)
826
828
827
829
828 @templatefunc(b'startswith(pattern, text)')
830 @templatefunc(b'startswith(pattern, text)')
829 def startswith(context, mapping, args):
831 def startswith(context, mapping, args):
830 """Returns the value from the "text" argument
832 """Returns the value from the "text" argument
831 if it begins with the content from the "pattern" argument."""
833 if it begins with the content from the "pattern" argument."""
832 if len(args) != 2:
834 if len(args) != 2:
833 # i18n: "startswith" is a keyword
835 # i18n: "startswith" is a keyword
834 raise error.ParseError(_(b"startswith expects two arguments"))
836 raise error.ParseError(_(b"startswith expects two arguments"))
835
837
836 patn = evalstring(context, mapping, args[0])
838 patn = evalstring(context, mapping, args[0])
837 text = evalstring(context, mapping, args[1])
839 text = evalstring(context, mapping, args[1])
838 if text.startswith(patn):
840 if text.startswith(patn):
839 return text
841 return text
840 return b''
842 return b''
841
843
842
844
843 @templatefunc(b'word(number, text[, separator])')
845 @templatefunc(b'word(number, text[, separator])')
844 def word(context, mapping, args):
846 def word(context, mapping, args):
845 """Return the nth word from a string."""
847 """Return the nth word from a string."""
846 if not (2 <= len(args) <= 3):
848 if not (2 <= len(args) <= 3):
847 # i18n: "word" is a keyword
849 # i18n: "word" is a keyword
848 raise error.ParseError(
850 raise error.ParseError(
849 _(b"word expects two or three arguments, got %d") % len(args)
851 _(b"word expects two or three arguments, got %d") % len(args)
850 )
852 )
851
853
852 num = evalinteger(
854 num = evalinteger(
853 context,
855 context,
854 mapping,
856 mapping,
855 args[0],
857 args[0],
856 # i18n: "word" is a keyword
858 # i18n: "word" is a keyword
857 _(b"word expects an integer index"),
859 _(b"word expects an integer index"),
858 )
860 )
859 text = evalstring(context, mapping, args[1])
861 text = evalstring(context, mapping, args[1])
860 if len(args) == 3:
862 if len(args) == 3:
861 splitter = evalstring(context, mapping, args[2])
863 splitter = evalstring(context, mapping, args[2])
862 else:
864 else:
863 splitter = None
865 splitter = None
864
866
865 tokens = text.split(splitter)
867 tokens = text.split(splitter)
866 if num >= len(tokens) or num < -len(tokens):
868 if num >= len(tokens) or num < -len(tokens):
867 return b''
869 return b''
868 else:
870 else:
869 return tokens[num]
871 return tokens[num]
870
872
871
873
872 def loadfunction(ui, extname, registrarobj):
874 def loadfunction(ui, extname, registrarobj):
873 """Load template function from specified registrarobj
875 """Load template function from specified registrarobj
874 """
876 """
875 for name, func in pycompat.iteritems(registrarobj._table):
877 for name, func in pycompat.iteritems(registrarobj._table):
876 funcs[name] = func
878 funcs[name] = func
877
879
878
880
879 # tell hggettext to extract docstrings from these functions:
881 # tell hggettext to extract docstrings from these functions:
880 i18nfunctions = funcs.values()
882 i18nfunctions = funcs.values()
@@ -1,1165 +1,1170
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import abc
10 import abc
11 import types
11 import types
12
12
13 from .i18n import _
13 from .i18n import _
14 from .pycompat import getattr
14 from .pycompat import getattr
15 from . import (
15 from . import (
16 error,
16 error,
17 pycompat,
17 pycompat,
18 smartset,
18 smartset,
19 util,
19 util,
20 )
20 )
21 from .utils import (
21 from .utils import (
22 dateutil,
22 dateutil,
23 stringutil,
23 stringutil,
24 )
24 )
25
25
26
26
27 class ResourceUnavailable(error.Abort):
27 class ResourceUnavailable(error.Abort):
28 pass
28 pass
29
29
30
30
31 class TemplateNotFound(error.Abort):
31 class TemplateNotFound(error.Abort):
32 pass
32 pass
33
33
34
34
35 class wrapped(object): # pytype: disable=ignored-metaclass
35 class wrapped(object): # pytype: disable=ignored-metaclass
36 """Object requiring extra conversion prior to displaying or processing
36 """Object requiring extra conversion prior to displaying or processing
37 as value
37 as value
38
38
39 Use unwrapvalue() or unwrapastype() to obtain the inner object.
39 Use unwrapvalue() or unwrapastype() to obtain the inner object.
40 """
40 """
41
41
42 __metaclass__ = abc.ABCMeta
42 __metaclass__ = abc.ABCMeta
43
43
44 @abc.abstractmethod
44 @abc.abstractmethod
45 def contains(self, context, mapping, item):
45 def contains(self, context, mapping, item):
46 """Test if the specified item is in self
46 """Test if the specified item is in self
47
47
48 The item argument may be a wrapped object.
48 The item argument may be a wrapped object.
49 """
49 """
50
50
51 @abc.abstractmethod
51 @abc.abstractmethod
52 def getmember(self, context, mapping, key):
52 def getmember(self, context, mapping, key):
53 """Return a member item for the specified key
53 """Return a member item for the specified key
54
54
55 The key argument may be a wrapped object.
55 The key argument may be a wrapped object.
56 A returned object may be either a wrapped object or a pure value
56 A returned object may be either a wrapped object or a pure value
57 depending on the self type.
57 depending on the self type.
58 """
58 """
59
59
60 @abc.abstractmethod
60 @abc.abstractmethod
61 def getmin(self, context, mapping):
61 def getmin(self, context, mapping):
62 """Return the smallest item, which may be either a wrapped or a pure
62 """Return the smallest item, which may be either a wrapped or a pure
63 value depending on the self type"""
63 value depending on the self type"""
64
64
65 @abc.abstractmethod
65 @abc.abstractmethod
66 def getmax(self, context, mapping):
66 def getmax(self, context, mapping):
67 """Return the largest item, which may be either a wrapped or a pure
67 """Return the largest item, which may be either a wrapped or a pure
68 value depending on the self type"""
68 value depending on the self type"""
69
69
70 @abc.abstractmethod
70 @abc.abstractmethod
71 def filter(self, context, mapping, select):
71 def filter(self, context, mapping, select):
72 """Return new container of the same type which includes only the
72 """Return new container of the same type which includes only the
73 selected elements
73 selected elements
74
74
75 select() takes each item as a wrapped object and returns True/False.
75 select() takes each item as a wrapped object and returns True/False.
76 """
76 """
77
77
78 @abc.abstractmethod
78 @abc.abstractmethod
79 def itermaps(self, context):
79 def itermaps(self, context):
80 """Yield each template mapping"""
80 """Yield each template mapping"""
81
81
82 @abc.abstractmethod
82 @abc.abstractmethod
83 def join(self, context, mapping, sep):
83 def join(self, context, mapping, sep):
84 """Join items with the separator; Returns a bytes or (possibly nested)
84 """Join items with the separator; Returns a bytes or (possibly nested)
85 generator of bytes
85 generator of bytes
86
86
87 A pre-configured template may be rendered per item if this container
87 A pre-configured template may be rendered per item if this container
88 holds unprintable items.
88 holds unprintable items.
89 """
89 """
90
90
91 @abc.abstractmethod
91 @abc.abstractmethod
92 def show(self, context, mapping):
92 def show(self, context, mapping):
93 """Return a bytes or (possibly nested) generator of bytes representing
93 """Return a bytes or (possibly nested) generator of bytes representing
94 the underlying object
94 the underlying object
95
95
96 A pre-configured template may be rendered if the underlying object is
96 A pre-configured template may be rendered if the underlying object is
97 not printable.
97 not printable.
98 """
98 """
99
99
100 @abc.abstractmethod
100 @abc.abstractmethod
101 def tobool(self, context, mapping):
101 def tobool(self, context, mapping):
102 """Return a boolean representation of the inner value"""
102 """Return a boolean representation of the inner value"""
103
103
104 @abc.abstractmethod
104 @abc.abstractmethod
105 def tovalue(self, context, mapping):
105 def tovalue(self, context, mapping):
106 """Move the inner value object out or create a value representation
106 """Move the inner value object out or create a value representation
107
107
108 A returned value must be serializable by templaterfilters.json().
108 A returned value must be serializable by templaterfilters.json().
109 """
109 """
110
110
111
111
112 class mappable(object): # pytype: disable=ignored-metaclass
112 class mappable(object): # pytype: disable=ignored-metaclass
113 """Object which can be converted to a single template mapping"""
113 """Object which can be converted to a single template mapping"""
114
114
115 __metaclass__ = abc.ABCMeta
115 __metaclass__ = abc.ABCMeta
116
116
117 def itermaps(self, context):
117 def itermaps(self, context):
118 yield self.tomap(context)
118 yield self.tomap(context)
119
119
120 @abc.abstractmethod
120 @abc.abstractmethod
121 def tomap(self, context):
121 def tomap(self, context):
122 """Create a single template mapping representing this"""
122 """Create a single template mapping representing this"""
123
123
124
124
125 class wrappedbytes(wrapped):
125 class wrappedbytes(wrapped):
126 """Wrapper for byte string"""
126 """Wrapper for byte string"""
127
127
128 def __init__(self, value):
128 def __init__(self, value):
129 self._value = value
129 self._value = value
130
130
131 def contains(self, context, mapping, item):
131 def contains(self, context, mapping, item):
132 item = stringify(context, mapping, item)
132 item = stringify(context, mapping, item)
133 return item in self._value
133 return item in self._value
134
134
135 def getmember(self, context, mapping, key):
135 def getmember(self, context, mapping, key):
136 raise error.ParseError(
136 raise error.ParseError(
137 _(b'%r is not a dictionary') % pycompat.bytestr(self._value)
137 _(b'%r is not a dictionary') % pycompat.bytestr(self._value)
138 )
138 )
139
139
140 def getmin(self, context, mapping):
140 def getmin(self, context, mapping):
141 return self._getby(context, mapping, min)
141 return self._getby(context, mapping, min)
142
142
143 def getmax(self, context, mapping):
143 def getmax(self, context, mapping):
144 return self._getby(context, mapping, max)
144 return self._getby(context, mapping, max)
145
145
146 def _getby(self, context, mapping, func):
146 def _getby(self, context, mapping, func):
147 if not self._value:
147 if not self._value:
148 raise error.ParseError(_(b'empty string'))
148 raise error.ParseError(_(b'empty string'))
149 return func(pycompat.iterbytestr(self._value))
149 return func(pycompat.iterbytestr(self._value))
150
150
151 def filter(self, context, mapping, select):
151 def filter(self, context, mapping, select):
152 raise error.ParseError(
152 raise error.ParseError(
153 _(b'%r is not filterable') % pycompat.bytestr(self._value)
153 _(b'%r is not filterable') % pycompat.bytestr(self._value)
154 )
154 )
155
155
156 def itermaps(self, context):
156 def itermaps(self, context):
157 raise error.ParseError(
157 raise error.ParseError(
158 _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value)
158 _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value)
159 )
159 )
160
160
161 def join(self, context, mapping, sep):
161 def join(self, context, mapping, sep):
162 return joinitems(pycompat.iterbytestr(self._value), sep)
162 return joinitems(pycompat.iterbytestr(self._value), sep)
163
163
164 def show(self, context, mapping):
164 def show(self, context, mapping):
165 return self._value
165 return self._value
166
166
167 def tobool(self, context, mapping):
167 def tobool(self, context, mapping):
168 return bool(self._value)
168 return bool(self._value)
169
169
170 def tovalue(self, context, mapping):
170 def tovalue(self, context, mapping):
171 return self._value
171 return self._value
172
172
173
173
174 class wrappedvalue(wrapped):
174 class wrappedvalue(wrapped):
175 """Generic wrapper for pure non-list/dict/bytes value"""
175 """Generic wrapper for pure non-list/dict/bytes value"""
176
176
177 def __init__(self, value):
177 def __init__(self, value):
178 self._value = value
178 self._value = value
179
179
180 def contains(self, context, mapping, item):
180 def contains(self, context, mapping, item):
181 raise error.ParseError(_(b"%r is not iterable") % self._value)
181 raise error.ParseError(_(b"%r is not iterable") % self._value)
182
182
183 def getmember(self, context, mapping, key):
183 def getmember(self, context, mapping, key):
184 raise error.ParseError(_(b'%r is not a dictionary') % self._value)
184 raise error.ParseError(_(b'%r is not a dictionary') % self._value)
185
185
186 def getmin(self, context, mapping):
186 def getmin(self, context, mapping):
187 raise error.ParseError(_(b"%r is not iterable") % self._value)
187 raise error.ParseError(_(b"%r is not iterable") % self._value)
188
188
189 def getmax(self, context, mapping):
189 def getmax(self, context, mapping):
190 raise error.ParseError(_(b"%r is not iterable") % self._value)
190 raise error.ParseError(_(b"%r is not iterable") % self._value)
191
191
192 def filter(self, context, mapping, select):
192 def filter(self, context, mapping, select):
193 raise error.ParseError(_(b"%r is not iterable") % self._value)
193 raise error.ParseError(_(b"%r is not iterable") % self._value)
194
194
195 def itermaps(self, context):
195 def itermaps(self, context):
196 raise error.ParseError(
196 raise error.ParseError(
197 _(b'%r is not iterable of mappings') % self._value
197 _(b'%r is not iterable of mappings') % self._value
198 )
198 )
199
199
200 def join(self, context, mapping, sep):
200 def join(self, context, mapping, sep):
201 raise error.ParseError(_(b'%r is not iterable') % self._value)
201 raise error.ParseError(_(b'%r is not iterable') % self._value)
202
202
203 def show(self, context, mapping):
203 def show(self, context, mapping):
204 if self._value is None:
204 if self._value is None:
205 return b''
205 return b''
206 return pycompat.bytestr(self._value)
206 return pycompat.bytestr(self._value)
207
207
208 def tobool(self, context, mapping):
208 def tobool(self, context, mapping):
209 if self._value is None:
209 if self._value is None:
210 return False
210 return False
211 if isinstance(self._value, bool):
211 if isinstance(self._value, bool):
212 return self._value
212 return self._value
213 # otherwise evaluate as string, which means 0 is True
213 # otherwise evaluate as string, which means 0 is True
214 return bool(pycompat.bytestr(self._value))
214 return bool(pycompat.bytestr(self._value))
215
215
216 def tovalue(self, context, mapping):
216 def tovalue(self, context, mapping):
217 return self._value
217 return self._value
218
218
219
219
220 class date(mappable, wrapped):
220 class date(mappable, wrapped):
221 """Wrapper for date tuple"""
221 """Wrapper for date tuple"""
222
222
223 def __init__(self, value, showfmt=b'%d %d'):
223 def __init__(self, value, showfmt=b'%d %d'):
224 # value may be (float, int), but public interface shouldn't support
224 # value may be (float, int), but public interface shouldn't support
225 # floating-point timestamp
225 # floating-point timestamp
226 self._unixtime, self._tzoffset = map(int, value)
226 self._unixtime, self._tzoffset = map(int, value)
227 self._showfmt = showfmt
227 self._showfmt = showfmt
228
228
229 def contains(self, context, mapping, item):
229 def contains(self, context, mapping, item):
230 raise error.ParseError(_(b'date is not iterable'))
230 raise error.ParseError(_(b'date is not iterable'))
231
231
232 def getmember(self, context, mapping, key):
232 def getmember(self, context, mapping, key):
233 raise error.ParseError(_(b'date is not a dictionary'))
233 raise error.ParseError(_(b'date is not a dictionary'))
234
234
235 def getmin(self, context, mapping):
235 def getmin(self, context, mapping):
236 raise error.ParseError(_(b'date is not iterable'))
236 raise error.ParseError(_(b'date is not iterable'))
237
237
238 def getmax(self, context, mapping):
238 def getmax(self, context, mapping):
239 raise error.ParseError(_(b'date is not iterable'))
239 raise error.ParseError(_(b'date is not iterable'))
240
240
241 def filter(self, context, mapping, select):
241 def filter(self, context, mapping, select):
242 raise error.ParseError(_(b'date is not iterable'))
242 raise error.ParseError(_(b'date is not iterable'))
243
243
244 def join(self, context, mapping, sep):
244 def join(self, context, mapping, sep):
245 raise error.ParseError(_(b"date is not iterable"))
245 raise error.ParseError(_(b"date is not iterable"))
246
246
247 def show(self, context, mapping):
247 def show(self, context, mapping):
248 return self._showfmt % (self._unixtime, self._tzoffset)
248 return self._showfmt % (self._unixtime, self._tzoffset)
249
249
250 def tomap(self, context):
250 def tomap(self, context):
251 return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset}
251 return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset}
252
252
253 def tobool(self, context, mapping):
253 def tobool(self, context, mapping):
254 return True
254 return True
255
255
256 def tovalue(self, context, mapping):
256 def tovalue(self, context, mapping):
257 return (self._unixtime, self._tzoffset)
257 return (self._unixtime, self._tzoffset)
258
258
259
259
260 class hybrid(wrapped):
260 class hybrid(wrapped):
261 """Wrapper for list or dict to support legacy template
261 """Wrapper for list or dict to support legacy template
262
262
263 This class allows us to handle both:
263 This class allows us to handle both:
264 - "{files}" (legacy command-line-specific list hack) and
264 - "{files}" (legacy command-line-specific list hack) and
265 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
265 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
266 and to access raw values:
266 and to access raw values:
267 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
267 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
268 - "{get(extras, key)}"
268 - "{get(extras, key)}"
269 - "{files|json}"
269 - "{files|json}"
270 """
270 """
271
271
272 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
272 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
273 self._gen = gen # generator or function returning generator
273 self._gen = gen # generator or function returning generator
274 self._values = values
274 self._values = values
275 self._makemap = makemap
275 self._makemap = makemap
276 self._joinfmt = joinfmt
276 self._joinfmt = joinfmt
277 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
277 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
278
278
279 def contains(self, context, mapping, item):
279 def contains(self, context, mapping, item):
280 item = unwrapastype(context, mapping, item, self._keytype)
280 item = unwrapastype(context, mapping, item, self._keytype)
281 return item in self._values
281 return item in self._values
282
282
283 def getmember(self, context, mapping, key):
283 def getmember(self, context, mapping, key):
284 # TODO: maybe split hybrid list/dict types?
284 # TODO: maybe split hybrid list/dict types?
285 if not util.safehasattr(self._values, b'get'):
285 if not util.safehasattr(self._values, b'get'):
286 raise error.ParseError(_(b'not a dictionary'))
286 raise error.ParseError(_(b'not a dictionary'))
287 key = unwrapastype(context, mapping, key, self._keytype)
287 key = unwrapastype(context, mapping, key, self._keytype)
288 return self._wrapvalue(key, self._values.get(key))
288 return self._wrapvalue(key, self._values.get(key))
289
289
290 def getmin(self, context, mapping):
290 def getmin(self, context, mapping):
291 return self._getby(context, mapping, min)
291 return self._getby(context, mapping, min)
292
292
293 def getmax(self, context, mapping):
293 def getmax(self, context, mapping):
294 return self._getby(context, mapping, max)
294 return self._getby(context, mapping, max)
295
295
296 def _getby(self, context, mapping, func):
296 def _getby(self, context, mapping, func):
297 if not self._values:
297 if not self._values:
298 raise error.ParseError(_(b'empty sequence'))
298 raise error.ParseError(_(b'empty sequence'))
299 val = func(self._values)
299 val = func(self._values)
300 return self._wrapvalue(val, val)
300 return self._wrapvalue(val, val)
301
301
302 def _wrapvalue(self, key, val):
302 def _wrapvalue(self, key, val):
303 if val is None:
303 if val is None:
304 return
304 return
305 if util.safehasattr(val, b'_makemap'):
305 if util.safehasattr(val, b'_makemap'):
306 # a nested hybrid list/dict, which has its own way of map operation
306 # a nested hybrid list/dict, which has its own way of map operation
307 return val
307 return val
308 return hybriditem(None, key, val, self._makemap)
308 return hybriditem(None, key, val, self._makemap)
309
309
310 def filter(self, context, mapping, select):
310 def filter(self, context, mapping, select):
311 if util.safehasattr(self._values, b'get'):
311 if util.safehasattr(self._values, b'get'):
312 values = {
312 values = {
313 k: v
313 k: v
314 for k, v in pycompat.iteritems(self._values)
314 for k, v in pycompat.iteritems(self._values)
315 if select(self._wrapvalue(k, v))
315 if select(self._wrapvalue(k, v))
316 }
316 }
317 else:
317 else:
318 values = [v for v in self._values if select(self._wrapvalue(v, v))]
318 values = [v for v in self._values if select(self._wrapvalue(v, v))]
319 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
319 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
320
320
321 def itermaps(self, context):
321 def itermaps(self, context):
322 makemap = self._makemap
322 makemap = self._makemap
323 for x in self._values:
323 for x in self._values:
324 yield makemap(x)
324 yield makemap(x)
325
325
326 def join(self, context, mapping, sep):
326 def join(self, context, mapping, sep):
327 # TODO: switch gen to (context, mapping) API?
327 # TODO: switch gen to (context, mapping) API?
328 return joinitems((self._joinfmt(x) for x in self._values), sep)
328 return joinitems((self._joinfmt(x) for x in self._values), sep)
329
329
330 def show(self, context, mapping):
330 def show(self, context, mapping):
331 # TODO: switch gen to (context, mapping) API?
331 # TODO: switch gen to (context, mapping) API?
332 gen = self._gen
332 gen = self._gen
333 if gen is None:
333 if gen is None:
334 return self.join(context, mapping, b' ')
334 return self.join(context, mapping, b' ')
335 if callable(gen):
335 if callable(gen):
336 return gen()
336 return gen()
337 return gen
337 return gen
338
338
339 def tobool(self, context, mapping):
339 def tobool(self, context, mapping):
340 return bool(self._values)
340 return bool(self._values)
341
341
342 def tovalue(self, context, mapping):
342 def tovalue(self, context, mapping):
343 # TODO: make it non-recursive for trivial lists/dicts
343 # TODO: make it non-recursive for trivial lists/dicts
344 xs = self._values
344 xs = self._values
345 if util.safehasattr(xs, b'get'):
345 if util.safehasattr(xs, b'get'):
346 return {
346 return {
347 k: unwrapvalue(context, mapping, v)
347 k: unwrapvalue(context, mapping, v)
348 for k, v in pycompat.iteritems(xs)
348 for k, v in pycompat.iteritems(xs)
349 }
349 }
350 return [unwrapvalue(context, mapping, x) for x in xs]
350 return [unwrapvalue(context, mapping, x) for x in xs]
351
351
352
352
353 class hybriditem(mappable, wrapped):
353 class hybriditem(mappable, wrapped):
354 """Wrapper for non-list/dict object to support map operation
354 """Wrapper for non-list/dict object to support map operation
355
355
356 This class allows us to handle both:
356 This class allows us to handle both:
357 - "{manifest}"
357 - "{manifest}"
358 - "{manifest % '{rev}:{node}'}"
358 - "{manifest % '{rev}:{node}'}"
359 - "{manifest.rev}"
359 - "{manifest.rev}"
360 """
360 """
361
361
362 def __init__(self, gen, key, value, makemap):
362 def __init__(self, gen, key, value, makemap):
363 self._gen = gen # generator or function returning generator
363 self._gen = gen # generator or function returning generator
364 self._key = key
364 self._key = key
365 self._value = value # may be generator of strings
365 self._value = value # may be generator of strings
366 self._makemap = makemap
366 self._makemap = makemap
367
367
368 def tomap(self, context):
368 def tomap(self, context):
369 return self._makemap(self._key)
369 return self._makemap(self._key)
370
370
371 def contains(self, context, mapping, item):
371 def contains(self, context, mapping, item):
372 w = makewrapped(context, mapping, self._value)
372 w = makewrapped(context, mapping, self._value)
373 return w.contains(context, mapping, item)
373 return w.contains(context, mapping, item)
374
374
375 def getmember(self, context, mapping, key):
375 def getmember(self, context, mapping, key):
376 w = makewrapped(context, mapping, self._value)
376 w = makewrapped(context, mapping, self._value)
377 return w.getmember(context, mapping, key)
377 return w.getmember(context, mapping, key)
378
378
379 def getmin(self, context, mapping):
379 def getmin(self, context, mapping):
380 w = makewrapped(context, mapping, self._value)
380 w = makewrapped(context, mapping, self._value)
381 return w.getmin(context, mapping)
381 return w.getmin(context, mapping)
382
382
383 def getmax(self, context, mapping):
383 def getmax(self, context, mapping):
384 w = makewrapped(context, mapping, self._value)
384 w = makewrapped(context, mapping, self._value)
385 return w.getmax(context, mapping)
385 return w.getmax(context, mapping)
386
386
387 def filter(self, context, mapping, select):
387 def filter(self, context, mapping, select):
388 w = makewrapped(context, mapping, self._value)
388 w = makewrapped(context, mapping, self._value)
389 return w.filter(context, mapping, select)
389 return w.filter(context, mapping, select)
390
390
391 def join(self, context, mapping, sep):
391 def join(self, context, mapping, sep):
392 w = makewrapped(context, mapping, self._value)
392 w = makewrapped(context, mapping, self._value)
393 return w.join(context, mapping, sep)
393 return w.join(context, mapping, sep)
394
394
395 def show(self, context, mapping):
395 def show(self, context, mapping):
396 # TODO: switch gen to (context, mapping) API?
396 # TODO: switch gen to (context, mapping) API?
397 gen = self._gen
397 gen = self._gen
398 if gen is None:
398 if gen is None:
399 return pycompat.bytestr(self._value)
399 return pycompat.bytestr(self._value)
400 if callable(gen):
400 if callable(gen):
401 return gen()
401 return gen()
402 return gen
402 return gen
403
403
404 def tobool(self, context, mapping):
404 def tobool(self, context, mapping):
405 w = makewrapped(context, mapping, self._value)
405 w = makewrapped(context, mapping, self._value)
406 return w.tobool(context, mapping)
406 return w.tobool(context, mapping)
407
407
408 def tovalue(self, context, mapping):
408 def tovalue(self, context, mapping):
409 return _unthunk(context, mapping, self._value)
409 return _unthunk(context, mapping, self._value)
410
410
411
411
412 class revslist(wrapped):
412 class revslist(wrapped):
413 """Wrapper for a smartset (a list/set of revision numbers)
413 """Wrapper for a smartset (a list/set of revision numbers)
414
414
415 If name specified, the revs will be rendered with the old-style list
415 If name specified, the revs will be rendered with the old-style list
416 template of the given name by default.
416 template of the given name by default.
417
418 The cachekey provides a hint to cache further computation on this
419 smartset. If the underlying smartset is dynamically created, the cachekey
420 should be None.
417 """
421 """
418
422
419 def __init__(self, repo, revs, name=None):
423 def __init__(self, repo, revs, name=None, cachekey=None):
420 assert isinstance(revs, smartset.abstractsmartset)
424 assert isinstance(revs, smartset.abstractsmartset)
421 self._repo = repo
425 self._repo = repo
422 self._revs = revs
426 self._revs = revs
423 self._name = name
427 self._name = name
428 self.cachekey = cachekey
424
429
425 def contains(self, context, mapping, item):
430 def contains(self, context, mapping, item):
426 rev = unwrapinteger(context, mapping, item)
431 rev = unwrapinteger(context, mapping, item)
427 return rev in self._revs
432 return rev in self._revs
428
433
429 def getmember(self, context, mapping, key):
434 def getmember(self, context, mapping, key):
430 raise error.ParseError(_(b'not a dictionary'))
435 raise error.ParseError(_(b'not a dictionary'))
431
436
432 def getmin(self, context, mapping):
437 def getmin(self, context, mapping):
433 makehybriditem = self._makehybriditemfunc()
438 makehybriditem = self._makehybriditemfunc()
434 return makehybriditem(self._revs.min())
439 return makehybriditem(self._revs.min())
435
440
436 def getmax(self, context, mapping):
441 def getmax(self, context, mapping):
437 makehybriditem = self._makehybriditemfunc()
442 makehybriditem = self._makehybriditemfunc()
438 return makehybriditem(self._revs.max())
443 return makehybriditem(self._revs.max())
439
444
440 def filter(self, context, mapping, select):
445 def filter(self, context, mapping, select):
441 makehybriditem = self._makehybriditemfunc()
446 makehybriditem = self._makehybriditemfunc()
442 frevs = self._revs.filter(lambda r: select(makehybriditem(r)))
447 frevs = self._revs.filter(lambda r: select(makehybriditem(r)))
443 # once filtered, no need to support old-style list template
448 # once filtered, no need to support old-style list template
444 return revslist(self._repo, frevs, name=None)
449 return revslist(self._repo, frevs, name=None)
445
450
446 def itermaps(self, context):
451 def itermaps(self, context):
447 makemap = self._makemapfunc()
452 makemap = self._makemapfunc()
448 for r in self._revs:
453 for r in self._revs:
449 yield makemap(r)
454 yield makemap(r)
450
455
451 def _makehybriditemfunc(self):
456 def _makehybriditemfunc(self):
452 makemap = self._makemapfunc()
457 makemap = self._makemapfunc()
453 return lambda r: hybriditem(None, r, r, makemap)
458 return lambda r: hybriditem(None, r, r, makemap)
454
459
455 def _makemapfunc(self):
460 def _makemapfunc(self):
456 repo = self._repo
461 repo = self._repo
457 name = self._name
462 name = self._name
458 if name:
463 if name:
459 return lambda r: {name: r, b'ctx': repo[r]}
464 return lambda r: {name: r, b'ctx': repo[r]}
460 else:
465 else:
461 return lambda r: {b'ctx': repo[r]}
466 return lambda r: {b'ctx': repo[r]}
462
467
463 def join(self, context, mapping, sep):
468 def join(self, context, mapping, sep):
464 return joinitems(self._revs, sep)
469 return joinitems(self._revs, sep)
465
470
466 def show(self, context, mapping):
471 def show(self, context, mapping):
467 if self._name:
472 if self._name:
468 srevs = [b'%d' % r for r in self._revs]
473 srevs = [b'%d' % r for r in self._revs]
469 return _showcompatlist(context, mapping, self._name, srevs)
474 return _showcompatlist(context, mapping, self._name, srevs)
470 else:
475 else:
471 return self.join(context, mapping, b' ')
476 return self.join(context, mapping, b' ')
472
477
473 def tobool(self, context, mapping):
478 def tobool(self, context, mapping):
474 return bool(self._revs)
479 return bool(self._revs)
475
480
476 def tovalue(self, context, mapping):
481 def tovalue(self, context, mapping):
477 return self._revs
482 return self._revs
478
483
479
484
480 class _mappingsequence(wrapped):
485 class _mappingsequence(wrapped):
481 """Wrapper for sequence of template mappings
486 """Wrapper for sequence of template mappings
482
487
483 This represents an inner template structure (i.e. a list of dicts),
488 This represents an inner template structure (i.e. a list of dicts),
484 which can also be rendered by the specified named/literal template.
489 which can also be rendered by the specified named/literal template.
485
490
486 Template mappings may be nested.
491 Template mappings may be nested.
487 """
492 """
488
493
489 def __init__(self, name=None, tmpl=None, sep=b''):
494 def __init__(self, name=None, tmpl=None, sep=b''):
490 if name is not None and tmpl is not None:
495 if name is not None and tmpl is not None:
491 raise error.ProgrammingError(
496 raise error.ProgrammingError(
492 b'name and tmpl are mutually exclusive'
497 b'name and tmpl are mutually exclusive'
493 )
498 )
494 self._name = name
499 self._name = name
495 self._tmpl = tmpl
500 self._tmpl = tmpl
496 self._defaultsep = sep
501 self._defaultsep = sep
497
502
498 def contains(self, context, mapping, item):
503 def contains(self, context, mapping, item):
499 raise error.ParseError(_(b'not comparable'))
504 raise error.ParseError(_(b'not comparable'))
500
505
501 def getmember(self, context, mapping, key):
506 def getmember(self, context, mapping, key):
502 raise error.ParseError(_(b'not a dictionary'))
507 raise error.ParseError(_(b'not a dictionary'))
503
508
504 def getmin(self, context, mapping):
509 def getmin(self, context, mapping):
505 raise error.ParseError(_(b'not comparable'))
510 raise error.ParseError(_(b'not comparable'))
506
511
507 def getmax(self, context, mapping):
512 def getmax(self, context, mapping):
508 raise error.ParseError(_(b'not comparable'))
513 raise error.ParseError(_(b'not comparable'))
509
514
510 def filter(self, context, mapping, select):
515 def filter(self, context, mapping, select):
511 # implement if necessary; we'll need a wrapped type for a mapping dict
516 # implement if necessary; we'll need a wrapped type for a mapping dict
512 raise error.ParseError(_(b'not filterable without template'))
517 raise error.ParseError(_(b'not filterable without template'))
513
518
514 def join(self, context, mapping, sep):
519 def join(self, context, mapping, sep):
515 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
520 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
516 if self._name:
521 if self._name:
517 itemiter = (context.process(self._name, m) for m in mapsiter)
522 itemiter = (context.process(self._name, m) for m in mapsiter)
518 elif self._tmpl:
523 elif self._tmpl:
519 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
524 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
520 else:
525 else:
521 raise error.ParseError(_(b'not displayable without template'))
526 raise error.ParseError(_(b'not displayable without template'))
522 return joinitems(itemiter, sep)
527 return joinitems(itemiter, sep)
523
528
524 def show(self, context, mapping):
529 def show(self, context, mapping):
525 return self.join(context, mapping, self._defaultsep)
530 return self.join(context, mapping, self._defaultsep)
526
531
527 def tovalue(self, context, mapping):
532 def tovalue(self, context, mapping):
528 knownres = context.knownresourcekeys()
533 knownres = context.knownresourcekeys()
529 items = []
534 items = []
530 for nm in self.itermaps(context):
535 for nm in self.itermaps(context):
531 # drop internal resources (recursively) which shouldn't be displayed
536 # drop internal resources (recursively) which shouldn't be displayed
532 lm = context.overlaymap(mapping, nm)
537 lm = context.overlaymap(mapping, nm)
533 items.append(
538 items.append(
534 {
539 {
535 k: unwrapvalue(context, lm, v)
540 k: unwrapvalue(context, lm, v)
536 for k, v in pycompat.iteritems(nm)
541 for k, v in pycompat.iteritems(nm)
537 if k not in knownres
542 if k not in knownres
538 }
543 }
539 )
544 )
540 return items
545 return items
541
546
542
547
543 class mappinggenerator(_mappingsequence):
548 class mappinggenerator(_mappingsequence):
544 """Wrapper for generator of template mappings
549 """Wrapper for generator of template mappings
545
550
546 The function ``make(context, *args)`` should return a generator of
551 The function ``make(context, *args)`` should return a generator of
547 mapping dicts.
552 mapping dicts.
548 """
553 """
549
554
550 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''):
555 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''):
551 super(mappinggenerator, self).__init__(name, tmpl, sep)
556 super(mappinggenerator, self).__init__(name, tmpl, sep)
552 self._make = make
557 self._make = make
553 self._args = args
558 self._args = args
554
559
555 def itermaps(self, context):
560 def itermaps(self, context):
556 return self._make(context, *self._args)
561 return self._make(context, *self._args)
557
562
558 def tobool(self, context, mapping):
563 def tobool(self, context, mapping):
559 return _nonempty(self.itermaps(context))
564 return _nonempty(self.itermaps(context))
560
565
561
566
562 class mappinglist(_mappingsequence):
567 class mappinglist(_mappingsequence):
563 """Wrapper for list of template mappings"""
568 """Wrapper for list of template mappings"""
564
569
565 def __init__(self, mappings, name=None, tmpl=None, sep=b''):
570 def __init__(self, mappings, name=None, tmpl=None, sep=b''):
566 super(mappinglist, self).__init__(name, tmpl, sep)
571 super(mappinglist, self).__init__(name, tmpl, sep)
567 self._mappings = mappings
572 self._mappings = mappings
568
573
569 def itermaps(self, context):
574 def itermaps(self, context):
570 return iter(self._mappings)
575 return iter(self._mappings)
571
576
572 def tobool(self, context, mapping):
577 def tobool(self, context, mapping):
573 return bool(self._mappings)
578 return bool(self._mappings)
574
579
575
580
576 class mappingdict(mappable, _mappingsequence):
581 class mappingdict(mappable, _mappingsequence):
577 """Wrapper for a single template mapping
582 """Wrapper for a single template mapping
578
583
579 This isn't a sequence in a way that the underlying dict won't be iterated
584 This isn't a sequence in a way that the underlying dict won't be iterated
580 as a dict, but shares most of the _mappingsequence functions.
585 as a dict, but shares most of the _mappingsequence functions.
581 """
586 """
582
587
583 def __init__(self, mapping, name=None, tmpl=None):
588 def __init__(self, mapping, name=None, tmpl=None):
584 super(mappingdict, self).__init__(name, tmpl)
589 super(mappingdict, self).__init__(name, tmpl)
585 self._mapping = mapping
590 self._mapping = mapping
586
591
587 def tomap(self, context):
592 def tomap(self, context):
588 return self._mapping
593 return self._mapping
589
594
590 def tobool(self, context, mapping):
595 def tobool(self, context, mapping):
591 # no idea when a template mapping should be considered an empty, but
596 # no idea when a template mapping should be considered an empty, but
592 # a mapping dict should have at least one item in practice, so always
597 # a mapping dict should have at least one item in practice, so always
593 # mark this as non-empty.
598 # mark this as non-empty.
594 return True
599 return True
595
600
596 def tovalue(self, context, mapping):
601 def tovalue(self, context, mapping):
597 return super(mappingdict, self).tovalue(context, mapping)[0]
602 return super(mappingdict, self).tovalue(context, mapping)[0]
598
603
599
604
600 class mappingnone(wrappedvalue):
605 class mappingnone(wrappedvalue):
601 """Wrapper for None, but supports map operation
606 """Wrapper for None, but supports map operation
602
607
603 This represents None of Optional[mappable]. It's similar to
608 This represents None of Optional[mappable]. It's similar to
604 mapplinglist([]), but the underlying value is not [], but None.
609 mapplinglist([]), but the underlying value is not [], but None.
605 """
610 """
606
611
607 def __init__(self):
612 def __init__(self):
608 super(mappingnone, self).__init__(None)
613 super(mappingnone, self).__init__(None)
609
614
610 def itermaps(self, context):
615 def itermaps(self, context):
611 return iter([])
616 return iter([])
612
617
613
618
614 class mappedgenerator(wrapped):
619 class mappedgenerator(wrapped):
615 """Wrapper for generator of strings which acts as a list
620 """Wrapper for generator of strings which acts as a list
616
621
617 The function ``make(context, *args)`` should return a generator of
622 The function ``make(context, *args)`` should return a generator of
618 byte strings, or a generator of (possibly nested) generators of byte
623 byte strings, or a generator of (possibly nested) generators of byte
619 strings (i.e. a generator for a list of byte strings.)
624 strings (i.e. a generator for a list of byte strings.)
620 """
625 """
621
626
622 def __init__(self, make, args=()):
627 def __init__(self, make, args=()):
623 self._make = make
628 self._make = make
624 self._args = args
629 self._args = args
625
630
626 def contains(self, context, mapping, item):
631 def contains(self, context, mapping, item):
627 item = stringify(context, mapping, item)
632 item = stringify(context, mapping, item)
628 return item in self.tovalue(context, mapping)
633 return item in self.tovalue(context, mapping)
629
634
630 def _gen(self, context):
635 def _gen(self, context):
631 return self._make(context, *self._args)
636 return self._make(context, *self._args)
632
637
633 def getmember(self, context, mapping, key):
638 def getmember(self, context, mapping, key):
634 raise error.ParseError(_(b'not a dictionary'))
639 raise error.ParseError(_(b'not a dictionary'))
635
640
636 def getmin(self, context, mapping):
641 def getmin(self, context, mapping):
637 return self._getby(context, mapping, min)
642 return self._getby(context, mapping, min)
638
643
639 def getmax(self, context, mapping):
644 def getmax(self, context, mapping):
640 return self._getby(context, mapping, max)
645 return self._getby(context, mapping, max)
641
646
642 def _getby(self, context, mapping, func):
647 def _getby(self, context, mapping, func):
643 xs = self.tovalue(context, mapping)
648 xs = self.tovalue(context, mapping)
644 if not xs:
649 if not xs:
645 raise error.ParseError(_(b'empty sequence'))
650 raise error.ParseError(_(b'empty sequence'))
646 return func(xs)
651 return func(xs)
647
652
648 @staticmethod
653 @staticmethod
649 def _filteredgen(context, mapping, make, args, select):
654 def _filteredgen(context, mapping, make, args, select):
650 for x in make(context, *args):
655 for x in make(context, *args):
651 s = stringify(context, mapping, x)
656 s = stringify(context, mapping, x)
652 if select(wrappedbytes(s)):
657 if select(wrappedbytes(s)):
653 yield s
658 yield s
654
659
655 def filter(self, context, mapping, select):
660 def filter(self, context, mapping, select):
656 args = (mapping, self._make, self._args, select)
661 args = (mapping, self._make, self._args, select)
657 return mappedgenerator(self._filteredgen, args)
662 return mappedgenerator(self._filteredgen, args)
658
663
659 def itermaps(self, context):
664 def itermaps(self, context):
660 raise error.ParseError(_(b'list of strings is not mappable'))
665 raise error.ParseError(_(b'list of strings is not mappable'))
661
666
662 def join(self, context, mapping, sep):
667 def join(self, context, mapping, sep):
663 return joinitems(self._gen(context), sep)
668 return joinitems(self._gen(context), sep)
664
669
665 def show(self, context, mapping):
670 def show(self, context, mapping):
666 return self.join(context, mapping, b'')
671 return self.join(context, mapping, b'')
667
672
668 def tobool(self, context, mapping):
673 def tobool(self, context, mapping):
669 return _nonempty(self._gen(context))
674 return _nonempty(self._gen(context))
670
675
671 def tovalue(self, context, mapping):
676 def tovalue(self, context, mapping):
672 return [stringify(context, mapping, x) for x in self._gen(context)]
677 return [stringify(context, mapping, x) for x in self._gen(context)]
673
678
674
679
675 def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None):
680 def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None):
676 """Wrap data to support both dict-like and string-like operations"""
681 """Wrap data to support both dict-like and string-like operations"""
677 prefmt = pycompat.identity
682 prefmt = pycompat.identity
678 if fmt is None:
683 if fmt is None:
679 fmt = b'%s=%s'
684 fmt = b'%s=%s'
680 prefmt = pycompat.bytestr
685 prefmt = pycompat.bytestr
681 return hybrid(
686 return hybrid(
682 gen,
687 gen,
683 data,
688 data,
684 lambda k: {key: k, value: data[k]},
689 lambda k: {key: k, value: data[k]},
685 lambda k: fmt % (prefmt(k), prefmt(data[k])),
690 lambda k: fmt % (prefmt(k), prefmt(data[k])),
686 )
691 )
687
692
688
693
689 def hybridlist(data, name, fmt=None, gen=None):
694 def hybridlist(data, name, fmt=None, gen=None):
690 """Wrap data to support both list-like and string-like operations"""
695 """Wrap data to support both list-like and string-like operations"""
691 prefmt = pycompat.identity
696 prefmt = pycompat.identity
692 if fmt is None:
697 if fmt is None:
693 fmt = b'%s'
698 fmt = b'%s'
694 prefmt = pycompat.bytestr
699 prefmt = pycompat.bytestr
695 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
700 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
696
701
697
702
698 def compatdict(
703 def compatdict(
699 context,
704 context,
700 mapping,
705 mapping,
701 name,
706 name,
702 data,
707 data,
703 key=b'key',
708 key=b'key',
704 value=b'value',
709 value=b'value',
705 fmt=None,
710 fmt=None,
706 plural=None,
711 plural=None,
707 separator=b' ',
712 separator=b' ',
708 ):
713 ):
709 """Wrap data like hybriddict(), but also supports old-style list template
714 """Wrap data like hybriddict(), but also supports old-style list template
710
715
711 This exists for backward compatibility with the old-style template. Use
716 This exists for backward compatibility with the old-style template. Use
712 hybriddict() for new template keywords.
717 hybriddict() for new template keywords.
713 """
718 """
714 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
719 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
715 f = _showcompatlist(context, mapping, name, c, plural, separator)
720 f = _showcompatlist(context, mapping, name, c, plural, separator)
716 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
721 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
717
722
718
723
719 def compatlist(
724 def compatlist(
720 context,
725 context,
721 mapping,
726 mapping,
722 name,
727 name,
723 data,
728 data,
724 element=None,
729 element=None,
725 fmt=None,
730 fmt=None,
726 plural=None,
731 plural=None,
727 separator=b' ',
732 separator=b' ',
728 ):
733 ):
729 """Wrap data like hybridlist(), but also supports old-style list template
734 """Wrap data like hybridlist(), but also supports old-style list template
730
735
731 This exists for backward compatibility with the old-style template. Use
736 This exists for backward compatibility with the old-style template. Use
732 hybridlist() for new template keywords.
737 hybridlist() for new template keywords.
733 """
738 """
734 f = _showcompatlist(context, mapping, name, data, plural, separator)
739 f = _showcompatlist(context, mapping, name, data, plural, separator)
735 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
740 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
736
741
737
742
738 def compatfilecopiesdict(context, mapping, name, copies):
743 def compatfilecopiesdict(context, mapping, name, copies):
739 """Wrap list of (dest, source) file names to support old-style list
744 """Wrap list of (dest, source) file names to support old-style list
740 template and field names
745 template and field names
741
746
742 This exists for backward compatibility. Use hybriddict for new template
747 This exists for backward compatibility. Use hybriddict for new template
743 keywords.
748 keywords.
744 """
749 """
745 # no need to provide {path} to old-style list template
750 # no need to provide {path} to old-style list template
746 c = [{b'name': k, b'source': v} for k, v in copies]
751 c = [{b'name': k, b'source': v} for k, v in copies]
747 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies')
752 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies')
748 copies = util.sortdict(copies)
753 copies = util.sortdict(copies)
749 return hybrid(
754 return hybrid(
750 f,
755 f,
751 copies,
756 copies,
752 lambda k: {b'name': k, b'path': k, b'source': copies[k]},
757 lambda k: {b'name': k, b'path': k, b'source': copies[k]},
753 lambda k: b'%s (%s)' % (k, copies[k]),
758 lambda k: b'%s (%s)' % (k, copies[k]),
754 )
759 )
755
760
756
761
757 def compatfileslist(context, mapping, name, files):
762 def compatfileslist(context, mapping, name, files):
758 """Wrap list of file names to support old-style list template and field
763 """Wrap list of file names to support old-style list template and field
759 names
764 names
760
765
761 This exists for backward compatibility. Use hybridlist for new template
766 This exists for backward compatibility. Use hybridlist for new template
762 keywords.
767 keywords.
763 """
768 """
764 f = _showcompatlist(context, mapping, name, files)
769 f = _showcompatlist(context, mapping, name, files)
765 return hybrid(
770 return hybrid(
766 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity
771 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity
767 )
772 )
768
773
769
774
770 def _showcompatlist(
775 def _showcompatlist(
771 context, mapping, name, values, plural=None, separator=b' '
776 context, mapping, name, values, plural=None, separator=b' '
772 ):
777 ):
773 """Return a generator that renders old-style list template
778 """Return a generator that renders old-style list template
774
779
775 name is name of key in template map.
780 name is name of key in template map.
776 values is list of strings or dicts.
781 values is list of strings or dicts.
777 plural is plural of name, if not simply name + 's'.
782 plural is plural of name, if not simply name + 's'.
778 separator is used to join values as a string
783 separator is used to join values as a string
779
784
780 expansion works like this, given name 'foo'.
785 expansion works like this, given name 'foo'.
781
786
782 if values is empty, expand 'no_foos'.
787 if values is empty, expand 'no_foos'.
783
788
784 if 'foo' not in template map, return values as a string,
789 if 'foo' not in template map, return values as a string,
785 joined by 'separator'.
790 joined by 'separator'.
786
791
787 expand 'start_foos'.
792 expand 'start_foos'.
788
793
789 for each value, expand 'foo'. if 'last_foo' in template
794 for each value, expand 'foo'. if 'last_foo' in template
790 map, expand it instead of 'foo' for last key.
795 map, expand it instead of 'foo' for last key.
791
796
792 expand 'end_foos'.
797 expand 'end_foos'.
793 """
798 """
794 if not plural:
799 if not plural:
795 plural = name + b's'
800 plural = name + b's'
796 if not values:
801 if not values:
797 noname = b'no_' + plural
802 noname = b'no_' + plural
798 if context.preload(noname):
803 if context.preload(noname):
799 yield context.process(noname, mapping)
804 yield context.process(noname, mapping)
800 return
805 return
801 if not context.preload(name):
806 if not context.preload(name):
802 if isinstance(values[0], bytes):
807 if isinstance(values[0], bytes):
803 yield separator.join(values)
808 yield separator.join(values)
804 else:
809 else:
805 for v in values:
810 for v in values:
806 r = dict(v)
811 r = dict(v)
807 r.update(mapping)
812 r.update(mapping)
808 yield r
813 yield r
809 return
814 return
810 startname = b'start_' + plural
815 startname = b'start_' + plural
811 if context.preload(startname):
816 if context.preload(startname):
812 yield context.process(startname, mapping)
817 yield context.process(startname, mapping)
813
818
814 def one(v, tag=name):
819 def one(v, tag=name):
815 vmapping = {}
820 vmapping = {}
816 try:
821 try:
817 vmapping.update(v)
822 vmapping.update(v)
818 # Python 2 raises ValueError if the type of v is wrong. Python
823 # Python 2 raises ValueError if the type of v is wrong. Python
819 # 3 raises TypeError.
824 # 3 raises TypeError.
820 except (AttributeError, TypeError, ValueError):
825 except (AttributeError, TypeError, ValueError):
821 try:
826 try:
822 # Python 2 raises ValueError trying to destructure an e.g.
827 # Python 2 raises ValueError trying to destructure an e.g.
823 # bytes. Python 3 raises TypeError.
828 # bytes. Python 3 raises TypeError.
824 for a, b in v:
829 for a, b in v:
825 vmapping[a] = b
830 vmapping[a] = b
826 except (TypeError, ValueError):
831 except (TypeError, ValueError):
827 vmapping[name] = v
832 vmapping[name] = v
828 vmapping = context.overlaymap(mapping, vmapping)
833 vmapping = context.overlaymap(mapping, vmapping)
829 return context.process(tag, vmapping)
834 return context.process(tag, vmapping)
830
835
831 lastname = b'last_' + name
836 lastname = b'last_' + name
832 if context.preload(lastname):
837 if context.preload(lastname):
833 last = values.pop()
838 last = values.pop()
834 else:
839 else:
835 last = None
840 last = None
836 for v in values:
841 for v in values:
837 yield one(v)
842 yield one(v)
838 if last is not None:
843 if last is not None:
839 yield one(last, tag=lastname)
844 yield one(last, tag=lastname)
840 endname = b'end_' + plural
845 endname = b'end_' + plural
841 if context.preload(endname):
846 if context.preload(endname):
842 yield context.process(endname, mapping)
847 yield context.process(endname, mapping)
843
848
844
849
845 def flatten(context, mapping, thing):
850 def flatten(context, mapping, thing):
846 """Yield a single stream from a possibly nested set of iterators"""
851 """Yield a single stream from a possibly nested set of iterators"""
847 if isinstance(thing, wrapped):
852 if isinstance(thing, wrapped):
848 thing = thing.show(context, mapping)
853 thing = thing.show(context, mapping)
849 if isinstance(thing, bytes):
854 if isinstance(thing, bytes):
850 yield thing
855 yield thing
851 elif isinstance(thing, str):
856 elif isinstance(thing, str):
852 # We can only hit this on Python 3, and it's here to guard
857 # We can only hit this on Python 3, and it's here to guard
853 # against infinite recursion.
858 # against infinite recursion.
854 raise error.ProgrammingError(
859 raise error.ProgrammingError(
855 b'Mercurial IO including templates is done'
860 b'Mercurial IO including templates is done'
856 b' with bytes, not strings, got %r' % thing
861 b' with bytes, not strings, got %r' % thing
857 )
862 )
858 elif thing is None:
863 elif thing is None:
859 pass
864 pass
860 elif not util.safehasattr(thing, b'__iter__'):
865 elif not util.safehasattr(thing, b'__iter__'):
861 yield pycompat.bytestr(thing)
866 yield pycompat.bytestr(thing)
862 else:
867 else:
863 for i in thing:
868 for i in thing:
864 if isinstance(i, wrapped):
869 if isinstance(i, wrapped):
865 i = i.show(context, mapping)
870 i = i.show(context, mapping)
866 if isinstance(i, bytes):
871 if isinstance(i, bytes):
867 yield i
872 yield i
868 elif i is None:
873 elif i is None:
869 pass
874 pass
870 elif not util.safehasattr(i, b'__iter__'):
875 elif not util.safehasattr(i, b'__iter__'):
871 yield pycompat.bytestr(i)
876 yield pycompat.bytestr(i)
872 else:
877 else:
873 for j in flatten(context, mapping, i):
878 for j in flatten(context, mapping, i):
874 yield j
879 yield j
875
880
876
881
877 def stringify(context, mapping, thing):
882 def stringify(context, mapping, thing):
878 """Turn values into bytes by converting into text and concatenating them"""
883 """Turn values into bytes by converting into text and concatenating them"""
879 if isinstance(thing, bytes):
884 if isinstance(thing, bytes):
880 return thing # retain localstr to be round-tripped
885 return thing # retain localstr to be round-tripped
881 return b''.join(flatten(context, mapping, thing))
886 return b''.join(flatten(context, mapping, thing))
882
887
883
888
884 def findsymbolicname(arg):
889 def findsymbolicname(arg):
885 """Find symbolic name for the given compiled expression; returns None
890 """Find symbolic name for the given compiled expression; returns None
886 if nothing found reliably"""
891 if nothing found reliably"""
887 while True:
892 while True:
888 func, data = arg
893 func, data = arg
889 if func is runsymbol:
894 if func is runsymbol:
890 return data
895 return data
891 elif func is runfilter:
896 elif func is runfilter:
892 arg = data[0]
897 arg = data[0]
893 else:
898 else:
894 return None
899 return None
895
900
896
901
897 def _nonempty(xiter):
902 def _nonempty(xiter):
898 try:
903 try:
899 next(xiter)
904 next(xiter)
900 return True
905 return True
901 except StopIteration:
906 except StopIteration:
902 return False
907 return False
903
908
904
909
905 def _unthunk(context, mapping, thing):
910 def _unthunk(context, mapping, thing):
906 """Evaluate a lazy byte string into value"""
911 """Evaluate a lazy byte string into value"""
907 if not isinstance(thing, types.GeneratorType):
912 if not isinstance(thing, types.GeneratorType):
908 return thing
913 return thing
909 return stringify(context, mapping, thing)
914 return stringify(context, mapping, thing)
910
915
911
916
912 def evalrawexp(context, mapping, arg):
917 def evalrawexp(context, mapping, arg):
913 """Evaluate given argument as a bare template object which may require
918 """Evaluate given argument as a bare template object which may require
914 further processing (such as folding generator of strings)"""
919 further processing (such as folding generator of strings)"""
915 func, data = arg
920 func, data = arg
916 return func(context, mapping, data)
921 return func(context, mapping, data)
917
922
918
923
919 def evalwrapped(context, mapping, arg):
924 def evalwrapped(context, mapping, arg):
920 """Evaluate given argument to wrapped object"""
925 """Evaluate given argument to wrapped object"""
921 thing = evalrawexp(context, mapping, arg)
926 thing = evalrawexp(context, mapping, arg)
922 return makewrapped(context, mapping, thing)
927 return makewrapped(context, mapping, thing)
923
928
924
929
925 def makewrapped(context, mapping, thing):
930 def makewrapped(context, mapping, thing):
926 """Lift object to a wrapped type"""
931 """Lift object to a wrapped type"""
927 if isinstance(thing, wrapped):
932 if isinstance(thing, wrapped):
928 return thing
933 return thing
929 thing = _unthunk(context, mapping, thing)
934 thing = _unthunk(context, mapping, thing)
930 if isinstance(thing, bytes):
935 if isinstance(thing, bytes):
931 return wrappedbytes(thing)
936 return wrappedbytes(thing)
932 return wrappedvalue(thing)
937 return wrappedvalue(thing)
933
938
934
939
935 def evalfuncarg(context, mapping, arg):
940 def evalfuncarg(context, mapping, arg):
936 """Evaluate given argument as value type"""
941 """Evaluate given argument as value type"""
937 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
942 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
938
943
939
944
940 def unwrapvalue(context, mapping, thing):
945 def unwrapvalue(context, mapping, thing):
941 """Move the inner value object out of the wrapper"""
946 """Move the inner value object out of the wrapper"""
942 if isinstance(thing, wrapped):
947 if isinstance(thing, wrapped):
943 return thing.tovalue(context, mapping)
948 return thing.tovalue(context, mapping)
944 # evalrawexp() may return string, generator of strings or arbitrary object
949 # evalrawexp() may return string, generator of strings or arbitrary object
945 # such as date tuple, but filter does not want generator.
950 # such as date tuple, but filter does not want generator.
946 return _unthunk(context, mapping, thing)
951 return _unthunk(context, mapping, thing)
947
952
948
953
949 def evalboolean(context, mapping, arg):
954 def evalboolean(context, mapping, arg):
950 """Evaluate given argument as boolean, but also takes boolean literals"""
955 """Evaluate given argument as boolean, but also takes boolean literals"""
951 func, data = arg
956 func, data = arg
952 if func is runsymbol:
957 if func is runsymbol:
953 thing = func(context, mapping, data, default=None)
958 thing = func(context, mapping, data, default=None)
954 if thing is None:
959 if thing is None:
955 # not a template keyword, takes as a boolean literal
960 # not a template keyword, takes as a boolean literal
956 thing = stringutil.parsebool(data)
961 thing = stringutil.parsebool(data)
957 else:
962 else:
958 thing = func(context, mapping, data)
963 thing = func(context, mapping, data)
959 return makewrapped(context, mapping, thing).tobool(context, mapping)
964 return makewrapped(context, mapping, thing).tobool(context, mapping)
960
965
961
966
962 def evaldate(context, mapping, arg, err=None):
967 def evaldate(context, mapping, arg, err=None):
963 """Evaluate given argument as a date tuple or a date string; returns
968 """Evaluate given argument as a date tuple or a date string; returns
964 a (unixtime, offset) tuple"""
969 a (unixtime, offset) tuple"""
965 thing = evalrawexp(context, mapping, arg)
970 thing = evalrawexp(context, mapping, arg)
966 return unwrapdate(context, mapping, thing, err)
971 return unwrapdate(context, mapping, thing, err)
967
972
968
973
969 def unwrapdate(context, mapping, thing, err=None):
974 def unwrapdate(context, mapping, thing, err=None):
970 if isinstance(thing, date):
975 if isinstance(thing, date):
971 return thing.tovalue(context, mapping)
976 return thing.tovalue(context, mapping)
972 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
977 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
973 thing = unwrapvalue(context, mapping, thing)
978 thing = unwrapvalue(context, mapping, thing)
974 try:
979 try:
975 return dateutil.parsedate(thing)
980 return dateutil.parsedate(thing)
976 except AttributeError:
981 except AttributeError:
977 raise error.ParseError(err or _(b'not a date tuple nor a string'))
982 raise error.ParseError(err or _(b'not a date tuple nor a string'))
978 except error.ParseError:
983 except error.ParseError:
979 if not err:
984 if not err:
980 raise
985 raise
981 raise error.ParseError(err)
986 raise error.ParseError(err)
982
987
983
988
984 def evalinteger(context, mapping, arg, err=None):
989 def evalinteger(context, mapping, arg, err=None):
985 thing = evalrawexp(context, mapping, arg)
990 thing = evalrawexp(context, mapping, arg)
986 return unwrapinteger(context, mapping, thing, err)
991 return unwrapinteger(context, mapping, thing, err)
987
992
988
993
989 def unwrapinteger(context, mapping, thing, err=None):
994 def unwrapinteger(context, mapping, thing, err=None):
990 thing = unwrapvalue(context, mapping, thing)
995 thing = unwrapvalue(context, mapping, thing)
991 try:
996 try:
992 return int(thing)
997 return int(thing)
993 except (TypeError, ValueError):
998 except (TypeError, ValueError):
994 raise error.ParseError(err or _(b'not an integer'))
999 raise error.ParseError(err or _(b'not an integer'))
995
1000
996
1001
997 def evalstring(context, mapping, arg):
1002 def evalstring(context, mapping, arg):
998 return stringify(context, mapping, evalrawexp(context, mapping, arg))
1003 return stringify(context, mapping, evalrawexp(context, mapping, arg))
999
1004
1000
1005
1001 def evalstringliteral(context, mapping, arg):
1006 def evalstringliteral(context, mapping, arg):
1002 """Evaluate given argument as string template, but returns symbol name
1007 """Evaluate given argument as string template, but returns symbol name
1003 if it is unknown"""
1008 if it is unknown"""
1004 func, data = arg
1009 func, data = arg
1005 if func is runsymbol:
1010 if func is runsymbol:
1006 thing = func(context, mapping, data, default=data)
1011 thing = func(context, mapping, data, default=data)
1007 else:
1012 else:
1008 thing = func(context, mapping, data)
1013 thing = func(context, mapping, data)
1009 return stringify(context, mapping, thing)
1014 return stringify(context, mapping, thing)
1010
1015
1011
1016
1012 _unwrapfuncbytype = {
1017 _unwrapfuncbytype = {
1013 None: unwrapvalue,
1018 None: unwrapvalue,
1014 bytes: stringify,
1019 bytes: stringify,
1015 date: unwrapdate,
1020 date: unwrapdate,
1016 int: unwrapinteger,
1021 int: unwrapinteger,
1017 }
1022 }
1018
1023
1019
1024
1020 def unwrapastype(context, mapping, thing, typ):
1025 def unwrapastype(context, mapping, thing, typ):
1021 """Move the inner value object out of the wrapper and coerce its type"""
1026 """Move the inner value object out of the wrapper and coerce its type"""
1022 try:
1027 try:
1023 f = _unwrapfuncbytype[typ]
1028 f = _unwrapfuncbytype[typ]
1024 except KeyError:
1029 except KeyError:
1025 raise error.ProgrammingError(b'invalid type specified: %r' % typ)
1030 raise error.ProgrammingError(b'invalid type specified: %r' % typ)
1026 return f(context, mapping, thing)
1031 return f(context, mapping, thing)
1027
1032
1028
1033
1029 def runinteger(context, mapping, data):
1034 def runinteger(context, mapping, data):
1030 return int(data)
1035 return int(data)
1031
1036
1032
1037
1033 def runstring(context, mapping, data):
1038 def runstring(context, mapping, data):
1034 return data
1039 return data
1035
1040
1036
1041
1037 def _recursivesymbolblocker(key):
1042 def _recursivesymbolblocker(key):
1038 def showrecursion(context, mapping):
1043 def showrecursion(context, mapping):
1039 raise error.Abort(_(b"recursive reference '%s' in template") % key)
1044 raise error.Abort(_(b"recursive reference '%s' in template") % key)
1040
1045
1041 return showrecursion
1046 return showrecursion
1042
1047
1043
1048
1044 def runsymbol(context, mapping, key, default=b''):
1049 def runsymbol(context, mapping, key, default=b''):
1045 v = context.symbol(mapping, key)
1050 v = context.symbol(mapping, key)
1046 if v is None:
1051 if v is None:
1047 # put poison to cut recursion. we can't move this to parsing phase
1052 # put poison to cut recursion. we can't move this to parsing phase
1048 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
1053 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
1049 safemapping = mapping.copy()
1054 safemapping = mapping.copy()
1050 safemapping[key] = _recursivesymbolblocker(key)
1055 safemapping[key] = _recursivesymbolblocker(key)
1051 try:
1056 try:
1052 v = context.process(key, safemapping)
1057 v = context.process(key, safemapping)
1053 except TemplateNotFound:
1058 except TemplateNotFound:
1054 v = default
1059 v = default
1055 if callable(v):
1060 if callable(v):
1056 # new templatekw
1061 # new templatekw
1057 try:
1062 try:
1058 return v(context, mapping)
1063 return v(context, mapping)
1059 except ResourceUnavailable:
1064 except ResourceUnavailable:
1060 # unsupported keyword is mapped to empty just like unknown keyword
1065 # unsupported keyword is mapped to empty just like unknown keyword
1061 return None
1066 return None
1062 return v
1067 return v
1063
1068
1064
1069
1065 def runtemplate(context, mapping, template):
1070 def runtemplate(context, mapping, template):
1066 for arg in template:
1071 for arg in template:
1067 yield evalrawexp(context, mapping, arg)
1072 yield evalrawexp(context, mapping, arg)
1068
1073
1069
1074
1070 def runfilter(context, mapping, data):
1075 def runfilter(context, mapping, data):
1071 arg, filt = data
1076 arg, filt = data
1072 thing = evalrawexp(context, mapping, arg)
1077 thing = evalrawexp(context, mapping, arg)
1073 intype = getattr(filt, '_intype', None)
1078 intype = getattr(filt, '_intype', None)
1074 try:
1079 try:
1075 thing = unwrapastype(context, mapping, thing, intype)
1080 thing = unwrapastype(context, mapping, thing, intype)
1076 return filt(thing)
1081 return filt(thing)
1077 except error.ParseError as e:
1082 except error.ParseError as e:
1078 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
1083 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
1079
1084
1080
1085
1081 def _formatfiltererror(arg, filt):
1086 def _formatfiltererror(arg, filt):
1082 fn = pycompat.sysbytes(filt.__name__)
1087 fn = pycompat.sysbytes(filt.__name__)
1083 sym = findsymbolicname(arg)
1088 sym = findsymbolicname(arg)
1084 if not sym:
1089 if not sym:
1085 return _(b"incompatible use of template filter '%s'") % fn
1090 return _(b"incompatible use of template filter '%s'") % fn
1086 return _(b"template filter '%s' is not compatible with keyword '%s'") % (
1091 return _(b"template filter '%s' is not compatible with keyword '%s'") % (
1087 fn,
1092 fn,
1088 sym,
1093 sym,
1089 )
1094 )
1090
1095
1091
1096
1092 def _iteroverlaymaps(context, origmapping, newmappings):
1097 def _iteroverlaymaps(context, origmapping, newmappings):
1093 """Generate combined mappings from the original mapping and an iterable
1098 """Generate combined mappings from the original mapping and an iterable
1094 of partial mappings to override the original"""
1099 of partial mappings to override the original"""
1095 for i, nm in enumerate(newmappings):
1100 for i, nm in enumerate(newmappings):
1096 lm = context.overlaymap(origmapping, nm)
1101 lm = context.overlaymap(origmapping, nm)
1097 lm[b'index'] = i
1102 lm[b'index'] = i
1098 yield lm
1103 yield lm
1099
1104
1100
1105
1101 def _applymap(context, mapping, d, darg, targ):
1106 def _applymap(context, mapping, d, darg, targ):
1102 try:
1107 try:
1103 diter = d.itermaps(context)
1108 diter = d.itermaps(context)
1104 except error.ParseError as err:
1109 except error.ParseError as err:
1105 sym = findsymbolicname(darg)
1110 sym = findsymbolicname(darg)
1106 if not sym:
1111 if not sym:
1107 raise
1112 raise
1108 hint = _(b"keyword '%s' does not support map operation") % sym
1113 hint = _(b"keyword '%s' does not support map operation") % sym
1109 raise error.ParseError(bytes(err), hint=hint)
1114 raise error.ParseError(bytes(err), hint=hint)
1110 for lm in _iteroverlaymaps(context, mapping, diter):
1115 for lm in _iteroverlaymaps(context, mapping, diter):
1111 yield evalrawexp(context, lm, targ)
1116 yield evalrawexp(context, lm, targ)
1112
1117
1113
1118
1114 def runmap(context, mapping, data):
1119 def runmap(context, mapping, data):
1115 darg, targ = data
1120 darg, targ = data
1116 d = evalwrapped(context, mapping, darg)
1121 d = evalwrapped(context, mapping, darg)
1117 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
1122 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
1118
1123
1119
1124
1120 def runmember(context, mapping, data):
1125 def runmember(context, mapping, data):
1121 darg, memb = data
1126 darg, memb = data
1122 d = evalwrapped(context, mapping, darg)
1127 d = evalwrapped(context, mapping, darg)
1123 if isinstance(d, mappable):
1128 if isinstance(d, mappable):
1124 lm = context.overlaymap(mapping, d.tomap(context))
1129 lm = context.overlaymap(mapping, d.tomap(context))
1125 return runsymbol(context, lm, memb)
1130 return runsymbol(context, lm, memb)
1126 try:
1131 try:
1127 return d.getmember(context, mapping, memb)
1132 return d.getmember(context, mapping, memb)
1128 except error.ParseError as err:
1133 except error.ParseError as err:
1129 sym = findsymbolicname(darg)
1134 sym = findsymbolicname(darg)
1130 if not sym:
1135 if not sym:
1131 raise
1136 raise
1132 hint = _(b"keyword '%s' does not support member operation") % sym
1137 hint = _(b"keyword '%s' does not support member operation") % sym
1133 raise error.ParseError(bytes(err), hint=hint)
1138 raise error.ParseError(bytes(err), hint=hint)
1134
1139
1135
1140
1136 def runnegate(context, mapping, data):
1141 def runnegate(context, mapping, data):
1137 data = evalinteger(
1142 data = evalinteger(
1138 context, mapping, data, _(b'negation needs an integer argument')
1143 context, mapping, data, _(b'negation needs an integer argument')
1139 )
1144 )
1140 return -data
1145 return -data
1141
1146
1142
1147
1143 def runarithmetic(context, mapping, data):
1148 def runarithmetic(context, mapping, data):
1144 func, left, right = data
1149 func, left, right = data
1145 left = evalinteger(
1150 left = evalinteger(
1146 context, mapping, left, _(b'arithmetic only defined on integers')
1151 context, mapping, left, _(b'arithmetic only defined on integers')
1147 )
1152 )
1148 right = evalinteger(
1153 right = evalinteger(
1149 context, mapping, right, _(b'arithmetic only defined on integers')
1154 context, mapping, right, _(b'arithmetic only defined on integers')
1150 )
1155 )
1151 try:
1156 try:
1152 return func(left, right)
1157 return func(left, right)
1153 except ZeroDivisionError:
1158 except ZeroDivisionError:
1154 raise error.Abort(_(b'division by zero is not defined'))
1159 raise error.Abort(_(b'division by zero is not defined'))
1155
1160
1156
1161
1157 def joinitems(itemiter, sep):
1162 def joinitems(itemiter, sep):
1158 """Join items with the separator; Returns generator of bytes"""
1163 """Join items with the separator; Returns generator of bytes"""
1159 first = True
1164 first = True
1160 for x in itemiter:
1165 for x in itemiter:
1161 if first:
1166 if first:
1162 first = False
1167 first = False
1163 elif sep:
1168 elif sep:
1164 yield sep
1169 yield sep
1165 yield x
1170 yield x
General Comments 0
You need to be logged in to leave comments. Login now