##// END OF EJS Templates
templater: drop 'templ' from resources dict...
Yuya Nishihara -
r37088:1101d674 default
parent child Browse files
Show More
@@ -1,809 +1,798 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 i18n,
20 i18n,
21 obsutil,
21 obsutil,
22 patch,
22 patch,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 scmutil,
25 scmutil,
26 templateutil,
26 templateutil,
27 util,
27 util,
28 )
28 )
29
29
30 _hybrid = templateutil.hybrid
30 _hybrid = templateutil.hybrid
31 _mappable = templateutil.mappable
31 _mappable = templateutil.mappable
32 hybriddict = templateutil.hybriddict
32 hybriddict = templateutil.hybriddict
33 hybridlist = templateutil.hybridlist
33 hybridlist = templateutil.hybridlist
34 compatdict = templateutil.compatdict
34 compatdict = templateutil.compatdict
35 compatlist = templateutil.compatlist
35 compatlist = templateutil.compatlist
36 _showcompatlist = templateutil._showcompatlist
36 _showcompatlist = templateutil._showcompatlist
37
37
38 # TODO: temporary hack for porting; will be removed soon
39 class _fakecontextwrapper(object):
40 def __init__(self, templ):
41 self._templ = templ
42
43 def preload(self, t):
44 return t in self._templ
45
46 def process(self, t, mapping):
47 return self._templ.generatenamed(t, mapping)
48
49 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
38 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
50 context = _fakecontextwrapper(templ)
39 context = templ # this is actually a template context, not a templater
51 return _showcompatlist(context, mapping, name, values, plural, separator)
40 return _showcompatlist(context, mapping, name, values, plural, separator)
52
41
53 def showdict(name, data, mapping, plural=None, key='key', value='value',
42 def showdict(name, data, mapping, plural=None, key='key', value='value',
54 fmt=None, separator=' '):
43 fmt=None, separator=' '):
55 ui = mapping.get('ui')
44 ui = mapping.get('ui')
56 if ui:
45 if ui:
57 ui.deprecwarn("templatekw.showdict() is deprecated, use "
46 ui.deprecwarn("templatekw.showdict() is deprecated, use "
58 "templateutil.compatdict()", '4.6')
47 "templateutil.compatdict()", '4.6')
59 c = [{key: k, value: v} for k, v in data.iteritems()]
48 c = [{key: k, value: v} for k, v in data.iteritems()]
60 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
49 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
61 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
50 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
62
51
63 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
52 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
64 ui = mapping.get('ui')
53 ui = mapping.get('ui')
65 if ui:
54 if ui:
66 ui.deprecwarn("templatekw.showlist() is deprecated, use "
55 ui.deprecwarn("templatekw.showlist() is deprecated, use "
67 "templateutil.compatlist()", '4.6')
56 "templateutil.compatlist()", '4.6')
68 if not element:
57 if not element:
69 element = name
58 element = name
70 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
59 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
71 return hybridlist(values, name=element, gen=f)
60 return hybridlist(values, name=element, gen=f)
72
61
73 def getlatesttags(context, mapping, pattern=None):
62 def getlatesttags(context, mapping, pattern=None):
74 '''return date, distance and name for the latest tag of rev'''
63 '''return date, distance and name for the latest tag of rev'''
75 repo = context.resource(mapping, 'repo')
64 repo = context.resource(mapping, 'repo')
76 ctx = context.resource(mapping, 'ctx')
65 ctx = context.resource(mapping, 'ctx')
77 cache = context.resource(mapping, 'cache')
66 cache = context.resource(mapping, 'cache')
78
67
79 cachename = 'latesttags'
68 cachename = 'latesttags'
80 if pattern is not None:
69 if pattern is not None:
81 cachename += '-' + pattern
70 cachename += '-' + pattern
82 match = util.stringmatcher(pattern)[2]
71 match = util.stringmatcher(pattern)[2]
83 else:
72 else:
84 match = util.always
73 match = util.always
85
74
86 if cachename not in cache:
75 if cachename not in cache:
87 # Cache mapping from rev to a tuple with tag date, tag
76 # Cache mapping from rev to a tuple with tag date, tag
88 # distance and tag name
77 # distance and tag name
89 cache[cachename] = {-1: (0, 0, ['null'])}
78 cache[cachename] = {-1: (0, 0, ['null'])}
90 latesttags = cache[cachename]
79 latesttags = cache[cachename]
91
80
92 rev = ctx.rev()
81 rev = ctx.rev()
93 todo = [rev]
82 todo = [rev]
94 while todo:
83 while todo:
95 rev = todo.pop()
84 rev = todo.pop()
96 if rev in latesttags:
85 if rev in latesttags:
97 continue
86 continue
98 ctx = repo[rev]
87 ctx = repo[rev]
99 tags = [t for t in ctx.tags()
88 tags = [t for t in ctx.tags()
100 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
89 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
101 and match(t))]
90 and match(t))]
102 if tags:
91 if tags:
103 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
92 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
104 continue
93 continue
105 try:
94 try:
106 ptags = [latesttags[p.rev()] for p in ctx.parents()]
95 ptags = [latesttags[p.rev()] for p in ctx.parents()]
107 if len(ptags) > 1:
96 if len(ptags) > 1:
108 if ptags[0][2] == ptags[1][2]:
97 if ptags[0][2] == ptags[1][2]:
109 # The tuples are laid out so the right one can be found by
98 # The tuples are laid out so the right one can be found by
110 # comparison in this case.
99 # comparison in this case.
111 pdate, pdist, ptag = max(ptags)
100 pdate, pdist, ptag = max(ptags)
112 else:
101 else:
113 def key(x):
102 def key(x):
114 changessincetag = len(repo.revs('only(%d, %s)',
103 changessincetag = len(repo.revs('only(%d, %s)',
115 ctx.rev(), x[2][0]))
104 ctx.rev(), x[2][0]))
116 # Smallest number of changes since tag wins. Date is
105 # Smallest number of changes since tag wins. Date is
117 # used as tiebreaker.
106 # used as tiebreaker.
118 return [-changessincetag, x[0]]
107 return [-changessincetag, x[0]]
119 pdate, pdist, ptag = max(ptags, key=key)
108 pdate, pdist, ptag = max(ptags, key=key)
120 else:
109 else:
121 pdate, pdist, ptag = ptags[0]
110 pdate, pdist, ptag = ptags[0]
122 except KeyError:
111 except KeyError:
123 # Cache miss - recurse
112 # Cache miss - recurse
124 todo.append(rev)
113 todo.append(rev)
125 todo.extend(p.rev() for p in ctx.parents())
114 todo.extend(p.rev() for p in ctx.parents())
126 continue
115 continue
127 latesttags[rev] = pdate, pdist + 1, ptag
116 latesttags[rev] = pdate, pdist + 1, ptag
128 return latesttags[rev]
117 return latesttags[rev]
129
118
130 def getrenamedfn(repo, endrev=None):
119 def getrenamedfn(repo, endrev=None):
131 rcache = {}
120 rcache = {}
132 if endrev is None:
121 if endrev is None:
133 endrev = len(repo)
122 endrev = len(repo)
134
123
135 def getrenamed(fn, rev):
124 def getrenamed(fn, rev):
136 '''looks up all renames for a file (up to endrev) the first
125 '''looks up all renames for a file (up to endrev) the first
137 time the file is given. It indexes on the changerev and only
126 time the file is given. It indexes on the changerev and only
138 parses the manifest if linkrev != changerev.
127 parses the manifest if linkrev != changerev.
139 Returns rename info for fn at changerev rev.'''
128 Returns rename info for fn at changerev rev.'''
140 if fn not in rcache:
129 if fn not in rcache:
141 rcache[fn] = {}
130 rcache[fn] = {}
142 fl = repo.file(fn)
131 fl = repo.file(fn)
143 for i in fl:
132 for i in fl:
144 lr = fl.linkrev(i)
133 lr = fl.linkrev(i)
145 renamed = fl.renamed(fl.node(i))
134 renamed = fl.renamed(fl.node(i))
146 rcache[fn][lr] = renamed
135 rcache[fn][lr] = renamed
147 if lr >= endrev:
136 if lr >= endrev:
148 break
137 break
149 if rev in rcache[fn]:
138 if rev in rcache[fn]:
150 return rcache[fn][rev]
139 return rcache[fn][rev]
151
140
152 # If linkrev != rev (i.e. rev not found in rcache) fallback to
141 # If linkrev != rev (i.e. rev not found in rcache) fallback to
153 # filectx logic.
142 # filectx logic.
154 try:
143 try:
155 return repo[rev][fn].renamed()
144 return repo[rev][fn].renamed()
156 except error.LookupError:
145 except error.LookupError:
157 return None
146 return None
158
147
159 return getrenamed
148 return getrenamed
160
149
161 def getlogcolumns():
150 def getlogcolumns():
162 """Return a dict of log column labels"""
151 """Return a dict of log column labels"""
163 _ = pycompat.identity # temporarily disable gettext
152 _ = pycompat.identity # temporarily disable gettext
164 # i18n: column positioning for "hg log"
153 # i18n: column positioning for "hg log"
165 columns = _('bookmark: %s\n'
154 columns = _('bookmark: %s\n'
166 'branch: %s\n'
155 'branch: %s\n'
167 'changeset: %s\n'
156 'changeset: %s\n'
168 'copies: %s\n'
157 'copies: %s\n'
169 'date: %s\n'
158 'date: %s\n'
170 'extra: %s=%s\n'
159 'extra: %s=%s\n'
171 'files+: %s\n'
160 'files+: %s\n'
172 'files-: %s\n'
161 'files-: %s\n'
173 'files: %s\n'
162 'files: %s\n'
174 'instability: %s\n'
163 'instability: %s\n'
175 'manifest: %s\n'
164 'manifest: %s\n'
176 'obsolete: %s\n'
165 'obsolete: %s\n'
177 'parent: %s\n'
166 'parent: %s\n'
178 'phase: %s\n'
167 'phase: %s\n'
179 'summary: %s\n'
168 'summary: %s\n'
180 'tag: %s\n'
169 'tag: %s\n'
181 'user: %s\n')
170 'user: %s\n')
182 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
171 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
183 i18n._(columns).splitlines(True)))
172 i18n._(columns).splitlines(True)))
184
173
185 # default templates internally used for rendering of lists
174 # default templates internally used for rendering of lists
186 defaulttempl = {
175 defaulttempl = {
187 'parent': '{rev}:{node|formatnode} ',
176 'parent': '{rev}:{node|formatnode} ',
188 'manifest': '{rev}:{node|formatnode}',
177 'manifest': '{rev}:{node|formatnode}',
189 'file_copy': '{name} ({source})',
178 'file_copy': '{name} ({source})',
190 'envvar': '{key}={value}',
179 'envvar': '{key}={value}',
191 'extra': '{key}={value|stringescape}'
180 'extra': '{key}={value|stringescape}'
192 }
181 }
193 # filecopy is preserved for compatibility reasons
182 # filecopy is preserved for compatibility reasons
194 defaulttempl['filecopy'] = defaulttempl['file_copy']
183 defaulttempl['filecopy'] = defaulttempl['file_copy']
195
184
196 # keywords are callables (see registrar.templatekeyword for details)
185 # keywords are callables (see registrar.templatekeyword for details)
197 keywords = {}
186 keywords = {}
198 templatekeyword = registrar.templatekeyword(keywords)
187 templatekeyword = registrar.templatekeyword(keywords)
199
188
200 @templatekeyword('author', requires={'ctx'})
189 @templatekeyword('author', requires={'ctx'})
201 def showauthor(context, mapping):
190 def showauthor(context, mapping):
202 """String. The unmodified author of the changeset."""
191 """String. The unmodified author of the changeset."""
203 ctx = context.resource(mapping, 'ctx')
192 ctx = context.resource(mapping, 'ctx')
204 return ctx.user()
193 return ctx.user()
205
194
206 @templatekeyword('bisect', requires={'repo', 'ctx'})
195 @templatekeyword('bisect', requires={'repo', 'ctx'})
207 def showbisect(context, mapping):
196 def showbisect(context, mapping):
208 """String. The changeset bisection status."""
197 """String. The changeset bisection status."""
209 repo = context.resource(mapping, 'repo')
198 repo = context.resource(mapping, 'repo')
210 ctx = context.resource(mapping, 'ctx')
199 ctx = context.resource(mapping, 'ctx')
211 return hbisect.label(repo, ctx.node())
200 return hbisect.label(repo, ctx.node())
212
201
213 @templatekeyword('branch', requires={'ctx'})
202 @templatekeyword('branch', requires={'ctx'})
214 def showbranch(context, mapping):
203 def showbranch(context, mapping):
215 """String. The name of the branch on which the changeset was
204 """String. The name of the branch on which the changeset was
216 committed.
205 committed.
217 """
206 """
218 ctx = context.resource(mapping, 'ctx')
207 ctx = context.resource(mapping, 'ctx')
219 return ctx.branch()
208 return ctx.branch()
220
209
221 @templatekeyword('branches', requires={'ctx'})
210 @templatekeyword('branches', requires={'ctx'})
222 def showbranches(context, mapping):
211 def showbranches(context, mapping):
223 """List of strings. The name of the branch on which the
212 """List of strings. The name of the branch on which the
224 changeset was committed. Will be empty if the branch name was
213 changeset was committed. Will be empty if the branch name was
225 default. (DEPRECATED)
214 default. (DEPRECATED)
226 """
215 """
227 ctx = context.resource(mapping, 'ctx')
216 ctx = context.resource(mapping, 'ctx')
228 branch = ctx.branch()
217 branch = ctx.branch()
229 if branch != 'default':
218 if branch != 'default':
230 return compatlist(context, mapping, 'branch', [branch],
219 return compatlist(context, mapping, 'branch', [branch],
231 plural='branches')
220 plural='branches')
232 return compatlist(context, mapping, 'branch', [], plural='branches')
221 return compatlist(context, mapping, 'branch', [], plural='branches')
233
222
234 @templatekeyword('bookmarks', requires={'repo', 'ctx'})
223 @templatekeyword('bookmarks', requires={'repo', 'ctx'})
235 def showbookmarks(context, mapping):
224 def showbookmarks(context, mapping):
236 """List of strings. Any bookmarks associated with the
225 """List of strings. Any bookmarks associated with the
237 changeset. Also sets 'active', the name of the active bookmark.
226 changeset. Also sets 'active', the name of the active bookmark.
238 """
227 """
239 repo = context.resource(mapping, 'repo')
228 repo = context.resource(mapping, 'repo')
240 ctx = context.resource(mapping, 'ctx')
229 ctx = context.resource(mapping, 'ctx')
241 bookmarks = ctx.bookmarks()
230 bookmarks = ctx.bookmarks()
242 active = repo._activebookmark
231 active = repo._activebookmark
243 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
232 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
244 f = _showcompatlist(context, mapping, 'bookmark', bookmarks)
233 f = _showcompatlist(context, mapping, 'bookmark', bookmarks)
245 return _hybrid(f, bookmarks, makemap, pycompat.identity)
234 return _hybrid(f, bookmarks, makemap, pycompat.identity)
246
235
247 @templatekeyword('children', requires={'ctx'})
236 @templatekeyword('children', requires={'ctx'})
248 def showchildren(context, mapping):
237 def showchildren(context, mapping):
249 """List of strings. The children of the changeset."""
238 """List of strings. The children of the changeset."""
250 ctx = context.resource(mapping, 'ctx')
239 ctx = context.resource(mapping, 'ctx')
251 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
240 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
252 return compatlist(context, mapping, 'children', childrevs, element='child')
241 return compatlist(context, mapping, 'children', childrevs, element='child')
253
242
254 # Deprecated, but kept alive for help generation a purpose.
243 # Deprecated, but kept alive for help generation a purpose.
255 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
244 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
256 def showcurrentbookmark(context, mapping):
245 def showcurrentbookmark(context, mapping):
257 """String. The active bookmark, if it is associated with the changeset.
246 """String. The active bookmark, if it is associated with the changeset.
258 (DEPRECATED)"""
247 (DEPRECATED)"""
259 return showactivebookmark(context, mapping)
248 return showactivebookmark(context, mapping)
260
249
261 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
250 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
262 def showactivebookmark(context, mapping):
251 def showactivebookmark(context, mapping):
263 """String. The active bookmark, if it is associated with the changeset."""
252 """String. The active bookmark, if it is associated with the changeset."""
264 repo = context.resource(mapping, 'repo')
253 repo = context.resource(mapping, 'repo')
265 ctx = context.resource(mapping, 'ctx')
254 ctx = context.resource(mapping, 'ctx')
266 active = repo._activebookmark
255 active = repo._activebookmark
267 if active and active in ctx.bookmarks():
256 if active and active in ctx.bookmarks():
268 return active
257 return active
269 return ''
258 return ''
270
259
271 @templatekeyword('date', requires={'ctx'})
260 @templatekeyword('date', requires={'ctx'})
272 def showdate(context, mapping):
261 def showdate(context, mapping):
273 """Date information. The date when the changeset was committed."""
262 """Date information. The date when the changeset was committed."""
274 ctx = context.resource(mapping, 'ctx')
263 ctx = context.resource(mapping, 'ctx')
275 return ctx.date()
264 return ctx.date()
276
265
277 @templatekeyword('desc', requires={'ctx'})
266 @templatekeyword('desc', requires={'ctx'})
278 def showdescription(context, mapping):
267 def showdescription(context, mapping):
279 """String. The text of the changeset description."""
268 """String. The text of the changeset description."""
280 ctx = context.resource(mapping, 'ctx')
269 ctx = context.resource(mapping, 'ctx')
281 s = ctx.description()
270 s = ctx.description()
282 if isinstance(s, encoding.localstr):
271 if isinstance(s, encoding.localstr):
283 # try hard to preserve utf-8 bytes
272 # try hard to preserve utf-8 bytes
284 return encoding.tolocal(encoding.fromlocal(s).strip())
273 return encoding.tolocal(encoding.fromlocal(s).strip())
285 else:
274 else:
286 return s.strip()
275 return s.strip()
287
276
288 @templatekeyword('diffstat', requires={'ctx'})
277 @templatekeyword('diffstat', requires={'ctx'})
289 def showdiffstat(context, mapping):
278 def showdiffstat(context, mapping):
290 """String. Statistics of changes with the following format:
279 """String. Statistics of changes with the following format:
291 "modified files: +added/-removed lines"
280 "modified files: +added/-removed lines"
292 """
281 """
293 ctx = context.resource(mapping, 'ctx')
282 ctx = context.resource(mapping, 'ctx')
294 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
283 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
295 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
284 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
296 return '%d: +%d/-%d' % (len(stats), adds, removes)
285 return '%d: +%d/-%d' % (len(stats), adds, removes)
297
286
298 @templatekeyword('envvars', requires={'ui'})
287 @templatekeyword('envvars', requires={'ui'})
299 def showenvvars(context, mapping):
288 def showenvvars(context, mapping):
300 """A dictionary of environment variables. (EXPERIMENTAL)"""
289 """A dictionary of environment variables. (EXPERIMENTAL)"""
301 ui = context.resource(mapping, 'ui')
290 ui = context.resource(mapping, 'ui')
302 env = ui.exportableenviron()
291 env = ui.exportableenviron()
303 env = util.sortdict((k, env[k]) for k in sorted(env))
292 env = util.sortdict((k, env[k]) for k in sorted(env))
304 return compatdict(context, mapping, 'envvar', env, plural='envvars')
293 return compatdict(context, mapping, 'envvar', env, plural='envvars')
305
294
306 @templatekeyword('extras', requires={'ctx'})
295 @templatekeyword('extras', requires={'ctx'})
307 def showextras(context, mapping):
296 def showextras(context, mapping):
308 """List of dicts with key, value entries of the 'extras'
297 """List of dicts with key, value entries of the 'extras'
309 field of this changeset."""
298 field of this changeset."""
310 ctx = context.resource(mapping, 'ctx')
299 ctx = context.resource(mapping, 'ctx')
311 extras = ctx.extra()
300 extras = ctx.extra()
312 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
301 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
313 makemap = lambda k: {'key': k, 'value': extras[k]}
302 makemap = lambda k: {'key': k, 'value': extras[k]}
314 c = [makemap(k) for k in extras]
303 c = [makemap(k) for k in extras]
315 f = _showcompatlist(context, mapping, 'extra', c, plural='extras')
304 f = _showcompatlist(context, mapping, 'extra', c, plural='extras')
316 return _hybrid(f, extras, makemap,
305 return _hybrid(f, extras, makemap,
317 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
306 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
318
307
319 def _showfilesbystat(context, mapping, name, index):
308 def _showfilesbystat(context, mapping, name, index):
320 repo = context.resource(mapping, 'repo')
309 repo = context.resource(mapping, 'repo')
321 ctx = context.resource(mapping, 'ctx')
310 ctx = context.resource(mapping, 'ctx')
322 revcache = context.resource(mapping, 'revcache')
311 revcache = context.resource(mapping, 'revcache')
323 if 'files' not in revcache:
312 if 'files' not in revcache:
324 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
313 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
325 files = revcache['files'][index]
314 files = revcache['files'][index]
326 return compatlist(context, mapping, name, files, element='file')
315 return compatlist(context, mapping, name, files, element='file')
327
316
328 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache'})
317 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache'})
329 def showfileadds(context, mapping):
318 def showfileadds(context, mapping):
330 """List of strings. Files added by this changeset."""
319 """List of strings. Files added by this changeset."""
331 return _showfilesbystat(context, mapping, 'file_add', 1)
320 return _showfilesbystat(context, mapping, 'file_add', 1)
332
321
333 @templatekeyword('file_copies',
322 @templatekeyword('file_copies',
334 requires={'repo', 'ctx', 'cache', 'revcache'})
323 requires={'repo', 'ctx', 'cache', 'revcache'})
335 def showfilecopies(context, mapping):
324 def showfilecopies(context, mapping):
336 """List of strings. Files copied in this changeset with
325 """List of strings. Files copied in this changeset with
337 their sources.
326 their sources.
338 """
327 """
339 repo = context.resource(mapping, 'repo')
328 repo = context.resource(mapping, 'repo')
340 ctx = context.resource(mapping, 'ctx')
329 ctx = context.resource(mapping, 'ctx')
341 cache = context.resource(mapping, 'cache')
330 cache = context.resource(mapping, 'cache')
342 copies = context.resource(mapping, 'revcache').get('copies')
331 copies = context.resource(mapping, 'revcache').get('copies')
343 if copies is None:
332 if copies is None:
344 if 'getrenamed' not in cache:
333 if 'getrenamed' not in cache:
345 cache['getrenamed'] = getrenamedfn(repo)
334 cache['getrenamed'] = getrenamedfn(repo)
346 copies = []
335 copies = []
347 getrenamed = cache['getrenamed']
336 getrenamed = cache['getrenamed']
348 for fn in ctx.files():
337 for fn in ctx.files():
349 rename = getrenamed(fn, ctx.rev())
338 rename = getrenamed(fn, ctx.rev())
350 if rename:
339 if rename:
351 copies.append((fn, rename[0]))
340 copies.append((fn, rename[0]))
352
341
353 copies = util.sortdict(copies)
342 copies = util.sortdict(copies)
354 return compatdict(context, mapping, 'file_copy', copies,
343 return compatdict(context, mapping, 'file_copy', copies,
355 key='name', value='source', fmt='%s (%s)',
344 key='name', value='source', fmt='%s (%s)',
356 plural='file_copies')
345 plural='file_copies')
357
346
358 # showfilecopiesswitch() displays file copies only if copy records are
347 # showfilecopiesswitch() displays file copies only if copy records are
359 # provided before calling the templater, usually with a --copies
348 # provided before calling the templater, usually with a --copies
360 # command line switch.
349 # command line switch.
361 @templatekeyword('file_copies_switch', requires={'revcache'})
350 @templatekeyword('file_copies_switch', requires={'revcache'})
362 def showfilecopiesswitch(context, mapping):
351 def showfilecopiesswitch(context, mapping):
363 """List of strings. Like "file_copies" but displayed
352 """List of strings. Like "file_copies" but displayed
364 only if the --copied switch is set.
353 only if the --copied switch is set.
365 """
354 """
366 copies = context.resource(mapping, 'revcache').get('copies') or []
355 copies = context.resource(mapping, 'revcache').get('copies') or []
367 copies = util.sortdict(copies)
356 copies = util.sortdict(copies)
368 return compatdict(context, mapping, 'file_copy', copies,
357 return compatdict(context, mapping, 'file_copy', copies,
369 key='name', value='source', fmt='%s (%s)',
358 key='name', value='source', fmt='%s (%s)',
370 plural='file_copies')
359 plural='file_copies')
371
360
372 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache'})
361 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache'})
373 def showfiledels(context, mapping):
362 def showfiledels(context, mapping):
374 """List of strings. Files removed by this changeset."""
363 """List of strings. Files removed by this changeset."""
375 return _showfilesbystat(context, mapping, 'file_del', 2)
364 return _showfilesbystat(context, mapping, 'file_del', 2)
376
365
377 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache'})
366 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache'})
378 def showfilemods(context, mapping):
367 def showfilemods(context, mapping):
379 """List of strings. Files modified by this changeset."""
368 """List of strings. Files modified by this changeset."""
380 return _showfilesbystat(context, mapping, 'file_mod', 0)
369 return _showfilesbystat(context, mapping, 'file_mod', 0)
381
370
382 @templatekeyword('files', requires={'ctx'})
371 @templatekeyword('files', requires={'ctx'})
383 def showfiles(context, mapping):
372 def showfiles(context, mapping):
384 """List of strings. All files modified, added, or removed by this
373 """List of strings. All files modified, added, or removed by this
385 changeset.
374 changeset.
386 """
375 """
387 ctx = context.resource(mapping, 'ctx')
376 ctx = context.resource(mapping, 'ctx')
388 return compatlist(context, mapping, 'file', ctx.files())
377 return compatlist(context, mapping, 'file', ctx.files())
389
378
390 @templatekeyword('graphnode', requires={'repo', 'ctx'})
379 @templatekeyword('graphnode', requires={'repo', 'ctx'})
391 def showgraphnode(context, mapping):
380 def showgraphnode(context, mapping):
392 """String. The character representing the changeset node in an ASCII
381 """String. The character representing the changeset node in an ASCII
393 revision graph."""
382 revision graph."""
394 repo = context.resource(mapping, 'repo')
383 repo = context.resource(mapping, 'repo')
395 ctx = context.resource(mapping, 'ctx')
384 ctx = context.resource(mapping, 'ctx')
396 return getgraphnode(repo, ctx)
385 return getgraphnode(repo, ctx)
397
386
398 def getgraphnode(repo, ctx):
387 def getgraphnode(repo, ctx):
399 wpnodes = repo.dirstate.parents()
388 wpnodes = repo.dirstate.parents()
400 if wpnodes[1] == nullid:
389 if wpnodes[1] == nullid:
401 wpnodes = wpnodes[:1]
390 wpnodes = wpnodes[:1]
402 if ctx.node() in wpnodes:
391 if ctx.node() in wpnodes:
403 return '@'
392 return '@'
404 elif ctx.obsolete():
393 elif ctx.obsolete():
405 return 'x'
394 return 'x'
406 elif ctx.isunstable():
395 elif ctx.isunstable():
407 return '*'
396 return '*'
408 elif ctx.closesbranch():
397 elif ctx.closesbranch():
409 return '_'
398 return '_'
410 else:
399 else:
411 return 'o'
400 return 'o'
412
401
413 @templatekeyword('graphwidth', requires=())
402 @templatekeyword('graphwidth', requires=())
414 def showgraphwidth(context, mapping):
403 def showgraphwidth(context, mapping):
415 """Integer. The width of the graph drawn by 'log --graph' or zero."""
404 """Integer. The width of the graph drawn by 'log --graph' or zero."""
416 # just hosts documentation; should be overridden by template mapping
405 # just hosts documentation; should be overridden by template mapping
417 return 0
406 return 0
418
407
419 @templatekeyword('index', requires=())
408 @templatekeyword('index', requires=())
420 def showindex(context, mapping):
409 def showindex(context, mapping):
421 """Integer. The current iteration of the loop. (0 indexed)"""
410 """Integer. The current iteration of the loop. (0 indexed)"""
422 # just hosts documentation; should be overridden by template mapping
411 # just hosts documentation; should be overridden by template mapping
423 raise error.Abort(_("can't use index in this context"))
412 raise error.Abort(_("can't use index in this context"))
424
413
425 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache'})
414 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache'})
426 def showlatesttag(context, mapping):
415 def showlatesttag(context, mapping):
427 """List of strings. The global tags on the most recent globally
416 """List of strings. The global tags on the most recent globally
428 tagged ancestor of this changeset. If no such tags exist, the list
417 tagged ancestor of this changeset. If no such tags exist, the list
429 consists of the single string "null".
418 consists of the single string "null".
430 """
419 """
431 return showlatesttags(context, mapping, None)
420 return showlatesttags(context, mapping, None)
432
421
433 def showlatesttags(context, mapping, pattern):
422 def showlatesttags(context, mapping, pattern):
434 """helper method for the latesttag keyword and function"""
423 """helper method for the latesttag keyword and function"""
435 latesttags = getlatesttags(context, mapping, pattern)
424 latesttags = getlatesttags(context, mapping, pattern)
436
425
437 # latesttag[0] is an implementation detail for sorting csets on different
426 # latesttag[0] is an implementation detail for sorting csets on different
438 # branches in a stable manner- it is the date the tagged cset was created,
427 # branches in a stable manner- it is the date the tagged cset was created,
439 # not the date the tag was created. Therefore it isn't made visible here.
428 # not the date the tag was created. Therefore it isn't made visible here.
440 makemap = lambda v: {
429 makemap = lambda v: {
441 'changes': _showchangessincetag,
430 'changes': _showchangessincetag,
442 'distance': latesttags[1],
431 'distance': latesttags[1],
443 'latesttag': v, # BC with {latesttag % '{latesttag}'}
432 'latesttag': v, # BC with {latesttag % '{latesttag}'}
444 'tag': v
433 'tag': v
445 }
434 }
446
435
447 tags = latesttags[2]
436 tags = latesttags[2]
448 f = _showcompatlist(context, mapping, 'latesttag', tags, separator=':')
437 f = _showcompatlist(context, mapping, 'latesttag', tags, separator=':')
449 return _hybrid(f, tags, makemap, pycompat.identity)
438 return _hybrid(f, tags, makemap, pycompat.identity)
450
439
451 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
440 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
452 def showlatesttagdistance(context, mapping):
441 def showlatesttagdistance(context, mapping):
453 """Integer. Longest path to the latest tag."""
442 """Integer. Longest path to the latest tag."""
454 return getlatesttags(context, mapping)[1]
443 return getlatesttags(context, mapping)[1]
455
444
456 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
445 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
457 def showchangessincelatesttag(context, mapping):
446 def showchangessincelatesttag(context, mapping):
458 """Integer. All ancestors not in the latest tag."""
447 """Integer. All ancestors not in the latest tag."""
459 mapping = mapping.copy()
448 mapping = mapping.copy()
460 mapping['tag'] = getlatesttags(context, mapping)[2][0]
449 mapping['tag'] = getlatesttags(context, mapping)[2][0]
461 return _showchangessincetag(context, mapping)
450 return _showchangessincetag(context, mapping)
462
451
463 def _showchangessincetag(context, mapping):
452 def _showchangessincetag(context, mapping):
464 repo = context.resource(mapping, 'repo')
453 repo = context.resource(mapping, 'repo')
465 ctx = context.resource(mapping, 'ctx')
454 ctx = context.resource(mapping, 'ctx')
466 offset = 0
455 offset = 0
467 revs = [ctx.rev()]
456 revs = [ctx.rev()]
468 tag = context.symbol(mapping, 'tag')
457 tag = context.symbol(mapping, 'tag')
469
458
470 # The only() revset doesn't currently support wdir()
459 # The only() revset doesn't currently support wdir()
471 if ctx.rev() is None:
460 if ctx.rev() is None:
472 offset = 1
461 offset = 1
473 revs = [p.rev() for p in ctx.parents()]
462 revs = [p.rev() for p in ctx.parents()]
474
463
475 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
464 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
476
465
477 # teach templater latesttags.changes is switched to (context, mapping) API
466 # teach templater latesttags.changes is switched to (context, mapping) API
478 _showchangessincetag._requires = {'repo', 'ctx'}
467 _showchangessincetag._requires = {'repo', 'ctx'}
479
468
480 @templatekeyword('manifest', requires={'repo', 'ctx'})
469 @templatekeyword('manifest', requires={'repo', 'ctx'})
481 def showmanifest(context, mapping):
470 def showmanifest(context, mapping):
482 repo = context.resource(mapping, 'repo')
471 repo = context.resource(mapping, 'repo')
483 ctx = context.resource(mapping, 'ctx')
472 ctx = context.resource(mapping, 'ctx')
484 mnode = ctx.manifestnode()
473 mnode = ctx.manifestnode()
485 if mnode is None:
474 if mnode is None:
486 # just avoid crash, we might want to use the 'ff...' hash in future
475 # just avoid crash, we might want to use the 'ff...' hash in future
487 return
476 return
488 mrev = repo.manifestlog._revlog.rev(mnode)
477 mrev = repo.manifestlog._revlog.rev(mnode)
489 mhex = hex(mnode)
478 mhex = hex(mnode)
490 mapping = mapping.copy()
479 mapping = mapping.copy()
491 mapping.update({'rev': mrev, 'node': mhex})
480 mapping.update({'rev': mrev, 'node': mhex})
492 f = context.process('manifest', mapping)
481 f = context.process('manifest', mapping)
493 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
482 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
494 # rev and node are completely different from changeset's.
483 # rev and node are completely different from changeset's.
495 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
484 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
496
485
497 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx'})
486 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx'})
498 def showobsfate(context, mapping):
487 def showobsfate(context, mapping):
499 # this function returns a list containing pre-formatted obsfate strings.
488 # this function returns a list containing pre-formatted obsfate strings.
500 #
489 #
501 # This function will be replaced by templates fragments when we will have
490 # This function will be replaced by templates fragments when we will have
502 # the verbosity templatekw available.
491 # the verbosity templatekw available.
503 succsandmarkers = showsuccsandmarkers(context, mapping)
492 succsandmarkers = showsuccsandmarkers(context, mapping)
504
493
505 ui = context.resource(mapping, 'ui')
494 ui = context.resource(mapping, 'ui')
506 values = []
495 values = []
507
496
508 for x in succsandmarkers:
497 for x in succsandmarkers:
509 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
498 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
510
499
511 return compatlist(context, mapping, "fate", values)
500 return compatlist(context, mapping, "fate", values)
512
501
513 def shownames(context, mapping, namespace):
502 def shownames(context, mapping, namespace):
514 """helper method to generate a template keyword for a namespace"""
503 """helper method to generate a template keyword for a namespace"""
515 repo = context.resource(mapping, 'repo')
504 repo = context.resource(mapping, 'repo')
516 ctx = context.resource(mapping, 'ctx')
505 ctx = context.resource(mapping, 'ctx')
517 ns = repo.names[namespace]
506 ns = repo.names[namespace]
518 names = ns.names(repo, ctx.node())
507 names = ns.names(repo, ctx.node())
519 return compatlist(context, mapping, ns.templatename, names,
508 return compatlist(context, mapping, ns.templatename, names,
520 plural=namespace)
509 plural=namespace)
521
510
522 @templatekeyword('namespaces', requires={'repo', 'ctx'})
511 @templatekeyword('namespaces', requires={'repo', 'ctx'})
523 def shownamespaces(context, mapping):
512 def shownamespaces(context, mapping):
524 """Dict of lists. Names attached to this changeset per
513 """Dict of lists. Names attached to this changeset per
525 namespace."""
514 namespace."""
526 repo = context.resource(mapping, 'repo')
515 repo = context.resource(mapping, 'repo')
527 ctx = context.resource(mapping, 'ctx')
516 ctx = context.resource(mapping, 'ctx')
528
517
529 namespaces = util.sortdict()
518 namespaces = util.sortdict()
530 def makensmapfn(ns):
519 def makensmapfn(ns):
531 # 'name' for iterating over namespaces, templatename for local reference
520 # 'name' for iterating over namespaces, templatename for local reference
532 return lambda v: {'name': v, ns.templatename: v}
521 return lambda v: {'name': v, ns.templatename: v}
533
522
534 for k, ns in repo.names.iteritems():
523 for k, ns in repo.names.iteritems():
535 names = ns.names(repo, ctx.node())
524 names = ns.names(repo, ctx.node())
536 f = _showcompatlist(context, mapping, 'name', names)
525 f = _showcompatlist(context, mapping, 'name', names)
537 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
526 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
538
527
539 f = _showcompatlist(context, mapping, 'namespace', list(namespaces))
528 f = _showcompatlist(context, mapping, 'namespace', list(namespaces))
540
529
541 def makemap(ns):
530 def makemap(ns):
542 return {
531 return {
543 'namespace': ns,
532 'namespace': ns,
544 'names': namespaces[ns],
533 'names': namespaces[ns],
545 'builtin': repo.names[ns].builtin,
534 'builtin': repo.names[ns].builtin,
546 'colorname': repo.names[ns].colorname,
535 'colorname': repo.names[ns].colorname,
547 }
536 }
548
537
549 return _hybrid(f, namespaces, makemap, pycompat.identity)
538 return _hybrid(f, namespaces, makemap, pycompat.identity)
550
539
551 @templatekeyword('node', requires={'ctx'})
540 @templatekeyword('node', requires={'ctx'})
552 def shownode(context, mapping):
541 def shownode(context, mapping):
553 """String. The changeset identification hash, as a 40 hexadecimal
542 """String. The changeset identification hash, as a 40 hexadecimal
554 digit string.
543 digit string.
555 """
544 """
556 ctx = context.resource(mapping, 'ctx')
545 ctx = context.resource(mapping, 'ctx')
557 return ctx.hex()
546 return ctx.hex()
558
547
559 @templatekeyword('obsolete', requires={'ctx'})
548 @templatekeyword('obsolete', requires={'ctx'})
560 def showobsolete(context, mapping):
549 def showobsolete(context, mapping):
561 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
550 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
562 ctx = context.resource(mapping, 'ctx')
551 ctx = context.resource(mapping, 'ctx')
563 if ctx.obsolete():
552 if ctx.obsolete():
564 return 'obsolete'
553 return 'obsolete'
565 return ''
554 return ''
566
555
567 @templatekeyword('peerurls', requires={'repo'})
556 @templatekeyword('peerurls', requires={'repo'})
568 def showpeerurls(context, mapping):
557 def showpeerurls(context, mapping):
569 """A dictionary of repository locations defined in the [paths] section
558 """A dictionary of repository locations defined in the [paths] section
570 of your configuration file."""
559 of your configuration file."""
571 repo = context.resource(mapping, 'repo')
560 repo = context.resource(mapping, 'repo')
572 # see commands.paths() for naming of dictionary keys
561 # see commands.paths() for naming of dictionary keys
573 paths = repo.ui.paths
562 paths = repo.ui.paths
574 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
563 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
575 def makemap(k):
564 def makemap(k):
576 p = paths[k]
565 p = paths[k]
577 d = {'name': k, 'url': p.rawloc}
566 d = {'name': k, 'url': p.rawloc}
578 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
567 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
579 return d
568 return d
580 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
569 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
581
570
582 @templatekeyword("predecessors", requires={'repo', 'ctx'})
571 @templatekeyword("predecessors", requires={'repo', 'ctx'})
583 def showpredecessors(context, mapping):
572 def showpredecessors(context, mapping):
584 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
573 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
585 repo = context.resource(mapping, 'repo')
574 repo = context.resource(mapping, 'repo')
586 ctx = context.resource(mapping, 'ctx')
575 ctx = context.resource(mapping, 'ctx')
587 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
576 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
588 predecessors = map(hex, predecessors)
577 predecessors = map(hex, predecessors)
589
578
590 return _hybrid(None, predecessors,
579 return _hybrid(None, predecessors,
591 lambda x: {'ctx': repo[x], 'revcache': {}},
580 lambda x: {'ctx': repo[x], 'revcache': {}},
592 lambda x: scmutil.formatchangeid(repo[x]))
581 lambda x: scmutil.formatchangeid(repo[x]))
593
582
594 @templatekeyword('reporoot', requires={'repo'})
583 @templatekeyword('reporoot', requires={'repo'})
595 def showreporoot(context, mapping):
584 def showreporoot(context, mapping):
596 """String. The root directory of the current repository."""
585 """String. The root directory of the current repository."""
597 repo = context.resource(mapping, 'repo')
586 repo = context.resource(mapping, 'repo')
598 return repo.root
587 return repo.root
599
588
600 @templatekeyword("successorssets", requires={'repo', 'ctx'})
589 @templatekeyword("successorssets", requires={'repo', 'ctx'})
601 def showsuccessorssets(context, mapping):
590 def showsuccessorssets(context, mapping):
602 """Returns a string of sets of successors for a changectx. Format used
591 """Returns a string of sets of successors for a changectx. Format used
603 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
592 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
604 while also diverged into ctx3. (EXPERIMENTAL)"""
593 while also diverged into ctx3. (EXPERIMENTAL)"""
605 repo = context.resource(mapping, 'repo')
594 repo = context.resource(mapping, 'repo')
606 ctx = context.resource(mapping, 'ctx')
595 ctx = context.resource(mapping, 'ctx')
607 if not ctx.obsolete():
596 if not ctx.obsolete():
608 return ''
597 return ''
609
598
610 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
599 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
611 ssets = [[hex(n) for n in ss] for ss in ssets]
600 ssets = [[hex(n) for n in ss] for ss in ssets]
612
601
613 data = []
602 data = []
614 for ss in ssets:
603 for ss in ssets:
615 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
604 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
616 lambda x: scmutil.formatchangeid(repo[x]))
605 lambda x: scmutil.formatchangeid(repo[x]))
617 data.append(h)
606 data.append(h)
618
607
619 # Format the successorssets
608 # Format the successorssets
620 def render(d):
609 def render(d):
621 t = []
610 t = []
622 for i in d.gen():
611 for i in d.gen():
623 t.append(i)
612 t.append(i)
624 return "".join(t)
613 return "".join(t)
625
614
626 def gen(data):
615 def gen(data):
627 yield "; ".join(render(d) for d in data)
616 yield "; ".join(render(d) for d in data)
628
617
629 return _hybrid(gen(data), data, lambda x: {'successorset': x},
618 return _hybrid(gen(data), data, lambda x: {'successorset': x},
630 pycompat.identity)
619 pycompat.identity)
631
620
632 @templatekeyword("succsandmarkers", requires={'repo', 'ctx'})
621 @templatekeyword("succsandmarkers", requires={'repo', 'ctx'})
633 def showsuccsandmarkers(context, mapping):
622 def showsuccsandmarkers(context, mapping):
634 """Returns a list of dict for each final successor of ctx. The dict
623 """Returns a list of dict for each final successor of ctx. The dict
635 contains successors node id in "successors" keys and the list of
624 contains successors node id in "successors" keys and the list of
636 obs-markers from ctx to the set of successors in "markers".
625 obs-markers from ctx to the set of successors in "markers".
637 (EXPERIMENTAL)
626 (EXPERIMENTAL)
638 """
627 """
639 repo = context.resource(mapping, 'repo')
628 repo = context.resource(mapping, 'repo')
640 ctx = context.resource(mapping, 'ctx')
629 ctx = context.resource(mapping, 'ctx')
641
630
642 values = obsutil.successorsandmarkers(repo, ctx)
631 values = obsutil.successorsandmarkers(repo, ctx)
643
632
644 if values is None:
633 if values is None:
645 values = []
634 values = []
646
635
647 # Format successors and markers to avoid exposing binary to templates
636 # Format successors and markers to avoid exposing binary to templates
648 data = []
637 data = []
649 for i in values:
638 for i in values:
650 # Format successors
639 # Format successors
651 successors = i['successors']
640 successors = i['successors']
652
641
653 successors = [hex(n) for n in successors]
642 successors = [hex(n) for n in successors]
654 successors = _hybrid(None, successors,
643 successors = _hybrid(None, successors,
655 lambda x: {'ctx': repo[x], 'revcache': {}},
644 lambda x: {'ctx': repo[x], 'revcache': {}},
656 lambda x: scmutil.formatchangeid(repo[x]))
645 lambda x: scmutil.formatchangeid(repo[x]))
657
646
658 # Format markers
647 # Format markers
659 finalmarkers = []
648 finalmarkers = []
660 for m in i['markers']:
649 for m in i['markers']:
661 hexprec = hex(m[0])
650 hexprec = hex(m[0])
662 hexsucs = tuple(hex(n) for n in m[1])
651 hexsucs = tuple(hex(n) for n in m[1])
663 hexparents = None
652 hexparents = None
664 if m[5] is not None:
653 if m[5] is not None:
665 hexparents = tuple(hex(n) for n in m[5])
654 hexparents = tuple(hex(n) for n in m[5])
666 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
655 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
667 finalmarkers.append(newmarker)
656 finalmarkers.append(newmarker)
668
657
669 data.append({'successors': successors, 'markers': finalmarkers})
658 data.append({'successors': successors, 'markers': finalmarkers})
670
659
671 f = _showcompatlist(context, mapping, 'succsandmarkers', data)
660 f = _showcompatlist(context, mapping, 'succsandmarkers', data)
672 return _hybrid(f, data, lambda x: x, pycompat.identity)
661 return _hybrid(f, data, lambda x: x, pycompat.identity)
673
662
674 @templatekeyword('p1rev', requires={'ctx'})
663 @templatekeyword('p1rev', requires={'ctx'})
675 def showp1rev(context, mapping):
664 def showp1rev(context, mapping):
676 """Integer. The repository-local revision number of the changeset's
665 """Integer. The repository-local revision number of the changeset's
677 first parent, or -1 if the changeset has no parents."""
666 first parent, or -1 if the changeset has no parents."""
678 ctx = context.resource(mapping, 'ctx')
667 ctx = context.resource(mapping, 'ctx')
679 return ctx.p1().rev()
668 return ctx.p1().rev()
680
669
681 @templatekeyword('p2rev', requires={'ctx'})
670 @templatekeyword('p2rev', requires={'ctx'})
682 def showp2rev(context, mapping):
671 def showp2rev(context, mapping):
683 """Integer. The repository-local revision number of the changeset's
672 """Integer. The repository-local revision number of the changeset's
684 second parent, or -1 if the changeset has no second parent."""
673 second parent, or -1 if the changeset has no second parent."""
685 ctx = context.resource(mapping, 'ctx')
674 ctx = context.resource(mapping, 'ctx')
686 return ctx.p2().rev()
675 return ctx.p2().rev()
687
676
688 @templatekeyword('p1node', requires={'ctx'})
677 @templatekeyword('p1node', requires={'ctx'})
689 def showp1node(context, mapping):
678 def showp1node(context, mapping):
690 """String. The identification hash of the changeset's first parent,
679 """String. The identification hash of the changeset's first parent,
691 as a 40 digit hexadecimal string. If the changeset has no parents, all
680 as a 40 digit hexadecimal string. If the changeset has no parents, all
692 digits are 0."""
681 digits are 0."""
693 ctx = context.resource(mapping, 'ctx')
682 ctx = context.resource(mapping, 'ctx')
694 return ctx.p1().hex()
683 return ctx.p1().hex()
695
684
696 @templatekeyword('p2node', requires={'ctx'})
685 @templatekeyword('p2node', requires={'ctx'})
697 def showp2node(context, mapping):
686 def showp2node(context, mapping):
698 """String. The identification hash of the changeset's second
687 """String. The identification hash of the changeset's second
699 parent, as a 40 digit hexadecimal string. If the changeset has no second
688 parent, as a 40 digit hexadecimal string. If the changeset has no second
700 parent, all digits are 0."""
689 parent, all digits are 0."""
701 ctx = context.resource(mapping, 'ctx')
690 ctx = context.resource(mapping, 'ctx')
702 return ctx.p2().hex()
691 return ctx.p2().hex()
703
692
704 @templatekeyword('parents', requires={'repo', 'ctx'})
693 @templatekeyword('parents', requires={'repo', 'ctx'})
705 def showparents(context, mapping):
694 def showparents(context, mapping):
706 """List of strings. The parents of the changeset in "rev:node"
695 """List of strings. The parents of the changeset in "rev:node"
707 format. If the changeset has only one "natural" parent (the predecessor
696 format. If the changeset has only one "natural" parent (the predecessor
708 revision) nothing is shown."""
697 revision) nothing is shown."""
709 repo = context.resource(mapping, 'repo')
698 repo = context.resource(mapping, 'repo')
710 ctx = context.resource(mapping, 'ctx')
699 ctx = context.resource(mapping, 'ctx')
711 pctxs = scmutil.meaningfulparents(repo, ctx)
700 pctxs = scmutil.meaningfulparents(repo, ctx)
712 prevs = [p.rev() for p in pctxs]
701 prevs = [p.rev() for p in pctxs]
713 parents = [[('rev', p.rev()),
702 parents = [[('rev', p.rev()),
714 ('node', p.hex()),
703 ('node', p.hex()),
715 ('phase', p.phasestr())]
704 ('phase', p.phasestr())]
716 for p in pctxs]
705 for p in pctxs]
717 f = _showcompatlist(context, mapping, 'parent', parents)
706 f = _showcompatlist(context, mapping, 'parent', parents)
718 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
707 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
719 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
708 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
720
709
721 @templatekeyword('phase', requires={'ctx'})
710 @templatekeyword('phase', requires={'ctx'})
722 def showphase(context, mapping):
711 def showphase(context, mapping):
723 """String. The changeset phase name."""
712 """String. The changeset phase name."""
724 ctx = context.resource(mapping, 'ctx')
713 ctx = context.resource(mapping, 'ctx')
725 return ctx.phasestr()
714 return ctx.phasestr()
726
715
727 @templatekeyword('phaseidx', requires={'ctx'})
716 @templatekeyword('phaseidx', requires={'ctx'})
728 def showphaseidx(context, mapping):
717 def showphaseidx(context, mapping):
729 """Integer. The changeset phase index. (ADVANCED)"""
718 """Integer. The changeset phase index. (ADVANCED)"""
730 ctx = context.resource(mapping, 'ctx')
719 ctx = context.resource(mapping, 'ctx')
731 return ctx.phase()
720 return ctx.phase()
732
721
733 @templatekeyword('rev', requires={'ctx'})
722 @templatekeyword('rev', requires={'ctx'})
734 def showrev(context, mapping):
723 def showrev(context, mapping):
735 """Integer. The repository-local changeset revision number."""
724 """Integer. The repository-local changeset revision number."""
736 ctx = context.resource(mapping, 'ctx')
725 ctx = context.resource(mapping, 'ctx')
737 return scmutil.intrev(ctx)
726 return scmutil.intrev(ctx)
738
727
739 def showrevslist(context, mapping, name, revs):
728 def showrevslist(context, mapping, name, revs):
740 """helper to generate a list of revisions in which a mapped template will
729 """helper to generate a list of revisions in which a mapped template will
741 be evaluated"""
730 be evaluated"""
742 repo = context.resource(mapping, 'repo')
731 repo = context.resource(mapping, 'repo')
743 f = _showcompatlist(context, mapping, name, ['%d' % r for r in revs])
732 f = _showcompatlist(context, mapping, name, ['%d' % r for r in revs])
744 return _hybrid(f, revs,
733 return _hybrid(f, revs,
745 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
734 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
746 pycompat.identity, keytype=int)
735 pycompat.identity, keytype=int)
747
736
748 @templatekeyword('subrepos', requires={'ctx'})
737 @templatekeyword('subrepos', requires={'ctx'})
749 def showsubrepos(context, mapping):
738 def showsubrepos(context, mapping):
750 """List of strings. Updated subrepositories in the changeset."""
739 """List of strings. Updated subrepositories in the changeset."""
751 ctx = context.resource(mapping, 'ctx')
740 ctx = context.resource(mapping, 'ctx')
752 substate = ctx.substate
741 substate = ctx.substate
753 if not substate:
742 if not substate:
754 return compatlist(context, mapping, 'subrepo', [])
743 return compatlist(context, mapping, 'subrepo', [])
755 psubstate = ctx.parents()[0].substate or {}
744 psubstate = ctx.parents()[0].substate or {}
756 subrepos = []
745 subrepos = []
757 for sub in substate:
746 for sub in substate:
758 if sub not in psubstate or substate[sub] != psubstate[sub]:
747 if sub not in psubstate or substate[sub] != psubstate[sub]:
759 subrepos.append(sub) # modified or newly added in ctx
748 subrepos.append(sub) # modified or newly added in ctx
760 for sub in psubstate:
749 for sub in psubstate:
761 if sub not in substate:
750 if sub not in substate:
762 subrepos.append(sub) # removed in ctx
751 subrepos.append(sub) # removed in ctx
763 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
752 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
764
753
765 # don't remove "showtags" definition, even though namespaces will put
754 # don't remove "showtags" definition, even though namespaces will put
766 # a helper function for "tags" keyword into "keywords" map automatically,
755 # a helper function for "tags" keyword into "keywords" map automatically,
767 # because online help text is built without namespaces initialization
756 # because online help text is built without namespaces initialization
768 @templatekeyword('tags', requires={'repo', 'ctx'})
757 @templatekeyword('tags', requires={'repo', 'ctx'})
769 def showtags(context, mapping):
758 def showtags(context, mapping):
770 """List of strings. Any tags associated with the changeset."""
759 """List of strings. Any tags associated with the changeset."""
771 return shownames(context, mapping, 'tags')
760 return shownames(context, mapping, 'tags')
772
761
773 @templatekeyword('termwidth', requires={'ui'})
762 @templatekeyword('termwidth', requires={'ui'})
774 def showtermwidth(context, mapping):
763 def showtermwidth(context, mapping):
775 """Integer. The width of the current terminal."""
764 """Integer. The width of the current terminal."""
776 ui = context.resource(mapping, 'ui')
765 ui = context.resource(mapping, 'ui')
777 return ui.termwidth()
766 return ui.termwidth()
778
767
779 @templatekeyword('instabilities', requires={'ctx'})
768 @templatekeyword('instabilities', requires={'ctx'})
780 def showinstabilities(context, mapping):
769 def showinstabilities(context, mapping):
781 """List of strings. Evolution instabilities affecting the changeset.
770 """List of strings. Evolution instabilities affecting the changeset.
782 (EXPERIMENTAL)
771 (EXPERIMENTAL)
783 """
772 """
784 ctx = context.resource(mapping, 'ctx')
773 ctx = context.resource(mapping, 'ctx')
785 return compatlist(context, mapping, 'instability', ctx.instabilities(),
774 return compatlist(context, mapping, 'instability', ctx.instabilities(),
786 plural='instabilities')
775 plural='instabilities')
787
776
788 @templatekeyword('verbosity', requires={'ui'})
777 @templatekeyword('verbosity', requires={'ui'})
789 def showverbosity(context, mapping):
778 def showverbosity(context, mapping):
790 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
779 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
791 or ''."""
780 or ''."""
792 ui = context.resource(mapping, 'ui')
781 ui = context.resource(mapping, 'ui')
793 # see logcmdutil.changesettemplater for priority of these flags
782 # see logcmdutil.changesettemplater for priority of these flags
794 if ui.debugflag:
783 if ui.debugflag:
795 return 'debug'
784 return 'debug'
796 elif ui.quiet:
785 elif ui.quiet:
797 return 'quiet'
786 return 'quiet'
798 elif ui.verbose:
787 elif ui.verbose:
799 return 'verbose'
788 return 'verbose'
800 return ''
789 return ''
801
790
802 def loadkeyword(ui, extname, registrarobj):
791 def loadkeyword(ui, extname, registrarobj):
803 """Load template keyword from specified registrarobj
792 """Load template keyword from specified registrarobj
804 """
793 """
805 for name, func in registrarobj._table.iteritems():
794 for name, func in registrarobj._table.iteritems():
806 keywords[name] = func
795 keywords[name] = func
807
796
808 # tell hggettext to extract docstrings from these functions:
797 # tell hggettext to extract docstrings from these functions:
809 i18nfunctions = keywords.values()
798 i18nfunctions = keywords.values()
@@ -1,850 +1,849 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 """Slightly complicated template engine for commands and hgweb
8 """Slightly complicated template engine for commands and hgweb
9
9
10 This module provides low-level interface to the template engine. See the
10 This module provides low-level interface to the template engine. See the
11 formatter and cmdutil modules if you are looking for high-level functions
11 formatter and cmdutil modules if you are looking for high-level functions
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
13
14 Internal Data Types
14 Internal Data Types
15 -------------------
15 -------------------
16
16
17 Template keywords and functions take a dictionary of current symbols and
17 Template keywords and functions take a dictionary of current symbols and
18 resources (a "mapping") and return result. Inputs and outputs must be one
18 resources (a "mapping") and return result. Inputs and outputs must be one
19 of the following data types:
19 of the following data types:
20
20
21 bytes
21 bytes
22 a byte string, which is generally a human-readable text in local encoding.
22 a byte string, which is generally a human-readable text in local encoding.
23
23
24 generator
24 generator
25 a lazily-evaluated byte string, which is a possibly nested generator of
25 a lazily-evaluated byte string, which is a possibly nested generator of
26 values of any printable types, and will be folded by ``stringify()``
26 values of any printable types, and will be folded by ``stringify()``
27 or ``flatten()``.
27 or ``flatten()``.
28
28
29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 returns a generator of dicts.)
30 returns a generator of dicts.)
31
31
32 None
32 None
33 sometimes represents an empty value, which can be stringified to ''.
33 sometimes represents an empty value, which can be stringified to ''.
34
34
35 True, False, int, float
35 True, False, int, float
36 can be stringified as such.
36 can be stringified as such.
37
37
38 date tuple
38 date tuple
39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
40
40
41 hybrid
41 hybrid
42 represents a list/dict of printable values, which can also be converted
42 represents a list/dict of printable values, which can also be converted
43 to mappings by % operator.
43 to mappings by % operator.
44
44
45 mappable
45 mappable
46 represents a scalar printable value, also supports % operator.
46 represents a scalar printable value, also supports % operator.
47 """
47 """
48
48
49 from __future__ import absolute_import, print_function
49 from __future__ import absolute_import, print_function
50
50
51 import os
51 import os
52
52
53 from .i18n import _
53 from .i18n import _
54 from . import (
54 from . import (
55 config,
55 config,
56 encoding,
56 encoding,
57 error,
57 error,
58 parser,
58 parser,
59 pycompat,
59 pycompat,
60 templatefilters,
60 templatefilters,
61 templatefuncs,
61 templatefuncs,
62 templateutil,
62 templateutil,
63 util,
63 util,
64 )
64 )
65
65
66 # template parsing
66 # template parsing
67
67
68 elements = {
68 elements = {
69 # token-type: binding-strength, primary, prefix, infix, suffix
69 # token-type: binding-strength, primary, prefix, infix, suffix
70 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
70 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
71 ".": (18, None, None, (".", 18), None),
71 ".": (18, None, None, (".", 18), None),
72 "%": (15, None, None, ("%", 15), None),
72 "%": (15, None, None, ("%", 15), None),
73 "|": (15, None, None, ("|", 15), None),
73 "|": (15, None, None, ("|", 15), None),
74 "*": (5, None, None, ("*", 5), None),
74 "*": (5, None, None, ("*", 5), None),
75 "/": (5, None, None, ("/", 5), None),
75 "/": (5, None, None, ("/", 5), None),
76 "+": (4, None, None, ("+", 4), None),
76 "+": (4, None, None, ("+", 4), None),
77 "-": (4, None, ("negate", 19), ("-", 4), None),
77 "-": (4, None, ("negate", 19), ("-", 4), None),
78 "=": (3, None, None, ("keyvalue", 3), None),
78 "=": (3, None, None, ("keyvalue", 3), None),
79 ",": (2, None, None, ("list", 2), None),
79 ",": (2, None, None, ("list", 2), None),
80 ")": (0, None, None, None, None),
80 ")": (0, None, None, None, None),
81 "integer": (0, "integer", None, None, None),
81 "integer": (0, "integer", None, None, None),
82 "symbol": (0, "symbol", None, None, None),
82 "symbol": (0, "symbol", None, None, None),
83 "string": (0, "string", None, None, None),
83 "string": (0, "string", None, None, None),
84 "template": (0, "template", None, None, None),
84 "template": (0, "template", None, None, None),
85 "end": (0, None, None, None, None),
85 "end": (0, None, None, None, None),
86 }
86 }
87
87
88 def tokenize(program, start, end, term=None):
88 def tokenize(program, start, end, term=None):
89 """Parse a template expression into a stream of tokens, which must end
89 """Parse a template expression into a stream of tokens, which must end
90 with term if specified"""
90 with term if specified"""
91 pos = start
91 pos = start
92 program = pycompat.bytestr(program)
92 program = pycompat.bytestr(program)
93 while pos < end:
93 while pos < end:
94 c = program[pos]
94 c = program[pos]
95 if c.isspace(): # skip inter-token whitespace
95 if c.isspace(): # skip inter-token whitespace
96 pass
96 pass
97 elif c in "(=,).%|+-*/": # handle simple operators
97 elif c in "(=,).%|+-*/": # handle simple operators
98 yield (c, None, pos)
98 yield (c, None, pos)
99 elif c in '"\'': # handle quoted templates
99 elif c in '"\'': # handle quoted templates
100 s = pos + 1
100 s = pos + 1
101 data, pos = _parsetemplate(program, s, end, c)
101 data, pos = _parsetemplate(program, s, end, c)
102 yield ('template', data, s)
102 yield ('template', data, s)
103 pos -= 1
103 pos -= 1
104 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
104 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
105 # handle quoted strings
105 # handle quoted strings
106 c = program[pos + 1]
106 c = program[pos + 1]
107 s = pos = pos + 2
107 s = pos = pos + 2
108 while pos < end: # find closing quote
108 while pos < end: # find closing quote
109 d = program[pos]
109 d = program[pos]
110 if d == '\\': # skip over escaped characters
110 if d == '\\': # skip over escaped characters
111 pos += 2
111 pos += 2
112 continue
112 continue
113 if d == c:
113 if d == c:
114 yield ('string', program[s:pos], s)
114 yield ('string', program[s:pos], s)
115 break
115 break
116 pos += 1
116 pos += 1
117 else:
117 else:
118 raise error.ParseError(_("unterminated string"), s)
118 raise error.ParseError(_("unterminated string"), s)
119 elif c.isdigit():
119 elif c.isdigit():
120 s = pos
120 s = pos
121 while pos < end:
121 while pos < end:
122 d = program[pos]
122 d = program[pos]
123 if not d.isdigit():
123 if not d.isdigit():
124 break
124 break
125 pos += 1
125 pos += 1
126 yield ('integer', program[s:pos], s)
126 yield ('integer', program[s:pos], s)
127 pos -= 1
127 pos -= 1
128 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
128 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
129 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
129 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
130 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
130 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
131 # where some of nested templates were preprocessed as strings and
131 # where some of nested templates were preprocessed as strings and
132 # then compiled. therefore, \"...\" was allowed. (issue4733)
132 # then compiled. therefore, \"...\" was allowed. (issue4733)
133 #
133 #
134 # processing flow of _evalifliteral() at 5ab28a2e9962:
134 # processing flow of _evalifliteral() at 5ab28a2e9962:
135 # outer template string -> stringify() -> compiletemplate()
135 # outer template string -> stringify() -> compiletemplate()
136 # ------------------------ ------------ ------------------
136 # ------------------------ ------------ ------------------
137 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
137 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
138 # ~~~~~~~~
138 # ~~~~~~~~
139 # escaped quoted string
139 # escaped quoted string
140 if c == 'r':
140 if c == 'r':
141 pos += 1
141 pos += 1
142 token = 'string'
142 token = 'string'
143 else:
143 else:
144 token = 'template'
144 token = 'template'
145 quote = program[pos:pos + 2]
145 quote = program[pos:pos + 2]
146 s = pos = pos + 2
146 s = pos = pos + 2
147 while pos < end: # find closing escaped quote
147 while pos < end: # find closing escaped quote
148 if program.startswith('\\\\\\', pos, end):
148 if program.startswith('\\\\\\', pos, end):
149 pos += 4 # skip over double escaped characters
149 pos += 4 # skip over double escaped characters
150 continue
150 continue
151 if program.startswith(quote, pos, end):
151 if program.startswith(quote, pos, end):
152 # interpret as if it were a part of an outer string
152 # interpret as if it were a part of an outer string
153 data = parser.unescapestr(program[s:pos])
153 data = parser.unescapestr(program[s:pos])
154 if token == 'template':
154 if token == 'template':
155 data = _parsetemplate(data, 0, len(data))[0]
155 data = _parsetemplate(data, 0, len(data))[0]
156 yield (token, data, s)
156 yield (token, data, s)
157 pos += 1
157 pos += 1
158 break
158 break
159 pos += 1
159 pos += 1
160 else:
160 else:
161 raise error.ParseError(_("unterminated string"), s)
161 raise error.ParseError(_("unterminated string"), s)
162 elif c.isalnum() or c in '_':
162 elif c.isalnum() or c in '_':
163 s = pos
163 s = pos
164 pos += 1
164 pos += 1
165 while pos < end: # find end of symbol
165 while pos < end: # find end of symbol
166 d = program[pos]
166 d = program[pos]
167 if not (d.isalnum() or d == "_"):
167 if not (d.isalnum() or d == "_"):
168 break
168 break
169 pos += 1
169 pos += 1
170 sym = program[s:pos]
170 sym = program[s:pos]
171 yield ('symbol', sym, s)
171 yield ('symbol', sym, s)
172 pos -= 1
172 pos -= 1
173 elif c == term:
173 elif c == term:
174 yield ('end', None, pos)
174 yield ('end', None, pos)
175 return
175 return
176 else:
176 else:
177 raise error.ParseError(_("syntax error"), pos)
177 raise error.ParseError(_("syntax error"), pos)
178 pos += 1
178 pos += 1
179 if term:
179 if term:
180 raise error.ParseError(_("unterminated template expansion"), start)
180 raise error.ParseError(_("unterminated template expansion"), start)
181 yield ('end', None, pos)
181 yield ('end', None, pos)
182
182
183 def _parsetemplate(tmpl, start, stop, quote=''):
183 def _parsetemplate(tmpl, start, stop, quote=''):
184 r"""
184 r"""
185 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
185 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
186 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
186 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
187 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
187 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
188 ([('string', 'foo'), ('symbol', 'bar')], 9)
188 ([('string', 'foo'), ('symbol', 'bar')], 9)
189 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
189 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
190 ([('string', 'foo')], 4)
190 ([('string', 'foo')], 4)
191 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
191 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
192 ([('string', 'foo"'), ('string', 'bar')], 9)
192 ([('string', 'foo"'), ('string', 'bar')], 9)
193 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
193 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
194 ([('string', 'foo\\')], 6)
194 ([('string', 'foo\\')], 6)
195 """
195 """
196 parsed = []
196 parsed = []
197 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
197 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
198 if typ == 'string':
198 if typ == 'string':
199 parsed.append((typ, val))
199 parsed.append((typ, val))
200 elif typ == 'template':
200 elif typ == 'template':
201 parsed.append(val)
201 parsed.append(val)
202 elif typ == 'end':
202 elif typ == 'end':
203 return parsed, pos
203 return parsed, pos
204 else:
204 else:
205 raise error.ProgrammingError('unexpected type: %s' % typ)
205 raise error.ProgrammingError('unexpected type: %s' % typ)
206 raise error.ProgrammingError('unterminated scanning of template')
206 raise error.ProgrammingError('unterminated scanning of template')
207
207
208 def scantemplate(tmpl, raw=False):
208 def scantemplate(tmpl, raw=False):
209 r"""Scan (type, start, end) positions of outermost elements in template
209 r"""Scan (type, start, end) positions of outermost elements in template
210
210
211 If raw=True, a backslash is not taken as an escape character just like
211 If raw=True, a backslash is not taken as an escape character just like
212 r'' string in Python. Note that this is different from r'' literal in
212 r'' string in Python. Note that this is different from r'' literal in
213 template in that no template fragment can appear in r'', e.g. r'{foo}'
213 template in that no template fragment can appear in r'', e.g. r'{foo}'
214 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
214 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
215 'foo'.
215 'foo'.
216
216
217 >>> list(scantemplate(b'foo{bar}"baz'))
217 >>> list(scantemplate(b'foo{bar}"baz'))
218 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
218 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
219 >>> list(scantemplate(b'outer{"inner"}outer'))
219 >>> list(scantemplate(b'outer{"inner"}outer'))
220 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
220 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
221 >>> list(scantemplate(b'foo\\{escaped}'))
221 >>> list(scantemplate(b'foo\\{escaped}'))
222 [('string', 0, 5), ('string', 5, 13)]
222 [('string', 0, 5), ('string', 5, 13)]
223 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
223 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
224 [('string', 0, 4), ('template', 4, 13)]
224 [('string', 0, 4), ('template', 4, 13)]
225 """
225 """
226 last = None
226 last = None
227 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
227 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
228 if last:
228 if last:
229 yield last + (pos,)
229 yield last + (pos,)
230 if typ == 'end':
230 if typ == 'end':
231 return
231 return
232 else:
232 else:
233 last = (typ, pos)
233 last = (typ, pos)
234 raise error.ProgrammingError('unterminated scanning of template')
234 raise error.ProgrammingError('unterminated scanning of template')
235
235
236 def _scantemplate(tmpl, start, stop, quote='', raw=False):
236 def _scantemplate(tmpl, start, stop, quote='', raw=False):
237 """Parse template string into chunks of strings and template expressions"""
237 """Parse template string into chunks of strings and template expressions"""
238 sepchars = '{' + quote
238 sepchars = '{' + quote
239 unescape = [parser.unescapestr, pycompat.identity][raw]
239 unescape = [parser.unescapestr, pycompat.identity][raw]
240 pos = start
240 pos = start
241 p = parser.parser(elements)
241 p = parser.parser(elements)
242 try:
242 try:
243 while pos < stop:
243 while pos < stop:
244 n = min((tmpl.find(c, pos, stop) for c in sepchars),
244 n = min((tmpl.find(c, pos, stop) for c in sepchars),
245 key=lambda n: (n < 0, n))
245 key=lambda n: (n < 0, n))
246 if n < 0:
246 if n < 0:
247 yield ('string', unescape(tmpl[pos:stop]), pos)
247 yield ('string', unescape(tmpl[pos:stop]), pos)
248 pos = stop
248 pos = stop
249 break
249 break
250 c = tmpl[n:n + 1]
250 c = tmpl[n:n + 1]
251 bs = 0 # count leading backslashes
251 bs = 0 # count leading backslashes
252 if not raw:
252 if not raw:
253 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
253 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
254 if bs % 2 == 1:
254 if bs % 2 == 1:
255 # escaped (e.g. '\{', '\\\{', but not '\\{')
255 # escaped (e.g. '\{', '\\\{', but not '\\{')
256 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
256 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
257 pos = n + 1
257 pos = n + 1
258 continue
258 continue
259 if n > pos:
259 if n > pos:
260 yield ('string', unescape(tmpl[pos:n]), pos)
260 yield ('string', unescape(tmpl[pos:n]), pos)
261 if c == quote:
261 if c == quote:
262 yield ('end', None, n + 1)
262 yield ('end', None, n + 1)
263 return
263 return
264
264
265 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
265 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
266 if not tmpl.startswith('}', pos):
266 if not tmpl.startswith('}', pos):
267 raise error.ParseError(_("invalid token"), pos)
267 raise error.ParseError(_("invalid token"), pos)
268 yield ('template', parseres, n)
268 yield ('template', parseres, n)
269 pos += 1
269 pos += 1
270
270
271 if quote:
271 if quote:
272 raise error.ParseError(_("unterminated string"), start)
272 raise error.ParseError(_("unterminated string"), start)
273 except error.ParseError as inst:
273 except error.ParseError as inst:
274 if len(inst.args) > 1: # has location
274 if len(inst.args) > 1: # has location
275 loc = inst.args[1]
275 loc = inst.args[1]
276 # Offset the caret location by the number of newlines before the
276 # Offset the caret location by the number of newlines before the
277 # location of the error, since we will replace one-char newlines
277 # location of the error, since we will replace one-char newlines
278 # with the two-char literal r'\n'.
278 # with the two-char literal r'\n'.
279 offset = tmpl[:loc].count('\n')
279 offset = tmpl[:loc].count('\n')
280 tmpl = tmpl.replace('\n', br'\n')
280 tmpl = tmpl.replace('\n', br'\n')
281 # We want the caret to point to the place in the template that
281 # We want the caret to point to the place in the template that
282 # failed to parse, but in a hint we get a open paren at the
282 # failed to parse, but in a hint we get a open paren at the
283 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
283 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
284 # to line up the caret with the location of the error.
284 # to line up the caret with the location of the error.
285 inst.hint = (tmpl + '\n'
285 inst.hint = (tmpl + '\n'
286 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
286 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
287 raise
287 raise
288 yield ('end', None, pos)
288 yield ('end', None, pos)
289
289
290 def _unnesttemplatelist(tree):
290 def _unnesttemplatelist(tree):
291 """Expand list of templates to node tuple
291 """Expand list of templates to node tuple
292
292
293 >>> def f(tree):
293 >>> def f(tree):
294 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
294 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
295 >>> f((b'template', []))
295 >>> f((b'template', []))
296 (string '')
296 (string '')
297 >>> f((b'template', [(b'string', b'foo')]))
297 >>> f((b'template', [(b'string', b'foo')]))
298 (string 'foo')
298 (string 'foo')
299 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
299 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
300 (template
300 (template
301 (string 'foo')
301 (string 'foo')
302 (symbol 'rev'))
302 (symbol 'rev'))
303 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
303 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
304 (template
304 (template
305 (symbol 'rev'))
305 (symbol 'rev'))
306 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
306 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
307 (string 'foo')
307 (string 'foo')
308 """
308 """
309 if not isinstance(tree, tuple):
309 if not isinstance(tree, tuple):
310 return tree
310 return tree
311 op = tree[0]
311 op = tree[0]
312 if op != 'template':
312 if op != 'template':
313 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
313 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
314
314
315 assert len(tree) == 2
315 assert len(tree) == 2
316 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
316 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
317 if not xs:
317 if not xs:
318 return ('string', '') # empty template ""
318 return ('string', '') # empty template ""
319 elif len(xs) == 1 and xs[0][0] == 'string':
319 elif len(xs) == 1 and xs[0][0] == 'string':
320 return xs[0] # fast path for string with no template fragment "x"
320 return xs[0] # fast path for string with no template fragment "x"
321 else:
321 else:
322 return (op,) + xs
322 return (op,) + xs
323
323
324 def parse(tmpl):
324 def parse(tmpl):
325 """Parse template string into tree"""
325 """Parse template string into tree"""
326 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
326 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
327 assert pos == len(tmpl), 'unquoted template should be consumed'
327 assert pos == len(tmpl), 'unquoted template should be consumed'
328 return _unnesttemplatelist(('template', parsed))
328 return _unnesttemplatelist(('template', parsed))
329
329
330 def _parseexpr(expr):
330 def _parseexpr(expr):
331 """Parse a template expression into tree
331 """Parse a template expression into tree
332
332
333 >>> _parseexpr(b'"foo"')
333 >>> _parseexpr(b'"foo"')
334 ('string', 'foo')
334 ('string', 'foo')
335 >>> _parseexpr(b'foo(bar)')
335 >>> _parseexpr(b'foo(bar)')
336 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
336 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
337 >>> _parseexpr(b'foo(')
337 >>> _parseexpr(b'foo(')
338 Traceback (most recent call last):
338 Traceback (most recent call last):
339 ...
339 ...
340 ParseError: ('not a prefix: end', 4)
340 ParseError: ('not a prefix: end', 4)
341 >>> _parseexpr(b'"foo" "bar"')
341 >>> _parseexpr(b'"foo" "bar"')
342 Traceback (most recent call last):
342 Traceback (most recent call last):
343 ...
343 ...
344 ParseError: ('invalid token', 7)
344 ParseError: ('invalid token', 7)
345 """
345 """
346 p = parser.parser(elements)
346 p = parser.parser(elements)
347 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
347 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
348 if pos != len(expr):
348 if pos != len(expr):
349 raise error.ParseError(_('invalid token'), pos)
349 raise error.ParseError(_('invalid token'), pos)
350 return _unnesttemplatelist(tree)
350 return _unnesttemplatelist(tree)
351
351
352 def prettyformat(tree):
352 def prettyformat(tree):
353 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
353 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
354
354
355 def compileexp(exp, context, curmethods):
355 def compileexp(exp, context, curmethods):
356 """Compile parsed template tree to (func, data) pair"""
356 """Compile parsed template tree to (func, data) pair"""
357 if not exp:
357 if not exp:
358 raise error.ParseError(_("missing argument"))
358 raise error.ParseError(_("missing argument"))
359 t = exp[0]
359 t = exp[0]
360 if t in curmethods:
360 if t in curmethods:
361 return curmethods[t](exp, context)
361 return curmethods[t](exp, context)
362 raise error.ParseError(_("unknown method '%s'") % t)
362 raise error.ParseError(_("unknown method '%s'") % t)
363
363
364 # template evaluation
364 # template evaluation
365
365
366 def getsymbol(exp):
366 def getsymbol(exp):
367 if exp[0] == 'symbol':
367 if exp[0] == 'symbol':
368 return exp[1]
368 return exp[1]
369 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
369 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
370
370
371 def getlist(x):
371 def getlist(x):
372 if not x:
372 if not x:
373 return []
373 return []
374 if x[0] == 'list':
374 if x[0] == 'list':
375 return getlist(x[1]) + [x[2]]
375 return getlist(x[1]) + [x[2]]
376 return [x]
376 return [x]
377
377
378 def gettemplate(exp, context):
378 def gettemplate(exp, context):
379 """Compile given template tree or load named template from map file;
379 """Compile given template tree or load named template from map file;
380 returns (func, data) pair"""
380 returns (func, data) pair"""
381 if exp[0] in ('template', 'string'):
381 if exp[0] in ('template', 'string'):
382 return compileexp(exp, context, methods)
382 return compileexp(exp, context, methods)
383 if exp[0] == 'symbol':
383 if exp[0] == 'symbol':
384 # unlike runsymbol(), here 'symbol' is always taken as template name
384 # unlike runsymbol(), here 'symbol' is always taken as template name
385 # even if it exists in mapping. this allows us to override mapping
385 # even if it exists in mapping. this allows us to override mapping
386 # by web templates, e.g. 'changelogtag' is redefined in map file.
386 # by web templates, e.g. 'changelogtag' is redefined in map file.
387 return context._load(exp[1])
387 return context._load(exp[1])
388 raise error.ParseError(_("expected template specifier"))
388 raise error.ParseError(_("expected template specifier"))
389
389
390 def _runrecursivesymbol(context, mapping, key):
390 def _runrecursivesymbol(context, mapping, key):
391 raise error.Abort(_("recursive reference '%s' in template") % key)
391 raise error.Abort(_("recursive reference '%s' in template") % key)
392
392
393 def buildtemplate(exp, context):
393 def buildtemplate(exp, context):
394 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
394 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
395 return (templateutil.runtemplate, ctmpl)
395 return (templateutil.runtemplate, ctmpl)
396
396
397 def buildfilter(exp, context):
397 def buildfilter(exp, context):
398 n = getsymbol(exp[2])
398 n = getsymbol(exp[2])
399 if n in context._filters:
399 if n in context._filters:
400 filt = context._filters[n]
400 filt = context._filters[n]
401 arg = compileexp(exp[1], context, methods)
401 arg = compileexp(exp[1], context, methods)
402 return (templateutil.runfilter, (arg, filt))
402 return (templateutil.runfilter, (arg, filt))
403 if n in context._funcs:
403 if n in context._funcs:
404 f = context._funcs[n]
404 f = context._funcs[n]
405 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
405 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
406 return (f, args)
406 return (f, args)
407 raise error.ParseError(_("unknown function '%s'") % n)
407 raise error.ParseError(_("unknown function '%s'") % n)
408
408
409 def buildmap(exp, context):
409 def buildmap(exp, context):
410 darg = compileexp(exp[1], context, methods)
410 darg = compileexp(exp[1], context, methods)
411 targ = gettemplate(exp[2], context)
411 targ = gettemplate(exp[2], context)
412 return (templateutil.runmap, (darg, targ))
412 return (templateutil.runmap, (darg, targ))
413
413
414 def buildmember(exp, context):
414 def buildmember(exp, context):
415 darg = compileexp(exp[1], context, methods)
415 darg = compileexp(exp[1], context, methods)
416 memb = getsymbol(exp[2])
416 memb = getsymbol(exp[2])
417 return (templateutil.runmember, (darg, memb))
417 return (templateutil.runmember, (darg, memb))
418
418
419 def buildnegate(exp, context):
419 def buildnegate(exp, context):
420 arg = compileexp(exp[1], context, exprmethods)
420 arg = compileexp(exp[1], context, exprmethods)
421 return (templateutil.runnegate, arg)
421 return (templateutil.runnegate, arg)
422
422
423 def buildarithmetic(exp, context, func):
423 def buildarithmetic(exp, context, func):
424 left = compileexp(exp[1], context, exprmethods)
424 left = compileexp(exp[1], context, exprmethods)
425 right = compileexp(exp[2], context, exprmethods)
425 right = compileexp(exp[2], context, exprmethods)
426 return (templateutil.runarithmetic, (func, left, right))
426 return (templateutil.runarithmetic, (func, left, right))
427
427
428 def buildfunc(exp, context):
428 def buildfunc(exp, context):
429 n = getsymbol(exp[1])
429 n = getsymbol(exp[1])
430 if n in context._funcs:
430 if n in context._funcs:
431 f = context._funcs[n]
431 f = context._funcs[n]
432 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
432 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
433 return (f, args)
433 return (f, args)
434 if n in context._filters:
434 if n in context._filters:
435 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
435 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
436 if len(args) != 1:
436 if len(args) != 1:
437 raise error.ParseError(_("filter %s expects one argument") % n)
437 raise error.ParseError(_("filter %s expects one argument") % n)
438 f = context._filters[n]
438 f = context._filters[n]
439 return (templateutil.runfilter, (args[0], f))
439 return (templateutil.runfilter, (args[0], f))
440 raise error.ParseError(_("unknown function '%s'") % n)
440 raise error.ParseError(_("unknown function '%s'") % n)
441
441
442 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
442 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
443 """Compile parsed tree of function arguments into list or dict of
443 """Compile parsed tree of function arguments into list or dict of
444 (func, data) pairs
444 (func, data) pairs
445
445
446 >>> context = engine(lambda t: (templateutil.runsymbol, t))
446 >>> context = engine(lambda t: (templateutil.runsymbol, t))
447 >>> def fargs(expr, argspec):
447 >>> def fargs(expr, argspec):
448 ... x = _parseexpr(expr)
448 ... x = _parseexpr(expr)
449 ... n = getsymbol(x[1])
449 ... n = getsymbol(x[1])
450 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
450 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
451 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
451 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
452 ['l', 'k']
452 ['l', 'k']
453 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
453 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
454 >>> list(args.keys()), list(args[b'opts'].keys())
454 >>> list(args.keys()), list(args[b'opts'].keys())
455 (['opts'], ['opts', 'k'])
455 (['opts'], ['opts', 'k'])
456 """
456 """
457 def compiledict(xs):
457 def compiledict(xs):
458 return util.sortdict((k, compileexp(x, context, curmethods))
458 return util.sortdict((k, compileexp(x, context, curmethods))
459 for k, x in xs.iteritems())
459 for k, x in xs.iteritems())
460 def compilelist(xs):
460 def compilelist(xs):
461 return [compileexp(x, context, curmethods) for x in xs]
461 return [compileexp(x, context, curmethods) for x in xs]
462
462
463 if not argspec:
463 if not argspec:
464 # filter or function with no argspec: return list of positional args
464 # filter or function with no argspec: return list of positional args
465 return compilelist(getlist(exp))
465 return compilelist(getlist(exp))
466
466
467 # function with argspec: return dict of named args
467 # function with argspec: return dict of named args
468 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
468 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
469 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
469 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
470 keyvaluenode='keyvalue', keynode='symbol')
470 keyvaluenode='keyvalue', keynode='symbol')
471 compargs = util.sortdict()
471 compargs = util.sortdict()
472 if varkey:
472 if varkey:
473 compargs[varkey] = compilelist(treeargs.pop(varkey))
473 compargs[varkey] = compilelist(treeargs.pop(varkey))
474 if optkey:
474 if optkey:
475 compargs[optkey] = compiledict(treeargs.pop(optkey))
475 compargs[optkey] = compiledict(treeargs.pop(optkey))
476 compargs.update(compiledict(treeargs))
476 compargs.update(compiledict(treeargs))
477 return compargs
477 return compargs
478
478
479 def buildkeyvaluepair(exp, content):
479 def buildkeyvaluepair(exp, content):
480 raise error.ParseError(_("can't use a key-value pair in this context"))
480 raise error.ParseError(_("can't use a key-value pair in this context"))
481
481
482 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
482 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
483 exprmethods = {
483 exprmethods = {
484 "integer": lambda e, c: (templateutil.runinteger, e[1]),
484 "integer": lambda e, c: (templateutil.runinteger, e[1]),
485 "string": lambda e, c: (templateutil.runstring, e[1]),
485 "string": lambda e, c: (templateutil.runstring, e[1]),
486 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
486 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
487 "template": buildtemplate,
487 "template": buildtemplate,
488 "group": lambda e, c: compileexp(e[1], c, exprmethods),
488 "group": lambda e, c: compileexp(e[1], c, exprmethods),
489 ".": buildmember,
489 ".": buildmember,
490 "|": buildfilter,
490 "|": buildfilter,
491 "%": buildmap,
491 "%": buildmap,
492 "func": buildfunc,
492 "func": buildfunc,
493 "keyvalue": buildkeyvaluepair,
493 "keyvalue": buildkeyvaluepair,
494 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
494 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
495 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
495 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
496 "negate": buildnegate,
496 "negate": buildnegate,
497 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
497 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
498 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
498 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
499 }
499 }
500
500
501 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
501 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
502 methods = exprmethods.copy()
502 methods = exprmethods.copy()
503 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
503 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
504
504
505 class _aliasrules(parser.basealiasrules):
505 class _aliasrules(parser.basealiasrules):
506 """Parsing and expansion rule set of template aliases"""
506 """Parsing and expansion rule set of template aliases"""
507 _section = _('template alias')
507 _section = _('template alias')
508 _parse = staticmethod(_parseexpr)
508 _parse = staticmethod(_parseexpr)
509
509
510 @staticmethod
510 @staticmethod
511 def _trygetfunc(tree):
511 def _trygetfunc(tree):
512 """Return (name, args) if tree is func(...) or ...|filter; otherwise
512 """Return (name, args) if tree is func(...) or ...|filter; otherwise
513 None"""
513 None"""
514 if tree[0] == 'func' and tree[1][0] == 'symbol':
514 if tree[0] == 'func' and tree[1][0] == 'symbol':
515 return tree[1][1], getlist(tree[2])
515 return tree[1][1], getlist(tree[2])
516 if tree[0] == '|' and tree[2][0] == 'symbol':
516 if tree[0] == '|' and tree[2][0] == 'symbol':
517 return tree[2][1], [tree[1]]
517 return tree[2][1], [tree[1]]
518
518
519 def expandaliases(tree, aliases):
519 def expandaliases(tree, aliases):
520 """Return new tree of aliases are expanded"""
520 """Return new tree of aliases are expanded"""
521 aliasmap = _aliasrules.buildmap(aliases)
521 aliasmap = _aliasrules.buildmap(aliases)
522 return _aliasrules.expand(aliasmap, tree)
522 return _aliasrules.expand(aliasmap, tree)
523
523
524 # template engine
524 # template engine
525
525
526 def _flatten(thing):
526 def _flatten(thing):
527 '''yield a single stream from a possibly nested set of iterators'''
527 '''yield a single stream from a possibly nested set of iterators'''
528 thing = templateutil.unwraphybrid(thing)
528 thing = templateutil.unwraphybrid(thing)
529 if isinstance(thing, bytes):
529 if isinstance(thing, bytes):
530 yield thing
530 yield thing
531 elif isinstance(thing, str):
531 elif isinstance(thing, str):
532 # We can only hit this on Python 3, and it's here to guard
532 # We can only hit this on Python 3, and it's here to guard
533 # against infinite recursion.
533 # against infinite recursion.
534 raise error.ProgrammingError('Mercurial IO including templates is done'
534 raise error.ProgrammingError('Mercurial IO including templates is done'
535 ' with bytes, not strings, got %r' % thing)
535 ' with bytes, not strings, got %r' % thing)
536 elif thing is None:
536 elif thing is None:
537 pass
537 pass
538 elif not util.safehasattr(thing, '__iter__'):
538 elif not util.safehasattr(thing, '__iter__'):
539 yield pycompat.bytestr(thing)
539 yield pycompat.bytestr(thing)
540 else:
540 else:
541 for i in thing:
541 for i in thing:
542 i = templateutil.unwraphybrid(i)
542 i = templateutil.unwraphybrid(i)
543 if isinstance(i, bytes):
543 if isinstance(i, bytes):
544 yield i
544 yield i
545 elif i is None:
545 elif i is None:
546 pass
546 pass
547 elif not util.safehasattr(i, '__iter__'):
547 elif not util.safehasattr(i, '__iter__'):
548 yield pycompat.bytestr(i)
548 yield pycompat.bytestr(i)
549 else:
549 else:
550 for j in _flatten(i):
550 for j in _flatten(i):
551 yield j
551 yield j
552
552
553 def unquotestring(s):
553 def unquotestring(s):
554 '''unwrap quotes if any; otherwise returns unmodified string'''
554 '''unwrap quotes if any; otherwise returns unmodified string'''
555 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
555 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
556 return s
556 return s
557 return s[1:-1]
557 return s[1:-1]
558
558
559 class engine(object):
559 class engine(object):
560 '''template expansion engine.
560 '''template expansion engine.
561
561
562 template expansion works like this. a map file contains key=value
562 template expansion works like this. a map file contains key=value
563 pairs. if value is quoted, it is treated as string. otherwise, it
563 pairs. if value is quoted, it is treated as string. otherwise, it
564 is treated as name of template file.
564 is treated as name of template file.
565
565
566 templater is asked to expand a key in map. it looks up key, and
566 templater is asked to expand a key in map. it looks up key, and
567 looks for strings like this: {foo}. it expands {foo} by looking up
567 looks for strings like this: {foo}. it expands {foo} by looking up
568 foo in map, and substituting it. expansion is recursive: it stops
568 foo in map, and substituting it. expansion is recursive: it stops
569 when there is no more {foo} to replace.
569 when there is no more {foo} to replace.
570
570
571 expansion also allows formatting and filtering.
571 expansion also allows formatting and filtering.
572
572
573 format uses key to expand each item in list. syntax is
573 format uses key to expand each item in list. syntax is
574 {key%format}.
574 {key%format}.
575
575
576 filter uses function to transform value. syntax is
576 filter uses function to transform value. syntax is
577 {key|filter1|filter2|...}.'''
577 {key|filter1|filter2|...}.'''
578
578
579 def __init__(self, loader, filters=None, defaults=None, resources=None,
579 def __init__(self, loader, filters=None, defaults=None, resources=None,
580 aliases=()):
580 aliases=()):
581 self._loader = loader
581 self._loader = loader
582 if filters is None:
582 if filters is None:
583 filters = {}
583 filters = {}
584 self._filters = filters
584 self._filters = filters
585 self._funcs = templatefuncs.funcs # make this a parameter if needed
585 self._funcs = templatefuncs.funcs # make this a parameter if needed
586 if defaults is None:
586 if defaults is None:
587 defaults = {}
587 defaults = {}
588 if resources is None:
588 if resources is None:
589 resources = {}
589 resources = {}
590 self._defaults = defaults
590 self._defaults = defaults
591 self._resources = resources
591 self._resources = resources
592 self._aliasmap = _aliasrules.buildmap(aliases)
592 self._aliasmap = _aliasrules.buildmap(aliases)
593 self._cache = {} # key: (func, data)
593 self._cache = {} # key: (func, data)
594
594
595 def symbol(self, mapping, key):
595 def symbol(self, mapping, key):
596 """Resolve symbol to value or function; None if nothing found"""
596 """Resolve symbol to value or function; None if nothing found"""
597 v = None
597 v = None
598 if key not in self._resources:
598 if key not in self._resources:
599 v = mapping.get(key)
599 v = mapping.get(key)
600 if v is None:
600 if v is None:
601 v = self._defaults.get(key)
601 v = self._defaults.get(key)
602 return v
602 return v
603
603
604 def resource(self, mapping, key):
604 def resource(self, mapping, key):
605 """Return internal data (e.g. cache) used for keyword/function
605 """Return internal data (e.g. cache) used for keyword/function
606 evaluation"""
606 evaluation"""
607 v = None
607 v = None
608 if key in self._resources:
608 if key in self._resources:
609 v = self._resources[key](self, mapping, key)
609 v = self._resources[key](self, mapping, key)
610 if v is None:
610 if v is None:
611 raise templateutil.ResourceUnavailable(
611 raise templateutil.ResourceUnavailable(
612 _('template resource not available: %s') % key)
612 _('template resource not available: %s') % key)
613 return v
613 return v
614
614
615 def _load(self, t):
615 def _load(self, t):
616 '''load, parse, and cache a template'''
616 '''load, parse, and cache a template'''
617 if t not in self._cache:
617 if t not in self._cache:
618 # put poison to cut recursion while compiling 't'
618 # put poison to cut recursion while compiling 't'
619 self._cache[t] = (_runrecursivesymbol, t)
619 self._cache[t] = (_runrecursivesymbol, t)
620 try:
620 try:
621 x = parse(self._loader(t))
621 x = parse(self._loader(t))
622 if self._aliasmap:
622 if self._aliasmap:
623 x = _aliasrules.expand(self._aliasmap, x)
623 x = _aliasrules.expand(self._aliasmap, x)
624 self._cache[t] = compileexp(x, self, methods)
624 self._cache[t] = compileexp(x, self, methods)
625 except: # re-raises
625 except: # re-raises
626 del self._cache[t]
626 del self._cache[t]
627 raise
627 raise
628 return self._cache[t]
628 return self._cache[t]
629
629
630 def preload(self, t):
630 def preload(self, t):
631 """Load, parse, and cache the specified template if available"""
631 """Load, parse, and cache the specified template if available"""
632 try:
632 try:
633 self._load(t)
633 self._load(t)
634 return True
634 return True
635 except templateutil.TemplateNotFound:
635 except templateutil.TemplateNotFound:
636 return False
636 return False
637
637
638 def process(self, t, mapping):
638 def process(self, t, mapping):
639 '''Perform expansion. t is name of map element to expand.
639 '''Perform expansion. t is name of map element to expand.
640 mapping contains added elements for use during expansion. Is a
640 mapping contains added elements for use during expansion. Is a
641 generator.'''
641 generator.'''
642 func, data = self._load(t)
642 func, data = self._load(t)
643 return _flatten(func(self, mapping, data))
643 return _flatten(func(self, mapping, data))
644
644
645 engines = {'default': engine}
645 engines = {'default': engine}
646
646
647 def stylelist():
647 def stylelist():
648 paths = templatepaths()
648 paths = templatepaths()
649 if not paths:
649 if not paths:
650 return _('no templates found, try `hg debuginstall` for more info')
650 return _('no templates found, try `hg debuginstall` for more info')
651 dirlist = os.listdir(paths[0])
651 dirlist = os.listdir(paths[0])
652 stylelist = []
652 stylelist = []
653 for file in dirlist:
653 for file in dirlist:
654 split = file.split(".")
654 split = file.split(".")
655 if split[-1] in ('orig', 'rej'):
655 if split[-1] in ('orig', 'rej'):
656 continue
656 continue
657 if split[0] == "map-cmdline":
657 if split[0] == "map-cmdline":
658 stylelist.append(split[1])
658 stylelist.append(split[1])
659 return ", ".join(sorted(stylelist))
659 return ", ".join(sorted(stylelist))
660
660
661 def _readmapfile(mapfile):
661 def _readmapfile(mapfile):
662 """Load template elements from the given map file"""
662 """Load template elements from the given map file"""
663 if not os.path.exists(mapfile):
663 if not os.path.exists(mapfile):
664 raise error.Abort(_("style '%s' not found") % mapfile,
664 raise error.Abort(_("style '%s' not found") % mapfile,
665 hint=_("available styles: %s") % stylelist())
665 hint=_("available styles: %s") % stylelist())
666
666
667 base = os.path.dirname(mapfile)
667 base = os.path.dirname(mapfile)
668 conf = config.config(includepaths=templatepaths())
668 conf = config.config(includepaths=templatepaths())
669 conf.read(mapfile, remap={'': 'templates'})
669 conf.read(mapfile, remap={'': 'templates'})
670
670
671 cache = {}
671 cache = {}
672 tmap = {}
672 tmap = {}
673 aliases = []
673 aliases = []
674
674
675 val = conf.get('templates', '__base__')
675 val = conf.get('templates', '__base__')
676 if val and val[0] not in "'\"":
676 if val and val[0] not in "'\"":
677 # treat as a pointer to a base class for this style
677 # treat as a pointer to a base class for this style
678 path = util.normpath(os.path.join(base, val))
678 path = util.normpath(os.path.join(base, val))
679
679
680 # fallback check in template paths
680 # fallback check in template paths
681 if not os.path.exists(path):
681 if not os.path.exists(path):
682 for p in templatepaths():
682 for p in templatepaths():
683 p2 = util.normpath(os.path.join(p, val))
683 p2 = util.normpath(os.path.join(p, val))
684 if os.path.isfile(p2):
684 if os.path.isfile(p2):
685 path = p2
685 path = p2
686 break
686 break
687 p3 = util.normpath(os.path.join(p2, "map"))
687 p3 = util.normpath(os.path.join(p2, "map"))
688 if os.path.isfile(p3):
688 if os.path.isfile(p3):
689 path = p3
689 path = p3
690 break
690 break
691
691
692 cache, tmap, aliases = _readmapfile(path)
692 cache, tmap, aliases = _readmapfile(path)
693
693
694 for key, val in conf['templates'].items():
694 for key, val in conf['templates'].items():
695 if not val:
695 if not val:
696 raise error.ParseError(_('missing value'),
696 raise error.ParseError(_('missing value'),
697 conf.source('templates', key))
697 conf.source('templates', key))
698 if val[0] in "'\"":
698 if val[0] in "'\"":
699 if val[0] != val[-1]:
699 if val[0] != val[-1]:
700 raise error.ParseError(_('unmatched quotes'),
700 raise error.ParseError(_('unmatched quotes'),
701 conf.source('templates', key))
701 conf.source('templates', key))
702 cache[key] = unquotestring(val)
702 cache[key] = unquotestring(val)
703 elif key != '__base__':
703 elif key != '__base__':
704 val = 'default', val
704 val = 'default', val
705 if ':' in val[1]:
705 if ':' in val[1]:
706 val = val[1].split(':', 1)
706 val = val[1].split(':', 1)
707 tmap[key] = val[0], os.path.join(base, val[1])
707 tmap[key] = val[0], os.path.join(base, val[1])
708 aliases.extend(conf['templatealias'].items())
708 aliases.extend(conf['templatealias'].items())
709 return cache, tmap, aliases
709 return cache, tmap, aliases
710
710
711 class templater(object):
711 class templater(object):
712
712
713 def __init__(self, filters=None, defaults=None, resources=None,
713 def __init__(self, filters=None, defaults=None, resources=None,
714 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
714 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
715 """Create template engine optionally with preloaded template fragments
715 """Create template engine optionally with preloaded template fragments
716
716
717 - ``filters``: a dict of functions to transform a value into another.
717 - ``filters``: a dict of functions to transform a value into another.
718 - ``defaults``: a dict of symbol values/functions; may be overridden
718 - ``defaults``: a dict of symbol values/functions; may be overridden
719 by a ``mapping`` dict.
719 by a ``mapping`` dict.
720 - ``resources``: a dict of functions returning internal data
720 - ``resources``: a dict of functions returning internal data
721 (e.g. cache), inaccessible from user template.
721 (e.g. cache), inaccessible from user template.
722 - ``cache``: a dict of preloaded template fragments.
722 - ``cache``: a dict of preloaded template fragments.
723 - ``aliases``: a list of alias (name, replacement) pairs.
723 - ``aliases``: a list of alias (name, replacement) pairs.
724
724
725 self.cache may be updated later to register additional template
725 self.cache may be updated later to register additional template
726 fragments.
726 fragments.
727 """
727 """
728 if filters is None:
728 if filters is None:
729 filters = {}
729 filters = {}
730 if defaults is None:
730 if defaults is None:
731 defaults = {}
731 defaults = {}
732 if resources is None:
732 if resources is None:
733 resources = {}
733 resources = {}
734 if cache is None:
734 if cache is None:
735 cache = {}
735 cache = {}
736 self.cache = cache.copy()
736 self.cache = cache.copy()
737 self.map = {}
737 self.map = {}
738 self.filters = templatefilters.filters.copy()
738 self.filters = templatefilters.filters.copy()
739 self.filters.update(filters)
739 self.filters.update(filters)
740 self.defaults = defaults
740 self.defaults = defaults
741 self._resources = {'templ': lambda context, mapping, key: self}
741 self._resources = resources
742 self._resources.update(resources)
743 self._aliases = aliases
742 self._aliases = aliases
744 self.minchunk, self.maxchunk = minchunk, maxchunk
743 self.minchunk, self.maxchunk = minchunk, maxchunk
745 self.ecache = {}
744 self.ecache = {}
746
745
747 @classmethod
746 @classmethod
748 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
747 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
749 cache=None, minchunk=1024, maxchunk=65536):
748 cache=None, minchunk=1024, maxchunk=65536):
750 """Create templater from the specified map file"""
749 """Create templater from the specified map file"""
751 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
750 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
752 cache, tmap, aliases = _readmapfile(mapfile)
751 cache, tmap, aliases = _readmapfile(mapfile)
753 t.cache.update(cache)
752 t.cache.update(cache)
754 t.map = tmap
753 t.map = tmap
755 t._aliases = aliases
754 t._aliases = aliases
756 return t
755 return t
757
756
758 def __contains__(self, key):
757 def __contains__(self, key):
759 return key in self.cache or key in self.map
758 return key in self.cache or key in self.map
760
759
761 def load(self, t):
760 def load(self, t):
762 '''Get the template for the given template name. Use a local cache.'''
761 '''Get the template for the given template name. Use a local cache.'''
763 if t not in self.cache:
762 if t not in self.cache:
764 try:
763 try:
765 self.cache[t] = util.readfile(self.map[t][1])
764 self.cache[t] = util.readfile(self.map[t][1])
766 except KeyError as inst:
765 except KeyError as inst:
767 raise templateutil.TemplateNotFound(
766 raise templateutil.TemplateNotFound(
768 _('"%s" not in template map') % inst.args[0])
767 _('"%s" not in template map') % inst.args[0])
769 except IOError as inst:
768 except IOError as inst:
770 reason = (_('template file %s: %s')
769 reason = (_('template file %s: %s')
771 % (self.map[t][1], util.forcebytestr(inst.args[1])))
770 % (self.map[t][1], util.forcebytestr(inst.args[1])))
772 raise IOError(inst.args[0], encoding.strfromlocal(reason))
771 raise IOError(inst.args[0], encoding.strfromlocal(reason))
773 return self.cache[t]
772 return self.cache[t]
774
773
775 def renderdefault(self, mapping):
774 def renderdefault(self, mapping):
776 """Render the default unnamed template and return result as string"""
775 """Render the default unnamed template and return result as string"""
777 return self.render('', mapping)
776 return self.render('', mapping)
778
777
779 def render(self, t, mapping):
778 def render(self, t, mapping):
780 """Render the specified named template and return result as string"""
779 """Render the specified named template and return result as string"""
781 return templateutil.stringify(self.generate(t, mapping))
780 return templateutil.stringify(self.generate(t, mapping))
782
781
783 def generate(self, t, mapping):
782 def generate(self, t, mapping):
784 """Return a generator that renders the specified named template and
783 """Return a generator that renders the specified named template and
785 yields chunks"""
784 yields chunks"""
786 ttype = t in self.map and self.map[t][0] or 'default'
785 ttype = t in self.map and self.map[t][0] or 'default'
787 if ttype not in self.ecache:
786 if ttype not in self.ecache:
788 try:
787 try:
789 ecls = engines[ttype]
788 ecls = engines[ttype]
790 except KeyError:
789 except KeyError:
791 raise error.Abort(_('invalid template engine: %s') % ttype)
790 raise error.Abort(_('invalid template engine: %s') % ttype)
792 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
791 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
793 self._resources, self._aliases)
792 self._resources, self._aliases)
794 proc = self.ecache[ttype]
793 proc = self.ecache[ttype]
795
794
796 stream = proc.process(t, mapping)
795 stream = proc.process(t, mapping)
797 if self.minchunk:
796 if self.minchunk:
798 stream = util.increasingchunks(stream, min=self.minchunk,
797 stream = util.increasingchunks(stream, min=self.minchunk,
799 max=self.maxchunk)
798 max=self.maxchunk)
800 return stream
799 return stream
801
800
802 def templatepaths():
801 def templatepaths():
803 '''return locations used for template files.'''
802 '''return locations used for template files.'''
804 pathsrel = ['templates']
803 pathsrel = ['templates']
805 paths = [os.path.normpath(os.path.join(util.datapath, f))
804 paths = [os.path.normpath(os.path.join(util.datapath, f))
806 for f in pathsrel]
805 for f in pathsrel]
807 return [p for p in paths if os.path.isdir(p)]
806 return [p for p in paths if os.path.isdir(p)]
808
807
809 def templatepath(name):
808 def templatepath(name):
810 '''return location of template file. returns None if not found.'''
809 '''return location of template file. returns None if not found.'''
811 for p in templatepaths():
810 for p in templatepaths():
812 f = os.path.join(p, name)
811 f = os.path.join(p, name)
813 if os.path.exists(f):
812 if os.path.exists(f):
814 return f
813 return f
815 return None
814 return None
816
815
817 def stylemap(styles, paths=None):
816 def stylemap(styles, paths=None):
818 """Return path to mapfile for a given style.
817 """Return path to mapfile for a given style.
819
818
820 Searches mapfile in the following locations:
819 Searches mapfile in the following locations:
821 1. templatepath/style/map
820 1. templatepath/style/map
822 2. templatepath/map-style
821 2. templatepath/map-style
823 3. templatepath/map
822 3. templatepath/map
824 """
823 """
825
824
826 if paths is None:
825 if paths is None:
827 paths = templatepaths()
826 paths = templatepaths()
828 elif isinstance(paths, bytes):
827 elif isinstance(paths, bytes):
829 paths = [paths]
828 paths = [paths]
830
829
831 if isinstance(styles, bytes):
830 if isinstance(styles, bytes):
832 styles = [styles]
831 styles = [styles]
833
832
834 for style in styles:
833 for style in styles:
835 # only plain name is allowed to honor template paths
834 # only plain name is allowed to honor template paths
836 if (not style
835 if (not style
837 or style in (pycompat.oscurdir, pycompat.ospardir)
836 or style in (pycompat.oscurdir, pycompat.ospardir)
838 or pycompat.ossep in style
837 or pycompat.ossep in style
839 or pycompat.osaltsep and pycompat.osaltsep in style):
838 or pycompat.osaltsep and pycompat.osaltsep in style):
840 continue
839 continue
841 locations = [os.path.join(style, 'map'), 'map-' + style]
840 locations = [os.path.join(style, 'map'), 'map-' + style]
842 locations.append('map')
841 locations.append('map')
843
842
844 for path in paths:
843 for path in paths:
845 for location in locations:
844 for location in locations:
846 mapfile = os.path.join(path, location)
845 mapfile = os.path.join(path, location)
847 if os.path.isfile(mapfile):
846 if os.path.isfile(mapfile):
848 return style, mapfile
847 return style, mapfile
849
848
850 raise RuntimeError("No hgweb templates found in %r" % paths)
849 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,447 +1,450 b''
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import types
10 import types
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 util,
16 util,
17 )
17 )
18
18
19 class ResourceUnavailable(error.Abort):
19 class ResourceUnavailable(error.Abort):
20 pass
20 pass
21
21
22 class TemplateNotFound(error.Abort):
22 class TemplateNotFound(error.Abort):
23 pass
23 pass
24
24
25 class hybrid(object):
25 class hybrid(object):
26 """Wrapper for list or dict to support legacy template
26 """Wrapper for list or dict to support legacy template
27
27
28 This class allows us to handle both:
28 This class allows us to handle both:
29 - "{files}" (legacy command-line-specific list hack) and
29 - "{files}" (legacy command-line-specific list hack) and
30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
31 and to access raw values:
31 and to access raw values:
32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
33 - "{get(extras, key)}"
33 - "{get(extras, key)}"
34 - "{files|json}"
34 - "{files|json}"
35 """
35 """
36
36
37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
38 if gen is not None:
38 if gen is not None:
39 self.gen = gen # generator or function returning generator
39 self.gen = gen # generator or function returning generator
40 self._values = values
40 self._values = values
41 self._makemap = makemap
41 self._makemap = makemap
42 self.joinfmt = joinfmt
42 self.joinfmt = joinfmt
43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
44 def gen(self):
44 def gen(self):
45 """Default generator to stringify this as {join(self, ' ')}"""
45 """Default generator to stringify this as {join(self, ' ')}"""
46 for i, x in enumerate(self._values):
46 for i, x in enumerate(self._values):
47 if i > 0:
47 if i > 0:
48 yield ' '
48 yield ' '
49 yield self.joinfmt(x)
49 yield self.joinfmt(x)
50 def itermaps(self):
50 def itermaps(self):
51 makemap = self._makemap
51 makemap = self._makemap
52 for x in self._values:
52 for x in self._values:
53 yield makemap(x)
53 yield makemap(x)
54 def __contains__(self, x):
54 def __contains__(self, x):
55 return x in self._values
55 return x in self._values
56 def __getitem__(self, key):
56 def __getitem__(self, key):
57 return self._values[key]
57 return self._values[key]
58 def __len__(self):
58 def __len__(self):
59 return len(self._values)
59 return len(self._values)
60 def __iter__(self):
60 def __iter__(self):
61 return iter(self._values)
61 return iter(self._values)
62 def __getattr__(self, name):
62 def __getattr__(self, name):
63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
64 r'itervalues', r'keys', r'values'):
64 r'itervalues', r'keys', r'values'):
65 raise AttributeError(name)
65 raise AttributeError(name)
66 return getattr(self._values, name)
66 return getattr(self._values, name)
67
67
68 class mappable(object):
68 class mappable(object):
69 """Wrapper for non-list/dict object to support map operation
69 """Wrapper for non-list/dict object to support map operation
70
70
71 This class allows us to handle both:
71 This class allows us to handle both:
72 - "{manifest}"
72 - "{manifest}"
73 - "{manifest % '{rev}:{node}'}"
73 - "{manifest % '{rev}:{node}'}"
74 - "{manifest.rev}"
74 - "{manifest.rev}"
75
75
76 Unlike a hybrid, this does not simulate the behavior of the underling
76 Unlike a hybrid, this does not simulate the behavior of the underling
77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
78 """
78 """
79
79
80 def __init__(self, gen, key, value, makemap):
80 def __init__(self, gen, key, value, makemap):
81 if gen is not None:
81 if gen is not None:
82 self.gen = gen # generator or function returning generator
82 self.gen = gen # generator or function returning generator
83 self._key = key
83 self._key = key
84 self._value = value # may be generator of strings
84 self._value = value # may be generator of strings
85 self._makemap = makemap
85 self._makemap = makemap
86
86
87 def gen(self):
87 def gen(self):
88 yield pycompat.bytestr(self._value)
88 yield pycompat.bytestr(self._value)
89
89
90 def tomap(self):
90 def tomap(self):
91 return self._makemap(self._key)
91 return self._makemap(self._key)
92
92
93 def itermaps(self):
93 def itermaps(self):
94 yield self.tomap()
94 yield self.tomap()
95
95
96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
97 """Wrap data to support both dict-like and string-like operations"""
97 """Wrap data to support both dict-like and string-like operations"""
98 prefmt = pycompat.identity
98 prefmt = pycompat.identity
99 if fmt is None:
99 if fmt is None:
100 fmt = '%s=%s'
100 fmt = '%s=%s'
101 prefmt = pycompat.bytestr
101 prefmt = pycompat.bytestr
102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
104
104
105 def hybridlist(data, name, fmt=None, gen=None):
105 def hybridlist(data, name, fmt=None, gen=None):
106 """Wrap data to support both list-like and string-like operations"""
106 """Wrap data to support both list-like and string-like operations"""
107 prefmt = pycompat.identity
107 prefmt = pycompat.identity
108 if fmt is None:
108 if fmt is None:
109 fmt = '%s'
109 fmt = '%s'
110 prefmt = pycompat.bytestr
110 prefmt = pycompat.bytestr
111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
112
112
113 def unwraphybrid(thing):
113 def unwraphybrid(thing):
114 """Return an object which can be stringified possibly by using a legacy
114 """Return an object which can be stringified possibly by using a legacy
115 template"""
115 template"""
116 gen = getattr(thing, 'gen', None)
116 gen = getattr(thing, 'gen', None)
117 if gen is None:
117 if gen is None:
118 return thing
118 return thing
119 if callable(gen):
119 if callable(gen):
120 return gen()
120 return gen()
121 return gen
121 return gen
122
122
123 def unwrapvalue(thing):
123 def unwrapvalue(thing):
124 """Move the inner value object out of the wrapper"""
124 """Move the inner value object out of the wrapper"""
125 if not util.safehasattr(thing, '_value'):
125 if not util.safehasattr(thing, '_value'):
126 return thing
126 return thing
127 return thing._value
127 return thing._value
128
128
129 def wraphybridvalue(container, key, value):
129 def wraphybridvalue(container, key, value):
130 """Wrap an element of hybrid container to be mappable
130 """Wrap an element of hybrid container to be mappable
131
131
132 The key is passed to the makemap function of the given container, which
132 The key is passed to the makemap function of the given container, which
133 should be an item generated by iter(container).
133 should be an item generated by iter(container).
134 """
134 """
135 makemap = getattr(container, '_makemap', None)
135 makemap = getattr(container, '_makemap', None)
136 if makemap is None:
136 if makemap is None:
137 return value
137 return value
138 if util.safehasattr(value, '_makemap'):
138 if util.safehasattr(value, '_makemap'):
139 # a nested hybrid list/dict, which has its own way of map operation
139 # a nested hybrid list/dict, which has its own way of map operation
140 return value
140 return value
141 return mappable(None, key, value, makemap)
141 return mappable(None, key, value, makemap)
142
142
143 def compatdict(context, mapping, name, data, key='key', value='value',
143 def compatdict(context, mapping, name, data, key='key', value='value',
144 fmt=None, plural=None, separator=' '):
144 fmt=None, plural=None, separator=' '):
145 """Wrap data like hybriddict(), but also supports old-style list template
145 """Wrap data like hybriddict(), but also supports old-style list template
146
146
147 This exists for backward compatibility with the old-style template. Use
147 This exists for backward compatibility with the old-style template. Use
148 hybriddict() for new template keywords.
148 hybriddict() for new template keywords.
149 """
149 """
150 c = [{key: k, value: v} for k, v in data.iteritems()]
150 c = [{key: k, value: v} for k, v in data.iteritems()]
151 f = _showcompatlist(context, mapping, name, c, plural, separator)
151 f = _showcompatlist(context, mapping, name, c, plural, separator)
152 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
152 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
153
153
154 def compatlist(context, mapping, name, data, element=None, fmt=None,
154 def compatlist(context, mapping, name, data, element=None, fmt=None,
155 plural=None, separator=' '):
155 plural=None, separator=' '):
156 """Wrap data like hybridlist(), but also supports old-style list template
156 """Wrap data like hybridlist(), but also supports old-style list template
157
157
158 This exists for backward compatibility with the old-style template. Use
158 This exists for backward compatibility with the old-style template. Use
159 hybridlist() for new template keywords.
159 hybridlist() for new template keywords.
160 """
160 """
161 f = _showcompatlist(context, mapping, name, data, plural, separator)
161 f = _showcompatlist(context, mapping, name, data, plural, separator)
162 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
162 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
163
163
164 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
164 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
165 """Return a generator that renders old-style list template
165 """Return a generator that renders old-style list template
166
166
167 name is name of key in template map.
167 name is name of key in template map.
168 values is list of strings or dicts.
168 values is list of strings or dicts.
169 plural is plural of name, if not simply name + 's'.
169 plural is plural of name, if not simply name + 's'.
170 separator is used to join values as a string
170 separator is used to join values as a string
171
171
172 expansion works like this, given name 'foo'.
172 expansion works like this, given name 'foo'.
173
173
174 if values is empty, expand 'no_foos'.
174 if values is empty, expand 'no_foos'.
175
175
176 if 'foo' not in template map, return values as a string,
176 if 'foo' not in template map, return values as a string,
177 joined by 'separator'.
177 joined by 'separator'.
178
178
179 expand 'start_foos'.
179 expand 'start_foos'.
180
180
181 for each value, expand 'foo'. if 'last_foo' in template
181 for each value, expand 'foo'. if 'last_foo' in template
182 map, expand it instead of 'foo' for last key.
182 map, expand it instead of 'foo' for last key.
183
183
184 expand 'end_foos'.
184 expand 'end_foos'.
185 """
185 """
186 if not plural:
186 if not plural:
187 plural = name + 's'
187 plural = name + 's'
188 if not values:
188 if not values:
189 noname = 'no_' + plural
189 noname = 'no_' + plural
190 if context.preload(noname):
190 if context.preload(noname):
191 yield context.process(noname, mapping)
191 yield context.process(noname, mapping)
192 return
192 return
193 if not context.preload(name):
193 if not context.preload(name):
194 if isinstance(values[0], bytes):
194 if isinstance(values[0], bytes):
195 yield separator.join(values)
195 yield separator.join(values)
196 else:
196 else:
197 for v in values:
197 for v in values:
198 r = dict(v)
198 r = dict(v)
199 r.update(mapping)
199 r.update(mapping)
200 yield r
200 yield r
201 return
201 return
202 startname = 'start_' + plural
202 startname = 'start_' + plural
203 if context.preload(startname):
203 if context.preload(startname):
204 yield context.process(startname, mapping)
204 yield context.process(startname, mapping)
205 vmapping = mapping.copy()
205 vmapping = mapping.copy()
206 def one(v, tag=name):
206 def one(v, tag=name):
207 try:
207 try:
208 vmapping.update(v)
208 vmapping.update(v)
209 # Python 2 raises ValueError if the type of v is wrong. Python
209 # Python 2 raises ValueError if the type of v is wrong. Python
210 # 3 raises TypeError.
210 # 3 raises TypeError.
211 except (AttributeError, TypeError, ValueError):
211 except (AttributeError, TypeError, ValueError):
212 try:
212 try:
213 # Python 2 raises ValueError trying to destructure an e.g.
213 # Python 2 raises ValueError trying to destructure an e.g.
214 # bytes. Python 3 raises TypeError.
214 # bytes. Python 3 raises TypeError.
215 for a, b in v:
215 for a, b in v:
216 vmapping[a] = b
216 vmapping[a] = b
217 except (TypeError, ValueError):
217 except (TypeError, ValueError):
218 vmapping[name] = v
218 vmapping[name] = v
219 return context.process(tag, vmapping)
219 return context.process(tag, vmapping)
220 lastname = 'last_' + name
220 lastname = 'last_' + name
221 if context.preload(lastname):
221 if context.preload(lastname):
222 last = values.pop()
222 last = values.pop()
223 else:
223 else:
224 last = None
224 last = None
225 for v in values:
225 for v in values:
226 yield one(v)
226 yield one(v)
227 if last is not None:
227 if last is not None:
228 yield one(last, tag=lastname)
228 yield one(last, tag=lastname)
229 endname = 'end_' + plural
229 endname = 'end_' + plural
230 if context.preload(endname):
230 if context.preload(endname):
231 yield context.process(endname, mapping)
231 yield context.process(endname, mapping)
232
232
233 def stringify(thing):
233 def stringify(thing):
234 """Turn values into bytes by converting into text and concatenating them"""
234 """Turn values into bytes by converting into text and concatenating them"""
235 thing = unwraphybrid(thing)
235 thing = unwraphybrid(thing)
236 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
236 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
237 if isinstance(thing, str):
237 if isinstance(thing, str):
238 # This is only reachable on Python 3 (otherwise
238 # This is only reachable on Python 3 (otherwise
239 # isinstance(thing, bytes) would have been true), and is
239 # isinstance(thing, bytes) would have been true), and is
240 # here to prevent infinite recursion bugs on Python 3.
240 # here to prevent infinite recursion bugs on Python 3.
241 raise error.ProgrammingError(
241 raise error.ProgrammingError(
242 'stringify got unexpected unicode string: %r' % thing)
242 'stringify got unexpected unicode string: %r' % thing)
243 return "".join([stringify(t) for t in thing if t is not None])
243 return "".join([stringify(t) for t in thing if t is not None])
244 if thing is None:
244 if thing is None:
245 return ""
245 return ""
246 return pycompat.bytestr(thing)
246 return pycompat.bytestr(thing)
247
247
248 def findsymbolicname(arg):
248 def findsymbolicname(arg):
249 """Find symbolic name for the given compiled expression; returns None
249 """Find symbolic name for the given compiled expression; returns None
250 if nothing found reliably"""
250 if nothing found reliably"""
251 while True:
251 while True:
252 func, data = arg
252 func, data = arg
253 if func is runsymbol:
253 if func is runsymbol:
254 return data
254 return data
255 elif func is runfilter:
255 elif func is runfilter:
256 arg = data[0]
256 arg = data[0]
257 else:
257 else:
258 return None
258 return None
259
259
260 def evalrawexp(context, mapping, arg):
260 def evalrawexp(context, mapping, arg):
261 """Evaluate given argument as a bare template object which may require
261 """Evaluate given argument as a bare template object which may require
262 further processing (such as folding generator of strings)"""
262 further processing (such as folding generator of strings)"""
263 func, data = arg
263 func, data = arg
264 return func(context, mapping, data)
264 return func(context, mapping, data)
265
265
266 def evalfuncarg(context, mapping, arg):
266 def evalfuncarg(context, mapping, arg):
267 """Evaluate given argument as value type"""
267 """Evaluate given argument as value type"""
268 thing = evalrawexp(context, mapping, arg)
268 thing = evalrawexp(context, mapping, arg)
269 thing = unwrapvalue(thing)
269 thing = unwrapvalue(thing)
270 # evalrawexp() may return string, generator of strings or arbitrary object
270 # evalrawexp() may return string, generator of strings or arbitrary object
271 # such as date tuple, but filter does not want generator.
271 # such as date tuple, but filter does not want generator.
272 if isinstance(thing, types.GeneratorType):
272 if isinstance(thing, types.GeneratorType):
273 thing = stringify(thing)
273 thing = stringify(thing)
274 return thing
274 return thing
275
275
276 def evalboolean(context, mapping, arg):
276 def evalboolean(context, mapping, arg):
277 """Evaluate given argument as boolean, but also takes boolean literals"""
277 """Evaluate given argument as boolean, but also takes boolean literals"""
278 func, data = arg
278 func, data = arg
279 if func is runsymbol:
279 if func is runsymbol:
280 thing = func(context, mapping, data, default=None)
280 thing = func(context, mapping, data, default=None)
281 if thing is None:
281 if thing is None:
282 # not a template keyword, takes as a boolean literal
282 # not a template keyword, takes as a boolean literal
283 thing = util.parsebool(data)
283 thing = util.parsebool(data)
284 else:
284 else:
285 thing = func(context, mapping, data)
285 thing = func(context, mapping, data)
286 thing = unwrapvalue(thing)
286 thing = unwrapvalue(thing)
287 if isinstance(thing, bool):
287 if isinstance(thing, bool):
288 return thing
288 return thing
289 # other objects are evaluated as strings, which means 0 is True, but
289 # other objects are evaluated as strings, which means 0 is True, but
290 # empty dict/list should be False as they are expected to be ''
290 # empty dict/list should be False as they are expected to be ''
291 return bool(stringify(thing))
291 return bool(stringify(thing))
292
292
293 def evalinteger(context, mapping, arg, err=None):
293 def evalinteger(context, mapping, arg, err=None):
294 v = evalfuncarg(context, mapping, arg)
294 v = evalfuncarg(context, mapping, arg)
295 try:
295 try:
296 return int(v)
296 return int(v)
297 except (TypeError, ValueError):
297 except (TypeError, ValueError):
298 raise error.ParseError(err or _('not an integer'))
298 raise error.ParseError(err or _('not an integer'))
299
299
300 def evalstring(context, mapping, arg):
300 def evalstring(context, mapping, arg):
301 return stringify(evalrawexp(context, mapping, arg))
301 return stringify(evalrawexp(context, mapping, arg))
302
302
303 def evalstringliteral(context, mapping, arg):
303 def evalstringliteral(context, mapping, arg):
304 """Evaluate given argument as string template, but returns symbol name
304 """Evaluate given argument as string template, but returns symbol name
305 if it is unknown"""
305 if it is unknown"""
306 func, data = arg
306 func, data = arg
307 if func is runsymbol:
307 if func is runsymbol:
308 thing = func(context, mapping, data, default=data)
308 thing = func(context, mapping, data, default=data)
309 else:
309 else:
310 thing = func(context, mapping, data)
310 thing = func(context, mapping, data)
311 return stringify(thing)
311 return stringify(thing)
312
312
313 _evalfuncbytype = {
313 _evalfuncbytype = {
314 bool: evalboolean,
314 bool: evalboolean,
315 bytes: evalstring,
315 bytes: evalstring,
316 int: evalinteger,
316 int: evalinteger,
317 }
317 }
318
318
319 def evalastype(context, mapping, arg, typ):
319 def evalastype(context, mapping, arg, typ):
320 """Evaluate given argument and coerce its type"""
320 """Evaluate given argument and coerce its type"""
321 try:
321 try:
322 f = _evalfuncbytype[typ]
322 f = _evalfuncbytype[typ]
323 except KeyError:
323 except KeyError:
324 raise error.ProgrammingError('invalid type specified: %r' % typ)
324 raise error.ProgrammingError('invalid type specified: %r' % typ)
325 return f(context, mapping, arg)
325 return f(context, mapping, arg)
326
326
327 def runinteger(context, mapping, data):
327 def runinteger(context, mapping, data):
328 return int(data)
328 return int(data)
329
329
330 def runstring(context, mapping, data):
330 def runstring(context, mapping, data):
331 return data
331 return data
332
332
333 def _recursivesymbolblocker(key):
333 def _recursivesymbolblocker(key):
334 def showrecursion(**args):
334 def showrecursion(**args):
335 raise error.Abort(_("recursive reference '%s' in template") % key)
335 raise error.Abort(_("recursive reference '%s' in template") % key)
336 return showrecursion
336 return showrecursion
337
337
338 def runsymbol(context, mapping, key, default=''):
338 def runsymbol(context, mapping, key, default=''):
339 v = context.symbol(mapping, key)
339 v = context.symbol(mapping, key)
340 if v is None:
340 if v is None:
341 # put poison to cut recursion. we can't move this to parsing phase
341 # put poison to cut recursion. we can't move this to parsing phase
342 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
342 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
343 safemapping = mapping.copy()
343 safemapping = mapping.copy()
344 safemapping[key] = _recursivesymbolblocker(key)
344 safemapping[key] = _recursivesymbolblocker(key)
345 try:
345 try:
346 v = context.process(key, safemapping)
346 v = context.process(key, safemapping)
347 except TemplateNotFound:
347 except TemplateNotFound:
348 v = default
348 v = default
349 if callable(v) and getattr(v, '_requires', None) is None:
349 if callable(v) and getattr(v, '_requires', None) is None:
350 # old templatekw: expand all keywords and resources
350 # old templatekw: expand all keywords and resources
351 # (TODO: deprecate this after porting web template keywords to new API)
351 props = {k: f(context, mapping, k)
352 props = {k: f(context, mapping, k)
352 for k, f in context._resources.items()}
353 for k, f in context._resources.items()}
354 # pass context to _showcompatlist() through templatekw._showlist()
355 props['templ'] = context
353 props.update(mapping)
356 props.update(mapping)
354 return v(**pycompat.strkwargs(props))
357 return v(**pycompat.strkwargs(props))
355 if callable(v):
358 if callable(v):
356 # new templatekw
359 # new templatekw
357 try:
360 try:
358 return v(context, mapping)
361 return v(context, mapping)
359 except ResourceUnavailable:
362 except ResourceUnavailable:
360 # unsupported keyword is mapped to empty just like unknown keyword
363 # unsupported keyword is mapped to empty just like unknown keyword
361 return None
364 return None
362 return v
365 return v
363
366
364 def runtemplate(context, mapping, template):
367 def runtemplate(context, mapping, template):
365 for arg in template:
368 for arg in template:
366 yield evalrawexp(context, mapping, arg)
369 yield evalrawexp(context, mapping, arg)
367
370
368 def runfilter(context, mapping, data):
371 def runfilter(context, mapping, data):
369 arg, filt = data
372 arg, filt = data
370 thing = evalfuncarg(context, mapping, arg)
373 thing = evalfuncarg(context, mapping, arg)
371 try:
374 try:
372 return filt(thing)
375 return filt(thing)
373 except (ValueError, AttributeError, TypeError):
376 except (ValueError, AttributeError, TypeError):
374 sym = findsymbolicname(arg)
377 sym = findsymbolicname(arg)
375 if sym:
378 if sym:
376 msg = (_("template filter '%s' is not compatible with keyword '%s'")
379 msg = (_("template filter '%s' is not compatible with keyword '%s'")
377 % (pycompat.sysbytes(filt.__name__), sym))
380 % (pycompat.sysbytes(filt.__name__), sym))
378 else:
381 else:
379 msg = (_("incompatible use of template filter '%s'")
382 msg = (_("incompatible use of template filter '%s'")
380 % pycompat.sysbytes(filt.__name__))
383 % pycompat.sysbytes(filt.__name__))
381 raise error.Abort(msg)
384 raise error.Abort(msg)
382
385
383 def runmap(context, mapping, data):
386 def runmap(context, mapping, data):
384 darg, targ = data
387 darg, targ = data
385 d = evalrawexp(context, mapping, darg)
388 d = evalrawexp(context, mapping, darg)
386 if util.safehasattr(d, 'itermaps'):
389 if util.safehasattr(d, 'itermaps'):
387 diter = d.itermaps()
390 diter = d.itermaps()
388 else:
391 else:
389 try:
392 try:
390 diter = iter(d)
393 diter = iter(d)
391 except TypeError:
394 except TypeError:
392 sym = findsymbolicname(darg)
395 sym = findsymbolicname(darg)
393 if sym:
396 if sym:
394 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
397 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
395 else:
398 else:
396 raise error.ParseError(_("%r is not iterable") % d)
399 raise error.ParseError(_("%r is not iterable") % d)
397
400
398 for i, v in enumerate(diter):
401 for i, v in enumerate(diter):
399 lm = mapping.copy()
402 lm = mapping.copy()
400 lm['index'] = i
403 lm['index'] = i
401 if isinstance(v, dict):
404 if isinstance(v, dict):
402 lm.update(v)
405 lm.update(v)
403 lm['originalnode'] = mapping.get('node')
406 lm['originalnode'] = mapping.get('node')
404 yield evalrawexp(context, lm, targ)
407 yield evalrawexp(context, lm, targ)
405 else:
408 else:
406 # v is not an iterable of dicts, this happen when 'key'
409 # v is not an iterable of dicts, this happen when 'key'
407 # has been fully expanded already and format is useless.
410 # has been fully expanded already and format is useless.
408 # If so, return the expanded value.
411 # If so, return the expanded value.
409 yield v
412 yield v
410
413
411 def runmember(context, mapping, data):
414 def runmember(context, mapping, data):
412 darg, memb = data
415 darg, memb = data
413 d = evalrawexp(context, mapping, darg)
416 d = evalrawexp(context, mapping, darg)
414 if util.safehasattr(d, 'tomap'):
417 if util.safehasattr(d, 'tomap'):
415 lm = mapping.copy()
418 lm = mapping.copy()
416 lm.update(d.tomap())
419 lm.update(d.tomap())
417 return runsymbol(context, lm, memb)
420 return runsymbol(context, lm, memb)
418 if util.safehasattr(d, 'get'):
421 if util.safehasattr(d, 'get'):
419 return getdictitem(d, memb)
422 return getdictitem(d, memb)
420
423
421 sym = findsymbolicname(darg)
424 sym = findsymbolicname(darg)
422 if sym:
425 if sym:
423 raise error.ParseError(_("keyword '%s' has no member") % sym)
426 raise error.ParseError(_("keyword '%s' has no member") % sym)
424 else:
427 else:
425 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
428 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
426
429
427 def runnegate(context, mapping, data):
430 def runnegate(context, mapping, data):
428 data = evalinteger(context, mapping, data,
431 data = evalinteger(context, mapping, data,
429 _('negation needs an integer argument'))
432 _('negation needs an integer argument'))
430 return -data
433 return -data
431
434
432 def runarithmetic(context, mapping, data):
435 def runarithmetic(context, mapping, data):
433 func, left, right = data
436 func, left, right = data
434 left = evalinteger(context, mapping, left,
437 left = evalinteger(context, mapping, left,
435 _('arithmetic only defined on integers'))
438 _('arithmetic only defined on integers'))
436 right = evalinteger(context, mapping, right,
439 right = evalinteger(context, mapping, right,
437 _('arithmetic only defined on integers'))
440 _('arithmetic only defined on integers'))
438 try:
441 try:
439 return func(left, right)
442 return func(left, right)
440 except ZeroDivisionError:
443 except ZeroDivisionError:
441 raise error.Abort(_('division by zero is not defined'))
444 raise error.Abort(_('division by zero is not defined'))
442
445
443 def getdictitem(dictarg, key):
446 def getdictitem(dictarg, key):
444 val = dictarg.get(key)
447 val = dictarg.get(key)
445 if val is None:
448 if val is None:
446 return
449 return
447 return wraphybridvalue(dictarg, key, val)
450 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now