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