##// END OF EJS Templates
templatekw: switch latesttags template keywords to new API
Yuya Nishihara -
r36614:b5d39a09 default
parent child Browse files
Show More
@@ -1,977 +1,983
1 # templatekw.py - common changeset template keywords
1 # templatekw.py - common changeset template keywords
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2009 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 from .i18n import _
10 from .i18n import _
11 from .node import (
11 from .node import (
12 hex,
12 hex,
13 nullid,
13 nullid,
14 )
14 )
15
15
16 from . import (
16 from . import (
17 encoding,
17 encoding,
18 error,
18 error,
19 hbisect,
19 hbisect,
20 i18n,
20 i18n,
21 obsutil,
21 obsutil,
22 patch,
22 patch,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 scmutil,
25 scmutil,
26 util,
26 util,
27 )
27 )
28
28
29 class _hybrid(object):
29 class _hybrid(object):
30 """Wrapper for list or dict to support legacy template
30 """Wrapper for list or dict to support legacy template
31
31
32 This class allows us to handle both:
32 This class allows us to handle both:
33 - "{files}" (legacy command-line-specific list hack) and
33 - "{files}" (legacy command-line-specific list hack) and
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 and to access raw values:
35 and to access raw values:
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 - "{get(extras, key)}"
37 - "{get(extras, key)}"
38 - "{files|json}"
38 - "{files|json}"
39 """
39 """
40
40
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 if gen is not None:
42 if gen is not None:
43 self.gen = gen # generator or function returning generator
43 self.gen = gen # generator or function returning generator
44 self._values = values
44 self._values = values
45 self._makemap = makemap
45 self._makemap = makemap
46 self.joinfmt = joinfmt
46 self.joinfmt = joinfmt
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 def gen(self):
48 def gen(self):
49 """Default generator to stringify this as {join(self, ' ')}"""
49 """Default generator to stringify this as {join(self, ' ')}"""
50 for i, x in enumerate(self._values):
50 for i, x in enumerate(self._values):
51 if i > 0:
51 if i > 0:
52 yield ' '
52 yield ' '
53 yield self.joinfmt(x)
53 yield self.joinfmt(x)
54 def itermaps(self):
54 def itermaps(self):
55 makemap = self._makemap
55 makemap = self._makemap
56 for x in self._values:
56 for x in self._values:
57 yield makemap(x)
57 yield makemap(x)
58 def __contains__(self, x):
58 def __contains__(self, x):
59 return x in self._values
59 return x in self._values
60 def __getitem__(self, key):
60 def __getitem__(self, key):
61 return self._values[key]
61 return self._values[key]
62 def __len__(self):
62 def __len__(self):
63 return len(self._values)
63 return len(self._values)
64 def __iter__(self):
64 def __iter__(self):
65 return iter(self._values)
65 return iter(self._values)
66 def __getattr__(self, name):
66 def __getattr__(self, name):
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 r'itervalues', r'keys', r'values'):
68 r'itervalues', r'keys', r'values'):
69 raise AttributeError(name)
69 raise AttributeError(name)
70 return getattr(self._values, name)
70 return getattr(self._values, name)
71
71
72 class _mappable(object):
72 class _mappable(object):
73 """Wrapper for non-list/dict object to support map operation
73 """Wrapper for non-list/dict object to support map operation
74
74
75 This class allows us to handle both:
75 This class allows us to handle both:
76 - "{manifest}"
76 - "{manifest}"
77 - "{manifest % '{rev}:{node}'}"
77 - "{manifest % '{rev}:{node}'}"
78 - "{manifest.rev}"
78 - "{manifest.rev}"
79
79
80 Unlike a _hybrid, this does not simulate the behavior of the underling
80 Unlike a _hybrid, this does not simulate the behavior of the underling
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
82 """
82 """
83
83
84 def __init__(self, gen, key, value, makemap):
84 def __init__(self, gen, key, value, makemap):
85 if gen is not None:
85 if gen is not None:
86 self.gen = gen # generator or function returning generator
86 self.gen = gen # generator or function returning generator
87 self._key = key
87 self._key = key
88 self._value = value # may be generator of strings
88 self._value = value # may be generator of strings
89 self._makemap = makemap
89 self._makemap = makemap
90
90
91 def gen(self):
91 def gen(self):
92 yield pycompat.bytestr(self._value)
92 yield pycompat.bytestr(self._value)
93
93
94 def tomap(self):
94 def tomap(self):
95 return self._makemap(self._key)
95 return self._makemap(self._key)
96
96
97 def itermaps(self):
97 def itermaps(self):
98 yield self.tomap()
98 yield self.tomap()
99
99
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
101 """Wrap data to support both dict-like and string-like operations"""
101 """Wrap data to support both dict-like and string-like operations"""
102 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
102 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 lambda k: fmt % (k, data[k]))
103 lambda k: fmt % (k, data[k]))
104
104
105 def hybridlist(data, name, fmt='%s', gen=None):
105 def hybridlist(data, name, fmt='%s', gen=None):
106 """Wrap data to support both list-like and string-like operations"""
106 """Wrap data to support both list-like and string-like operations"""
107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
108
108
109 def unwraphybrid(thing):
109 def unwraphybrid(thing):
110 """Return an object which can be stringified possibly by using a legacy
110 """Return an object which can be stringified possibly by using a legacy
111 template"""
111 template"""
112 gen = getattr(thing, 'gen', None)
112 gen = getattr(thing, 'gen', None)
113 if gen is None:
113 if gen is None:
114 return thing
114 return thing
115 if callable(gen):
115 if callable(gen):
116 return gen()
116 return gen()
117 return gen
117 return gen
118
118
119 def unwrapvalue(thing):
119 def unwrapvalue(thing):
120 """Move the inner value object out of the wrapper"""
120 """Move the inner value object out of the wrapper"""
121 if not util.safehasattr(thing, '_value'):
121 if not util.safehasattr(thing, '_value'):
122 return thing
122 return thing
123 return thing._value
123 return thing._value
124
124
125 def wraphybridvalue(container, key, value):
125 def wraphybridvalue(container, key, value):
126 """Wrap an element of hybrid container to be mappable
126 """Wrap an element of hybrid container to be mappable
127
127
128 The key is passed to the makemap function of the given container, which
128 The key is passed to the makemap function of the given container, which
129 should be an item generated by iter(container).
129 should be an item generated by iter(container).
130 """
130 """
131 makemap = getattr(container, '_makemap', None)
131 makemap = getattr(container, '_makemap', None)
132 if makemap is None:
132 if makemap is None:
133 return value
133 return value
134 if util.safehasattr(value, '_makemap'):
134 if util.safehasattr(value, '_makemap'):
135 # a nested hybrid list/dict, which has its own way of map operation
135 # a nested hybrid list/dict, which has its own way of map operation
136 return value
136 return value
137 return _mappable(None, key, value, makemap)
137 return _mappable(None, key, value, makemap)
138
138
139 def compatdict(context, mapping, name, data, key='key', value='value',
139 def compatdict(context, mapping, name, data, key='key', value='value',
140 fmt='%s=%s', plural=None, separator=' '):
140 fmt='%s=%s', plural=None, separator=' '):
141 """Wrap data like hybriddict(), but also supports old-style list template
141 """Wrap data like hybriddict(), but also supports old-style list template
142
142
143 This exists for backward compatibility with the old-style template. Use
143 This exists for backward compatibility with the old-style template. Use
144 hybriddict() for new template keywords.
144 hybriddict() for new template keywords.
145 """
145 """
146 c = [{key: k, value: v} for k, v in data.iteritems()]
146 c = [{key: k, value: v} for k, v in data.iteritems()]
147 t = context.resource(mapping, 'templ')
147 t = context.resource(mapping, 'templ')
148 f = _showlist(name, c, t, mapping, plural, separator)
148 f = _showlist(name, c, t, mapping, plural, separator)
149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
150
150
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
152 plural=None, separator=' '):
152 plural=None, separator=' '):
153 """Wrap data like hybridlist(), but also supports old-style list template
153 """Wrap data like hybridlist(), but also supports old-style list template
154
154
155 This exists for backward compatibility with the old-style template. Use
155 This exists for backward compatibility with the old-style template. Use
156 hybridlist() for new template keywords.
156 hybridlist() for new template keywords.
157 """
157 """
158 t = context.resource(mapping, 'templ')
158 t = context.resource(mapping, 'templ')
159 f = _showlist(name, data, t, mapping, plural, separator)
159 f = _showlist(name, data, t, mapping, plural, separator)
160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
161
161
162 def showdict(name, data, mapping, plural=None, key='key', value='value',
162 def showdict(name, data, mapping, plural=None, key='key', value='value',
163 fmt='%s=%s', separator=' '):
163 fmt='%s=%s', separator=' '):
164 c = [{key: k, value: v} for k, v in data.iteritems()]
164 c = [{key: k, value: v} for k, v in data.iteritems()]
165 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
165 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
166 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
166 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
167
167
168 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
168 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
169 if not element:
169 if not element:
170 element = name
170 element = name
171 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
171 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
172 return hybridlist(values, name=element, gen=f)
172 return hybridlist(values, name=element, gen=f)
173
173
174 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
174 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
175 '''expand set of values.
175 '''expand set of values.
176 name is name of key in template map.
176 name is name of key in template map.
177 values is list of strings or dicts.
177 values is list of strings or dicts.
178 plural is plural of name, if not simply name + 's'.
178 plural is plural of name, if not simply name + 's'.
179 separator is used to join values as a string
179 separator is used to join values as a string
180
180
181 expansion works like this, given name 'foo'.
181 expansion works like this, given name 'foo'.
182
182
183 if values is empty, expand 'no_foos'.
183 if values is empty, expand 'no_foos'.
184
184
185 if 'foo' not in template map, return values as a string,
185 if 'foo' not in template map, return values as a string,
186 joined by 'separator'.
186 joined by 'separator'.
187
187
188 expand 'start_foos'.
188 expand 'start_foos'.
189
189
190 for each value, expand 'foo'. if 'last_foo' in template
190 for each value, expand 'foo'. if 'last_foo' in template
191 map, expand it instead of 'foo' for last key.
191 map, expand it instead of 'foo' for last key.
192
192
193 expand 'end_foos'.
193 expand 'end_foos'.
194 '''
194 '''
195 strmapping = pycompat.strkwargs(mapping)
195 strmapping = pycompat.strkwargs(mapping)
196 if not plural:
196 if not plural:
197 plural = name + 's'
197 plural = name + 's'
198 if not values:
198 if not values:
199 noname = 'no_' + plural
199 noname = 'no_' + plural
200 if noname in templ:
200 if noname in templ:
201 yield templ(noname, **strmapping)
201 yield templ(noname, **strmapping)
202 return
202 return
203 if name not in templ:
203 if name not in templ:
204 if isinstance(values[0], bytes):
204 if isinstance(values[0], bytes):
205 yield separator.join(values)
205 yield separator.join(values)
206 else:
206 else:
207 for v in values:
207 for v in values:
208 r = dict(v)
208 r = dict(v)
209 r.update(mapping)
209 r.update(mapping)
210 yield r
210 yield r
211 return
211 return
212 startname = 'start_' + plural
212 startname = 'start_' + plural
213 if startname in templ:
213 if startname in templ:
214 yield templ(startname, **strmapping)
214 yield templ(startname, **strmapping)
215 vmapping = mapping.copy()
215 vmapping = mapping.copy()
216 def one(v, tag=name):
216 def one(v, tag=name):
217 try:
217 try:
218 vmapping.update(v)
218 vmapping.update(v)
219 # Python 2 raises ValueError if the type of v is wrong. Python
219 # Python 2 raises ValueError if the type of v is wrong. Python
220 # 3 raises TypeError.
220 # 3 raises TypeError.
221 except (AttributeError, TypeError, ValueError):
221 except (AttributeError, TypeError, ValueError):
222 try:
222 try:
223 # Python 2 raises ValueError trying to destructure an e.g.
223 # Python 2 raises ValueError trying to destructure an e.g.
224 # bytes. Python 3 raises TypeError.
224 # bytes. Python 3 raises TypeError.
225 for a, b in v:
225 for a, b in v:
226 vmapping[a] = b
226 vmapping[a] = b
227 except (TypeError, ValueError):
227 except (TypeError, ValueError):
228 vmapping[name] = v
228 vmapping[name] = v
229 return templ(tag, **pycompat.strkwargs(vmapping))
229 return templ(tag, **pycompat.strkwargs(vmapping))
230 lastname = 'last_' + name
230 lastname = 'last_' + name
231 if lastname in templ:
231 if lastname in templ:
232 last = values.pop()
232 last = values.pop()
233 else:
233 else:
234 last = None
234 last = None
235 for v in values:
235 for v in values:
236 yield one(v)
236 yield one(v)
237 if last is not None:
237 if last is not None:
238 yield one(last, tag=lastname)
238 yield one(last, tag=lastname)
239 endname = 'end_' + plural
239 endname = 'end_' + plural
240 if endname in templ:
240 if endname in templ:
241 yield templ(endname, **strmapping)
241 yield templ(endname, **strmapping)
242
242
243 def getlatesttags(repo, ctx, cache, pattern=None):
243 def getlatesttags(context, mapping, pattern=None):
244 '''return date, distance and name for the latest tag of rev'''
244 '''return date, distance and name for the latest tag of rev'''
245 repo = context.resource(mapping, 'repo')
246 ctx = context.resource(mapping, 'ctx')
247 cache = context.resource(mapping, 'cache')
245
248
246 cachename = 'latesttags'
249 cachename = 'latesttags'
247 if pattern is not None:
250 if pattern is not None:
248 cachename += '-' + pattern
251 cachename += '-' + pattern
249 match = util.stringmatcher(pattern)[2]
252 match = util.stringmatcher(pattern)[2]
250 else:
253 else:
251 match = util.always
254 match = util.always
252
255
253 if cachename not in cache:
256 if cachename not in cache:
254 # Cache mapping from rev to a tuple with tag date, tag
257 # Cache mapping from rev to a tuple with tag date, tag
255 # distance and tag name
258 # distance and tag name
256 cache[cachename] = {-1: (0, 0, ['null'])}
259 cache[cachename] = {-1: (0, 0, ['null'])}
257 latesttags = cache[cachename]
260 latesttags = cache[cachename]
258
261
259 rev = ctx.rev()
262 rev = ctx.rev()
260 todo = [rev]
263 todo = [rev]
261 while todo:
264 while todo:
262 rev = todo.pop()
265 rev = todo.pop()
263 if rev in latesttags:
266 if rev in latesttags:
264 continue
267 continue
265 ctx = repo[rev]
268 ctx = repo[rev]
266 tags = [t for t in ctx.tags()
269 tags = [t for t in ctx.tags()
267 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
270 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
268 and match(t))]
271 and match(t))]
269 if tags:
272 if tags:
270 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
273 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
271 continue
274 continue
272 try:
275 try:
273 ptags = [latesttags[p.rev()] for p in ctx.parents()]
276 ptags = [latesttags[p.rev()] for p in ctx.parents()]
274 if len(ptags) > 1:
277 if len(ptags) > 1:
275 if ptags[0][2] == ptags[1][2]:
278 if ptags[0][2] == ptags[1][2]:
276 # The tuples are laid out so the right one can be found by
279 # The tuples are laid out so the right one can be found by
277 # comparison in this case.
280 # comparison in this case.
278 pdate, pdist, ptag = max(ptags)
281 pdate, pdist, ptag = max(ptags)
279 else:
282 else:
280 def key(x):
283 def key(x):
281 changessincetag = len(repo.revs('only(%d, %s)',
284 changessincetag = len(repo.revs('only(%d, %s)',
282 ctx.rev(), x[2][0]))
285 ctx.rev(), x[2][0]))
283 # Smallest number of changes since tag wins. Date is
286 # Smallest number of changes since tag wins. Date is
284 # used as tiebreaker.
287 # used as tiebreaker.
285 return [-changessincetag, x[0]]
288 return [-changessincetag, x[0]]
286 pdate, pdist, ptag = max(ptags, key=key)
289 pdate, pdist, ptag = max(ptags, key=key)
287 else:
290 else:
288 pdate, pdist, ptag = ptags[0]
291 pdate, pdist, ptag = ptags[0]
289 except KeyError:
292 except KeyError:
290 # Cache miss - recurse
293 # Cache miss - recurse
291 todo.append(rev)
294 todo.append(rev)
292 todo.extend(p.rev() for p in ctx.parents())
295 todo.extend(p.rev() for p in ctx.parents())
293 continue
296 continue
294 latesttags[rev] = pdate, pdist + 1, ptag
297 latesttags[rev] = pdate, pdist + 1, ptag
295 return latesttags[rev]
298 return latesttags[rev]
296
299
297 def getrenamedfn(repo, endrev=None):
300 def getrenamedfn(repo, endrev=None):
298 rcache = {}
301 rcache = {}
299 if endrev is None:
302 if endrev is None:
300 endrev = len(repo)
303 endrev = len(repo)
301
304
302 def getrenamed(fn, rev):
305 def getrenamed(fn, rev):
303 '''looks up all renames for a file (up to endrev) the first
306 '''looks up all renames for a file (up to endrev) the first
304 time the file is given. It indexes on the changerev and only
307 time the file is given. It indexes on the changerev and only
305 parses the manifest if linkrev != changerev.
308 parses the manifest if linkrev != changerev.
306 Returns rename info for fn at changerev rev.'''
309 Returns rename info for fn at changerev rev.'''
307 if fn not in rcache:
310 if fn not in rcache:
308 rcache[fn] = {}
311 rcache[fn] = {}
309 fl = repo.file(fn)
312 fl = repo.file(fn)
310 for i in fl:
313 for i in fl:
311 lr = fl.linkrev(i)
314 lr = fl.linkrev(i)
312 renamed = fl.renamed(fl.node(i))
315 renamed = fl.renamed(fl.node(i))
313 rcache[fn][lr] = renamed
316 rcache[fn][lr] = renamed
314 if lr >= endrev:
317 if lr >= endrev:
315 break
318 break
316 if rev in rcache[fn]:
319 if rev in rcache[fn]:
317 return rcache[fn][rev]
320 return rcache[fn][rev]
318
321
319 # If linkrev != rev (i.e. rev not found in rcache) fallback to
322 # If linkrev != rev (i.e. rev not found in rcache) fallback to
320 # filectx logic.
323 # filectx logic.
321 try:
324 try:
322 return repo[rev][fn].renamed()
325 return repo[rev][fn].renamed()
323 except error.LookupError:
326 except error.LookupError:
324 return None
327 return None
325
328
326 return getrenamed
329 return getrenamed
327
330
328 def getlogcolumns():
331 def getlogcolumns():
329 """Return a dict of log column labels"""
332 """Return a dict of log column labels"""
330 _ = pycompat.identity # temporarily disable gettext
333 _ = pycompat.identity # temporarily disable gettext
331 # i18n: column positioning for "hg log"
334 # i18n: column positioning for "hg log"
332 columns = _('bookmark: %s\n'
335 columns = _('bookmark: %s\n'
333 'branch: %s\n'
336 'branch: %s\n'
334 'changeset: %s\n'
337 'changeset: %s\n'
335 'copies: %s\n'
338 'copies: %s\n'
336 'date: %s\n'
339 'date: %s\n'
337 'extra: %s=%s\n'
340 'extra: %s=%s\n'
338 'files+: %s\n'
341 'files+: %s\n'
339 'files-: %s\n'
342 'files-: %s\n'
340 'files: %s\n'
343 'files: %s\n'
341 'instability: %s\n'
344 'instability: %s\n'
342 'manifest: %s\n'
345 'manifest: %s\n'
343 'obsolete: %s\n'
346 'obsolete: %s\n'
344 'parent: %s\n'
347 'parent: %s\n'
345 'phase: %s\n'
348 'phase: %s\n'
346 'summary: %s\n'
349 'summary: %s\n'
347 'tag: %s\n'
350 'tag: %s\n'
348 'user: %s\n')
351 'user: %s\n')
349 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
352 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
350 i18n._(columns).splitlines(True)))
353 i18n._(columns).splitlines(True)))
351
354
352 # default templates internally used for rendering of lists
355 # default templates internally used for rendering of lists
353 defaulttempl = {
356 defaulttempl = {
354 'parent': '{rev}:{node|formatnode} ',
357 'parent': '{rev}:{node|formatnode} ',
355 'manifest': '{rev}:{node|formatnode}',
358 'manifest': '{rev}:{node|formatnode}',
356 'file_copy': '{name} ({source})',
359 'file_copy': '{name} ({source})',
357 'envvar': '{key}={value}',
360 'envvar': '{key}={value}',
358 'extra': '{key}={value|stringescape}'
361 'extra': '{key}={value|stringescape}'
359 }
362 }
360 # filecopy is preserved for compatibility reasons
363 # filecopy is preserved for compatibility reasons
361 defaulttempl['filecopy'] = defaulttempl['file_copy']
364 defaulttempl['filecopy'] = defaulttempl['file_copy']
362
365
363 # keywords are callables (see registrar.templatekeyword for details)
366 # keywords are callables (see registrar.templatekeyword for details)
364 keywords = {}
367 keywords = {}
365 templatekeyword = registrar.templatekeyword(keywords)
368 templatekeyword = registrar.templatekeyword(keywords)
366
369
367 @templatekeyword('author', requires={'ctx'})
370 @templatekeyword('author', requires={'ctx'})
368 def showauthor(context, mapping):
371 def showauthor(context, mapping):
369 """String. The unmodified author of the changeset."""
372 """String. The unmodified author of the changeset."""
370 ctx = context.resource(mapping, 'ctx')
373 ctx = context.resource(mapping, 'ctx')
371 return ctx.user()
374 return ctx.user()
372
375
373 @templatekeyword('bisect', requires={'repo', 'ctx'})
376 @templatekeyword('bisect', requires={'repo', 'ctx'})
374 def showbisect(context, mapping):
377 def showbisect(context, mapping):
375 """String. The changeset bisection status."""
378 """String. The changeset bisection status."""
376 repo = context.resource(mapping, 'repo')
379 repo = context.resource(mapping, 'repo')
377 ctx = context.resource(mapping, 'ctx')
380 ctx = context.resource(mapping, 'ctx')
378 return hbisect.label(repo, ctx.node())
381 return hbisect.label(repo, ctx.node())
379
382
380 @templatekeyword('branch', requires={'ctx'})
383 @templatekeyword('branch', requires={'ctx'})
381 def showbranch(context, mapping):
384 def showbranch(context, mapping):
382 """String. The name of the branch on which the changeset was
385 """String. The name of the branch on which the changeset was
383 committed.
386 committed.
384 """
387 """
385 ctx = context.resource(mapping, 'ctx')
388 ctx = context.resource(mapping, 'ctx')
386 return ctx.branch()
389 return ctx.branch()
387
390
388 @templatekeyword('branches', requires={'ctx', 'templ'})
391 @templatekeyword('branches', requires={'ctx', 'templ'})
389 def showbranches(context, mapping):
392 def showbranches(context, mapping):
390 """List of strings. The name of the branch on which the
393 """List of strings. The name of the branch on which the
391 changeset was committed. Will be empty if the branch name was
394 changeset was committed. Will be empty if the branch name was
392 default. (DEPRECATED)
395 default. (DEPRECATED)
393 """
396 """
394 ctx = context.resource(mapping, 'ctx')
397 ctx = context.resource(mapping, 'ctx')
395 branch = ctx.branch()
398 branch = ctx.branch()
396 if branch != 'default':
399 if branch != 'default':
397 return compatlist(context, mapping, 'branch', [branch],
400 return compatlist(context, mapping, 'branch', [branch],
398 plural='branches')
401 plural='branches')
399 return compatlist(context, mapping, 'branch', [], plural='branches')
402 return compatlist(context, mapping, 'branch', [], plural='branches')
400
403
401 @templatekeyword('bookmarks')
404 @templatekeyword('bookmarks')
402 def showbookmarks(**args):
405 def showbookmarks(**args):
403 """List of strings. Any bookmarks associated with the
406 """List of strings. Any bookmarks associated with the
404 changeset. Also sets 'active', the name of the active bookmark.
407 changeset. Also sets 'active', the name of the active bookmark.
405 """
408 """
406 args = pycompat.byteskwargs(args)
409 args = pycompat.byteskwargs(args)
407 repo = args['ctx']._repo
410 repo = args['ctx']._repo
408 bookmarks = args['ctx'].bookmarks()
411 bookmarks = args['ctx'].bookmarks()
409 active = repo._activebookmark
412 active = repo._activebookmark
410 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
413 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
411 f = _showlist('bookmark', bookmarks, args['templ'], args)
414 f = _showlist('bookmark', bookmarks, args['templ'], args)
412 return _hybrid(f, bookmarks, makemap, pycompat.identity)
415 return _hybrid(f, bookmarks, makemap, pycompat.identity)
413
416
414 @templatekeyword('children', requires={'ctx', 'templ'})
417 @templatekeyword('children', requires={'ctx', 'templ'})
415 def showchildren(context, mapping):
418 def showchildren(context, mapping):
416 """List of strings. The children of the changeset."""
419 """List of strings. The children of the changeset."""
417 ctx = context.resource(mapping, 'ctx')
420 ctx = context.resource(mapping, 'ctx')
418 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
421 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
419 return compatlist(context, mapping, 'children', childrevs, element='child')
422 return compatlist(context, mapping, 'children', childrevs, element='child')
420
423
421 # Deprecated, but kept alive for help generation a purpose.
424 # Deprecated, but kept alive for help generation a purpose.
422 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
425 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
423 def showcurrentbookmark(context, mapping):
426 def showcurrentbookmark(context, mapping):
424 """String. The active bookmark, if it is associated with the changeset.
427 """String. The active bookmark, if it is associated with the changeset.
425 (DEPRECATED)"""
428 (DEPRECATED)"""
426 return showactivebookmark(context, mapping)
429 return showactivebookmark(context, mapping)
427
430
428 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
431 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
429 def showactivebookmark(context, mapping):
432 def showactivebookmark(context, mapping):
430 """String. The active bookmark, if it is associated with the changeset."""
433 """String. The active bookmark, if it is associated with the changeset."""
431 repo = context.resource(mapping, 'repo')
434 repo = context.resource(mapping, 'repo')
432 ctx = context.resource(mapping, 'ctx')
435 ctx = context.resource(mapping, 'ctx')
433 active = repo._activebookmark
436 active = repo._activebookmark
434 if active and active in ctx.bookmarks():
437 if active and active in ctx.bookmarks():
435 return active
438 return active
436 return ''
439 return ''
437
440
438 @templatekeyword('date', requires={'ctx'})
441 @templatekeyword('date', requires={'ctx'})
439 def showdate(context, mapping):
442 def showdate(context, mapping):
440 """Date information. The date when the changeset was committed."""
443 """Date information. The date when the changeset was committed."""
441 ctx = context.resource(mapping, 'ctx')
444 ctx = context.resource(mapping, 'ctx')
442 return ctx.date()
445 return ctx.date()
443
446
444 @templatekeyword('desc', requires={'ctx'})
447 @templatekeyword('desc', requires={'ctx'})
445 def showdescription(context, mapping):
448 def showdescription(context, mapping):
446 """String. The text of the changeset description."""
449 """String. The text of the changeset description."""
447 ctx = context.resource(mapping, 'ctx')
450 ctx = context.resource(mapping, 'ctx')
448 s = ctx.description()
451 s = ctx.description()
449 if isinstance(s, encoding.localstr):
452 if isinstance(s, encoding.localstr):
450 # try hard to preserve utf-8 bytes
453 # try hard to preserve utf-8 bytes
451 return encoding.tolocal(encoding.fromlocal(s).strip())
454 return encoding.tolocal(encoding.fromlocal(s).strip())
452 else:
455 else:
453 return s.strip()
456 return s.strip()
454
457
455 @templatekeyword('diffstat', requires={'ctx'})
458 @templatekeyword('diffstat', requires={'ctx'})
456 def showdiffstat(context, mapping):
459 def showdiffstat(context, mapping):
457 """String. Statistics of changes with the following format:
460 """String. Statistics of changes with the following format:
458 "modified files: +added/-removed lines"
461 "modified files: +added/-removed lines"
459 """
462 """
460 ctx = context.resource(mapping, 'ctx')
463 ctx = context.resource(mapping, 'ctx')
461 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
464 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
462 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
465 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
463 return '%d: +%d/-%d' % (len(stats), adds, removes)
466 return '%d: +%d/-%d' % (len(stats), adds, removes)
464
467
465 @templatekeyword('envvars', requires={'ui', 'templ'})
468 @templatekeyword('envvars', requires={'ui', 'templ'})
466 def showenvvars(context, mapping):
469 def showenvvars(context, mapping):
467 """A dictionary of environment variables. (EXPERIMENTAL)"""
470 """A dictionary of environment variables. (EXPERIMENTAL)"""
468 ui = context.resource(mapping, 'ui')
471 ui = context.resource(mapping, 'ui')
469 env = ui.exportableenviron()
472 env = ui.exportableenviron()
470 env = util.sortdict((k, env[k]) for k in sorted(env))
473 env = util.sortdict((k, env[k]) for k in sorted(env))
471 return compatdict(context, mapping, 'envvar', env, plural='envvars')
474 return compatdict(context, mapping, 'envvar', env, plural='envvars')
472
475
473 @templatekeyword('extras')
476 @templatekeyword('extras')
474 def showextras(**args):
477 def showextras(**args):
475 """List of dicts with key, value entries of the 'extras'
478 """List of dicts with key, value entries of the 'extras'
476 field of this changeset."""
479 field of this changeset."""
477 args = pycompat.byteskwargs(args)
480 args = pycompat.byteskwargs(args)
478 extras = args['ctx'].extra()
481 extras = args['ctx'].extra()
479 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
482 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
480 makemap = lambda k: {'key': k, 'value': extras[k]}
483 makemap = lambda k: {'key': k, 'value': extras[k]}
481 c = [makemap(k) for k in extras]
484 c = [makemap(k) for k in extras]
482 f = _showlist('extra', c, args['templ'], args, plural='extras')
485 f = _showlist('extra', c, args['templ'], args, plural='extras')
483 return _hybrid(f, extras, makemap,
486 return _hybrid(f, extras, makemap,
484 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
487 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
485
488
486 def _showfilesbystat(context, mapping, name, index):
489 def _showfilesbystat(context, mapping, name, index):
487 repo = context.resource(mapping, 'repo')
490 repo = context.resource(mapping, 'repo')
488 ctx = context.resource(mapping, 'ctx')
491 ctx = context.resource(mapping, 'ctx')
489 revcache = context.resource(mapping, 'revcache')
492 revcache = context.resource(mapping, 'revcache')
490 if 'files' not in revcache:
493 if 'files' not in revcache:
491 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
494 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
492 files = revcache['files'][index]
495 files = revcache['files'][index]
493 return compatlist(context, mapping, name, files, element='file')
496 return compatlist(context, mapping, name, files, element='file')
494
497
495 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
498 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
496 def showfileadds(context, mapping):
499 def showfileadds(context, mapping):
497 """List of strings. Files added by this changeset."""
500 """List of strings. Files added by this changeset."""
498 return _showfilesbystat(context, mapping, 'file_add', 1)
501 return _showfilesbystat(context, mapping, 'file_add', 1)
499
502
500 @templatekeyword('file_copies',
503 @templatekeyword('file_copies',
501 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
504 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
502 def showfilecopies(context, mapping):
505 def showfilecopies(context, mapping):
503 """List of strings. Files copied in this changeset with
506 """List of strings. Files copied in this changeset with
504 their sources.
507 their sources.
505 """
508 """
506 repo = context.resource(mapping, 'repo')
509 repo = context.resource(mapping, 'repo')
507 ctx = context.resource(mapping, 'ctx')
510 ctx = context.resource(mapping, 'ctx')
508 cache = context.resource(mapping, 'cache')
511 cache = context.resource(mapping, 'cache')
509 copies = context.resource(mapping, 'revcache').get('copies')
512 copies = context.resource(mapping, 'revcache').get('copies')
510 if copies is None:
513 if copies is None:
511 if 'getrenamed' not in cache:
514 if 'getrenamed' not in cache:
512 cache['getrenamed'] = getrenamedfn(repo)
515 cache['getrenamed'] = getrenamedfn(repo)
513 copies = []
516 copies = []
514 getrenamed = cache['getrenamed']
517 getrenamed = cache['getrenamed']
515 for fn in ctx.files():
518 for fn in ctx.files():
516 rename = getrenamed(fn, ctx.rev())
519 rename = getrenamed(fn, ctx.rev())
517 if rename:
520 if rename:
518 copies.append((fn, rename[0]))
521 copies.append((fn, rename[0]))
519
522
520 copies = util.sortdict(copies)
523 copies = util.sortdict(copies)
521 return compatdict(context, mapping, 'file_copy', copies,
524 return compatdict(context, mapping, 'file_copy', copies,
522 key='name', value='source', fmt='%s (%s)',
525 key='name', value='source', fmt='%s (%s)',
523 plural='file_copies')
526 plural='file_copies')
524
527
525 # showfilecopiesswitch() displays file copies only if copy records are
528 # showfilecopiesswitch() displays file copies only if copy records are
526 # provided before calling the templater, usually with a --copies
529 # provided before calling the templater, usually with a --copies
527 # command line switch.
530 # command line switch.
528 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
531 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
529 def showfilecopiesswitch(context, mapping):
532 def showfilecopiesswitch(context, mapping):
530 """List of strings. Like "file_copies" but displayed
533 """List of strings. Like "file_copies" but displayed
531 only if the --copied switch is set.
534 only if the --copied switch is set.
532 """
535 """
533 copies = context.resource(mapping, 'revcache').get('copies') or []
536 copies = context.resource(mapping, 'revcache').get('copies') or []
534 copies = util.sortdict(copies)
537 copies = util.sortdict(copies)
535 return compatdict(context, mapping, 'file_copy', copies,
538 return compatdict(context, mapping, 'file_copy', copies,
536 key='name', value='source', fmt='%s (%s)',
539 key='name', value='source', fmt='%s (%s)',
537 plural='file_copies')
540 plural='file_copies')
538
541
539 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
542 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
540 def showfiledels(context, mapping):
543 def showfiledels(context, mapping):
541 """List of strings. Files removed by this changeset."""
544 """List of strings. Files removed by this changeset."""
542 return _showfilesbystat(context, mapping, 'file_del', 2)
545 return _showfilesbystat(context, mapping, 'file_del', 2)
543
546
544 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
547 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
545 def showfilemods(context, mapping):
548 def showfilemods(context, mapping):
546 """List of strings. Files modified by this changeset."""
549 """List of strings. Files modified by this changeset."""
547 return _showfilesbystat(context, mapping, 'file_mod', 0)
550 return _showfilesbystat(context, mapping, 'file_mod', 0)
548
551
549 @templatekeyword('files', requires={'ctx', 'templ'})
552 @templatekeyword('files', requires={'ctx', 'templ'})
550 def showfiles(context, mapping):
553 def showfiles(context, mapping):
551 """List of strings. All files modified, added, or removed by this
554 """List of strings. All files modified, added, or removed by this
552 changeset.
555 changeset.
553 """
556 """
554 ctx = context.resource(mapping, 'ctx')
557 ctx = context.resource(mapping, 'ctx')
555 return compatlist(context, mapping, 'file', ctx.files())
558 return compatlist(context, mapping, 'file', ctx.files())
556
559
557 @templatekeyword('graphnode', requires={'repo', 'ctx'})
560 @templatekeyword('graphnode', requires={'repo', 'ctx'})
558 def showgraphnode(context, mapping):
561 def showgraphnode(context, mapping):
559 """String. The character representing the changeset node in an ASCII
562 """String. The character representing the changeset node in an ASCII
560 revision graph."""
563 revision graph."""
561 repo = context.resource(mapping, 'repo')
564 repo = context.resource(mapping, 'repo')
562 ctx = context.resource(mapping, 'ctx')
565 ctx = context.resource(mapping, 'ctx')
563 return getgraphnode(repo, ctx)
566 return getgraphnode(repo, ctx)
564
567
565 def getgraphnode(repo, ctx):
568 def getgraphnode(repo, ctx):
566 wpnodes = repo.dirstate.parents()
569 wpnodes = repo.dirstate.parents()
567 if wpnodes[1] == nullid:
570 if wpnodes[1] == nullid:
568 wpnodes = wpnodes[:1]
571 wpnodes = wpnodes[:1]
569 if ctx.node() in wpnodes:
572 if ctx.node() in wpnodes:
570 return '@'
573 return '@'
571 elif ctx.obsolete():
574 elif ctx.obsolete():
572 return 'x'
575 return 'x'
573 elif ctx.isunstable():
576 elif ctx.isunstable():
574 return '*'
577 return '*'
575 elif ctx.closesbranch():
578 elif ctx.closesbranch():
576 return '_'
579 return '_'
577 else:
580 else:
578 return 'o'
581 return 'o'
579
582
580 @templatekeyword('graphwidth', requires=())
583 @templatekeyword('graphwidth', requires=())
581 def showgraphwidth(context, mapping):
584 def showgraphwidth(context, mapping):
582 """Integer. The width of the graph drawn by 'log --graph' or zero."""
585 """Integer. The width of the graph drawn by 'log --graph' or zero."""
583 # just hosts documentation; should be overridden by template mapping
586 # just hosts documentation; should be overridden by template mapping
584 return 0
587 return 0
585
588
586 @templatekeyword('index', requires=())
589 @templatekeyword('index', requires=())
587 def showindex(context, mapping):
590 def showindex(context, mapping):
588 """Integer. The current iteration of the loop. (0 indexed)"""
591 """Integer. The current iteration of the loop. (0 indexed)"""
589 # just hosts documentation; should be overridden by template mapping
592 # just hosts documentation; should be overridden by template mapping
590 raise error.Abort(_("can't use index in this context"))
593 raise error.Abort(_("can't use index in this context"))
591
594
592 @templatekeyword('latesttag')
595 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
593 def showlatesttag(**args):
596 def showlatesttag(context, mapping):
594 """List of strings. The global tags on the most recent globally
597 """List of strings. The global tags on the most recent globally
595 tagged ancestor of this changeset. If no such tags exist, the list
598 tagged ancestor of this changeset. If no such tags exist, the list
596 consists of the single string "null".
599 consists of the single string "null".
597 """
600 """
598 return showlatesttags(None, **args)
601 return showlatesttags(context, mapping, None)
599
602
600 def showlatesttags(pattern, **args):
603 def showlatesttags(context, mapping, pattern):
601 """helper method for the latesttag keyword and function"""
604 """helper method for the latesttag keyword and function"""
602 args = pycompat.byteskwargs(args)
605 latesttags = getlatesttags(context, mapping, pattern)
603 repo, ctx = args['repo'], args['ctx']
604 cache = args['cache']
605 latesttags = getlatesttags(repo, ctx, cache, pattern)
606
606
607 # latesttag[0] is an implementation detail for sorting csets on different
607 # latesttag[0] is an implementation detail for sorting csets on different
608 # branches in a stable manner- it is the date the tagged cset was created,
608 # branches in a stable manner- it is the date the tagged cset was created,
609 # not the date the tag was created. Therefore it isn't made visible here.
609 # not the date the tag was created. Therefore it isn't made visible here.
610 makemap = lambda v: {
610 makemap = lambda v: {
611 'changes': _showchangessincetag,
611 'changes': _showchangessincetag,
612 'distance': latesttags[1],
612 'distance': latesttags[1],
613 'latesttag': v, # BC with {latesttag % '{latesttag}'}
613 'latesttag': v, # BC with {latesttag % '{latesttag}'}
614 'tag': v
614 'tag': v
615 }
615 }
616
616
617 tags = latesttags[2]
617 tags = latesttags[2]
618 f = _showlist('latesttag', tags, args['templ'], args, separator=':')
618 templ = context.resource(mapping, 'templ')
619 f = _showlist('latesttag', tags, templ, mapping, separator=':')
619 return _hybrid(f, tags, makemap, pycompat.identity)
620 return _hybrid(f, tags, makemap, pycompat.identity)
620
621
621 @templatekeyword('latesttagdistance')
622 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
622 def showlatesttagdistance(repo, ctx, templ, cache, **args):
623 def showlatesttagdistance(context, mapping):
623 """Integer. Longest path to the latest tag."""
624 """Integer. Longest path to the latest tag."""
624 return getlatesttags(repo, ctx, cache)[1]
625 return getlatesttags(context, mapping)[1]
625
626
626 @templatekeyword('changessincelatesttag')
627 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
627 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
628 def showchangessincelatesttag(context, mapping):
628 """Integer. All ancestors not in the latest tag."""
629 """Integer. All ancestors not in the latest tag."""
629 latesttag = getlatesttags(repo, ctx, cache)[2][0]
630 mapping = mapping.copy()
631 mapping['tag'] = getlatesttags(context, mapping)[2][0]
632 return _showchangessincetag(context, mapping)
630
633
631 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
634 def _showchangessincetag(context, mapping):
632
635 repo = context.resource(mapping, 'repo')
633 def _showchangessincetag(repo, ctx, **args):
636 ctx = context.resource(mapping, 'ctx')
634 offset = 0
637 offset = 0
635 revs = [ctx.rev()]
638 revs = [ctx.rev()]
636 tag = args[r'tag']
639 tag = context.symbol(mapping, 'tag')
637
640
638 # The only() revset doesn't currently support wdir()
641 # The only() revset doesn't currently support wdir()
639 if ctx.rev() is None:
642 if ctx.rev() is None:
640 offset = 1
643 offset = 1
641 revs = [p.rev() for p in ctx.parents()]
644 revs = [p.rev() for p in ctx.parents()]
642
645
643 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
646 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
644
647
648 # teach templater latesttags.changes is switched to (context, mapping) API
649 _showchangessincetag._requires = {'repo', 'ctx'}
650
645 @templatekeyword('manifest')
651 @templatekeyword('manifest')
646 def showmanifest(**args):
652 def showmanifest(**args):
647 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
653 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
648 mnode = ctx.manifestnode()
654 mnode = ctx.manifestnode()
649 if mnode is None:
655 if mnode is None:
650 # just avoid crash, we might want to use the 'ff...' hash in future
656 # just avoid crash, we might want to use the 'ff...' hash in future
651 return
657 return
652 mrev = repo.manifestlog._revlog.rev(mnode)
658 mrev = repo.manifestlog._revlog.rev(mnode)
653 mhex = hex(mnode)
659 mhex = hex(mnode)
654 args = args.copy()
660 args = args.copy()
655 args.update({r'rev': mrev, r'node': mhex})
661 args.update({r'rev': mrev, r'node': mhex})
656 f = templ('manifest', **args)
662 f = templ('manifest', **args)
657 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
663 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
658 # rev and node are completely different from changeset's.
664 # rev and node are completely different from changeset's.
659 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
665 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
660
666
661 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
667 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
662 def showobsfate(context, mapping):
668 def showobsfate(context, mapping):
663 # this function returns a list containing pre-formatted obsfate strings.
669 # this function returns a list containing pre-formatted obsfate strings.
664 #
670 #
665 # This function will be replaced by templates fragments when we will have
671 # This function will be replaced by templates fragments when we will have
666 # the verbosity templatekw available.
672 # the verbosity templatekw available.
667 succsandmarkers = showsuccsandmarkers(context, mapping)
673 succsandmarkers = showsuccsandmarkers(context, mapping)
668
674
669 ui = context.resource(mapping, 'ui')
675 ui = context.resource(mapping, 'ui')
670 values = []
676 values = []
671
677
672 for x in succsandmarkers:
678 for x in succsandmarkers:
673 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
679 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
674
680
675 return compatlist(context, mapping, "fate", values)
681 return compatlist(context, mapping, "fate", values)
676
682
677 def shownames(context, mapping, namespace):
683 def shownames(context, mapping, namespace):
678 """helper method to generate a template keyword for a namespace"""
684 """helper method to generate a template keyword for a namespace"""
679 repo = context.resource(mapping, 'repo')
685 repo = context.resource(mapping, 'repo')
680 ctx = context.resource(mapping, 'ctx')
686 ctx = context.resource(mapping, 'ctx')
681 ns = repo.names[namespace]
687 ns = repo.names[namespace]
682 names = ns.names(repo, ctx.node())
688 names = ns.names(repo, ctx.node())
683 return compatlist(context, mapping, ns.templatename, names,
689 return compatlist(context, mapping, ns.templatename, names,
684 plural=namespace)
690 plural=namespace)
685
691
686 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
692 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
687 def shownamespaces(context, mapping):
693 def shownamespaces(context, mapping):
688 """Dict of lists. Names attached to this changeset per
694 """Dict of lists. Names attached to this changeset per
689 namespace."""
695 namespace."""
690 repo = context.resource(mapping, 'repo')
696 repo = context.resource(mapping, 'repo')
691 ctx = context.resource(mapping, 'ctx')
697 ctx = context.resource(mapping, 'ctx')
692 templ = context.resource(mapping, 'templ')
698 templ = context.resource(mapping, 'templ')
693
699
694 namespaces = util.sortdict()
700 namespaces = util.sortdict()
695 def makensmapfn(ns):
701 def makensmapfn(ns):
696 # 'name' for iterating over namespaces, templatename for local reference
702 # 'name' for iterating over namespaces, templatename for local reference
697 return lambda v: {'name': v, ns.templatename: v}
703 return lambda v: {'name': v, ns.templatename: v}
698
704
699 for k, ns in repo.names.iteritems():
705 for k, ns in repo.names.iteritems():
700 names = ns.names(repo, ctx.node())
706 names = ns.names(repo, ctx.node())
701 f = _showlist('name', names, templ, mapping)
707 f = _showlist('name', names, templ, mapping)
702 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
708 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
703
709
704 f = _showlist('namespace', list(namespaces), templ, mapping)
710 f = _showlist('namespace', list(namespaces), templ, mapping)
705
711
706 def makemap(ns):
712 def makemap(ns):
707 return {
713 return {
708 'namespace': ns,
714 'namespace': ns,
709 'names': namespaces[ns],
715 'names': namespaces[ns],
710 'builtin': repo.names[ns].builtin,
716 'builtin': repo.names[ns].builtin,
711 'colorname': repo.names[ns].colorname,
717 'colorname': repo.names[ns].colorname,
712 }
718 }
713
719
714 return _hybrid(f, namespaces, makemap, pycompat.identity)
720 return _hybrid(f, namespaces, makemap, pycompat.identity)
715
721
716 @templatekeyword('node', requires={'ctx'})
722 @templatekeyword('node', requires={'ctx'})
717 def shownode(context, mapping):
723 def shownode(context, mapping):
718 """String. The changeset identification hash, as a 40 hexadecimal
724 """String. The changeset identification hash, as a 40 hexadecimal
719 digit string.
725 digit string.
720 """
726 """
721 ctx = context.resource(mapping, 'ctx')
727 ctx = context.resource(mapping, 'ctx')
722 return ctx.hex()
728 return ctx.hex()
723
729
724 @templatekeyword('obsolete', requires={'ctx'})
730 @templatekeyword('obsolete', requires={'ctx'})
725 def showobsolete(context, mapping):
731 def showobsolete(context, mapping):
726 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
732 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
727 ctx = context.resource(mapping, 'ctx')
733 ctx = context.resource(mapping, 'ctx')
728 if ctx.obsolete():
734 if ctx.obsolete():
729 return 'obsolete'
735 return 'obsolete'
730 return ''
736 return ''
731
737
732 @templatekeyword('peerurls', requires={'repo'})
738 @templatekeyword('peerurls', requires={'repo'})
733 def showpeerurls(context, mapping):
739 def showpeerurls(context, mapping):
734 """A dictionary of repository locations defined in the [paths] section
740 """A dictionary of repository locations defined in the [paths] section
735 of your configuration file."""
741 of your configuration file."""
736 repo = context.resource(mapping, 'repo')
742 repo = context.resource(mapping, 'repo')
737 # see commands.paths() for naming of dictionary keys
743 # see commands.paths() for naming of dictionary keys
738 paths = repo.ui.paths
744 paths = repo.ui.paths
739 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
745 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
740 def makemap(k):
746 def makemap(k):
741 p = paths[k]
747 p = paths[k]
742 d = {'name': k, 'url': p.rawloc}
748 d = {'name': k, 'url': p.rawloc}
743 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
749 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
744 return d
750 return d
745 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
751 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
746
752
747 @templatekeyword("predecessors", requires={'repo', 'ctx'})
753 @templatekeyword("predecessors", requires={'repo', 'ctx'})
748 def showpredecessors(context, mapping):
754 def showpredecessors(context, mapping):
749 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
755 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
750 repo = context.resource(mapping, 'repo')
756 repo = context.resource(mapping, 'repo')
751 ctx = context.resource(mapping, 'ctx')
757 ctx = context.resource(mapping, 'ctx')
752 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
758 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
753 predecessors = map(hex, predecessors)
759 predecessors = map(hex, predecessors)
754
760
755 return _hybrid(None, predecessors,
761 return _hybrid(None, predecessors,
756 lambda x: {'ctx': repo[x], 'revcache': {}},
762 lambda x: {'ctx': repo[x], 'revcache': {}},
757 lambda x: scmutil.formatchangeid(repo[x]))
763 lambda x: scmutil.formatchangeid(repo[x]))
758
764
759 @templatekeyword('reporoot', requires={'repo'})
765 @templatekeyword('reporoot', requires={'repo'})
760 def showreporoot(context, mapping):
766 def showreporoot(context, mapping):
761 """String. The root directory of the current repository."""
767 """String. The root directory of the current repository."""
762 repo = context.resource(mapping, 'repo')
768 repo = context.resource(mapping, 'repo')
763 return repo.root
769 return repo.root
764
770
765 @templatekeyword("successorssets", requires={'repo', 'ctx'})
771 @templatekeyword("successorssets", requires={'repo', 'ctx'})
766 def showsuccessorssets(context, mapping):
772 def showsuccessorssets(context, mapping):
767 """Returns a string of sets of successors for a changectx. Format used
773 """Returns a string of sets of successors for a changectx. Format used
768 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
774 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
769 while also diverged into ctx3. (EXPERIMENTAL)"""
775 while also diverged into ctx3. (EXPERIMENTAL)"""
770 repo = context.resource(mapping, 'repo')
776 repo = context.resource(mapping, 'repo')
771 ctx = context.resource(mapping, 'ctx')
777 ctx = context.resource(mapping, 'ctx')
772 if not ctx.obsolete():
778 if not ctx.obsolete():
773 return ''
779 return ''
774
780
775 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
781 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
776 ssets = [[hex(n) for n in ss] for ss in ssets]
782 ssets = [[hex(n) for n in ss] for ss in ssets]
777
783
778 data = []
784 data = []
779 for ss in ssets:
785 for ss in ssets:
780 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
786 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
781 lambda x: scmutil.formatchangeid(repo[x]))
787 lambda x: scmutil.formatchangeid(repo[x]))
782 data.append(h)
788 data.append(h)
783
789
784 # Format the successorssets
790 # Format the successorssets
785 def render(d):
791 def render(d):
786 t = []
792 t = []
787 for i in d.gen():
793 for i in d.gen():
788 t.append(i)
794 t.append(i)
789 return "".join(t)
795 return "".join(t)
790
796
791 def gen(data):
797 def gen(data):
792 yield "; ".join(render(d) for d in data)
798 yield "; ".join(render(d) for d in data)
793
799
794 return _hybrid(gen(data), data, lambda x: {'successorset': x},
800 return _hybrid(gen(data), data, lambda x: {'successorset': x},
795 pycompat.identity)
801 pycompat.identity)
796
802
797 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
803 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
798 def showsuccsandmarkers(context, mapping):
804 def showsuccsandmarkers(context, mapping):
799 """Returns a list of dict for each final successor of ctx. The dict
805 """Returns a list of dict for each final successor of ctx. The dict
800 contains successors node id in "successors" keys and the list of
806 contains successors node id in "successors" keys and the list of
801 obs-markers from ctx to the set of successors in "markers".
807 obs-markers from ctx to the set of successors in "markers".
802 (EXPERIMENTAL)
808 (EXPERIMENTAL)
803 """
809 """
804 repo = context.resource(mapping, 'repo')
810 repo = context.resource(mapping, 'repo')
805 ctx = context.resource(mapping, 'ctx')
811 ctx = context.resource(mapping, 'ctx')
806 templ = context.resource(mapping, 'templ')
812 templ = context.resource(mapping, 'templ')
807
813
808 values = obsutil.successorsandmarkers(repo, ctx)
814 values = obsutil.successorsandmarkers(repo, ctx)
809
815
810 if values is None:
816 if values is None:
811 values = []
817 values = []
812
818
813 # Format successors and markers to avoid exposing binary to templates
819 # Format successors and markers to avoid exposing binary to templates
814 data = []
820 data = []
815 for i in values:
821 for i in values:
816 # Format successors
822 # Format successors
817 successors = i['successors']
823 successors = i['successors']
818
824
819 successors = [hex(n) for n in successors]
825 successors = [hex(n) for n in successors]
820 successors = _hybrid(None, successors,
826 successors = _hybrid(None, successors,
821 lambda x: {'ctx': repo[x], 'revcache': {}},
827 lambda x: {'ctx': repo[x], 'revcache': {}},
822 lambda x: scmutil.formatchangeid(repo[x]))
828 lambda x: scmutil.formatchangeid(repo[x]))
823
829
824 # Format markers
830 # Format markers
825 finalmarkers = []
831 finalmarkers = []
826 for m in i['markers']:
832 for m in i['markers']:
827 hexprec = hex(m[0])
833 hexprec = hex(m[0])
828 hexsucs = tuple(hex(n) for n in m[1])
834 hexsucs = tuple(hex(n) for n in m[1])
829 hexparents = None
835 hexparents = None
830 if m[5] is not None:
836 if m[5] is not None:
831 hexparents = tuple(hex(n) for n in m[5])
837 hexparents = tuple(hex(n) for n in m[5])
832 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
838 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
833 finalmarkers.append(newmarker)
839 finalmarkers.append(newmarker)
834
840
835 data.append({'successors': successors, 'markers': finalmarkers})
841 data.append({'successors': successors, 'markers': finalmarkers})
836
842
837 f = _showlist('succsandmarkers', data, templ, mapping)
843 f = _showlist('succsandmarkers', data, templ, mapping)
838 return _hybrid(f, data, lambda x: x, pycompat.identity)
844 return _hybrid(f, data, lambda x: x, pycompat.identity)
839
845
840 @templatekeyword('p1rev', requires={'ctx'})
846 @templatekeyword('p1rev', requires={'ctx'})
841 def showp1rev(context, mapping):
847 def showp1rev(context, mapping):
842 """Integer. The repository-local revision number of the changeset's
848 """Integer. The repository-local revision number of the changeset's
843 first parent, or -1 if the changeset has no parents."""
849 first parent, or -1 if the changeset has no parents."""
844 ctx = context.resource(mapping, 'ctx')
850 ctx = context.resource(mapping, 'ctx')
845 return ctx.p1().rev()
851 return ctx.p1().rev()
846
852
847 @templatekeyword('p2rev', requires={'ctx'})
853 @templatekeyword('p2rev', requires={'ctx'})
848 def showp2rev(context, mapping):
854 def showp2rev(context, mapping):
849 """Integer. The repository-local revision number of the changeset's
855 """Integer. The repository-local revision number of the changeset's
850 second parent, or -1 if the changeset has no second parent."""
856 second parent, or -1 if the changeset has no second parent."""
851 ctx = context.resource(mapping, 'ctx')
857 ctx = context.resource(mapping, 'ctx')
852 return ctx.p2().rev()
858 return ctx.p2().rev()
853
859
854 @templatekeyword('p1node', requires={'ctx'})
860 @templatekeyword('p1node', requires={'ctx'})
855 def showp1node(context, mapping):
861 def showp1node(context, mapping):
856 """String. The identification hash of the changeset's first parent,
862 """String. The identification hash of the changeset's first parent,
857 as a 40 digit hexadecimal string. If the changeset has no parents, all
863 as a 40 digit hexadecimal string. If the changeset has no parents, all
858 digits are 0."""
864 digits are 0."""
859 ctx = context.resource(mapping, 'ctx')
865 ctx = context.resource(mapping, 'ctx')
860 return ctx.p1().hex()
866 return ctx.p1().hex()
861
867
862 @templatekeyword('p2node', requires={'ctx'})
868 @templatekeyword('p2node', requires={'ctx'})
863 def showp2node(context, mapping):
869 def showp2node(context, mapping):
864 """String. The identification hash of the changeset's second
870 """String. The identification hash of the changeset's second
865 parent, as a 40 digit hexadecimal string. If the changeset has no second
871 parent, as a 40 digit hexadecimal string. If the changeset has no second
866 parent, all digits are 0."""
872 parent, all digits are 0."""
867 ctx = context.resource(mapping, 'ctx')
873 ctx = context.resource(mapping, 'ctx')
868 return ctx.p2().hex()
874 return ctx.p2().hex()
869
875
870 @templatekeyword('parents')
876 @templatekeyword('parents')
871 def showparents(**args):
877 def showparents(**args):
872 """List of strings. The parents of the changeset in "rev:node"
878 """List of strings. The parents of the changeset in "rev:node"
873 format. If the changeset has only one "natural" parent (the predecessor
879 format. If the changeset has only one "natural" parent (the predecessor
874 revision) nothing is shown."""
880 revision) nothing is shown."""
875 args = pycompat.byteskwargs(args)
881 args = pycompat.byteskwargs(args)
876 repo = args['repo']
882 repo = args['repo']
877 ctx = args['ctx']
883 ctx = args['ctx']
878 pctxs = scmutil.meaningfulparents(repo, ctx)
884 pctxs = scmutil.meaningfulparents(repo, ctx)
879 prevs = [p.rev() for p in pctxs]
885 prevs = [p.rev() for p in pctxs]
880 parents = [[('rev', p.rev()),
886 parents = [[('rev', p.rev()),
881 ('node', p.hex()),
887 ('node', p.hex()),
882 ('phase', p.phasestr())]
888 ('phase', p.phasestr())]
883 for p in pctxs]
889 for p in pctxs]
884 f = _showlist('parent', parents, args['templ'], args)
890 f = _showlist('parent', parents, args['templ'], args)
885 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
891 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
886 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
892 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
887
893
888 @templatekeyword('phase', requires={'ctx'})
894 @templatekeyword('phase', requires={'ctx'})
889 def showphase(context, mapping):
895 def showphase(context, mapping):
890 """String. The changeset phase name."""
896 """String. The changeset phase name."""
891 ctx = context.resource(mapping, 'ctx')
897 ctx = context.resource(mapping, 'ctx')
892 return ctx.phasestr()
898 return ctx.phasestr()
893
899
894 @templatekeyword('phaseidx', requires={'ctx'})
900 @templatekeyword('phaseidx', requires={'ctx'})
895 def showphaseidx(context, mapping):
901 def showphaseidx(context, mapping):
896 """Integer. The changeset phase index. (ADVANCED)"""
902 """Integer. The changeset phase index. (ADVANCED)"""
897 ctx = context.resource(mapping, 'ctx')
903 ctx = context.resource(mapping, 'ctx')
898 return ctx.phase()
904 return ctx.phase()
899
905
900 @templatekeyword('rev', requires={'ctx'})
906 @templatekeyword('rev', requires={'ctx'})
901 def showrev(context, mapping):
907 def showrev(context, mapping):
902 """Integer. The repository-local changeset revision number."""
908 """Integer. The repository-local changeset revision number."""
903 ctx = context.resource(mapping, 'ctx')
909 ctx = context.resource(mapping, 'ctx')
904 return scmutil.intrev(ctx)
910 return scmutil.intrev(ctx)
905
911
906 def showrevslist(context, mapping, name, revs):
912 def showrevslist(context, mapping, name, revs):
907 """helper to generate a list of revisions in which a mapped template will
913 """helper to generate a list of revisions in which a mapped template will
908 be evaluated"""
914 be evaluated"""
909 repo = context.resource(mapping, 'repo')
915 repo = context.resource(mapping, 'repo')
910 templ = context.resource(mapping, 'templ')
916 templ = context.resource(mapping, 'templ')
911 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
917 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
912 return _hybrid(f, revs,
918 return _hybrid(f, revs,
913 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
919 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
914 pycompat.identity, keytype=int)
920 pycompat.identity, keytype=int)
915
921
916 @templatekeyword('subrepos', requires={'ctx', 'templ'})
922 @templatekeyword('subrepos', requires={'ctx', 'templ'})
917 def showsubrepos(context, mapping):
923 def showsubrepos(context, mapping):
918 """List of strings. Updated subrepositories in the changeset."""
924 """List of strings. Updated subrepositories in the changeset."""
919 ctx = context.resource(mapping, 'ctx')
925 ctx = context.resource(mapping, 'ctx')
920 substate = ctx.substate
926 substate = ctx.substate
921 if not substate:
927 if not substate:
922 return compatlist(context, mapping, 'subrepo', [])
928 return compatlist(context, mapping, 'subrepo', [])
923 psubstate = ctx.parents()[0].substate or {}
929 psubstate = ctx.parents()[0].substate or {}
924 subrepos = []
930 subrepos = []
925 for sub in substate:
931 for sub in substate:
926 if sub not in psubstate or substate[sub] != psubstate[sub]:
932 if sub not in psubstate or substate[sub] != psubstate[sub]:
927 subrepos.append(sub) # modified or newly added in ctx
933 subrepos.append(sub) # modified or newly added in ctx
928 for sub in psubstate:
934 for sub in psubstate:
929 if sub not in substate:
935 if sub not in substate:
930 subrepos.append(sub) # removed in ctx
936 subrepos.append(sub) # removed in ctx
931 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
937 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
932
938
933 # don't remove "showtags" definition, even though namespaces will put
939 # don't remove "showtags" definition, even though namespaces will put
934 # a helper function for "tags" keyword into "keywords" map automatically,
940 # a helper function for "tags" keyword into "keywords" map automatically,
935 # because online help text is built without namespaces initialization
941 # because online help text is built without namespaces initialization
936 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
942 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
937 def showtags(context, mapping):
943 def showtags(context, mapping):
938 """List of strings. Any tags associated with the changeset."""
944 """List of strings. Any tags associated with the changeset."""
939 return shownames(context, mapping, 'tags')
945 return shownames(context, mapping, 'tags')
940
946
941 @templatekeyword('termwidth', requires={'ui'})
947 @templatekeyword('termwidth', requires={'ui'})
942 def showtermwidth(context, mapping):
948 def showtermwidth(context, mapping):
943 """Integer. The width of the current terminal."""
949 """Integer. The width of the current terminal."""
944 ui = context.resource(mapping, 'ui')
950 ui = context.resource(mapping, 'ui')
945 return ui.termwidth()
951 return ui.termwidth()
946
952
947 @templatekeyword('instabilities', requires={'ctx', 'templ'})
953 @templatekeyword('instabilities', requires={'ctx', 'templ'})
948 def showinstabilities(context, mapping):
954 def showinstabilities(context, mapping):
949 """List of strings. Evolution instabilities affecting the changeset.
955 """List of strings. Evolution instabilities affecting the changeset.
950 (EXPERIMENTAL)
956 (EXPERIMENTAL)
951 """
957 """
952 ctx = context.resource(mapping, 'ctx')
958 ctx = context.resource(mapping, 'ctx')
953 return compatlist(context, mapping, 'instability', ctx.instabilities(),
959 return compatlist(context, mapping, 'instability', ctx.instabilities(),
954 plural='instabilities')
960 plural='instabilities')
955
961
956 @templatekeyword('verbosity', requires={'ui'})
962 @templatekeyword('verbosity', requires={'ui'})
957 def showverbosity(context, mapping):
963 def showverbosity(context, mapping):
958 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
964 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
959 or ''."""
965 or ''."""
960 ui = context.resource(mapping, 'ui')
966 ui = context.resource(mapping, 'ui')
961 # see logcmdutil.changesettemplater for priority of these flags
967 # see logcmdutil.changesettemplater for priority of these flags
962 if ui.debugflag:
968 if ui.debugflag:
963 return 'debug'
969 return 'debug'
964 elif ui.quiet:
970 elif ui.quiet:
965 return 'quiet'
971 return 'quiet'
966 elif ui.verbose:
972 elif ui.verbose:
967 return 'verbose'
973 return 'verbose'
968 return ''
974 return ''
969
975
970 def loadkeyword(ui, extname, registrarobj):
976 def loadkeyword(ui, extname, registrarobj):
971 """Load template keyword from specified registrarobj
977 """Load template keyword from specified registrarobj
972 """
978 """
973 for name, func in registrarobj._table.iteritems():
979 for name, func in registrarobj._table.iteritems():
974 keywords[name] = func
980 keywords[name] = func
975
981
976 # tell hggettext to extract docstrings from these functions:
982 # tell hggettext to extract docstrings from these functions:
977 i18nfunctions = keywords.values()
983 i18nfunctions = keywords.values()
@@ -1,1628 +1,1624
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11 import re
11 import re
12 import types
12 import types
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 color,
16 color,
17 config,
17 config,
18 encoding,
18 encoding,
19 error,
19 error,
20 minirst,
20 minirst,
21 obsutil,
21 obsutil,
22 parser,
22 parser,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 revset as revsetmod,
25 revset as revsetmod,
26 revsetlang,
26 revsetlang,
27 scmutil,
27 scmutil,
28 templatefilters,
28 templatefilters,
29 templatekw,
29 templatekw,
30 util,
30 util,
31 )
31 )
32
32
33 class ResourceUnavailable(error.Abort):
33 class ResourceUnavailable(error.Abort):
34 pass
34 pass
35
35
36 class TemplateNotFound(error.Abort):
36 class TemplateNotFound(error.Abort):
37 pass
37 pass
38
38
39 # template parsing
39 # template parsing
40
40
41 elements = {
41 elements = {
42 # token-type: binding-strength, primary, prefix, infix, suffix
42 # token-type: binding-strength, primary, prefix, infix, suffix
43 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
43 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
44 ".": (18, None, None, (".", 18), None),
44 ".": (18, None, None, (".", 18), None),
45 "%": (15, None, None, ("%", 15), None),
45 "%": (15, None, None, ("%", 15), None),
46 "|": (15, None, None, ("|", 15), None),
46 "|": (15, None, None, ("|", 15), None),
47 "*": (5, None, None, ("*", 5), None),
47 "*": (5, None, None, ("*", 5), None),
48 "/": (5, None, None, ("/", 5), None),
48 "/": (5, None, None, ("/", 5), None),
49 "+": (4, None, None, ("+", 4), None),
49 "+": (4, None, None, ("+", 4), None),
50 "-": (4, None, ("negate", 19), ("-", 4), None),
50 "-": (4, None, ("negate", 19), ("-", 4), None),
51 "=": (3, None, None, ("keyvalue", 3), None),
51 "=": (3, None, None, ("keyvalue", 3), None),
52 ",": (2, None, None, ("list", 2), None),
52 ",": (2, None, None, ("list", 2), None),
53 ")": (0, None, None, None, None),
53 ")": (0, None, None, None, None),
54 "integer": (0, "integer", None, None, None),
54 "integer": (0, "integer", None, None, None),
55 "symbol": (0, "symbol", None, None, None),
55 "symbol": (0, "symbol", None, None, None),
56 "string": (0, "string", None, None, None),
56 "string": (0, "string", None, None, None),
57 "template": (0, "template", None, None, None),
57 "template": (0, "template", None, None, None),
58 "end": (0, None, None, None, None),
58 "end": (0, None, None, None, None),
59 }
59 }
60
60
61 def tokenize(program, start, end, term=None):
61 def tokenize(program, start, end, term=None):
62 """Parse a template expression into a stream of tokens, which must end
62 """Parse a template expression into a stream of tokens, which must end
63 with term if specified"""
63 with term if specified"""
64 pos = start
64 pos = start
65 program = pycompat.bytestr(program)
65 program = pycompat.bytestr(program)
66 while pos < end:
66 while pos < end:
67 c = program[pos]
67 c = program[pos]
68 if c.isspace(): # skip inter-token whitespace
68 if c.isspace(): # skip inter-token whitespace
69 pass
69 pass
70 elif c in "(=,).%|+-*/": # handle simple operators
70 elif c in "(=,).%|+-*/": # handle simple operators
71 yield (c, None, pos)
71 yield (c, None, pos)
72 elif c in '"\'': # handle quoted templates
72 elif c in '"\'': # handle quoted templates
73 s = pos + 1
73 s = pos + 1
74 data, pos = _parsetemplate(program, s, end, c)
74 data, pos = _parsetemplate(program, s, end, c)
75 yield ('template', data, s)
75 yield ('template', data, s)
76 pos -= 1
76 pos -= 1
77 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
77 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
78 # handle quoted strings
78 # handle quoted strings
79 c = program[pos + 1]
79 c = program[pos + 1]
80 s = pos = pos + 2
80 s = pos = pos + 2
81 while pos < end: # find closing quote
81 while pos < end: # find closing quote
82 d = program[pos]
82 d = program[pos]
83 if d == '\\': # skip over escaped characters
83 if d == '\\': # skip over escaped characters
84 pos += 2
84 pos += 2
85 continue
85 continue
86 if d == c:
86 if d == c:
87 yield ('string', program[s:pos], s)
87 yield ('string', program[s:pos], s)
88 break
88 break
89 pos += 1
89 pos += 1
90 else:
90 else:
91 raise error.ParseError(_("unterminated string"), s)
91 raise error.ParseError(_("unterminated string"), s)
92 elif c.isdigit():
92 elif c.isdigit():
93 s = pos
93 s = pos
94 while pos < end:
94 while pos < end:
95 d = program[pos]
95 d = program[pos]
96 if not d.isdigit():
96 if not d.isdigit():
97 break
97 break
98 pos += 1
98 pos += 1
99 yield ('integer', program[s:pos], s)
99 yield ('integer', program[s:pos], s)
100 pos -= 1
100 pos -= 1
101 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
101 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
102 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
102 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
103 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
103 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
104 # where some of nested templates were preprocessed as strings and
104 # where some of nested templates were preprocessed as strings and
105 # then compiled. therefore, \"...\" was allowed. (issue4733)
105 # then compiled. therefore, \"...\" was allowed. (issue4733)
106 #
106 #
107 # processing flow of _evalifliteral() at 5ab28a2e9962:
107 # processing flow of _evalifliteral() at 5ab28a2e9962:
108 # outer template string -> stringify() -> compiletemplate()
108 # outer template string -> stringify() -> compiletemplate()
109 # ------------------------ ------------ ------------------
109 # ------------------------ ------------ ------------------
110 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
110 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
111 # ~~~~~~~~
111 # ~~~~~~~~
112 # escaped quoted string
112 # escaped quoted string
113 if c == 'r':
113 if c == 'r':
114 pos += 1
114 pos += 1
115 token = 'string'
115 token = 'string'
116 else:
116 else:
117 token = 'template'
117 token = 'template'
118 quote = program[pos:pos + 2]
118 quote = program[pos:pos + 2]
119 s = pos = pos + 2
119 s = pos = pos + 2
120 while pos < end: # find closing escaped quote
120 while pos < end: # find closing escaped quote
121 if program.startswith('\\\\\\', pos, end):
121 if program.startswith('\\\\\\', pos, end):
122 pos += 4 # skip over double escaped characters
122 pos += 4 # skip over double escaped characters
123 continue
123 continue
124 if program.startswith(quote, pos, end):
124 if program.startswith(quote, pos, end):
125 # interpret as if it were a part of an outer string
125 # interpret as if it were a part of an outer string
126 data = parser.unescapestr(program[s:pos])
126 data = parser.unescapestr(program[s:pos])
127 if token == 'template':
127 if token == 'template':
128 data = _parsetemplate(data, 0, len(data))[0]
128 data = _parsetemplate(data, 0, len(data))[0]
129 yield (token, data, s)
129 yield (token, data, s)
130 pos += 1
130 pos += 1
131 break
131 break
132 pos += 1
132 pos += 1
133 else:
133 else:
134 raise error.ParseError(_("unterminated string"), s)
134 raise error.ParseError(_("unterminated string"), s)
135 elif c.isalnum() or c in '_':
135 elif c.isalnum() or c in '_':
136 s = pos
136 s = pos
137 pos += 1
137 pos += 1
138 while pos < end: # find end of symbol
138 while pos < end: # find end of symbol
139 d = program[pos]
139 d = program[pos]
140 if not (d.isalnum() or d == "_"):
140 if not (d.isalnum() or d == "_"):
141 break
141 break
142 pos += 1
142 pos += 1
143 sym = program[s:pos]
143 sym = program[s:pos]
144 yield ('symbol', sym, s)
144 yield ('symbol', sym, s)
145 pos -= 1
145 pos -= 1
146 elif c == term:
146 elif c == term:
147 yield ('end', None, pos + 1)
147 yield ('end', None, pos + 1)
148 return
148 return
149 else:
149 else:
150 raise error.ParseError(_("syntax error"), pos)
150 raise error.ParseError(_("syntax error"), pos)
151 pos += 1
151 pos += 1
152 if term:
152 if term:
153 raise error.ParseError(_("unterminated template expansion"), start)
153 raise error.ParseError(_("unterminated template expansion"), start)
154 yield ('end', None, pos)
154 yield ('end', None, pos)
155
155
156 def _parsetemplate(tmpl, start, stop, quote=''):
156 def _parsetemplate(tmpl, start, stop, quote=''):
157 r"""
157 r"""
158 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
158 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
159 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
159 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
160 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
160 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
161 ([('string', 'foo'), ('symbol', 'bar')], 9)
161 ([('string', 'foo'), ('symbol', 'bar')], 9)
162 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
162 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
163 ([('string', 'foo')], 4)
163 ([('string', 'foo')], 4)
164 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
164 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
165 ([('string', 'foo"'), ('string', 'bar')], 9)
165 ([('string', 'foo"'), ('string', 'bar')], 9)
166 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
166 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
167 ([('string', 'foo\\')], 6)
167 ([('string', 'foo\\')], 6)
168 """
168 """
169 parsed = []
169 parsed = []
170 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
170 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
171 if typ == 'string':
171 if typ == 'string':
172 parsed.append((typ, val))
172 parsed.append((typ, val))
173 elif typ == 'template':
173 elif typ == 'template':
174 parsed.append(val)
174 parsed.append(val)
175 elif typ == 'end':
175 elif typ == 'end':
176 return parsed, pos
176 return parsed, pos
177 else:
177 else:
178 raise error.ProgrammingError('unexpected type: %s' % typ)
178 raise error.ProgrammingError('unexpected type: %s' % typ)
179 raise error.ProgrammingError('unterminated scanning of template')
179 raise error.ProgrammingError('unterminated scanning of template')
180
180
181 def scantemplate(tmpl, raw=False):
181 def scantemplate(tmpl, raw=False):
182 r"""Scan (type, start, end) positions of outermost elements in template
182 r"""Scan (type, start, end) positions of outermost elements in template
183
183
184 If raw=True, a backslash is not taken as an escape character just like
184 If raw=True, a backslash is not taken as an escape character just like
185 r'' string in Python. Note that this is different from r'' literal in
185 r'' string in Python. Note that this is different from r'' literal in
186 template in that no template fragment can appear in r'', e.g. r'{foo}'
186 template in that no template fragment can appear in r'', e.g. r'{foo}'
187 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
187 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
188 'foo'.
188 'foo'.
189
189
190 >>> list(scantemplate(b'foo{bar}"baz'))
190 >>> list(scantemplate(b'foo{bar}"baz'))
191 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
191 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
192 >>> list(scantemplate(b'outer{"inner"}outer'))
192 >>> list(scantemplate(b'outer{"inner"}outer'))
193 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
193 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
194 >>> list(scantemplate(b'foo\\{escaped}'))
194 >>> list(scantemplate(b'foo\\{escaped}'))
195 [('string', 0, 5), ('string', 5, 13)]
195 [('string', 0, 5), ('string', 5, 13)]
196 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
196 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
197 [('string', 0, 4), ('template', 4, 13)]
197 [('string', 0, 4), ('template', 4, 13)]
198 """
198 """
199 last = None
199 last = None
200 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
200 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
201 if last:
201 if last:
202 yield last + (pos,)
202 yield last + (pos,)
203 if typ == 'end':
203 if typ == 'end':
204 return
204 return
205 else:
205 else:
206 last = (typ, pos)
206 last = (typ, pos)
207 raise error.ProgrammingError('unterminated scanning of template')
207 raise error.ProgrammingError('unterminated scanning of template')
208
208
209 def _scantemplate(tmpl, start, stop, quote='', raw=False):
209 def _scantemplate(tmpl, start, stop, quote='', raw=False):
210 """Parse template string into chunks of strings and template expressions"""
210 """Parse template string into chunks of strings and template expressions"""
211 sepchars = '{' + quote
211 sepchars = '{' + quote
212 unescape = [parser.unescapestr, pycompat.identity][raw]
212 unescape = [parser.unescapestr, pycompat.identity][raw]
213 pos = start
213 pos = start
214 p = parser.parser(elements)
214 p = parser.parser(elements)
215 while pos < stop:
215 while pos < stop:
216 n = min((tmpl.find(c, pos, stop) for c in sepchars),
216 n = min((tmpl.find(c, pos, stop) for c in sepchars),
217 key=lambda n: (n < 0, n))
217 key=lambda n: (n < 0, n))
218 if n < 0:
218 if n < 0:
219 yield ('string', unescape(tmpl[pos:stop]), pos)
219 yield ('string', unescape(tmpl[pos:stop]), pos)
220 pos = stop
220 pos = stop
221 break
221 break
222 c = tmpl[n:n + 1]
222 c = tmpl[n:n + 1]
223 bs = 0 # count leading backslashes
223 bs = 0 # count leading backslashes
224 if not raw:
224 if not raw:
225 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
225 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
226 if bs % 2 == 1:
226 if bs % 2 == 1:
227 # escaped (e.g. '\{', '\\\{', but not '\\{')
227 # escaped (e.g. '\{', '\\\{', but not '\\{')
228 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
228 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
229 pos = n + 1
229 pos = n + 1
230 continue
230 continue
231 if n > pos:
231 if n > pos:
232 yield ('string', unescape(tmpl[pos:n]), pos)
232 yield ('string', unescape(tmpl[pos:n]), pos)
233 if c == quote:
233 if c == quote:
234 yield ('end', None, n + 1)
234 yield ('end', None, n + 1)
235 return
235 return
236
236
237 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
237 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
238 if not tmpl.endswith('}', n + 1, pos):
238 if not tmpl.endswith('}', n + 1, pos):
239 raise error.ParseError(_("invalid token"), pos)
239 raise error.ParseError(_("invalid token"), pos)
240 yield ('template', parseres, n)
240 yield ('template', parseres, n)
241
241
242 if quote:
242 if quote:
243 raise error.ParseError(_("unterminated string"), start)
243 raise error.ParseError(_("unterminated string"), start)
244 yield ('end', None, pos)
244 yield ('end', None, pos)
245
245
246 def _unnesttemplatelist(tree):
246 def _unnesttemplatelist(tree):
247 """Expand list of templates to node tuple
247 """Expand list of templates to node tuple
248
248
249 >>> def f(tree):
249 >>> def f(tree):
250 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
250 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
251 >>> f((b'template', []))
251 >>> f((b'template', []))
252 (string '')
252 (string '')
253 >>> f((b'template', [(b'string', b'foo')]))
253 >>> f((b'template', [(b'string', b'foo')]))
254 (string 'foo')
254 (string 'foo')
255 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
255 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
256 (template
256 (template
257 (string 'foo')
257 (string 'foo')
258 (symbol 'rev'))
258 (symbol 'rev'))
259 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
259 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
260 (template
260 (template
261 (symbol 'rev'))
261 (symbol 'rev'))
262 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
262 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
263 (string 'foo')
263 (string 'foo')
264 """
264 """
265 if not isinstance(tree, tuple):
265 if not isinstance(tree, tuple):
266 return tree
266 return tree
267 op = tree[0]
267 op = tree[0]
268 if op != 'template':
268 if op != 'template':
269 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
269 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
270
270
271 assert len(tree) == 2
271 assert len(tree) == 2
272 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
272 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
273 if not xs:
273 if not xs:
274 return ('string', '') # empty template ""
274 return ('string', '') # empty template ""
275 elif len(xs) == 1 and xs[0][0] == 'string':
275 elif len(xs) == 1 and xs[0][0] == 'string':
276 return xs[0] # fast path for string with no template fragment "x"
276 return xs[0] # fast path for string with no template fragment "x"
277 else:
277 else:
278 return (op,) + xs
278 return (op,) + xs
279
279
280 def parse(tmpl):
280 def parse(tmpl):
281 """Parse template string into tree"""
281 """Parse template string into tree"""
282 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
282 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
283 assert pos == len(tmpl), 'unquoted template should be consumed'
283 assert pos == len(tmpl), 'unquoted template should be consumed'
284 return _unnesttemplatelist(('template', parsed))
284 return _unnesttemplatelist(('template', parsed))
285
285
286 def _parseexpr(expr):
286 def _parseexpr(expr):
287 """Parse a template expression into tree
287 """Parse a template expression into tree
288
288
289 >>> _parseexpr(b'"foo"')
289 >>> _parseexpr(b'"foo"')
290 ('string', 'foo')
290 ('string', 'foo')
291 >>> _parseexpr(b'foo(bar)')
291 >>> _parseexpr(b'foo(bar)')
292 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
292 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
293 >>> _parseexpr(b'foo(')
293 >>> _parseexpr(b'foo(')
294 Traceback (most recent call last):
294 Traceback (most recent call last):
295 ...
295 ...
296 ParseError: ('not a prefix: end', 4)
296 ParseError: ('not a prefix: end', 4)
297 >>> _parseexpr(b'"foo" "bar"')
297 >>> _parseexpr(b'"foo" "bar"')
298 Traceback (most recent call last):
298 Traceback (most recent call last):
299 ...
299 ...
300 ParseError: ('invalid token', 7)
300 ParseError: ('invalid token', 7)
301 """
301 """
302 p = parser.parser(elements)
302 p = parser.parser(elements)
303 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
303 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
304 if pos != len(expr):
304 if pos != len(expr):
305 raise error.ParseError(_('invalid token'), pos)
305 raise error.ParseError(_('invalid token'), pos)
306 return _unnesttemplatelist(tree)
306 return _unnesttemplatelist(tree)
307
307
308 def prettyformat(tree):
308 def prettyformat(tree):
309 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
309 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
310
310
311 def compileexp(exp, context, curmethods):
311 def compileexp(exp, context, curmethods):
312 """Compile parsed template tree to (func, data) pair"""
312 """Compile parsed template tree to (func, data) pair"""
313 if not exp:
313 if not exp:
314 raise error.ParseError(_("missing argument"))
314 raise error.ParseError(_("missing argument"))
315 t = exp[0]
315 t = exp[0]
316 if t in curmethods:
316 if t in curmethods:
317 return curmethods[t](exp, context)
317 return curmethods[t](exp, context)
318 raise error.ParseError(_("unknown method '%s'") % t)
318 raise error.ParseError(_("unknown method '%s'") % t)
319
319
320 # template evaluation
320 # template evaluation
321
321
322 def getsymbol(exp):
322 def getsymbol(exp):
323 if exp[0] == 'symbol':
323 if exp[0] == 'symbol':
324 return exp[1]
324 return exp[1]
325 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
325 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
326
326
327 def getlist(x):
327 def getlist(x):
328 if not x:
328 if not x:
329 return []
329 return []
330 if x[0] == 'list':
330 if x[0] == 'list':
331 return getlist(x[1]) + [x[2]]
331 return getlist(x[1]) + [x[2]]
332 return [x]
332 return [x]
333
333
334 def gettemplate(exp, context):
334 def gettemplate(exp, context):
335 """Compile given template tree or load named template from map file;
335 """Compile given template tree or load named template from map file;
336 returns (func, data) pair"""
336 returns (func, data) pair"""
337 if exp[0] in ('template', 'string'):
337 if exp[0] in ('template', 'string'):
338 return compileexp(exp, context, methods)
338 return compileexp(exp, context, methods)
339 if exp[0] == 'symbol':
339 if exp[0] == 'symbol':
340 # unlike runsymbol(), here 'symbol' is always taken as template name
340 # unlike runsymbol(), here 'symbol' is always taken as template name
341 # even if it exists in mapping. this allows us to override mapping
341 # even if it exists in mapping. this allows us to override mapping
342 # by web templates, e.g. 'changelogtag' is redefined in map file.
342 # by web templates, e.g. 'changelogtag' is redefined in map file.
343 return context._load(exp[1])
343 return context._load(exp[1])
344 raise error.ParseError(_("expected template specifier"))
344 raise error.ParseError(_("expected template specifier"))
345
345
346 def findsymbolicname(arg):
346 def findsymbolicname(arg):
347 """Find symbolic name for the given compiled expression; returns None
347 """Find symbolic name for the given compiled expression; returns None
348 if nothing found reliably"""
348 if nothing found reliably"""
349 while True:
349 while True:
350 func, data = arg
350 func, data = arg
351 if func is runsymbol:
351 if func is runsymbol:
352 return data
352 return data
353 elif func is runfilter:
353 elif func is runfilter:
354 arg = data[0]
354 arg = data[0]
355 else:
355 else:
356 return None
356 return None
357
357
358 def evalrawexp(context, mapping, arg):
358 def evalrawexp(context, mapping, arg):
359 """Evaluate given argument as a bare template object which may require
359 """Evaluate given argument as a bare template object which may require
360 further processing (such as folding generator of strings)"""
360 further processing (such as folding generator of strings)"""
361 func, data = arg
361 func, data = arg
362 return func(context, mapping, data)
362 return func(context, mapping, data)
363
363
364 def evalfuncarg(context, mapping, arg):
364 def evalfuncarg(context, mapping, arg):
365 """Evaluate given argument as value type"""
365 """Evaluate given argument as value type"""
366 thing = evalrawexp(context, mapping, arg)
366 thing = evalrawexp(context, mapping, arg)
367 thing = templatekw.unwrapvalue(thing)
367 thing = templatekw.unwrapvalue(thing)
368 # evalrawexp() may return string, generator of strings or arbitrary object
368 # evalrawexp() may return string, generator of strings or arbitrary object
369 # such as date tuple, but filter does not want generator.
369 # such as date tuple, but filter does not want generator.
370 if isinstance(thing, types.GeneratorType):
370 if isinstance(thing, types.GeneratorType):
371 thing = stringify(thing)
371 thing = stringify(thing)
372 return thing
372 return thing
373
373
374 def evalboolean(context, mapping, arg):
374 def evalboolean(context, mapping, arg):
375 """Evaluate given argument as boolean, but also takes boolean literals"""
375 """Evaluate given argument as boolean, but also takes boolean literals"""
376 func, data = arg
376 func, data = arg
377 if func is runsymbol:
377 if func is runsymbol:
378 thing = func(context, mapping, data, default=None)
378 thing = func(context, mapping, data, default=None)
379 if thing is None:
379 if thing is None:
380 # not a template keyword, takes as a boolean literal
380 # not a template keyword, takes as a boolean literal
381 thing = util.parsebool(data)
381 thing = util.parsebool(data)
382 else:
382 else:
383 thing = func(context, mapping, data)
383 thing = func(context, mapping, data)
384 thing = templatekw.unwrapvalue(thing)
384 thing = templatekw.unwrapvalue(thing)
385 if isinstance(thing, bool):
385 if isinstance(thing, bool):
386 return thing
386 return thing
387 # other objects are evaluated as strings, which means 0 is True, but
387 # other objects are evaluated as strings, which means 0 is True, but
388 # empty dict/list should be False as they are expected to be ''
388 # empty dict/list should be False as they are expected to be ''
389 return bool(stringify(thing))
389 return bool(stringify(thing))
390
390
391 def evalinteger(context, mapping, arg, err=None):
391 def evalinteger(context, mapping, arg, err=None):
392 v = evalfuncarg(context, mapping, arg)
392 v = evalfuncarg(context, mapping, arg)
393 try:
393 try:
394 return int(v)
394 return int(v)
395 except (TypeError, ValueError):
395 except (TypeError, ValueError):
396 raise error.ParseError(err or _('not an integer'))
396 raise error.ParseError(err or _('not an integer'))
397
397
398 def evalstring(context, mapping, arg):
398 def evalstring(context, mapping, arg):
399 return stringify(evalrawexp(context, mapping, arg))
399 return stringify(evalrawexp(context, mapping, arg))
400
400
401 def evalstringliteral(context, mapping, arg):
401 def evalstringliteral(context, mapping, arg):
402 """Evaluate given argument as string template, but returns symbol name
402 """Evaluate given argument as string template, but returns symbol name
403 if it is unknown"""
403 if it is unknown"""
404 func, data = arg
404 func, data = arg
405 if func is runsymbol:
405 if func is runsymbol:
406 thing = func(context, mapping, data, default=data)
406 thing = func(context, mapping, data, default=data)
407 else:
407 else:
408 thing = func(context, mapping, data)
408 thing = func(context, mapping, data)
409 return stringify(thing)
409 return stringify(thing)
410
410
411 _evalfuncbytype = {
411 _evalfuncbytype = {
412 bool: evalboolean,
412 bool: evalboolean,
413 bytes: evalstring,
413 bytes: evalstring,
414 int: evalinteger,
414 int: evalinteger,
415 }
415 }
416
416
417 def evalastype(context, mapping, arg, typ):
417 def evalastype(context, mapping, arg, typ):
418 """Evaluate given argument and coerce its type"""
418 """Evaluate given argument and coerce its type"""
419 try:
419 try:
420 f = _evalfuncbytype[typ]
420 f = _evalfuncbytype[typ]
421 except KeyError:
421 except KeyError:
422 raise error.ProgrammingError('invalid type specified: %r' % typ)
422 raise error.ProgrammingError('invalid type specified: %r' % typ)
423 return f(context, mapping, arg)
423 return f(context, mapping, arg)
424
424
425 def runinteger(context, mapping, data):
425 def runinteger(context, mapping, data):
426 return int(data)
426 return int(data)
427
427
428 def runstring(context, mapping, data):
428 def runstring(context, mapping, data):
429 return data
429 return data
430
430
431 def _recursivesymbolblocker(key):
431 def _recursivesymbolblocker(key):
432 def showrecursion(**args):
432 def showrecursion(**args):
433 raise error.Abort(_("recursive reference '%s' in template") % key)
433 raise error.Abort(_("recursive reference '%s' in template") % key)
434 return showrecursion
434 return showrecursion
435
435
436 def _runrecursivesymbol(context, mapping, key):
436 def _runrecursivesymbol(context, mapping, key):
437 raise error.Abort(_("recursive reference '%s' in template") % key)
437 raise error.Abort(_("recursive reference '%s' in template") % key)
438
438
439 def runsymbol(context, mapping, key, default=''):
439 def runsymbol(context, mapping, key, default=''):
440 v = context.symbol(mapping, key)
440 v = context.symbol(mapping, key)
441 if v is None:
441 if v is None:
442 # put poison to cut recursion. we can't move this to parsing phase
442 # put poison to cut recursion. we can't move this to parsing phase
443 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
443 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
444 safemapping = mapping.copy()
444 safemapping = mapping.copy()
445 safemapping[key] = _recursivesymbolblocker(key)
445 safemapping[key] = _recursivesymbolblocker(key)
446 try:
446 try:
447 v = context.process(key, safemapping)
447 v = context.process(key, safemapping)
448 except TemplateNotFound:
448 except TemplateNotFound:
449 v = default
449 v = default
450 if callable(v) and getattr(v, '_requires', None) is None:
450 if callable(v) and getattr(v, '_requires', None) is None:
451 # old templatekw: expand all keywords and resources
451 # old templatekw: expand all keywords and resources
452 props = context._resources.copy()
452 props = context._resources.copy()
453 props.update(mapping)
453 props.update(mapping)
454 return v(**pycompat.strkwargs(props))
454 return v(**pycompat.strkwargs(props))
455 if callable(v):
455 if callable(v):
456 # new templatekw
456 # new templatekw
457 try:
457 try:
458 return v(context, mapping)
458 return v(context, mapping)
459 except ResourceUnavailable:
459 except ResourceUnavailable:
460 # unsupported keyword is mapped to empty just like unknown keyword
460 # unsupported keyword is mapped to empty just like unknown keyword
461 return None
461 return None
462 return v
462 return v
463
463
464 def buildtemplate(exp, context):
464 def buildtemplate(exp, context):
465 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
465 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
466 return (runtemplate, ctmpl)
466 return (runtemplate, ctmpl)
467
467
468 def runtemplate(context, mapping, template):
468 def runtemplate(context, mapping, template):
469 for arg in template:
469 for arg in template:
470 yield evalrawexp(context, mapping, arg)
470 yield evalrawexp(context, mapping, arg)
471
471
472 def buildfilter(exp, context):
472 def buildfilter(exp, context):
473 n = getsymbol(exp[2])
473 n = getsymbol(exp[2])
474 if n in context._filters:
474 if n in context._filters:
475 filt = context._filters[n]
475 filt = context._filters[n]
476 arg = compileexp(exp[1], context, methods)
476 arg = compileexp(exp[1], context, methods)
477 return (runfilter, (arg, filt))
477 return (runfilter, (arg, filt))
478 if n in funcs:
478 if n in funcs:
479 f = funcs[n]
479 f = funcs[n]
480 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
480 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
481 return (f, args)
481 return (f, args)
482 raise error.ParseError(_("unknown function '%s'") % n)
482 raise error.ParseError(_("unknown function '%s'") % n)
483
483
484 def runfilter(context, mapping, data):
484 def runfilter(context, mapping, data):
485 arg, filt = data
485 arg, filt = data
486 thing = evalfuncarg(context, mapping, arg)
486 thing = evalfuncarg(context, mapping, arg)
487 try:
487 try:
488 return filt(thing)
488 return filt(thing)
489 except (ValueError, AttributeError, TypeError):
489 except (ValueError, AttributeError, TypeError):
490 sym = findsymbolicname(arg)
490 sym = findsymbolicname(arg)
491 if sym:
491 if sym:
492 msg = (_("template filter '%s' is not compatible with keyword '%s'")
492 msg = (_("template filter '%s' is not compatible with keyword '%s'")
493 % (pycompat.sysbytes(filt.__name__), sym))
493 % (pycompat.sysbytes(filt.__name__), sym))
494 else:
494 else:
495 msg = (_("incompatible use of template filter '%s'")
495 msg = (_("incompatible use of template filter '%s'")
496 % pycompat.sysbytes(filt.__name__))
496 % pycompat.sysbytes(filt.__name__))
497 raise error.Abort(msg)
497 raise error.Abort(msg)
498
498
499 def buildmap(exp, context):
499 def buildmap(exp, context):
500 darg = compileexp(exp[1], context, methods)
500 darg = compileexp(exp[1], context, methods)
501 targ = gettemplate(exp[2], context)
501 targ = gettemplate(exp[2], context)
502 return (runmap, (darg, targ))
502 return (runmap, (darg, targ))
503
503
504 def runmap(context, mapping, data):
504 def runmap(context, mapping, data):
505 darg, targ = data
505 darg, targ = data
506 d = evalrawexp(context, mapping, darg)
506 d = evalrawexp(context, mapping, darg)
507 if util.safehasattr(d, 'itermaps'):
507 if util.safehasattr(d, 'itermaps'):
508 diter = d.itermaps()
508 diter = d.itermaps()
509 else:
509 else:
510 try:
510 try:
511 diter = iter(d)
511 diter = iter(d)
512 except TypeError:
512 except TypeError:
513 sym = findsymbolicname(darg)
513 sym = findsymbolicname(darg)
514 if sym:
514 if sym:
515 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
515 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
516 else:
516 else:
517 raise error.ParseError(_("%r is not iterable") % d)
517 raise error.ParseError(_("%r is not iterable") % d)
518
518
519 for i, v in enumerate(diter):
519 for i, v in enumerate(diter):
520 lm = mapping.copy()
520 lm = mapping.copy()
521 lm['index'] = i
521 lm['index'] = i
522 if isinstance(v, dict):
522 if isinstance(v, dict):
523 lm.update(v)
523 lm.update(v)
524 lm['originalnode'] = mapping.get('node')
524 lm['originalnode'] = mapping.get('node')
525 yield evalrawexp(context, lm, targ)
525 yield evalrawexp(context, lm, targ)
526 else:
526 else:
527 # v is not an iterable of dicts, this happen when 'key'
527 # v is not an iterable of dicts, this happen when 'key'
528 # has been fully expanded already and format is useless.
528 # has been fully expanded already and format is useless.
529 # If so, return the expanded value.
529 # If so, return the expanded value.
530 yield v
530 yield v
531
531
532 def buildmember(exp, context):
532 def buildmember(exp, context):
533 darg = compileexp(exp[1], context, methods)
533 darg = compileexp(exp[1], context, methods)
534 memb = getsymbol(exp[2])
534 memb = getsymbol(exp[2])
535 return (runmember, (darg, memb))
535 return (runmember, (darg, memb))
536
536
537 def runmember(context, mapping, data):
537 def runmember(context, mapping, data):
538 darg, memb = data
538 darg, memb = data
539 d = evalrawexp(context, mapping, darg)
539 d = evalrawexp(context, mapping, darg)
540 if util.safehasattr(d, 'tomap'):
540 if util.safehasattr(d, 'tomap'):
541 lm = mapping.copy()
541 lm = mapping.copy()
542 lm.update(d.tomap())
542 lm.update(d.tomap())
543 return runsymbol(context, lm, memb)
543 return runsymbol(context, lm, memb)
544 if util.safehasattr(d, 'get'):
544 if util.safehasattr(d, 'get'):
545 return _getdictitem(d, memb)
545 return _getdictitem(d, memb)
546
546
547 sym = findsymbolicname(darg)
547 sym = findsymbolicname(darg)
548 if sym:
548 if sym:
549 raise error.ParseError(_("keyword '%s' has no member") % sym)
549 raise error.ParseError(_("keyword '%s' has no member") % sym)
550 else:
550 else:
551 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
551 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
552
552
553 def buildnegate(exp, context):
553 def buildnegate(exp, context):
554 arg = compileexp(exp[1], context, exprmethods)
554 arg = compileexp(exp[1], context, exprmethods)
555 return (runnegate, arg)
555 return (runnegate, arg)
556
556
557 def runnegate(context, mapping, data):
557 def runnegate(context, mapping, data):
558 data = evalinteger(context, mapping, data,
558 data = evalinteger(context, mapping, data,
559 _('negation needs an integer argument'))
559 _('negation needs an integer argument'))
560 return -data
560 return -data
561
561
562 def buildarithmetic(exp, context, func):
562 def buildarithmetic(exp, context, func):
563 left = compileexp(exp[1], context, exprmethods)
563 left = compileexp(exp[1], context, exprmethods)
564 right = compileexp(exp[2], context, exprmethods)
564 right = compileexp(exp[2], context, exprmethods)
565 return (runarithmetic, (func, left, right))
565 return (runarithmetic, (func, left, right))
566
566
567 def runarithmetic(context, mapping, data):
567 def runarithmetic(context, mapping, data):
568 func, left, right = data
568 func, left, right = data
569 left = evalinteger(context, mapping, left,
569 left = evalinteger(context, mapping, left,
570 _('arithmetic only defined on integers'))
570 _('arithmetic only defined on integers'))
571 right = evalinteger(context, mapping, right,
571 right = evalinteger(context, mapping, right,
572 _('arithmetic only defined on integers'))
572 _('arithmetic only defined on integers'))
573 try:
573 try:
574 return func(left, right)
574 return func(left, right)
575 except ZeroDivisionError:
575 except ZeroDivisionError:
576 raise error.Abort(_('division by zero is not defined'))
576 raise error.Abort(_('division by zero is not defined'))
577
577
578 def buildfunc(exp, context):
578 def buildfunc(exp, context):
579 n = getsymbol(exp[1])
579 n = getsymbol(exp[1])
580 if n in funcs:
580 if n in funcs:
581 f = funcs[n]
581 f = funcs[n]
582 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
582 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
583 return (f, args)
583 return (f, args)
584 if n in context._filters:
584 if n in context._filters:
585 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
585 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
586 if len(args) != 1:
586 if len(args) != 1:
587 raise error.ParseError(_("filter %s expects one argument") % n)
587 raise error.ParseError(_("filter %s expects one argument") % n)
588 f = context._filters[n]
588 f = context._filters[n]
589 return (runfilter, (args[0], f))
589 return (runfilter, (args[0], f))
590 raise error.ParseError(_("unknown function '%s'") % n)
590 raise error.ParseError(_("unknown function '%s'") % n)
591
591
592 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
592 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
593 """Compile parsed tree of function arguments into list or dict of
593 """Compile parsed tree of function arguments into list or dict of
594 (func, data) pairs
594 (func, data) pairs
595
595
596 >>> context = engine(lambda t: (runsymbol, t))
596 >>> context = engine(lambda t: (runsymbol, t))
597 >>> def fargs(expr, argspec):
597 >>> def fargs(expr, argspec):
598 ... x = _parseexpr(expr)
598 ... x = _parseexpr(expr)
599 ... n = getsymbol(x[1])
599 ... n = getsymbol(x[1])
600 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
600 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
601 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
601 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
602 ['l', 'k']
602 ['l', 'k']
603 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
603 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
604 >>> list(args.keys()), list(args[b'opts'].keys())
604 >>> list(args.keys()), list(args[b'opts'].keys())
605 (['opts'], ['opts', 'k'])
605 (['opts'], ['opts', 'k'])
606 """
606 """
607 def compiledict(xs):
607 def compiledict(xs):
608 return util.sortdict((k, compileexp(x, context, curmethods))
608 return util.sortdict((k, compileexp(x, context, curmethods))
609 for k, x in xs.iteritems())
609 for k, x in xs.iteritems())
610 def compilelist(xs):
610 def compilelist(xs):
611 return [compileexp(x, context, curmethods) for x in xs]
611 return [compileexp(x, context, curmethods) for x in xs]
612
612
613 if not argspec:
613 if not argspec:
614 # filter or function with no argspec: return list of positional args
614 # filter or function with no argspec: return list of positional args
615 return compilelist(getlist(exp))
615 return compilelist(getlist(exp))
616
616
617 # function with argspec: return dict of named args
617 # function with argspec: return dict of named args
618 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
618 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
619 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
619 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
620 keyvaluenode='keyvalue', keynode='symbol')
620 keyvaluenode='keyvalue', keynode='symbol')
621 compargs = util.sortdict()
621 compargs = util.sortdict()
622 if varkey:
622 if varkey:
623 compargs[varkey] = compilelist(treeargs.pop(varkey))
623 compargs[varkey] = compilelist(treeargs.pop(varkey))
624 if optkey:
624 if optkey:
625 compargs[optkey] = compiledict(treeargs.pop(optkey))
625 compargs[optkey] = compiledict(treeargs.pop(optkey))
626 compargs.update(compiledict(treeargs))
626 compargs.update(compiledict(treeargs))
627 return compargs
627 return compargs
628
628
629 def buildkeyvaluepair(exp, content):
629 def buildkeyvaluepair(exp, content):
630 raise error.ParseError(_("can't use a key-value pair in this context"))
630 raise error.ParseError(_("can't use a key-value pair in this context"))
631
631
632 # dict of template built-in functions
632 # dict of template built-in functions
633 funcs = {}
633 funcs = {}
634
634
635 templatefunc = registrar.templatefunc(funcs)
635 templatefunc = registrar.templatefunc(funcs)
636
636
637 @templatefunc('date(date[, fmt])')
637 @templatefunc('date(date[, fmt])')
638 def date(context, mapping, args):
638 def date(context, mapping, args):
639 """Format a date. See :hg:`help dates` for formatting
639 """Format a date. See :hg:`help dates` for formatting
640 strings. The default is a Unix date format, including the timezone:
640 strings. The default is a Unix date format, including the timezone:
641 "Mon Sep 04 15:13:13 2006 0700"."""
641 "Mon Sep 04 15:13:13 2006 0700"."""
642 if not (1 <= len(args) <= 2):
642 if not (1 <= len(args) <= 2):
643 # i18n: "date" is a keyword
643 # i18n: "date" is a keyword
644 raise error.ParseError(_("date expects one or two arguments"))
644 raise error.ParseError(_("date expects one or two arguments"))
645
645
646 date = evalfuncarg(context, mapping, args[0])
646 date = evalfuncarg(context, mapping, args[0])
647 fmt = None
647 fmt = None
648 if len(args) == 2:
648 if len(args) == 2:
649 fmt = evalstring(context, mapping, args[1])
649 fmt = evalstring(context, mapping, args[1])
650 try:
650 try:
651 if fmt is None:
651 if fmt is None:
652 return util.datestr(date)
652 return util.datestr(date)
653 else:
653 else:
654 return util.datestr(date, fmt)
654 return util.datestr(date, fmt)
655 except (TypeError, ValueError):
655 except (TypeError, ValueError):
656 # i18n: "date" is a keyword
656 # i18n: "date" is a keyword
657 raise error.ParseError(_("date expects a date information"))
657 raise error.ParseError(_("date expects a date information"))
658
658
659 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
659 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
660 def dict_(context, mapping, args):
660 def dict_(context, mapping, args):
661 """Construct a dict from key-value pairs. A key may be omitted if
661 """Construct a dict from key-value pairs. A key may be omitted if
662 a value expression can provide an unambiguous name."""
662 a value expression can provide an unambiguous name."""
663 data = util.sortdict()
663 data = util.sortdict()
664
664
665 for v in args['args']:
665 for v in args['args']:
666 k = findsymbolicname(v)
666 k = findsymbolicname(v)
667 if not k:
667 if not k:
668 raise error.ParseError(_('dict key cannot be inferred'))
668 raise error.ParseError(_('dict key cannot be inferred'))
669 if k in data or k in args['kwargs']:
669 if k in data or k in args['kwargs']:
670 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
670 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
671 data[k] = evalfuncarg(context, mapping, v)
671 data[k] = evalfuncarg(context, mapping, v)
672
672
673 data.update((k, evalfuncarg(context, mapping, v))
673 data.update((k, evalfuncarg(context, mapping, v))
674 for k, v in args['kwargs'].iteritems())
674 for k, v in args['kwargs'].iteritems())
675 return templatekw.hybriddict(data)
675 return templatekw.hybriddict(data)
676
676
677 @templatefunc('diff([includepattern [, excludepattern]])')
677 @templatefunc('diff([includepattern [, excludepattern]])')
678 def diff(context, mapping, args):
678 def diff(context, mapping, args):
679 """Show a diff, optionally
679 """Show a diff, optionally
680 specifying files to include or exclude."""
680 specifying files to include or exclude."""
681 if len(args) > 2:
681 if len(args) > 2:
682 # i18n: "diff" is a keyword
682 # i18n: "diff" is a keyword
683 raise error.ParseError(_("diff expects zero, one, or two arguments"))
683 raise error.ParseError(_("diff expects zero, one, or two arguments"))
684
684
685 def getpatterns(i):
685 def getpatterns(i):
686 if i < len(args):
686 if i < len(args):
687 s = evalstring(context, mapping, args[i]).strip()
687 s = evalstring(context, mapping, args[i]).strip()
688 if s:
688 if s:
689 return [s]
689 return [s]
690 return []
690 return []
691
691
692 ctx = context.resource(mapping, 'ctx')
692 ctx = context.resource(mapping, 'ctx')
693 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
693 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
694
694
695 return ''.join(chunks)
695 return ''.join(chunks)
696
696
697 @templatefunc('extdata(source)', argspec='source')
697 @templatefunc('extdata(source)', argspec='source')
698 def extdata(context, mapping, args):
698 def extdata(context, mapping, args):
699 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
699 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
700 if 'source' not in args:
700 if 'source' not in args:
701 # i18n: "extdata" is a keyword
701 # i18n: "extdata" is a keyword
702 raise error.ParseError(_('extdata expects one argument'))
702 raise error.ParseError(_('extdata expects one argument'))
703
703
704 source = evalstring(context, mapping, args['source'])
704 source = evalstring(context, mapping, args['source'])
705 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
705 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
706 ctx = context.resource(mapping, 'ctx')
706 ctx = context.resource(mapping, 'ctx')
707 if source in cache:
707 if source in cache:
708 data = cache[source]
708 data = cache[source]
709 else:
709 else:
710 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
710 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
711 return data.get(ctx.rev(), '')
711 return data.get(ctx.rev(), '')
712
712
713 @templatefunc('files(pattern)')
713 @templatefunc('files(pattern)')
714 def files(context, mapping, args):
714 def files(context, mapping, args):
715 """All files of the current changeset matching the pattern. See
715 """All files of the current changeset matching the pattern. See
716 :hg:`help patterns`."""
716 :hg:`help patterns`."""
717 if not len(args) == 1:
717 if not len(args) == 1:
718 # i18n: "files" is a keyword
718 # i18n: "files" is a keyword
719 raise error.ParseError(_("files expects one argument"))
719 raise error.ParseError(_("files expects one argument"))
720
720
721 raw = evalstring(context, mapping, args[0])
721 raw = evalstring(context, mapping, args[0])
722 ctx = context.resource(mapping, 'ctx')
722 ctx = context.resource(mapping, 'ctx')
723 m = ctx.match([raw])
723 m = ctx.match([raw])
724 files = list(ctx.matches(m))
724 files = list(ctx.matches(m))
725 return templatekw.compatlist(context, mapping, "file", files)
725 return templatekw.compatlist(context, mapping, "file", files)
726
726
727 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
727 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
728 def fill(context, mapping, args):
728 def fill(context, mapping, args):
729 """Fill many
729 """Fill many
730 paragraphs with optional indentation. See the "fill" filter."""
730 paragraphs with optional indentation. See the "fill" filter."""
731 if not (1 <= len(args) <= 4):
731 if not (1 <= len(args) <= 4):
732 # i18n: "fill" is a keyword
732 # i18n: "fill" is a keyword
733 raise error.ParseError(_("fill expects one to four arguments"))
733 raise error.ParseError(_("fill expects one to four arguments"))
734
734
735 text = evalstring(context, mapping, args[0])
735 text = evalstring(context, mapping, args[0])
736 width = 76
736 width = 76
737 initindent = ''
737 initindent = ''
738 hangindent = ''
738 hangindent = ''
739 if 2 <= len(args) <= 4:
739 if 2 <= len(args) <= 4:
740 width = evalinteger(context, mapping, args[1],
740 width = evalinteger(context, mapping, args[1],
741 # i18n: "fill" is a keyword
741 # i18n: "fill" is a keyword
742 _("fill expects an integer width"))
742 _("fill expects an integer width"))
743 try:
743 try:
744 initindent = evalstring(context, mapping, args[2])
744 initindent = evalstring(context, mapping, args[2])
745 hangindent = evalstring(context, mapping, args[3])
745 hangindent = evalstring(context, mapping, args[3])
746 except IndexError:
746 except IndexError:
747 pass
747 pass
748
748
749 return templatefilters.fill(text, width, initindent, hangindent)
749 return templatefilters.fill(text, width, initindent, hangindent)
750
750
751 @templatefunc('formatnode(node)')
751 @templatefunc('formatnode(node)')
752 def formatnode(context, mapping, args):
752 def formatnode(context, mapping, args):
753 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
753 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
754 if len(args) != 1:
754 if len(args) != 1:
755 # i18n: "formatnode" is a keyword
755 # i18n: "formatnode" is a keyword
756 raise error.ParseError(_("formatnode expects one argument"))
756 raise error.ParseError(_("formatnode expects one argument"))
757
757
758 ui = context.resource(mapping, 'ui')
758 ui = context.resource(mapping, 'ui')
759 node = evalstring(context, mapping, args[0])
759 node = evalstring(context, mapping, args[0])
760 if ui.debugflag:
760 if ui.debugflag:
761 return node
761 return node
762 return templatefilters.short(node)
762 return templatefilters.short(node)
763
763
764 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
764 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
765 argspec='text width fillchar left')
765 argspec='text width fillchar left')
766 def pad(context, mapping, args):
766 def pad(context, mapping, args):
767 """Pad text with a
767 """Pad text with a
768 fill character."""
768 fill character."""
769 if 'text' not in args or 'width' not in args:
769 if 'text' not in args or 'width' not in args:
770 # i18n: "pad" is a keyword
770 # i18n: "pad" is a keyword
771 raise error.ParseError(_("pad() expects two to four arguments"))
771 raise error.ParseError(_("pad() expects two to four arguments"))
772
772
773 width = evalinteger(context, mapping, args['width'],
773 width = evalinteger(context, mapping, args['width'],
774 # i18n: "pad" is a keyword
774 # i18n: "pad" is a keyword
775 _("pad() expects an integer width"))
775 _("pad() expects an integer width"))
776
776
777 text = evalstring(context, mapping, args['text'])
777 text = evalstring(context, mapping, args['text'])
778
778
779 left = False
779 left = False
780 fillchar = ' '
780 fillchar = ' '
781 if 'fillchar' in args:
781 if 'fillchar' in args:
782 fillchar = evalstring(context, mapping, args['fillchar'])
782 fillchar = evalstring(context, mapping, args['fillchar'])
783 if len(color.stripeffects(fillchar)) != 1:
783 if len(color.stripeffects(fillchar)) != 1:
784 # i18n: "pad" is a keyword
784 # i18n: "pad" is a keyword
785 raise error.ParseError(_("pad() expects a single fill character"))
785 raise error.ParseError(_("pad() expects a single fill character"))
786 if 'left' in args:
786 if 'left' in args:
787 left = evalboolean(context, mapping, args['left'])
787 left = evalboolean(context, mapping, args['left'])
788
788
789 fillwidth = width - encoding.colwidth(color.stripeffects(text))
789 fillwidth = width - encoding.colwidth(color.stripeffects(text))
790 if fillwidth <= 0:
790 if fillwidth <= 0:
791 return text
791 return text
792 if left:
792 if left:
793 return fillchar * fillwidth + text
793 return fillchar * fillwidth + text
794 else:
794 else:
795 return text + fillchar * fillwidth
795 return text + fillchar * fillwidth
796
796
797 @templatefunc('indent(text, indentchars[, firstline])')
797 @templatefunc('indent(text, indentchars[, firstline])')
798 def indent(context, mapping, args):
798 def indent(context, mapping, args):
799 """Indents all non-empty lines
799 """Indents all non-empty lines
800 with the characters given in the indentchars string. An optional
800 with the characters given in the indentchars string. An optional
801 third parameter will override the indent for the first line only
801 third parameter will override the indent for the first line only
802 if present."""
802 if present."""
803 if not (2 <= len(args) <= 3):
803 if not (2 <= len(args) <= 3):
804 # i18n: "indent" is a keyword
804 # i18n: "indent" is a keyword
805 raise error.ParseError(_("indent() expects two or three arguments"))
805 raise error.ParseError(_("indent() expects two or three arguments"))
806
806
807 text = evalstring(context, mapping, args[0])
807 text = evalstring(context, mapping, args[0])
808 indent = evalstring(context, mapping, args[1])
808 indent = evalstring(context, mapping, args[1])
809
809
810 if len(args) == 3:
810 if len(args) == 3:
811 firstline = evalstring(context, mapping, args[2])
811 firstline = evalstring(context, mapping, args[2])
812 else:
812 else:
813 firstline = indent
813 firstline = indent
814
814
815 # the indent function doesn't indent the first line, so we do it here
815 # the indent function doesn't indent the first line, so we do it here
816 return templatefilters.indent(firstline + text, indent)
816 return templatefilters.indent(firstline + text, indent)
817
817
818 @templatefunc('get(dict, key)')
818 @templatefunc('get(dict, key)')
819 def get(context, mapping, args):
819 def get(context, mapping, args):
820 """Get an attribute/key from an object. Some keywords
820 """Get an attribute/key from an object. Some keywords
821 are complex types. This function allows you to obtain the value of an
821 are complex types. This function allows you to obtain the value of an
822 attribute on these types."""
822 attribute on these types."""
823 if len(args) != 2:
823 if len(args) != 2:
824 # i18n: "get" is a keyword
824 # i18n: "get" is a keyword
825 raise error.ParseError(_("get() expects two arguments"))
825 raise error.ParseError(_("get() expects two arguments"))
826
826
827 dictarg = evalfuncarg(context, mapping, args[0])
827 dictarg = evalfuncarg(context, mapping, args[0])
828 if not util.safehasattr(dictarg, 'get'):
828 if not util.safehasattr(dictarg, 'get'):
829 # i18n: "get" is a keyword
829 # i18n: "get" is a keyword
830 raise error.ParseError(_("get() expects a dict as first argument"))
830 raise error.ParseError(_("get() expects a dict as first argument"))
831
831
832 key = evalfuncarg(context, mapping, args[1])
832 key = evalfuncarg(context, mapping, args[1])
833 return _getdictitem(dictarg, key)
833 return _getdictitem(dictarg, key)
834
834
835 def _getdictitem(dictarg, key):
835 def _getdictitem(dictarg, key):
836 val = dictarg.get(key)
836 val = dictarg.get(key)
837 if val is None:
837 if val is None:
838 return
838 return
839 return templatekw.wraphybridvalue(dictarg, key, val)
839 return templatekw.wraphybridvalue(dictarg, key, val)
840
840
841 @templatefunc('if(expr, then[, else])')
841 @templatefunc('if(expr, then[, else])')
842 def if_(context, mapping, args):
842 def if_(context, mapping, args):
843 """Conditionally execute based on the result of
843 """Conditionally execute based on the result of
844 an expression."""
844 an expression."""
845 if not (2 <= len(args) <= 3):
845 if not (2 <= len(args) <= 3):
846 # i18n: "if" is a keyword
846 # i18n: "if" is a keyword
847 raise error.ParseError(_("if expects two or three arguments"))
847 raise error.ParseError(_("if expects two or three arguments"))
848
848
849 test = evalboolean(context, mapping, args[0])
849 test = evalboolean(context, mapping, args[0])
850 if test:
850 if test:
851 yield evalrawexp(context, mapping, args[1])
851 yield evalrawexp(context, mapping, args[1])
852 elif len(args) == 3:
852 elif len(args) == 3:
853 yield evalrawexp(context, mapping, args[2])
853 yield evalrawexp(context, mapping, args[2])
854
854
855 @templatefunc('ifcontains(needle, haystack, then[, else])')
855 @templatefunc('ifcontains(needle, haystack, then[, else])')
856 def ifcontains(context, mapping, args):
856 def ifcontains(context, mapping, args):
857 """Conditionally execute based
857 """Conditionally execute based
858 on whether the item "needle" is in "haystack"."""
858 on whether the item "needle" is in "haystack"."""
859 if not (3 <= len(args) <= 4):
859 if not (3 <= len(args) <= 4):
860 # i18n: "ifcontains" is a keyword
860 # i18n: "ifcontains" is a keyword
861 raise error.ParseError(_("ifcontains expects three or four arguments"))
861 raise error.ParseError(_("ifcontains expects three or four arguments"))
862
862
863 haystack = evalfuncarg(context, mapping, args[1])
863 haystack = evalfuncarg(context, mapping, args[1])
864 try:
864 try:
865 needle = evalastype(context, mapping, args[0],
865 needle = evalastype(context, mapping, args[0],
866 getattr(haystack, 'keytype', None) or bytes)
866 getattr(haystack, 'keytype', None) or bytes)
867 found = (needle in haystack)
867 found = (needle in haystack)
868 except error.ParseError:
868 except error.ParseError:
869 found = False
869 found = False
870
870
871 if found:
871 if found:
872 yield evalrawexp(context, mapping, args[2])
872 yield evalrawexp(context, mapping, args[2])
873 elif len(args) == 4:
873 elif len(args) == 4:
874 yield evalrawexp(context, mapping, args[3])
874 yield evalrawexp(context, mapping, args[3])
875
875
876 @templatefunc('ifeq(expr1, expr2, then[, else])')
876 @templatefunc('ifeq(expr1, expr2, then[, else])')
877 def ifeq(context, mapping, args):
877 def ifeq(context, mapping, args):
878 """Conditionally execute based on
878 """Conditionally execute based on
879 whether 2 items are equivalent."""
879 whether 2 items are equivalent."""
880 if not (3 <= len(args) <= 4):
880 if not (3 <= len(args) <= 4):
881 # i18n: "ifeq" is a keyword
881 # i18n: "ifeq" is a keyword
882 raise error.ParseError(_("ifeq expects three or four arguments"))
882 raise error.ParseError(_("ifeq expects three or four arguments"))
883
883
884 test = evalstring(context, mapping, args[0])
884 test = evalstring(context, mapping, args[0])
885 match = evalstring(context, mapping, args[1])
885 match = evalstring(context, mapping, args[1])
886 if test == match:
886 if test == match:
887 yield evalrawexp(context, mapping, args[2])
887 yield evalrawexp(context, mapping, args[2])
888 elif len(args) == 4:
888 elif len(args) == 4:
889 yield evalrawexp(context, mapping, args[3])
889 yield evalrawexp(context, mapping, args[3])
890
890
891 @templatefunc('join(list, sep)')
891 @templatefunc('join(list, sep)')
892 def join(context, mapping, args):
892 def join(context, mapping, args):
893 """Join items in a list with a delimiter."""
893 """Join items in a list with a delimiter."""
894 if not (1 <= len(args) <= 2):
894 if not (1 <= len(args) <= 2):
895 # i18n: "join" is a keyword
895 # i18n: "join" is a keyword
896 raise error.ParseError(_("join expects one or two arguments"))
896 raise error.ParseError(_("join expects one or two arguments"))
897
897
898 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
898 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
899 # abuses generator as a keyword that returns a list of dicts.
899 # abuses generator as a keyword that returns a list of dicts.
900 joinset = evalrawexp(context, mapping, args[0])
900 joinset = evalrawexp(context, mapping, args[0])
901 joinset = templatekw.unwrapvalue(joinset)
901 joinset = templatekw.unwrapvalue(joinset)
902 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
902 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
903 joiner = " "
903 joiner = " "
904 if len(args) > 1:
904 if len(args) > 1:
905 joiner = evalstring(context, mapping, args[1])
905 joiner = evalstring(context, mapping, args[1])
906
906
907 first = True
907 first = True
908 for x in pycompat.maybebytestr(joinset):
908 for x in pycompat.maybebytestr(joinset):
909 if first:
909 if first:
910 first = False
910 first = False
911 else:
911 else:
912 yield joiner
912 yield joiner
913 yield joinfmt(x)
913 yield joinfmt(x)
914
914
915 @templatefunc('label(label, expr)')
915 @templatefunc('label(label, expr)')
916 def label(context, mapping, args):
916 def label(context, mapping, args):
917 """Apply a label to generated content. Content with
917 """Apply a label to generated content. Content with
918 a label applied can result in additional post-processing, such as
918 a label applied can result in additional post-processing, such as
919 automatic colorization."""
919 automatic colorization."""
920 if len(args) != 2:
920 if len(args) != 2:
921 # i18n: "label" is a keyword
921 # i18n: "label" is a keyword
922 raise error.ParseError(_("label expects two arguments"))
922 raise error.ParseError(_("label expects two arguments"))
923
923
924 ui = context.resource(mapping, 'ui')
924 ui = context.resource(mapping, 'ui')
925 thing = evalstring(context, mapping, args[1])
925 thing = evalstring(context, mapping, args[1])
926 # preserve unknown symbol as literal so effects like 'red', 'bold',
926 # preserve unknown symbol as literal so effects like 'red', 'bold',
927 # etc. don't need to be quoted
927 # etc. don't need to be quoted
928 label = evalstringliteral(context, mapping, args[0])
928 label = evalstringliteral(context, mapping, args[0])
929
929
930 return ui.label(thing, label)
930 return ui.label(thing, label)
931
931
932 @templatefunc('latesttag([pattern])')
932 @templatefunc('latesttag([pattern])')
933 def latesttag(context, mapping, args):
933 def latesttag(context, mapping, args):
934 """The global tags matching the given pattern on the
934 """The global tags matching the given pattern on the
935 most recent globally tagged ancestor of this changeset.
935 most recent globally tagged ancestor of this changeset.
936 If no such tags exist, the "{tag}" template resolves to
936 If no such tags exist, the "{tag}" template resolves to
937 the string "null"."""
937 the string "null"."""
938 if len(args) > 1:
938 if len(args) > 1:
939 # i18n: "latesttag" is a keyword
939 # i18n: "latesttag" is a keyword
940 raise error.ParseError(_("latesttag expects at most one argument"))
940 raise error.ParseError(_("latesttag expects at most one argument"))
941
941
942 pattern = None
942 pattern = None
943 if len(args) == 1:
943 if len(args) == 1:
944 pattern = evalstring(context, mapping, args[0])
944 pattern = evalstring(context, mapping, args[0])
945
945 return templatekw.showlatesttags(context, mapping, pattern)
946 # TODO: pass (context, mapping) pair to keyword function
947 props = context._resources.copy()
948 props.update(mapping)
949 return templatekw.showlatesttags(pattern, **pycompat.strkwargs(props))
950
946
951 @templatefunc('localdate(date[, tz])')
947 @templatefunc('localdate(date[, tz])')
952 def localdate(context, mapping, args):
948 def localdate(context, mapping, args):
953 """Converts a date to the specified timezone.
949 """Converts a date to the specified timezone.
954 The default is local date."""
950 The default is local date."""
955 if not (1 <= len(args) <= 2):
951 if not (1 <= len(args) <= 2):
956 # i18n: "localdate" is a keyword
952 # i18n: "localdate" is a keyword
957 raise error.ParseError(_("localdate expects one or two arguments"))
953 raise error.ParseError(_("localdate expects one or two arguments"))
958
954
959 date = evalfuncarg(context, mapping, args[0])
955 date = evalfuncarg(context, mapping, args[0])
960 try:
956 try:
961 date = util.parsedate(date)
957 date = util.parsedate(date)
962 except AttributeError: # not str nor date tuple
958 except AttributeError: # not str nor date tuple
963 # i18n: "localdate" is a keyword
959 # i18n: "localdate" is a keyword
964 raise error.ParseError(_("localdate expects a date information"))
960 raise error.ParseError(_("localdate expects a date information"))
965 if len(args) >= 2:
961 if len(args) >= 2:
966 tzoffset = None
962 tzoffset = None
967 tz = evalfuncarg(context, mapping, args[1])
963 tz = evalfuncarg(context, mapping, args[1])
968 if isinstance(tz, bytes):
964 if isinstance(tz, bytes):
969 tzoffset, remainder = util.parsetimezone(tz)
965 tzoffset, remainder = util.parsetimezone(tz)
970 if remainder:
966 if remainder:
971 tzoffset = None
967 tzoffset = None
972 if tzoffset is None:
968 if tzoffset is None:
973 try:
969 try:
974 tzoffset = int(tz)
970 tzoffset = int(tz)
975 except (TypeError, ValueError):
971 except (TypeError, ValueError):
976 # i18n: "localdate" is a keyword
972 # i18n: "localdate" is a keyword
977 raise error.ParseError(_("localdate expects a timezone"))
973 raise error.ParseError(_("localdate expects a timezone"))
978 else:
974 else:
979 tzoffset = util.makedate()[1]
975 tzoffset = util.makedate()[1]
980 return (date[0], tzoffset)
976 return (date[0], tzoffset)
981
977
982 @templatefunc('max(iterable)')
978 @templatefunc('max(iterable)')
983 def max_(context, mapping, args, **kwargs):
979 def max_(context, mapping, args, **kwargs):
984 """Return the max of an iterable"""
980 """Return the max of an iterable"""
985 if len(args) != 1:
981 if len(args) != 1:
986 # i18n: "max" is a keyword
982 # i18n: "max" is a keyword
987 raise error.ParseError(_("max expects one argument"))
983 raise error.ParseError(_("max expects one argument"))
988
984
989 iterable = evalfuncarg(context, mapping, args[0])
985 iterable = evalfuncarg(context, mapping, args[0])
990 try:
986 try:
991 x = max(pycompat.maybebytestr(iterable))
987 x = max(pycompat.maybebytestr(iterable))
992 except (TypeError, ValueError):
988 except (TypeError, ValueError):
993 # i18n: "max" is a keyword
989 # i18n: "max" is a keyword
994 raise error.ParseError(_("max first argument should be an iterable"))
990 raise error.ParseError(_("max first argument should be an iterable"))
995 return templatekw.wraphybridvalue(iterable, x, x)
991 return templatekw.wraphybridvalue(iterable, x, x)
996
992
997 @templatefunc('min(iterable)')
993 @templatefunc('min(iterable)')
998 def min_(context, mapping, args, **kwargs):
994 def min_(context, mapping, args, **kwargs):
999 """Return the min of an iterable"""
995 """Return the min of an iterable"""
1000 if len(args) != 1:
996 if len(args) != 1:
1001 # i18n: "min" is a keyword
997 # i18n: "min" is a keyword
1002 raise error.ParseError(_("min expects one argument"))
998 raise error.ParseError(_("min expects one argument"))
1003
999
1004 iterable = evalfuncarg(context, mapping, args[0])
1000 iterable = evalfuncarg(context, mapping, args[0])
1005 try:
1001 try:
1006 x = min(pycompat.maybebytestr(iterable))
1002 x = min(pycompat.maybebytestr(iterable))
1007 except (TypeError, ValueError):
1003 except (TypeError, ValueError):
1008 # i18n: "min" is a keyword
1004 # i18n: "min" is a keyword
1009 raise error.ParseError(_("min first argument should be an iterable"))
1005 raise error.ParseError(_("min first argument should be an iterable"))
1010 return templatekw.wraphybridvalue(iterable, x, x)
1006 return templatekw.wraphybridvalue(iterable, x, x)
1011
1007
1012 @templatefunc('mod(a, b)')
1008 @templatefunc('mod(a, b)')
1013 def mod(context, mapping, args):
1009 def mod(context, mapping, args):
1014 """Calculate a mod b such that a / b + a mod b == a"""
1010 """Calculate a mod b such that a / b + a mod b == a"""
1015 if not len(args) == 2:
1011 if not len(args) == 2:
1016 # i18n: "mod" is a keyword
1012 # i18n: "mod" is a keyword
1017 raise error.ParseError(_("mod expects two arguments"))
1013 raise error.ParseError(_("mod expects two arguments"))
1018
1014
1019 func = lambda a, b: a % b
1015 func = lambda a, b: a % b
1020 return runarithmetic(context, mapping, (func, args[0], args[1]))
1016 return runarithmetic(context, mapping, (func, args[0], args[1]))
1021
1017
1022 @templatefunc('obsfateoperations(markers)')
1018 @templatefunc('obsfateoperations(markers)')
1023 def obsfateoperations(context, mapping, args):
1019 def obsfateoperations(context, mapping, args):
1024 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1020 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1025 if len(args) != 1:
1021 if len(args) != 1:
1026 # i18n: "obsfateoperations" is a keyword
1022 # i18n: "obsfateoperations" is a keyword
1027 raise error.ParseError(_("obsfateoperations expects one argument"))
1023 raise error.ParseError(_("obsfateoperations expects one argument"))
1028
1024
1029 markers = evalfuncarg(context, mapping, args[0])
1025 markers = evalfuncarg(context, mapping, args[0])
1030
1026
1031 try:
1027 try:
1032 data = obsutil.markersoperations(markers)
1028 data = obsutil.markersoperations(markers)
1033 return templatekw.hybridlist(data, name='operation')
1029 return templatekw.hybridlist(data, name='operation')
1034 except (TypeError, KeyError):
1030 except (TypeError, KeyError):
1035 # i18n: "obsfateoperations" is a keyword
1031 # i18n: "obsfateoperations" is a keyword
1036 errmsg = _("obsfateoperations first argument should be an iterable")
1032 errmsg = _("obsfateoperations first argument should be an iterable")
1037 raise error.ParseError(errmsg)
1033 raise error.ParseError(errmsg)
1038
1034
1039 @templatefunc('obsfatedate(markers)')
1035 @templatefunc('obsfatedate(markers)')
1040 def obsfatedate(context, mapping, args):
1036 def obsfatedate(context, mapping, args):
1041 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1037 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1042 if len(args) != 1:
1038 if len(args) != 1:
1043 # i18n: "obsfatedate" is a keyword
1039 # i18n: "obsfatedate" is a keyword
1044 raise error.ParseError(_("obsfatedate expects one argument"))
1040 raise error.ParseError(_("obsfatedate expects one argument"))
1045
1041
1046 markers = evalfuncarg(context, mapping, args[0])
1042 markers = evalfuncarg(context, mapping, args[0])
1047
1043
1048 try:
1044 try:
1049 data = obsutil.markersdates(markers)
1045 data = obsutil.markersdates(markers)
1050 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1046 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1051 except (TypeError, KeyError):
1047 except (TypeError, KeyError):
1052 # i18n: "obsfatedate" is a keyword
1048 # i18n: "obsfatedate" is a keyword
1053 errmsg = _("obsfatedate first argument should be an iterable")
1049 errmsg = _("obsfatedate first argument should be an iterable")
1054 raise error.ParseError(errmsg)
1050 raise error.ParseError(errmsg)
1055
1051
1056 @templatefunc('obsfateusers(markers)')
1052 @templatefunc('obsfateusers(markers)')
1057 def obsfateusers(context, mapping, args):
1053 def obsfateusers(context, mapping, args):
1058 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1054 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1059 if len(args) != 1:
1055 if len(args) != 1:
1060 # i18n: "obsfateusers" is a keyword
1056 # i18n: "obsfateusers" is a keyword
1061 raise error.ParseError(_("obsfateusers expects one argument"))
1057 raise error.ParseError(_("obsfateusers expects one argument"))
1062
1058
1063 markers = evalfuncarg(context, mapping, args[0])
1059 markers = evalfuncarg(context, mapping, args[0])
1064
1060
1065 try:
1061 try:
1066 data = obsutil.markersusers(markers)
1062 data = obsutil.markersusers(markers)
1067 return templatekw.hybridlist(data, name='user')
1063 return templatekw.hybridlist(data, name='user')
1068 except (TypeError, KeyError, ValueError):
1064 except (TypeError, KeyError, ValueError):
1069 # i18n: "obsfateusers" is a keyword
1065 # i18n: "obsfateusers" is a keyword
1070 msg = _("obsfateusers first argument should be an iterable of "
1066 msg = _("obsfateusers first argument should be an iterable of "
1071 "obsmakers")
1067 "obsmakers")
1072 raise error.ParseError(msg)
1068 raise error.ParseError(msg)
1073
1069
1074 @templatefunc('obsfateverb(successors, markers)')
1070 @templatefunc('obsfateverb(successors, markers)')
1075 def obsfateverb(context, mapping, args):
1071 def obsfateverb(context, mapping, args):
1076 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1072 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1077 if len(args) != 2:
1073 if len(args) != 2:
1078 # i18n: "obsfateverb" is a keyword
1074 # i18n: "obsfateverb" is a keyword
1079 raise error.ParseError(_("obsfateverb expects two arguments"))
1075 raise error.ParseError(_("obsfateverb expects two arguments"))
1080
1076
1081 successors = evalfuncarg(context, mapping, args[0])
1077 successors = evalfuncarg(context, mapping, args[0])
1082 markers = evalfuncarg(context, mapping, args[1])
1078 markers = evalfuncarg(context, mapping, args[1])
1083
1079
1084 try:
1080 try:
1085 return obsutil.obsfateverb(successors, markers)
1081 return obsutil.obsfateverb(successors, markers)
1086 except TypeError:
1082 except TypeError:
1087 # i18n: "obsfateverb" is a keyword
1083 # i18n: "obsfateverb" is a keyword
1088 errmsg = _("obsfateverb first argument should be countable")
1084 errmsg = _("obsfateverb first argument should be countable")
1089 raise error.ParseError(errmsg)
1085 raise error.ParseError(errmsg)
1090
1086
1091 @templatefunc('relpath(path)')
1087 @templatefunc('relpath(path)')
1092 def relpath(context, mapping, args):
1088 def relpath(context, mapping, args):
1093 """Convert a repository-absolute path into a filesystem path relative to
1089 """Convert a repository-absolute path into a filesystem path relative to
1094 the current working directory."""
1090 the current working directory."""
1095 if len(args) != 1:
1091 if len(args) != 1:
1096 # i18n: "relpath" is a keyword
1092 # i18n: "relpath" is a keyword
1097 raise error.ParseError(_("relpath expects one argument"))
1093 raise error.ParseError(_("relpath expects one argument"))
1098
1094
1099 repo = context.resource(mapping, 'ctx').repo()
1095 repo = context.resource(mapping, 'ctx').repo()
1100 path = evalstring(context, mapping, args[0])
1096 path = evalstring(context, mapping, args[0])
1101 return repo.pathto(path)
1097 return repo.pathto(path)
1102
1098
1103 @templatefunc('revset(query[, formatargs...])')
1099 @templatefunc('revset(query[, formatargs...])')
1104 def revset(context, mapping, args):
1100 def revset(context, mapping, args):
1105 """Execute a revision set query. See
1101 """Execute a revision set query. See
1106 :hg:`help revset`."""
1102 :hg:`help revset`."""
1107 if not len(args) > 0:
1103 if not len(args) > 0:
1108 # i18n: "revset" is a keyword
1104 # i18n: "revset" is a keyword
1109 raise error.ParseError(_("revset expects one or more arguments"))
1105 raise error.ParseError(_("revset expects one or more arguments"))
1110
1106
1111 raw = evalstring(context, mapping, args[0])
1107 raw = evalstring(context, mapping, args[0])
1112 ctx = context.resource(mapping, 'ctx')
1108 ctx = context.resource(mapping, 'ctx')
1113 repo = ctx.repo()
1109 repo = ctx.repo()
1114
1110
1115 def query(expr):
1111 def query(expr):
1116 m = revsetmod.match(repo.ui, expr, repo=repo)
1112 m = revsetmod.match(repo.ui, expr, repo=repo)
1117 return m(repo)
1113 return m(repo)
1118
1114
1119 if len(args) > 1:
1115 if len(args) > 1:
1120 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1116 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1121 revs = query(revsetlang.formatspec(raw, *formatargs))
1117 revs = query(revsetlang.formatspec(raw, *formatargs))
1122 revs = list(revs)
1118 revs = list(revs)
1123 else:
1119 else:
1124 cache = context.resource(mapping, 'cache')
1120 cache = context.resource(mapping, 'cache')
1125 revsetcache = cache.setdefault("revsetcache", {})
1121 revsetcache = cache.setdefault("revsetcache", {})
1126 if raw in revsetcache:
1122 if raw in revsetcache:
1127 revs = revsetcache[raw]
1123 revs = revsetcache[raw]
1128 else:
1124 else:
1129 revs = query(raw)
1125 revs = query(raw)
1130 revs = list(revs)
1126 revs = list(revs)
1131 revsetcache[raw] = revs
1127 revsetcache[raw] = revs
1132 return templatekw.showrevslist(context, mapping, "revision", revs)
1128 return templatekw.showrevslist(context, mapping, "revision", revs)
1133
1129
1134 @templatefunc('rstdoc(text, style)')
1130 @templatefunc('rstdoc(text, style)')
1135 def rstdoc(context, mapping, args):
1131 def rstdoc(context, mapping, args):
1136 """Format reStructuredText."""
1132 """Format reStructuredText."""
1137 if len(args) != 2:
1133 if len(args) != 2:
1138 # i18n: "rstdoc" is a keyword
1134 # i18n: "rstdoc" is a keyword
1139 raise error.ParseError(_("rstdoc expects two arguments"))
1135 raise error.ParseError(_("rstdoc expects two arguments"))
1140
1136
1141 text = evalstring(context, mapping, args[0])
1137 text = evalstring(context, mapping, args[0])
1142 style = evalstring(context, mapping, args[1])
1138 style = evalstring(context, mapping, args[1])
1143
1139
1144 return minirst.format(text, style=style, keep=['verbose'])
1140 return minirst.format(text, style=style, keep=['verbose'])
1145
1141
1146 @templatefunc('separate(sep, args)', argspec='sep *args')
1142 @templatefunc('separate(sep, args)', argspec='sep *args')
1147 def separate(context, mapping, args):
1143 def separate(context, mapping, args):
1148 """Add a separator between non-empty arguments."""
1144 """Add a separator between non-empty arguments."""
1149 if 'sep' not in args:
1145 if 'sep' not in args:
1150 # i18n: "separate" is a keyword
1146 # i18n: "separate" is a keyword
1151 raise error.ParseError(_("separate expects at least one argument"))
1147 raise error.ParseError(_("separate expects at least one argument"))
1152
1148
1153 sep = evalstring(context, mapping, args['sep'])
1149 sep = evalstring(context, mapping, args['sep'])
1154 first = True
1150 first = True
1155 for arg in args['args']:
1151 for arg in args['args']:
1156 argstr = evalstring(context, mapping, arg)
1152 argstr = evalstring(context, mapping, arg)
1157 if not argstr:
1153 if not argstr:
1158 continue
1154 continue
1159 if first:
1155 if first:
1160 first = False
1156 first = False
1161 else:
1157 else:
1162 yield sep
1158 yield sep
1163 yield argstr
1159 yield argstr
1164
1160
1165 @templatefunc('shortest(node, minlength=4)')
1161 @templatefunc('shortest(node, minlength=4)')
1166 def shortest(context, mapping, args):
1162 def shortest(context, mapping, args):
1167 """Obtain the shortest representation of
1163 """Obtain the shortest representation of
1168 a node."""
1164 a node."""
1169 if not (1 <= len(args) <= 2):
1165 if not (1 <= len(args) <= 2):
1170 # i18n: "shortest" is a keyword
1166 # i18n: "shortest" is a keyword
1171 raise error.ParseError(_("shortest() expects one or two arguments"))
1167 raise error.ParseError(_("shortest() expects one or two arguments"))
1172
1168
1173 node = evalstring(context, mapping, args[0])
1169 node = evalstring(context, mapping, args[0])
1174
1170
1175 minlength = 4
1171 minlength = 4
1176 if len(args) > 1:
1172 if len(args) > 1:
1177 minlength = evalinteger(context, mapping, args[1],
1173 minlength = evalinteger(context, mapping, args[1],
1178 # i18n: "shortest" is a keyword
1174 # i18n: "shortest" is a keyword
1179 _("shortest() expects an integer minlength"))
1175 _("shortest() expects an integer minlength"))
1180
1176
1181 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1177 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1182 # which would be unacceptably slow. so we look for hash collision in
1178 # which would be unacceptably slow. so we look for hash collision in
1183 # unfiltered space, which means some hashes may be slightly longer.
1179 # unfiltered space, which means some hashes may be slightly longer.
1184 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1180 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1185 return cl.shortest(node, minlength)
1181 return cl.shortest(node, minlength)
1186
1182
1187 @templatefunc('strip(text[, chars])')
1183 @templatefunc('strip(text[, chars])')
1188 def strip(context, mapping, args):
1184 def strip(context, mapping, args):
1189 """Strip characters from a string. By default,
1185 """Strip characters from a string. By default,
1190 strips all leading and trailing whitespace."""
1186 strips all leading and trailing whitespace."""
1191 if not (1 <= len(args) <= 2):
1187 if not (1 <= len(args) <= 2):
1192 # i18n: "strip" is a keyword
1188 # i18n: "strip" is a keyword
1193 raise error.ParseError(_("strip expects one or two arguments"))
1189 raise error.ParseError(_("strip expects one or two arguments"))
1194
1190
1195 text = evalstring(context, mapping, args[0])
1191 text = evalstring(context, mapping, args[0])
1196 if len(args) == 2:
1192 if len(args) == 2:
1197 chars = evalstring(context, mapping, args[1])
1193 chars = evalstring(context, mapping, args[1])
1198 return text.strip(chars)
1194 return text.strip(chars)
1199 return text.strip()
1195 return text.strip()
1200
1196
1201 @templatefunc('sub(pattern, replacement, expression)')
1197 @templatefunc('sub(pattern, replacement, expression)')
1202 def sub(context, mapping, args):
1198 def sub(context, mapping, args):
1203 """Perform text substitution
1199 """Perform text substitution
1204 using regular expressions."""
1200 using regular expressions."""
1205 if len(args) != 3:
1201 if len(args) != 3:
1206 # i18n: "sub" is a keyword
1202 # i18n: "sub" is a keyword
1207 raise error.ParseError(_("sub expects three arguments"))
1203 raise error.ParseError(_("sub expects three arguments"))
1208
1204
1209 pat = evalstring(context, mapping, args[0])
1205 pat = evalstring(context, mapping, args[0])
1210 rpl = evalstring(context, mapping, args[1])
1206 rpl = evalstring(context, mapping, args[1])
1211 src = evalstring(context, mapping, args[2])
1207 src = evalstring(context, mapping, args[2])
1212 try:
1208 try:
1213 patre = re.compile(pat)
1209 patre = re.compile(pat)
1214 except re.error:
1210 except re.error:
1215 # i18n: "sub" is a keyword
1211 # i18n: "sub" is a keyword
1216 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1212 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1217 try:
1213 try:
1218 yield patre.sub(rpl, src)
1214 yield patre.sub(rpl, src)
1219 except re.error:
1215 except re.error:
1220 # i18n: "sub" is a keyword
1216 # i18n: "sub" is a keyword
1221 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1217 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1222
1218
1223 @templatefunc('startswith(pattern, text)')
1219 @templatefunc('startswith(pattern, text)')
1224 def startswith(context, mapping, args):
1220 def startswith(context, mapping, args):
1225 """Returns the value from the "text" argument
1221 """Returns the value from the "text" argument
1226 if it begins with the content from the "pattern" argument."""
1222 if it begins with the content from the "pattern" argument."""
1227 if len(args) != 2:
1223 if len(args) != 2:
1228 # i18n: "startswith" is a keyword
1224 # i18n: "startswith" is a keyword
1229 raise error.ParseError(_("startswith expects two arguments"))
1225 raise error.ParseError(_("startswith expects two arguments"))
1230
1226
1231 patn = evalstring(context, mapping, args[0])
1227 patn = evalstring(context, mapping, args[0])
1232 text = evalstring(context, mapping, args[1])
1228 text = evalstring(context, mapping, args[1])
1233 if text.startswith(patn):
1229 if text.startswith(patn):
1234 return text
1230 return text
1235 return ''
1231 return ''
1236
1232
1237 @templatefunc('word(number, text[, separator])')
1233 @templatefunc('word(number, text[, separator])')
1238 def word(context, mapping, args):
1234 def word(context, mapping, args):
1239 """Return the nth word from a string."""
1235 """Return the nth word from a string."""
1240 if not (2 <= len(args) <= 3):
1236 if not (2 <= len(args) <= 3):
1241 # i18n: "word" is a keyword
1237 # i18n: "word" is a keyword
1242 raise error.ParseError(_("word expects two or three arguments, got %d")
1238 raise error.ParseError(_("word expects two or three arguments, got %d")
1243 % len(args))
1239 % len(args))
1244
1240
1245 num = evalinteger(context, mapping, args[0],
1241 num = evalinteger(context, mapping, args[0],
1246 # i18n: "word" is a keyword
1242 # i18n: "word" is a keyword
1247 _("word expects an integer index"))
1243 _("word expects an integer index"))
1248 text = evalstring(context, mapping, args[1])
1244 text = evalstring(context, mapping, args[1])
1249 if len(args) == 3:
1245 if len(args) == 3:
1250 splitter = evalstring(context, mapping, args[2])
1246 splitter = evalstring(context, mapping, args[2])
1251 else:
1247 else:
1252 splitter = None
1248 splitter = None
1253
1249
1254 tokens = text.split(splitter)
1250 tokens = text.split(splitter)
1255 if num >= len(tokens) or num < -len(tokens):
1251 if num >= len(tokens) or num < -len(tokens):
1256 return ''
1252 return ''
1257 else:
1253 else:
1258 return tokens[num]
1254 return tokens[num]
1259
1255
1260 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1256 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1261 exprmethods = {
1257 exprmethods = {
1262 "integer": lambda e, c: (runinteger, e[1]),
1258 "integer": lambda e, c: (runinteger, e[1]),
1263 "string": lambda e, c: (runstring, e[1]),
1259 "string": lambda e, c: (runstring, e[1]),
1264 "symbol": lambda e, c: (runsymbol, e[1]),
1260 "symbol": lambda e, c: (runsymbol, e[1]),
1265 "template": buildtemplate,
1261 "template": buildtemplate,
1266 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1262 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1267 ".": buildmember,
1263 ".": buildmember,
1268 "|": buildfilter,
1264 "|": buildfilter,
1269 "%": buildmap,
1265 "%": buildmap,
1270 "func": buildfunc,
1266 "func": buildfunc,
1271 "keyvalue": buildkeyvaluepair,
1267 "keyvalue": buildkeyvaluepair,
1272 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1268 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1273 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1269 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1274 "negate": buildnegate,
1270 "negate": buildnegate,
1275 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1271 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1276 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1272 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1277 }
1273 }
1278
1274
1279 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1275 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1280 methods = exprmethods.copy()
1276 methods = exprmethods.copy()
1281 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1277 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1282
1278
1283 class _aliasrules(parser.basealiasrules):
1279 class _aliasrules(parser.basealiasrules):
1284 """Parsing and expansion rule set of template aliases"""
1280 """Parsing and expansion rule set of template aliases"""
1285 _section = _('template alias')
1281 _section = _('template alias')
1286 _parse = staticmethod(_parseexpr)
1282 _parse = staticmethod(_parseexpr)
1287
1283
1288 @staticmethod
1284 @staticmethod
1289 def _trygetfunc(tree):
1285 def _trygetfunc(tree):
1290 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1286 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1291 None"""
1287 None"""
1292 if tree[0] == 'func' and tree[1][0] == 'symbol':
1288 if tree[0] == 'func' and tree[1][0] == 'symbol':
1293 return tree[1][1], getlist(tree[2])
1289 return tree[1][1], getlist(tree[2])
1294 if tree[0] == '|' and tree[2][0] == 'symbol':
1290 if tree[0] == '|' and tree[2][0] == 'symbol':
1295 return tree[2][1], [tree[1]]
1291 return tree[2][1], [tree[1]]
1296
1292
1297 def expandaliases(tree, aliases):
1293 def expandaliases(tree, aliases):
1298 """Return new tree of aliases are expanded"""
1294 """Return new tree of aliases are expanded"""
1299 aliasmap = _aliasrules.buildmap(aliases)
1295 aliasmap = _aliasrules.buildmap(aliases)
1300 return _aliasrules.expand(aliasmap, tree)
1296 return _aliasrules.expand(aliasmap, tree)
1301
1297
1302 # template engine
1298 # template engine
1303
1299
1304 stringify = templatefilters.stringify
1300 stringify = templatefilters.stringify
1305
1301
1306 def _flatten(thing):
1302 def _flatten(thing):
1307 '''yield a single stream from a possibly nested set of iterators'''
1303 '''yield a single stream from a possibly nested set of iterators'''
1308 thing = templatekw.unwraphybrid(thing)
1304 thing = templatekw.unwraphybrid(thing)
1309 if isinstance(thing, bytes):
1305 if isinstance(thing, bytes):
1310 yield thing
1306 yield thing
1311 elif isinstance(thing, str):
1307 elif isinstance(thing, str):
1312 # We can only hit this on Python 3, and it's here to guard
1308 # We can only hit this on Python 3, and it's here to guard
1313 # against infinite recursion.
1309 # against infinite recursion.
1314 raise error.ProgrammingError('Mercurial IO including templates is done'
1310 raise error.ProgrammingError('Mercurial IO including templates is done'
1315 ' with bytes, not strings')
1311 ' with bytes, not strings')
1316 elif thing is None:
1312 elif thing is None:
1317 pass
1313 pass
1318 elif not util.safehasattr(thing, '__iter__'):
1314 elif not util.safehasattr(thing, '__iter__'):
1319 yield pycompat.bytestr(thing)
1315 yield pycompat.bytestr(thing)
1320 else:
1316 else:
1321 for i in thing:
1317 for i in thing:
1322 i = templatekw.unwraphybrid(i)
1318 i = templatekw.unwraphybrid(i)
1323 if isinstance(i, bytes):
1319 if isinstance(i, bytes):
1324 yield i
1320 yield i
1325 elif i is None:
1321 elif i is None:
1326 pass
1322 pass
1327 elif not util.safehasattr(i, '__iter__'):
1323 elif not util.safehasattr(i, '__iter__'):
1328 yield pycompat.bytestr(i)
1324 yield pycompat.bytestr(i)
1329 else:
1325 else:
1330 for j in _flatten(i):
1326 for j in _flatten(i):
1331 yield j
1327 yield j
1332
1328
1333 def unquotestring(s):
1329 def unquotestring(s):
1334 '''unwrap quotes if any; otherwise returns unmodified string'''
1330 '''unwrap quotes if any; otherwise returns unmodified string'''
1335 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1331 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1336 return s
1332 return s
1337 return s[1:-1]
1333 return s[1:-1]
1338
1334
1339 class engine(object):
1335 class engine(object):
1340 '''template expansion engine.
1336 '''template expansion engine.
1341
1337
1342 template expansion works like this. a map file contains key=value
1338 template expansion works like this. a map file contains key=value
1343 pairs. if value is quoted, it is treated as string. otherwise, it
1339 pairs. if value is quoted, it is treated as string. otherwise, it
1344 is treated as name of template file.
1340 is treated as name of template file.
1345
1341
1346 templater is asked to expand a key in map. it looks up key, and
1342 templater is asked to expand a key in map. it looks up key, and
1347 looks for strings like this: {foo}. it expands {foo} by looking up
1343 looks for strings like this: {foo}. it expands {foo} by looking up
1348 foo in map, and substituting it. expansion is recursive: it stops
1344 foo in map, and substituting it. expansion is recursive: it stops
1349 when there is no more {foo} to replace.
1345 when there is no more {foo} to replace.
1350
1346
1351 expansion also allows formatting and filtering.
1347 expansion also allows formatting and filtering.
1352
1348
1353 format uses key to expand each item in list. syntax is
1349 format uses key to expand each item in list. syntax is
1354 {key%format}.
1350 {key%format}.
1355
1351
1356 filter uses function to transform value. syntax is
1352 filter uses function to transform value. syntax is
1357 {key|filter1|filter2|...}.'''
1353 {key|filter1|filter2|...}.'''
1358
1354
1359 def __init__(self, loader, filters=None, defaults=None, resources=None,
1355 def __init__(self, loader, filters=None, defaults=None, resources=None,
1360 aliases=()):
1356 aliases=()):
1361 self._loader = loader
1357 self._loader = loader
1362 if filters is None:
1358 if filters is None:
1363 filters = {}
1359 filters = {}
1364 self._filters = filters
1360 self._filters = filters
1365 if defaults is None:
1361 if defaults is None:
1366 defaults = {}
1362 defaults = {}
1367 if resources is None:
1363 if resources is None:
1368 resources = {}
1364 resources = {}
1369 self._defaults = defaults
1365 self._defaults = defaults
1370 self._resources = resources
1366 self._resources = resources
1371 self._aliasmap = _aliasrules.buildmap(aliases)
1367 self._aliasmap = _aliasrules.buildmap(aliases)
1372 self._cache = {} # key: (func, data)
1368 self._cache = {} # key: (func, data)
1373
1369
1374 def symbol(self, mapping, key):
1370 def symbol(self, mapping, key):
1375 """Resolve symbol to value or function; None if nothing found"""
1371 """Resolve symbol to value or function; None if nothing found"""
1376 v = None
1372 v = None
1377 if key not in self._resources:
1373 if key not in self._resources:
1378 v = mapping.get(key)
1374 v = mapping.get(key)
1379 if v is None:
1375 if v is None:
1380 v = self._defaults.get(key)
1376 v = self._defaults.get(key)
1381 return v
1377 return v
1382
1378
1383 def resource(self, mapping, key):
1379 def resource(self, mapping, key):
1384 """Return internal data (e.g. cache) used for keyword/function
1380 """Return internal data (e.g. cache) used for keyword/function
1385 evaluation"""
1381 evaluation"""
1386 v = None
1382 v = None
1387 if key in self._resources:
1383 if key in self._resources:
1388 v = mapping.get(key)
1384 v = mapping.get(key)
1389 if v is None:
1385 if v is None:
1390 v = self._resources.get(key)
1386 v = self._resources.get(key)
1391 if v is None:
1387 if v is None:
1392 raise ResourceUnavailable(_('template resource not available: %s')
1388 raise ResourceUnavailable(_('template resource not available: %s')
1393 % key)
1389 % key)
1394 return v
1390 return v
1395
1391
1396 def _load(self, t):
1392 def _load(self, t):
1397 '''load, parse, and cache a template'''
1393 '''load, parse, and cache a template'''
1398 if t not in self._cache:
1394 if t not in self._cache:
1399 # put poison to cut recursion while compiling 't'
1395 # put poison to cut recursion while compiling 't'
1400 self._cache[t] = (_runrecursivesymbol, t)
1396 self._cache[t] = (_runrecursivesymbol, t)
1401 try:
1397 try:
1402 x = parse(self._loader(t))
1398 x = parse(self._loader(t))
1403 if self._aliasmap:
1399 if self._aliasmap:
1404 x = _aliasrules.expand(self._aliasmap, x)
1400 x = _aliasrules.expand(self._aliasmap, x)
1405 self._cache[t] = compileexp(x, self, methods)
1401 self._cache[t] = compileexp(x, self, methods)
1406 except: # re-raises
1402 except: # re-raises
1407 del self._cache[t]
1403 del self._cache[t]
1408 raise
1404 raise
1409 return self._cache[t]
1405 return self._cache[t]
1410
1406
1411 def process(self, t, mapping):
1407 def process(self, t, mapping):
1412 '''Perform expansion. t is name of map element to expand.
1408 '''Perform expansion. t is name of map element to expand.
1413 mapping contains added elements for use during expansion. Is a
1409 mapping contains added elements for use during expansion. Is a
1414 generator.'''
1410 generator.'''
1415 func, data = self._load(t)
1411 func, data = self._load(t)
1416 return _flatten(func(self, mapping, data))
1412 return _flatten(func(self, mapping, data))
1417
1413
1418 engines = {'default': engine}
1414 engines = {'default': engine}
1419
1415
1420 def stylelist():
1416 def stylelist():
1421 paths = templatepaths()
1417 paths = templatepaths()
1422 if not paths:
1418 if not paths:
1423 return _('no templates found, try `hg debuginstall` for more info')
1419 return _('no templates found, try `hg debuginstall` for more info')
1424 dirlist = os.listdir(paths[0])
1420 dirlist = os.listdir(paths[0])
1425 stylelist = []
1421 stylelist = []
1426 for file in dirlist:
1422 for file in dirlist:
1427 split = file.split(".")
1423 split = file.split(".")
1428 if split[-1] in ('orig', 'rej'):
1424 if split[-1] in ('orig', 'rej'):
1429 continue
1425 continue
1430 if split[0] == "map-cmdline":
1426 if split[0] == "map-cmdline":
1431 stylelist.append(split[1])
1427 stylelist.append(split[1])
1432 return ", ".join(sorted(stylelist))
1428 return ", ".join(sorted(stylelist))
1433
1429
1434 def _readmapfile(mapfile):
1430 def _readmapfile(mapfile):
1435 """Load template elements from the given map file"""
1431 """Load template elements from the given map file"""
1436 if not os.path.exists(mapfile):
1432 if not os.path.exists(mapfile):
1437 raise error.Abort(_("style '%s' not found") % mapfile,
1433 raise error.Abort(_("style '%s' not found") % mapfile,
1438 hint=_("available styles: %s") % stylelist())
1434 hint=_("available styles: %s") % stylelist())
1439
1435
1440 base = os.path.dirname(mapfile)
1436 base = os.path.dirname(mapfile)
1441 conf = config.config(includepaths=templatepaths())
1437 conf = config.config(includepaths=templatepaths())
1442 conf.read(mapfile, remap={'': 'templates'})
1438 conf.read(mapfile, remap={'': 'templates'})
1443
1439
1444 cache = {}
1440 cache = {}
1445 tmap = {}
1441 tmap = {}
1446 aliases = []
1442 aliases = []
1447
1443
1448 val = conf.get('templates', '__base__')
1444 val = conf.get('templates', '__base__')
1449 if val and val[0] not in "'\"":
1445 if val and val[0] not in "'\"":
1450 # treat as a pointer to a base class for this style
1446 # treat as a pointer to a base class for this style
1451 path = util.normpath(os.path.join(base, val))
1447 path = util.normpath(os.path.join(base, val))
1452
1448
1453 # fallback check in template paths
1449 # fallback check in template paths
1454 if not os.path.exists(path):
1450 if not os.path.exists(path):
1455 for p in templatepaths():
1451 for p in templatepaths():
1456 p2 = util.normpath(os.path.join(p, val))
1452 p2 = util.normpath(os.path.join(p, val))
1457 if os.path.isfile(p2):
1453 if os.path.isfile(p2):
1458 path = p2
1454 path = p2
1459 break
1455 break
1460 p3 = util.normpath(os.path.join(p2, "map"))
1456 p3 = util.normpath(os.path.join(p2, "map"))
1461 if os.path.isfile(p3):
1457 if os.path.isfile(p3):
1462 path = p3
1458 path = p3
1463 break
1459 break
1464
1460
1465 cache, tmap, aliases = _readmapfile(path)
1461 cache, tmap, aliases = _readmapfile(path)
1466
1462
1467 for key, val in conf['templates'].items():
1463 for key, val in conf['templates'].items():
1468 if not val:
1464 if not val:
1469 raise error.ParseError(_('missing value'),
1465 raise error.ParseError(_('missing value'),
1470 conf.source('templates', key))
1466 conf.source('templates', key))
1471 if val[0] in "'\"":
1467 if val[0] in "'\"":
1472 if val[0] != val[-1]:
1468 if val[0] != val[-1]:
1473 raise error.ParseError(_('unmatched quotes'),
1469 raise error.ParseError(_('unmatched quotes'),
1474 conf.source('templates', key))
1470 conf.source('templates', key))
1475 cache[key] = unquotestring(val)
1471 cache[key] = unquotestring(val)
1476 elif key != '__base__':
1472 elif key != '__base__':
1477 val = 'default', val
1473 val = 'default', val
1478 if ':' in val[1]:
1474 if ':' in val[1]:
1479 val = val[1].split(':', 1)
1475 val = val[1].split(':', 1)
1480 tmap[key] = val[0], os.path.join(base, val[1])
1476 tmap[key] = val[0], os.path.join(base, val[1])
1481 aliases.extend(conf['templatealias'].items())
1477 aliases.extend(conf['templatealias'].items())
1482 return cache, tmap, aliases
1478 return cache, tmap, aliases
1483
1479
1484 class templater(object):
1480 class templater(object):
1485
1481
1486 def __init__(self, filters=None, defaults=None, resources=None,
1482 def __init__(self, filters=None, defaults=None, resources=None,
1487 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1483 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1488 """Create template engine optionally with preloaded template fragments
1484 """Create template engine optionally with preloaded template fragments
1489
1485
1490 - ``filters``: a dict of functions to transform a value into another.
1486 - ``filters``: a dict of functions to transform a value into another.
1491 - ``defaults``: a dict of symbol values/functions; may be overridden
1487 - ``defaults``: a dict of symbol values/functions; may be overridden
1492 by a ``mapping`` dict.
1488 by a ``mapping`` dict.
1493 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1489 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1494 from user template; may be overridden by a ``mapping`` dict.
1490 from user template; may be overridden by a ``mapping`` dict.
1495 - ``cache``: a dict of preloaded template fragments.
1491 - ``cache``: a dict of preloaded template fragments.
1496 - ``aliases``: a list of alias (name, replacement) pairs.
1492 - ``aliases``: a list of alias (name, replacement) pairs.
1497
1493
1498 self.cache may be updated later to register additional template
1494 self.cache may be updated later to register additional template
1499 fragments.
1495 fragments.
1500 """
1496 """
1501 if filters is None:
1497 if filters is None:
1502 filters = {}
1498 filters = {}
1503 if defaults is None:
1499 if defaults is None:
1504 defaults = {}
1500 defaults = {}
1505 if resources is None:
1501 if resources is None:
1506 resources = {}
1502 resources = {}
1507 if cache is None:
1503 if cache is None:
1508 cache = {}
1504 cache = {}
1509 self.cache = cache.copy()
1505 self.cache = cache.copy()
1510 self.map = {}
1506 self.map = {}
1511 self.filters = templatefilters.filters.copy()
1507 self.filters = templatefilters.filters.copy()
1512 self.filters.update(filters)
1508 self.filters.update(filters)
1513 self.defaults = defaults
1509 self.defaults = defaults
1514 self._resources = {'templ': self}
1510 self._resources = {'templ': self}
1515 self._resources.update(resources)
1511 self._resources.update(resources)
1516 self._aliases = aliases
1512 self._aliases = aliases
1517 self.minchunk, self.maxchunk = minchunk, maxchunk
1513 self.minchunk, self.maxchunk = minchunk, maxchunk
1518 self.ecache = {}
1514 self.ecache = {}
1519
1515
1520 @classmethod
1516 @classmethod
1521 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1517 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1522 cache=None, minchunk=1024, maxchunk=65536):
1518 cache=None, minchunk=1024, maxchunk=65536):
1523 """Create templater from the specified map file"""
1519 """Create templater from the specified map file"""
1524 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1520 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1525 cache, tmap, aliases = _readmapfile(mapfile)
1521 cache, tmap, aliases = _readmapfile(mapfile)
1526 t.cache.update(cache)
1522 t.cache.update(cache)
1527 t.map = tmap
1523 t.map = tmap
1528 t._aliases = aliases
1524 t._aliases = aliases
1529 return t
1525 return t
1530
1526
1531 def __contains__(self, key):
1527 def __contains__(self, key):
1532 return key in self.cache or key in self.map
1528 return key in self.cache or key in self.map
1533
1529
1534 def load(self, t):
1530 def load(self, t):
1535 '''Get the template for the given template name. Use a local cache.'''
1531 '''Get the template for the given template name. Use a local cache.'''
1536 if t not in self.cache:
1532 if t not in self.cache:
1537 try:
1533 try:
1538 self.cache[t] = util.readfile(self.map[t][1])
1534 self.cache[t] = util.readfile(self.map[t][1])
1539 except KeyError as inst:
1535 except KeyError as inst:
1540 raise TemplateNotFound(_('"%s" not in template map') %
1536 raise TemplateNotFound(_('"%s" not in template map') %
1541 inst.args[0])
1537 inst.args[0])
1542 except IOError as inst:
1538 except IOError as inst:
1543 reason = (_('template file %s: %s')
1539 reason = (_('template file %s: %s')
1544 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1540 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1545 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1541 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1546 return self.cache[t]
1542 return self.cache[t]
1547
1543
1548 def render(self, mapping):
1544 def render(self, mapping):
1549 """Render the default unnamed template and return result as string"""
1545 """Render the default unnamed template and return result as string"""
1550 mapping = pycompat.strkwargs(mapping)
1546 mapping = pycompat.strkwargs(mapping)
1551 return stringify(self('', **mapping))
1547 return stringify(self('', **mapping))
1552
1548
1553 def __call__(self, t, **mapping):
1549 def __call__(self, t, **mapping):
1554 mapping = pycompat.byteskwargs(mapping)
1550 mapping = pycompat.byteskwargs(mapping)
1555 ttype = t in self.map and self.map[t][0] or 'default'
1551 ttype = t in self.map and self.map[t][0] or 'default'
1556 if ttype not in self.ecache:
1552 if ttype not in self.ecache:
1557 try:
1553 try:
1558 ecls = engines[ttype]
1554 ecls = engines[ttype]
1559 except KeyError:
1555 except KeyError:
1560 raise error.Abort(_('invalid template engine: %s') % ttype)
1556 raise error.Abort(_('invalid template engine: %s') % ttype)
1561 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1557 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1562 self._resources, self._aliases)
1558 self._resources, self._aliases)
1563 proc = self.ecache[ttype]
1559 proc = self.ecache[ttype]
1564
1560
1565 stream = proc.process(t, mapping)
1561 stream = proc.process(t, mapping)
1566 if self.minchunk:
1562 if self.minchunk:
1567 stream = util.increasingchunks(stream, min=self.minchunk,
1563 stream = util.increasingchunks(stream, min=self.minchunk,
1568 max=self.maxchunk)
1564 max=self.maxchunk)
1569 return stream
1565 return stream
1570
1566
1571 def templatepaths():
1567 def templatepaths():
1572 '''return locations used for template files.'''
1568 '''return locations used for template files.'''
1573 pathsrel = ['templates']
1569 pathsrel = ['templates']
1574 paths = [os.path.normpath(os.path.join(util.datapath, f))
1570 paths = [os.path.normpath(os.path.join(util.datapath, f))
1575 for f in pathsrel]
1571 for f in pathsrel]
1576 return [p for p in paths if os.path.isdir(p)]
1572 return [p for p in paths if os.path.isdir(p)]
1577
1573
1578 def templatepath(name):
1574 def templatepath(name):
1579 '''return location of template file. returns None if not found.'''
1575 '''return location of template file. returns None if not found.'''
1580 for p in templatepaths():
1576 for p in templatepaths():
1581 f = os.path.join(p, name)
1577 f = os.path.join(p, name)
1582 if os.path.exists(f):
1578 if os.path.exists(f):
1583 return f
1579 return f
1584 return None
1580 return None
1585
1581
1586 def stylemap(styles, paths=None):
1582 def stylemap(styles, paths=None):
1587 """Return path to mapfile for a given style.
1583 """Return path to mapfile for a given style.
1588
1584
1589 Searches mapfile in the following locations:
1585 Searches mapfile in the following locations:
1590 1. templatepath/style/map
1586 1. templatepath/style/map
1591 2. templatepath/map-style
1587 2. templatepath/map-style
1592 3. templatepath/map
1588 3. templatepath/map
1593 """
1589 """
1594
1590
1595 if paths is None:
1591 if paths is None:
1596 paths = templatepaths()
1592 paths = templatepaths()
1597 elif isinstance(paths, bytes):
1593 elif isinstance(paths, bytes):
1598 paths = [paths]
1594 paths = [paths]
1599
1595
1600 if isinstance(styles, bytes):
1596 if isinstance(styles, bytes):
1601 styles = [styles]
1597 styles = [styles]
1602
1598
1603 for style in styles:
1599 for style in styles:
1604 # only plain name is allowed to honor template paths
1600 # only plain name is allowed to honor template paths
1605 if (not style
1601 if (not style
1606 or style in (os.curdir, os.pardir)
1602 or style in (os.curdir, os.pardir)
1607 or pycompat.ossep in style
1603 or pycompat.ossep in style
1608 or pycompat.osaltsep and pycompat.osaltsep in style):
1604 or pycompat.osaltsep and pycompat.osaltsep in style):
1609 continue
1605 continue
1610 locations = [os.path.join(style, 'map'), 'map-' + style]
1606 locations = [os.path.join(style, 'map'), 'map-' + style]
1611 locations.append('map')
1607 locations.append('map')
1612
1608
1613 for path in paths:
1609 for path in paths:
1614 for location in locations:
1610 for location in locations:
1615 mapfile = os.path.join(path, location)
1611 mapfile = os.path.join(path, location)
1616 if os.path.isfile(mapfile):
1612 if os.path.isfile(mapfile):
1617 return style, mapfile
1613 return style, mapfile
1618
1614
1619 raise RuntimeError("No hgweb templates found in %r" % paths)
1615 raise RuntimeError("No hgweb templates found in %r" % paths)
1620
1616
1621 def loadfunction(ui, extname, registrarobj):
1617 def loadfunction(ui, extname, registrarobj):
1622 """Load template function from specified registrarobj
1618 """Load template function from specified registrarobj
1623 """
1619 """
1624 for name, func in registrarobj._table.iteritems():
1620 for name, func in registrarobj._table.iteritems():
1625 funcs[name] = func
1621 funcs[name] = func
1626
1622
1627 # tell hggettext to extract docstrings from these functions:
1623 # tell hggettext to extract docstrings from these functions:
1628 i18nfunctions = funcs.values()
1624 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now