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