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