##// END OF EJS Templates
templater: resolve type of dict key in getmember()...
Yuya Nishihara -
r38262:688fbb75 @31 default
parent child Browse files
Show More
@@ -1,786 +1,787 b''
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import difflib
12 import difflib
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, nullid, short
17 from ..node import hex, nullid, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_BAD_REQUEST,
21 HTTP_BAD_REQUEST,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 paritygen,
23 paritygen,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 context,
27 context,
28 error,
28 error,
29 match,
29 match,
30 mdiff,
30 mdiff,
31 obsutil,
31 obsutil,
32 patch,
32 patch,
33 pathutil,
33 pathutil,
34 pycompat,
34 pycompat,
35 scmutil,
35 scmutil,
36 templatefilters,
36 templatefilters,
37 templatekw,
37 templatekw,
38 templateutil,
38 templateutil,
39 ui as uimod,
39 ui as uimod,
40 util,
40 util,
41 )
41 )
42
42
43 from ..utils import (
43 from ..utils import (
44 stringutil,
44 stringutil,
45 )
45 )
46
46
47 archivespecs = util.sortdict((
47 archivespecs = util.sortdict((
48 ('zip', ('application/zip', 'zip', '.zip', None)),
48 ('zip', ('application/zip', 'zip', '.zip', None)),
49 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
49 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
50 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
50 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
51 ))
51 ))
52
52
53 def archivelist(ui, nodeid, url=None):
53 def archivelist(ui, nodeid, url=None):
54 allowed = ui.configlist('web', 'allow-archive', untrusted=True)
54 allowed = ui.configlist('web', 'allow-archive', untrusted=True)
55 archives = []
55 archives = []
56
56
57 for typ, spec in archivespecs.iteritems():
57 for typ, spec in archivespecs.iteritems():
58 if typ in allowed or ui.configbool('web', 'allow' + typ,
58 if typ in allowed or ui.configbool('web', 'allow' + typ,
59 untrusted=True):
59 untrusted=True):
60 archives.append({
60 archives.append({
61 'type': typ,
61 'type': typ,
62 'extension': spec[2],
62 'extension': spec[2],
63 'node': nodeid,
63 'node': nodeid,
64 'url': url,
64 'url': url,
65 })
65 })
66
66
67 return templateutil.mappinglist(archives)
67 return templateutil.mappinglist(archives)
68
68
69 def up(p):
69 def up(p):
70 if p[0:1] != "/":
70 if p[0:1] != "/":
71 p = "/" + p
71 p = "/" + p
72 if p[-1:] == "/":
72 if p[-1:] == "/":
73 p = p[:-1]
73 p = p[:-1]
74 up = os.path.dirname(p)
74 up = os.path.dirname(p)
75 if up == "/":
75 if up == "/":
76 return "/"
76 return "/"
77 return up + "/"
77 return up + "/"
78
78
79 def _navseq(step, firststep=None):
79 def _navseq(step, firststep=None):
80 if firststep:
80 if firststep:
81 yield firststep
81 yield firststep
82 if firststep >= 20 and firststep <= 40:
82 if firststep >= 20 and firststep <= 40:
83 firststep = 50
83 firststep = 50
84 yield firststep
84 yield firststep
85 assert step > 0
85 assert step > 0
86 assert firststep > 0
86 assert firststep > 0
87 while step <= firststep:
87 while step <= firststep:
88 step *= 10
88 step *= 10
89 while True:
89 while True:
90 yield 1 * step
90 yield 1 * step
91 yield 3 * step
91 yield 3 * step
92 step *= 10
92 step *= 10
93
93
94 class revnav(object):
94 class revnav(object):
95
95
96 def __init__(self, repo):
96 def __init__(self, repo):
97 """Navigation generation object
97 """Navigation generation object
98
98
99 :repo: repo object we generate nav for
99 :repo: repo object we generate nav for
100 """
100 """
101 # used for hex generation
101 # used for hex generation
102 self._revlog = repo.changelog
102 self._revlog = repo.changelog
103
103
104 def __nonzero__(self):
104 def __nonzero__(self):
105 """return True if any revision to navigate over"""
105 """return True if any revision to navigate over"""
106 return self._first() is not None
106 return self._first() is not None
107
107
108 __bool__ = __nonzero__
108 __bool__ = __nonzero__
109
109
110 def _first(self):
110 def _first(self):
111 """return the minimum non-filtered changeset or None"""
111 """return the minimum non-filtered changeset or None"""
112 try:
112 try:
113 return next(iter(self._revlog))
113 return next(iter(self._revlog))
114 except StopIteration:
114 except StopIteration:
115 return None
115 return None
116
116
117 def hex(self, rev):
117 def hex(self, rev):
118 return hex(self._revlog.node(rev))
118 return hex(self._revlog.node(rev))
119
119
120 def gen(self, pos, pagelen, limit):
120 def gen(self, pos, pagelen, limit):
121 """computes label and revision id for navigation link
121 """computes label and revision id for navigation link
122
122
123 :pos: is the revision relative to which we generate navigation.
123 :pos: is the revision relative to which we generate navigation.
124 :pagelen: the size of each navigation page
124 :pagelen: the size of each navigation page
125 :limit: how far shall we link
125 :limit: how far shall we link
126
126
127 The return is:
127 The return is:
128 - a single element mappinglist
128 - a single element mappinglist
129 - containing a dictionary with a `before` and `after` key
129 - containing a dictionary with a `before` and `after` key
130 - values are dictionaries with `label` and `node` keys
130 - values are dictionaries with `label` and `node` keys
131 """
131 """
132 if not self:
132 if not self:
133 # empty repo
133 # empty repo
134 return templateutil.mappinglist([
134 return templateutil.mappinglist([
135 {'before': templateutil.mappinglist([]),
135 {'before': templateutil.mappinglist([]),
136 'after': templateutil.mappinglist([])},
136 'after': templateutil.mappinglist([])},
137 ])
137 ])
138
138
139 targets = []
139 targets = []
140 for f in _navseq(1, pagelen):
140 for f in _navseq(1, pagelen):
141 if f > limit:
141 if f > limit:
142 break
142 break
143 targets.append(pos + f)
143 targets.append(pos + f)
144 targets.append(pos - f)
144 targets.append(pos - f)
145 targets.sort()
145 targets.sort()
146
146
147 first = self._first()
147 first = self._first()
148 navbefore = [{'label': '(%i)' % first, 'node': self.hex(first)}]
148 navbefore = [{'label': '(%i)' % first, 'node': self.hex(first)}]
149 navafter = []
149 navafter = []
150 for rev in targets:
150 for rev in targets:
151 if rev not in self._revlog:
151 if rev not in self._revlog:
152 continue
152 continue
153 if pos < rev < limit:
153 if pos < rev < limit:
154 navafter.append({'label': '+%d' % abs(rev - pos),
154 navafter.append({'label': '+%d' % abs(rev - pos),
155 'node': self.hex(rev)})
155 'node': self.hex(rev)})
156 if 0 < rev < pos:
156 if 0 < rev < pos:
157 navbefore.append({'label': '-%d' % abs(rev - pos),
157 navbefore.append({'label': '-%d' % abs(rev - pos),
158 'node': self.hex(rev)})
158 'node': self.hex(rev)})
159
159
160 navafter.append({'label': 'tip', 'node': 'tip'})
160 navafter.append({'label': 'tip', 'node': 'tip'})
161
161
162 # TODO: maybe this can be a scalar object supporting tomap()
162 # TODO: maybe this can be a scalar object supporting tomap()
163 return templateutil.mappinglist([
163 return templateutil.mappinglist([
164 {'before': templateutil.mappinglist(navbefore),
164 {'before': templateutil.mappinglist(navbefore),
165 'after': templateutil.mappinglist(navafter)},
165 'after': templateutil.mappinglist(navafter)},
166 ])
166 ])
167
167
168 class filerevnav(revnav):
168 class filerevnav(revnav):
169
169
170 def __init__(self, repo, path):
170 def __init__(self, repo, path):
171 """Navigation generation object
171 """Navigation generation object
172
172
173 :repo: repo object we generate nav for
173 :repo: repo object we generate nav for
174 :path: path of the file we generate nav for
174 :path: path of the file we generate nav for
175 """
175 """
176 # used for iteration
176 # used for iteration
177 self._changelog = repo.unfiltered().changelog
177 self._changelog = repo.unfiltered().changelog
178 # used for hex generation
178 # used for hex generation
179 self._revlog = repo.file(path)
179 self._revlog = repo.file(path)
180
180
181 def hex(self, rev):
181 def hex(self, rev):
182 return hex(self._changelog.node(self._revlog.linkrev(rev)))
182 return hex(self._changelog.node(self._revlog.linkrev(rev)))
183
183
184 # TODO: maybe this can be a wrapper class for changectx/filectx list, which
184 # TODO: maybe this can be a wrapper class for changectx/filectx list, which
185 # yields {'ctx': ctx}
185 # yields {'ctx': ctx}
186 def _ctxsgen(context, ctxs):
186 def _ctxsgen(context, ctxs):
187 for s in ctxs:
187 for s in ctxs:
188 d = {
188 d = {
189 'node': s.hex(),
189 'node': s.hex(),
190 'rev': s.rev(),
190 'rev': s.rev(),
191 'user': s.user(),
191 'user': s.user(),
192 'date': s.date(),
192 'date': s.date(),
193 'description': s.description(),
193 'description': s.description(),
194 'branch': s.branch(),
194 'branch': s.branch(),
195 }
195 }
196 if util.safehasattr(s, 'path'):
196 if util.safehasattr(s, 'path'):
197 d['file'] = s.path()
197 d['file'] = s.path()
198 yield d
198 yield d
199
199
200 def _siblings(siblings=None, hiderev=None):
200 def _siblings(siblings=None, hiderev=None):
201 if siblings is None:
201 if siblings is None:
202 siblings = []
202 siblings = []
203 siblings = [s for s in siblings if s.node() != nullid]
203 siblings = [s for s in siblings if s.node() != nullid]
204 if len(siblings) == 1 and siblings[0].rev() == hiderev:
204 if len(siblings) == 1 and siblings[0].rev() == hiderev:
205 siblings = []
205 siblings = []
206 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,))
206 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,))
207
207
208 def difffeatureopts(req, ui, section):
208 def difffeatureopts(req, ui, section):
209 diffopts = patch.difffeatureopts(ui, untrusted=True,
209 diffopts = patch.difffeatureopts(ui, untrusted=True,
210 section=section, whitespace=True)
210 section=section, whitespace=True)
211
211
212 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
212 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
213 v = req.qsparams.get(k)
213 v = req.qsparams.get(k)
214 if v is not None:
214 if v is not None:
215 v = stringutil.parsebool(v)
215 v = stringutil.parsebool(v)
216 setattr(diffopts, k, v if v is not None else True)
216 setattr(diffopts, k, v if v is not None else True)
217
217
218 return diffopts
218 return diffopts
219
219
220 def annotate(req, fctx, ui):
220 def annotate(req, fctx, ui):
221 diffopts = difffeatureopts(req, ui, 'annotate')
221 diffopts = difffeatureopts(req, ui, 'annotate')
222 return fctx.annotate(follow=True, diffopts=diffopts)
222 return fctx.annotate(follow=True, diffopts=diffopts)
223
223
224 def parents(ctx, hide=None):
224 def parents(ctx, hide=None):
225 if isinstance(ctx, context.basefilectx):
225 if isinstance(ctx, context.basefilectx):
226 introrev = ctx.introrev()
226 introrev = ctx.introrev()
227 if ctx.changectx().rev() != introrev:
227 if ctx.changectx().rev() != introrev:
228 return _siblings([ctx.repo()[introrev]], hide)
228 return _siblings([ctx.repo()[introrev]], hide)
229 return _siblings(ctx.parents(), hide)
229 return _siblings(ctx.parents(), hide)
230
230
231 def children(ctx, hide=None):
231 def children(ctx, hide=None):
232 return _siblings(ctx.children(), hide)
232 return _siblings(ctx.children(), hide)
233
233
234 def renamelink(fctx):
234 def renamelink(fctx):
235 r = fctx.renamed()
235 r = fctx.renamed()
236 if r:
236 if r:
237 return templateutil.mappinglist([{'file': r[0], 'node': hex(r[1])}])
237 return templateutil.mappinglist([{'file': r[0], 'node': hex(r[1])}])
238 return templateutil.mappinglist([])
238 return templateutil.mappinglist([])
239
239
240 def nodetagsdict(repo, node):
240 def nodetagsdict(repo, node):
241 return templateutil.hybridlist(repo.nodetags(node), name='name')
241 return templateutil.hybridlist(repo.nodetags(node), name='name')
242
242
243 def nodebookmarksdict(repo, node):
243 def nodebookmarksdict(repo, node):
244 return templateutil.hybridlist(repo.nodebookmarks(node), name='name')
244 return templateutil.hybridlist(repo.nodebookmarks(node), name='name')
245
245
246 def nodebranchdict(repo, ctx):
246 def nodebranchdict(repo, ctx):
247 branches = []
247 branches = []
248 branch = ctx.branch()
248 branch = ctx.branch()
249 # If this is an empty repo, ctx.node() == nullid,
249 # If this is an empty repo, ctx.node() == nullid,
250 # ctx.branch() == 'default'.
250 # ctx.branch() == 'default'.
251 try:
251 try:
252 branchnode = repo.branchtip(branch)
252 branchnode = repo.branchtip(branch)
253 except error.RepoLookupError:
253 except error.RepoLookupError:
254 branchnode = None
254 branchnode = None
255 if branchnode == ctx.node():
255 if branchnode == ctx.node():
256 branches.append(branch)
256 branches.append(branch)
257 return templateutil.hybridlist(branches, name='name')
257 return templateutil.hybridlist(branches, name='name')
258
258
259 def nodeinbranch(repo, ctx):
259 def nodeinbranch(repo, ctx):
260 branches = []
260 branches = []
261 branch = ctx.branch()
261 branch = ctx.branch()
262 try:
262 try:
263 branchnode = repo.branchtip(branch)
263 branchnode = repo.branchtip(branch)
264 except error.RepoLookupError:
264 except error.RepoLookupError:
265 branchnode = None
265 branchnode = None
266 if branch != 'default' and branchnode != ctx.node():
266 if branch != 'default' and branchnode != ctx.node():
267 branches.append(branch)
267 branches.append(branch)
268 return templateutil.hybridlist(branches, name='name')
268 return templateutil.hybridlist(branches, name='name')
269
269
270 def nodebranchnodefault(ctx):
270 def nodebranchnodefault(ctx):
271 branches = []
271 branches = []
272 branch = ctx.branch()
272 branch = ctx.branch()
273 if branch != 'default':
273 if branch != 'default':
274 branches.append(branch)
274 branches.append(branch)
275 return templateutil.hybridlist(branches, name='name')
275 return templateutil.hybridlist(branches, name='name')
276
276
277 def _nodenamesgen(context, f, node, name):
277 def _nodenamesgen(context, f, node, name):
278 for t in f(node):
278 for t in f(node):
279 yield {name: t}
279 yield {name: t}
280
280
281 def showtag(repo, t1, node=nullid):
281 def showtag(repo, t1, node=nullid):
282 args = (repo.nodetags, node, 'tag')
282 args = (repo.nodetags, node, 'tag')
283 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
283 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
284
284
285 def showbookmark(repo, t1, node=nullid):
285 def showbookmark(repo, t1, node=nullid):
286 args = (repo.nodebookmarks, node, 'bookmark')
286 args = (repo.nodebookmarks, node, 'bookmark')
287 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
287 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
288
288
289 def branchentries(repo, stripecount, limit=0):
289 def branchentries(repo, stripecount, limit=0):
290 tips = []
290 tips = []
291 heads = repo.heads()
291 heads = repo.heads()
292 parity = paritygen(stripecount)
292 parity = paritygen(stripecount)
293 sortkey = lambda item: (not item[1], item[0].rev())
293 sortkey = lambda item: (not item[1], item[0].rev())
294
294
295 def entries(context):
295 def entries(context):
296 count = 0
296 count = 0
297 if not tips:
297 if not tips:
298 for tag, hs, tip, closed in repo.branchmap().iterbranches():
298 for tag, hs, tip, closed in repo.branchmap().iterbranches():
299 tips.append((repo[tip], closed))
299 tips.append((repo[tip], closed))
300 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
300 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
301 if limit > 0 and count >= limit:
301 if limit > 0 and count >= limit:
302 return
302 return
303 count += 1
303 count += 1
304 if closed:
304 if closed:
305 status = 'closed'
305 status = 'closed'
306 elif ctx.node() not in heads:
306 elif ctx.node() not in heads:
307 status = 'inactive'
307 status = 'inactive'
308 else:
308 else:
309 status = 'open'
309 status = 'open'
310 yield {
310 yield {
311 'parity': next(parity),
311 'parity': next(parity),
312 'branch': ctx.branch(),
312 'branch': ctx.branch(),
313 'status': status,
313 'status': status,
314 'node': ctx.hex(),
314 'node': ctx.hex(),
315 'date': ctx.date()
315 'date': ctx.date()
316 }
316 }
317
317
318 return templateutil.mappinggenerator(entries)
318 return templateutil.mappinggenerator(entries)
319
319
320 def cleanpath(repo, path):
320 def cleanpath(repo, path):
321 path = path.lstrip('/')
321 path = path.lstrip('/')
322 return pathutil.canonpath(repo.root, '', path)
322 return pathutil.canonpath(repo.root, '', path)
323
323
324 def changectx(repo, req):
324 def changectx(repo, req):
325 changeid = "tip"
325 changeid = "tip"
326 if 'node' in req.qsparams:
326 if 'node' in req.qsparams:
327 changeid = req.qsparams['node']
327 changeid = req.qsparams['node']
328 ipos = changeid.find(':')
328 ipos = changeid.find(':')
329 if ipos != -1:
329 if ipos != -1:
330 changeid = changeid[(ipos + 1):]
330 changeid = changeid[(ipos + 1):]
331
331
332 return scmutil.revsymbol(repo, changeid)
332 return scmutil.revsymbol(repo, changeid)
333
333
334 def basechangectx(repo, req):
334 def basechangectx(repo, req):
335 if 'node' in req.qsparams:
335 if 'node' in req.qsparams:
336 changeid = req.qsparams['node']
336 changeid = req.qsparams['node']
337 ipos = changeid.find(':')
337 ipos = changeid.find(':')
338 if ipos != -1:
338 if ipos != -1:
339 changeid = changeid[:ipos]
339 changeid = changeid[:ipos]
340 return scmutil.revsymbol(repo, changeid)
340 return scmutil.revsymbol(repo, changeid)
341
341
342 return None
342 return None
343
343
344 def filectx(repo, req):
344 def filectx(repo, req):
345 if 'file' not in req.qsparams:
345 if 'file' not in req.qsparams:
346 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
346 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
347 path = cleanpath(repo, req.qsparams['file'])
347 path = cleanpath(repo, req.qsparams['file'])
348 if 'node' in req.qsparams:
348 if 'node' in req.qsparams:
349 changeid = req.qsparams['node']
349 changeid = req.qsparams['node']
350 elif 'filenode' in req.qsparams:
350 elif 'filenode' in req.qsparams:
351 changeid = req.qsparams['filenode']
351 changeid = req.qsparams['filenode']
352 else:
352 else:
353 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
353 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
354 try:
354 try:
355 fctx = scmutil.revsymbol(repo, changeid)[path]
355 fctx = scmutil.revsymbol(repo, changeid)[path]
356 except error.RepoError:
356 except error.RepoError:
357 fctx = repo.filectx(path, fileid=changeid)
357 fctx = repo.filectx(path, fileid=changeid)
358
358
359 return fctx
359 return fctx
360
360
361 def linerange(req):
361 def linerange(req):
362 linerange = req.qsparams.getall('linerange')
362 linerange = req.qsparams.getall('linerange')
363 if not linerange:
363 if not linerange:
364 return None
364 return None
365 if len(linerange) > 1:
365 if len(linerange) > 1:
366 raise ErrorResponse(HTTP_BAD_REQUEST,
366 raise ErrorResponse(HTTP_BAD_REQUEST,
367 'redundant linerange parameter')
367 'redundant linerange parameter')
368 try:
368 try:
369 fromline, toline = map(int, linerange[0].split(':', 1))
369 fromline, toline = map(int, linerange[0].split(':', 1))
370 except ValueError:
370 except ValueError:
371 raise ErrorResponse(HTTP_BAD_REQUEST,
371 raise ErrorResponse(HTTP_BAD_REQUEST,
372 'invalid linerange parameter')
372 'invalid linerange parameter')
373 try:
373 try:
374 return util.processlinerange(fromline, toline)
374 return util.processlinerange(fromline, toline)
375 except error.ParseError as exc:
375 except error.ParseError as exc:
376 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
376 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
377
377
378 def formatlinerange(fromline, toline):
378 def formatlinerange(fromline, toline):
379 return '%d:%d' % (fromline + 1, toline)
379 return '%d:%d' % (fromline + 1, toline)
380
380
381 def _succsandmarkersgen(context, mapping):
381 def _succsandmarkersgen(context, mapping):
382 repo = context.resource(mapping, 'repo')
382 repo = context.resource(mapping, 'repo')
383 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
383 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
384 for item in itemmappings.tovalue(context, mapping):
384 for item in itemmappings.tovalue(context, mapping):
385 item['successors'] = _siblings(repo[successor]
385 item['successors'] = _siblings(repo[successor]
386 for successor in item['successors'])
386 for successor in item['successors'])
387 yield item
387 yield item
388
388
389 def succsandmarkers(context, mapping):
389 def succsandmarkers(context, mapping):
390 return templateutil.mappinggenerator(_succsandmarkersgen, args=(mapping,))
390 return templateutil.mappinggenerator(_succsandmarkersgen, args=(mapping,))
391
391
392 # teach templater succsandmarkers is switched to (context, mapping) API
392 # teach templater succsandmarkers is switched to (context, mapping) API
393 succsandmarkers._requires = {'repo', 'ctx'}
393 succsandmarkers._requires = {'repo', 'ctx'}
394
394
395 def _whyunstablegen(context, mapping):
395 def _whyunstablegen(context, mapping):
396 repo = context.resource(mapping, 'repo')
396 repo = context.resource(mapping, 'repo')
397 ctx = context.resource(mapping, 'ctx')
397 ctx = context.resource(mapping, 'ctx')
398
398
399 entries = obsutil.whyunstable(repo, ctx)
399 entries = obsutil.whyunstable(repo, ctx)
400 for entry in entries:
400 for entry in entries:
401 if entry.get('divergentnodes'):
401 if entry.get('divergentnodes'):
402 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
402 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
403 yield entry
403 yield entry
404
404
405 def whyunstable(context, mapping):
405 def whyunstable(context, mapping):
406 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,))
406 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,))
407
407
408 whyunstable._requires = {'repo', 'ctx'}
408 whyunstable._requires = {'repo', 'ctx'}
409
409
410 def commonentry(repo, ctx):
410 def commonentry(repo, ctx):
411 node = ctx.node()
411 node = ctx.node()
412 return {
412 return {
413 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
413 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
414 # filectx, but I'm not pretty sure if that would always work because
414 # filectx, but I'm not pretty sure if that would always work because
415 # fctx.parents() != fctx.changectx.parents() for example.
415 # fctx.parents() != fctx.changectx.parents() for example.
416 'ctx': ctx,
416 'ctx': ctx,
417 'rev': ctx.rev(),
417 'rev': ctx.rev(),
418 'node': hex(node),
418 'node': hex(node),
419 'author': ctx.user(),
419 'author': ctx.user(),
420 'desc': ctx.description(),
420 'desc': ctx.description(),
421 'date': ctx.date(),
421 'date': ctx.date(),
422 'extra': ctx.extra(),
422 'extra': ctx.extra(),
423 'phase': ctx.phasestr(),
423 'phase': ctx.phasestr(),
424 'obsolete': ctx.obsolete(),
424 'obsolete': ctx.obsolete(),
425 'succsandmarkers': succsandmarkers,
425 'succsandmarkers': succsandmarkers,
426 'instabilities': templateutil.hybridlist(ctx.instabilities(),
426 'instabilities': templateutil.hybridlist(ctx.instabilities(),
427 name='instability'),
427 name='instability'),
428 'whyunstable': whyunstable,
428 'whyunstable': whyunstable,
429 'branch': nodebranchnodefault(ctx),
429 'branch': nodebranchnodefault(ctx),
430 'inbranch': nodeinbranch(repo, ctx),
430 'inbranch': nodeinbranch(repo, ctx),
431 'branches': nodebranchdict(repo, ctx),
431 'branches': nodebranchdict(repo, ctx),
432 'tags': nodetagsdict(repo, node),
432 'tags': nodetagsdict(repo, node),
433 'bookmarks': nodebookmarksdict(repo, node),
433 'bookmarks': nodebookmarksdict(repo, node),
434 'parent': lambda **x: parents(ctx),
434 'parent': lambda **x: parents(ctx),
435 'child': lambda **x: children(ctx),
435 'child': lambda **x: children(ctx),
436 }
436 }
437
437
438 def changelistentry(web, ctx):
438 def changelistentry(web, ctx):
439 '''Obtain a dictionary to be used for entries in a changelist.
439 '''Obtain a dictionary to be used for entries in a changelist.
440
440
441 This function is called when producing items for the "entries" list passed
441 This function is called when producing items for the "entries" list passed
442 to the "shortlog" and "changelog" templates.
442 to the "shortlog" and "changelog" templates.
443 '''
443 '''
444 repo = web.repo
444 repo = web.repo
445 rev = ctx.rev()
445 rev = ctx.rev()
446 n = ctx.node()
446 n = ctx.node()
447 showtags = showtag(repo, 'changelogtag', n)
447 showtags = showtag(repo, 'changelogtag', n)
448 files = listfilediffs(ctx.files(), n, web.maxfiles)
448 files = listfilediffs(ctx.files(), n, web.maxfiles)
449
449
450 entry = commonentry(repo, ctx)
450 entry = commonentry(repo, ctx)
451 entry.update(
451 entry.update(
452 allparents=lambda **x: parents(ctx),
452 allparents=lambda **x: parents(ctx),
453 parent=lambda **x: parents(ctx, rev - 1),
453 parent=lambda **x: parents(ctx, rev - 1),
454 child=lambda **x: children(ctx, rev + 1),
454 child=lambda **x: children(ctx, rev + 1),
455 changelogtag=showtags,
455 changelogtag=showtags,
456 files=files,
456 files=files,
457 )
457 )
458 return entry
458 return entry
459
459
460 def changelistentries(web, revs, maxcount, parityfn):
460 def changelistentries(web, revs, maxcount, parityfn):
461 """Emit up to N records for an iterable of revisions."""
461 """Emit up to N records for an iterable of revisions."""
462 repo = web.repo
462 repo = web.repo
463
463
464 count = 0
464 count = 0
465 for rev in revs:
465 for rev in revs:
466 if count >= maxcount:
466 if count >= maxcount:
467 break
467 break
468
468
469 count += 1
469 count += 1
470
470
471 entry = changelistentry(web, repo[rev])
471 entry = changelistentry(web, repo[rev])
472 entry['parity'] = next(parityfn)
472 entry['parity'] = next(parityfn)
473
473
474 yield entry
474 yield entry
475
475
476 def symrevorshortnode(req, ctx):
476 def symrevorshortnode(req, ctx):
477 if 'node' in req.qsparams:
477 if 'node' in req.qsparams:
478 return templatefilters.revescape(req.qsparams['node'])
478 return templatefilters.revescape(req.qsparams['node'])
479 else:
479 else:
480 return short(ctx.node())
480 return short(ctx.node())
481
481
482 def _listfilesgen(context, ctx, stripecount):
482 def _listfilesgen(context, ctx, stripecount):
483 parity = paritygen(stripecount)
483 parity = paritygen(stripecount)
484 for blockno, f in enumerate(ctx.files()):
484 for blockno, f in enumerate(ctx.files()):
485 template = 'filenodelink' if f in ctx else 'filenolink'
485 template = 'filenodelink' if f in ctx else 'filenolink'
486 yield context.process(template, {
486 yield context.process(template, {
487 'node': ctx.hex(),
487 'node': ctx.hex(),
488 'file': f,
488 'file': f,
489 'blockno': blockno + 1,
489 'blockno': blockno + 1,
490 'parity': next(parity),
490 'parity': next(parity),
491 })
491 })
492
492
493 def changesetentry(web, ctx):
493 def changesetentry(web, ctx):
494 '''Obtain a dictionary to be used to render the "changeset" template.'''
494 '''Obtain a dictionary to be used to render the "changeset" template.'''
495
495
496 showtags = showtag(web.repo, 'changesettag', ctx.node())
496 showtags = showtag(web.repo, 'changesettag', ctx.node())
497 showbookmarks = showbookmark(web.repo, 'changesetbookmark', ctx.node())
497 showbookmarks = showbookmark(web.repo, 'changesetbookmark', ctx.node())
498 showbranch = nodebranchnodefault(ctx)
498 showbranch = nodebranchnodefault(ctx)
499
499
500 basectx = basechangectx(web.repo, web.req)
500 basectx = basechangectx(web.repo, web.req)
501 if basectx is None:
501 if basectx is None:
502 basectx = ctx.p1()
502 basectx = ctx.p1()
503
503
504 style = web.config('web', 'style')
504 style = web.config('web', 'style')
505 if 'style' in web.req.qsparams:
505 if 'style' in web.req.qsparams:
506 style = web.req.qsparams['style']
506 style = web.req.qsparams['style']
507
507
508 diff = diffs(web, ctx, basectx, None, style)
508 diff = diffs(web, ctx, basectx, None, style)
509
509
510 parity = paritygen(web.stripecount)
510 parity = paritygen(web.stripecount)
511 diffstatsgen = diffstatgen(ctx, basectx)
511 diffstatsgen = diffstatgen(ctx, basectx)
512 diffstats = diffstat(ctx, diffstatsgen, parity)
512 diffstats = diffstat(ctx, diffstatsgen, parity)
513
513
514 return dict(
514 return dict(
515 diff=diff,
515 diff=diff,
516 symrev=symrevorshortnode(web.req, ctx),
516 symrev=symrevorshortnode(web.req, ctx),
517 basenode=basectx.hex(),
517 basenode=basectx.hex(),
518 changesettag=showtags,
518 changesettag=showtags,
519 changesetbookmark=showbookmarks,
519 changesetbookmark=showbookmarks,
520 changesetbranch=showbranch,
520 changesetbranch=showbranch,
521 files=templateutil.mappedgenerator(_listfilesgen,
521 files=templateutil.mappedgenerator(_listfilesgen,
522 args=(ctx, web.stripecount)),
522 args=(ctx, web.stripecount)),
523 diffsummary=lambda **x: diffsummary(diffstatsgen),
523 diffsummary=lambda **x: diffsummary(diffstatsgen),
524 diffstat=diffstats,
524 diffstat=diffstats,
525 archives=web.archivelist(ctx.hex()),
525 archives=web.archivelist(ctx.hex()),
526 **pycompat.strkwargs(commonentry(web.repo, ctx)))
526 **pycompat.strkwargs(commonentry(web.repo, ctx)))
527
527
528 def _listfilediffsgen(context, files, node, max):
528 def _listfilediffsgen(context, files, node, max):
529 for f in files[:max]:
529 for f in files[:max]:
530 yield context.process('filedifflink', {'node': hex(node), 'file': f})
530 yield context.process('filedifflink', {'node': hex(node), 'file': f})
531 if len(files) > max:
531 if len(files) > max:
532 yield context.process('fileellipses', {})
532 yield context.process('fileellipses', {})
533
533
534 def listfilediffs(files, node, max):
534 def listfilediffs(files, node, max):
535 return templateutil.mappedgenerator(_listfilediffsgen,
535 return templateutil.mappedgenerator(_listfilediffsgen,
536 args=(files, node, max))
536 args=(files, node, max))
537
537
538 def _prettyprintdifflines(context, lines, blockno, lineidprefix):
538 def _prettyprintdifflines(context, lines, blockno, lineidprefix):
539 for lineno, l in enumerate(lines, 1):
539 for lineno, l in enumerate(lines, 1):
540 difflineno = "%d.%d" % (blockno, lineno)
540 difflineno = "%d.%d" % (blockno, lineno)
541 if l.startswith('+'):
541 if l.startswith('+'):
542 ltype = "difflineplus"
542 ltype = "difflineplus"
543 elif l.startswith('-'):
543 elif l.startswith('-'):
544 ltype = "difflineminus"
544 ltype = "difflineminus"
545 elif l.startswith('@'):
545 elif l.startswith('@'):
546 ltype = "difflineat"
546 ltype = "difflineat"
547 else:
547 else:
548 ltype = "diffline"
548 ltype = "diffline"
549 yield context.process(ltype, {
549 yield context.process(ltype, {
550 'line': l,
550 'line': l,
551 'lineno': lineno,
551 'lineno': lineno,
552 'lineid': lineidprefix + "l%s" % difflineno,
552 'lineid': lineidprefix + "l%s" % difflineno,
553 'linenumber': "% 8s" % difflineno,
553 'linenumber': "% 8s" % difflineno,
554 })
554 })
555
555
556 def _diffsgen(context, repo, ctx, basectx, files, style, stripecount,
556 def _diffsgen(context, repo, ctx, basectx, files, style, stripecount,
557 linerange, lineidprefix):
557 linerange, lineidprefix):
558 if files:
558 if files:
559 m = match.exact(repo.root, repo.getcwd(), files)
559 m = match.exact(repo.root, repo.getcwd(), files)
560 else:
560 else:
561 m = match.always(repo.root, repo.getcwd())
561 m = match.always(repo.root, repo.getcwd())
562
562
563 diffopts = patch.diffopts(repo.ui, untrusted=True)
563 diffopts = patch.diffopts(repo.ui, untrusted=True)
564 node1 = basectx.node()
564 node1 = basectx.node()
565 node2 = ctx.node()
565 node2 = ctx.node()
566 parity = paritygen(stripecount)
566 parity = paritygen(stripecount)
567
567
568 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
568 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
569 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
569 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
570 if style != 'raw':
570 if style != 'raw':
571 header = header[1:]
571 header = header[1:]
572 lines = [h + '\n' for h in header]
572 lines = [h + '\n' for h in header]
573 for hunkrange, hunklines in hunks:
573 for hunkrange, hunklines in hunks:
574 if linerange is not None and hunkrange is not None:
574 if linerange is not None and hunkrange is not None:
575 s1, l1, s2, l2 = hunkrange
575 s1, l1, s2, l2 = hunkrange
576 if not mdiff.hunkinrange((s2, l2), linerange):
576 if not mdiff.hunkinrange((s2, l2), linerange):
577 continue
577 continue
578 lines.extend(hunklines)
578 lines.extend(hunklines)
579 if lines:
579 if lines:
580 l = templateutil.mappedgenerator(_prettyprintdifflines,
580 l = templateutil.mappedgenerator(_prettyprintdifflines,
581 args=(lines, blockno,
581 args=(lines, blockno,
582 lineidprefix))
582 lineidprefix))
583 yield {
583 yield {
584 'parity': next(parity),
584 'parity': next(parity),
585 'blockno': blockno,
585 'blockno': blockno,
586 'lines': l,
586 'lines': l,
587 }
587 }
588
588
589 def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=''):
589 def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=''):
590 args = (web.repo, ctx, basectx, files, style, web.stripecount,
590 args = (web.repo, ctx, basectx, files, style, web.stripecount,
591 linerange, lineidprefix)
591 linerange, lineidprefix)
592 return templateutil.mappinggenerator(_diffsgen, args=args, name='diffblock')
592 return templateutil.mappinggenerator(_diffsgen, args=args, name='diffblock')
593
593
594 def _compline(type, leftlineno, leftline, rightlineno, rightline):
594 def _compline(type, leftlineno, leftline, rightlineno, rightline):
595 lineid = leftlineno and ("l%d" % leftlineno) or ''
595 lineid = leftlineno and ("l%d" % leftlineno) or ''
596 lineid += rightlineno and ("r%d" % rightlineno) or ''
596 lineid += rightlineno and ("r%d" % rightlineno) or ''
597 llno = '%d' % leftlineno if leftlineno else ''
597 llno = '%d' % leftlineno if leftlineno else ''
598 rlno = '%d' % rightlineno if rightlineno else ''
598 rlno = '%d' % rightlineno if rightlineno else ''
599 return {
599 return {
600 'type': type,
600 'type': type,
601 'lineid': lineid,
601 'lineid': lineid,
602 'leftlineno': leftlineno,
602 'leftlineno': leftlineno,
603 'leftlinenumber': "% 6s" % llno,
603 'leftlinenumber': "% 6s" % llno,
604 'leftline': leftline or '',
604 'leftline': leftline or '',
605 'rightlineno': rightlineno,
605 'rightlineno': rightlineno,
606 'rightlinenumber': "% 6s" % rlno,
606 'rightlinenumber': "% 6s" % rlno,
607 'rightline': rightline or '',
607 'rightline': rightline or '',
608 }
608 }
609
609
610 def _getcompblockgen(context, leftlines, rightlines, opcodes):
610 def _getcompblockgen(context, leftlines, rightlines, opcodes):
611 for type, llo, lhi, rlo, rhi in opcodes:
611 for type, llo, lhi, rlo, rhi in opcodes:
612 len1 = lhi - llo
612 len1 = lhi - llo
613 len2 = rhi - rlo
613 len2 = rhi - rlo
614 count = min(len1, len2)
614 count = min(len1, len2)
615 for i in xrange(count):
615 for i in xrange(count):
616 yield _compline(type=type,
616 yield _compline(type=type,
617 leftlineno=llo + i + 1,
617 leftlineno=llo + i + 1,
618 leftline=leftlines[llo + i],
618 leftline=leftlines[llo + i],
619 rightlineno=rlo + i + 1,
619 rightlineno=rlo + i + 1,
620 rightline=rightlines[rlo + i])
620 rightline=rightlines[rlo + i])
621 if len1 > len2:
621 if len1 > len2:
622 for i in xrange(llo + count, lhi):
622 for i in xrange(llo + count, lhi):
623 yield _compline(type=type,
623 yield _compline(type=type,
624 leftlineno=i + 1,
624 leftlineno=i + 1,
625 leftline=leftlines[i],
625 leftline=leftlines[i],
626 rightlineno=None,
626 rightlineno=None,
627 rightline=None)
627 rightline=None)
628 elif len2 > len1:
628 elif len2 > len1:
629 for i in xrange(rlo + count, rhi):
629 for i in xrange(rlo + count, rhi):
630 yield _compline(type=type,
630 yield _compline(type=type,
631 leftlineno=None,
631 leftlineno=None,
632 leftline=None,
632 leftline=None,
633 rightlineno=i + 1,
633 rightlineno=i + 1,
634 rightline=rightlines[i])
634 rightline=rightlines[i])
635
635
636 def _getcompblock(leftlines, rightlines, opcodes):
636 def _getcompblock(leftlines, rightlines, opcodes):
637 args = (leftlines, rightlines, opcodes)
637 args = (leftlines, rightlines, opcodes)
638 return templateutil.mappinggenerator(_getcompblockgen, args=args,
638 return templateutil.mappinggenerator(_getcompblockgen, args=args,
639 name='comparisonline')
639 name='comparisonline')
640
640
641 def _comparegen(context, contextnum, leftlines, rightlines):
641 def _comparegen(context, contextnum, leftlines, rightlines):
642 '''Generator function that provides side-by-side comparison data.'''
642 '''Generator function that provides side-by-side comparison data.'''
643 s = difflib.SequenceMatcher(None, leftlines, rightlines)
643 s = difflib.SequenceMatcher(None, leftlines, rightlines)
644 if contextnum < 0:
644 if contextnum < 0:
645 l = _getcompblock(leftlines, rightlines, s.get_opcodes())
645 l = _getcompblock(leftlines, rightlines, s.get_opcodes())
646 yield {'lines': l}
646 yield {'lines': l}
647 else:
647 else:
648 for oc in s.get_grouped_opcodes(n=contextnum):
648 for oc in s.get_grouped_opcodes(n=contextnum):
649 l = _getcompblock(leftlines, rightlines, oc)
649 l = _getcompblock(leftlines, rightlines, oc)
650 yield {'lines': l}
650 yield {'lines': l}
651
651
652 def compare(contextnum, leftlines, rightlines):
652 def compare(contextnum, leftlines, rightlines):
653 args = (contextnum, leftlines, rightlines)
653 args = (contextnum, leftlines, rightlines)
654 return templateutil.mappinggenerator(_comparegen, args=args,
654 return templateutil.mappinggenerator(_comparegen, args=args,
655 name='comparisonblock')
655 name='comparisonblock')
656
656
657 def diffstatgen(ctx, basectx):
657 def diffstatgen(ctx, basectx):
658 '''Generator function that provides the diffstat data.'''
658 '''Generator function that provides the diffstat data.'''
659
659
660 stats = patch.diffstatdata(
660 stats = patch.diffstatdata(
661 util.iterlines(ctx.diff(basectx, noprefix=False)))
661 util.iterlines(ctx.diff(basectx, noprefix=False)))
662 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
662 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
663 while True:
663 while True:
664 yield stats, maxname, maxtotal, addtotal, removetotal, binary
664 yield stats, maxname, maxtotal, addtotal, removetotal, binary
665
665
666 def diffsummary(statgen):
666 def diffsummary(statgen):
667 '''Return a short summary of the diff.'''
667 '''Return a short summary of the diff.'''
668
668
669 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
669 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
670 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
670 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
671 len(stats), addtotal, removetotal)
671 len(stats), addtotal, removetotal)
672
672
673 def _diffstattmplgen(context, ctx, statgen, parity):
673 def _diffstattmplgen(context, ctx, statgen, parity):
674 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
674 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
675 files = ctx.files()
675 files = ctx.files()
676
676
677 def pct(i):
677 def pct(i):
678 if maxtotal == 0:
678 if maxtotal == 0:
679 return 0
679 return 0
680 return (float(i) / maxtotal) * 100
680 return (float(i) / maxtotal) * 100
681
681
682 fileno = 0
682 fileno = 0
683 for filename, adds, removes, isbinary in stats:
683 for filename, adds, removes, isbinary in stats:
684 template = 'diffstatlink' if filename in files else 'diffstatnolink'
684 template = 'diffstatlink' if filename in files else 'diffstatnolink'
685 total = adds + removes
685 total = adds + removes
686 fileno += 1
686 fileno += 1
687 yield context.process(template, {
687 yield context.process(template, {
688 'node': ctx.hex(),
688 'node': ctx.hex(),
689 'file': filename,
689 'file': filename,
690 'fileno': fileno,
690 'fileno': fileno,
691 'total': total,
691 'total': total,
692 'addpct': pct(adds),
692 'addpct': pct(adds),
693 'removepct': pct(removes),
693 'removepct': pct(removes),
694 'parity': next(parity),
694 'parity': next(parity),
695 })
695 })
696
696
697 def diffstat(ctx, statgen, parity):
697 def diffstat(ctx, statgen, parity):
698 '''Return a diffstat template for each file in the diff.'''
698 '''Return a diffstat template for each file in the diff.'''
699 args = (ctx, statgen, parity)
699 args = (ctx, statgen, parity)
700 return templateutil.mappedgenerator(_diffstattmplgen, args=args)
700 return templateutil.mappedgenerator(_diffstattmplgen, args=args)
701
701
702 class sessionvars(templateutil.wrapped):
702 class sessionvars(templateutil.wrapped):
703 def __init__(self, vars, start='?'):
703 def __init__(self, vars, start='?'):
704 self._start = start
704 self._start = start
705 self._vars = vars
705 self._vars = vars
706
706
707 def __getitem__(self, key):
707 def __getitem__(self, key):
708 return self._vars[key]
708 return self._vars[key]
709
709
710 def __setitem__(self, key, value):
710 def __setitem__(self, key, value):
711 self._vars[key] = value
711 self._vars[key] = value
712
712
713 def __copy__(self):
713 def __copy__(self):
714 return sessionvars(copy.copy(self._vars), self._start)
714 return sessionvars(copy.copy(self._vars), self._start)
715
715
716 def getmember(self, context, mapping, key):
716 def getmember(self, context, mapping, key):
717 key = templateutil.unwrapvalue(context, mapping, key)
717 return self._vars.get(key)
718 return self._vars.get(key)
718
719
719 def itermaps(self, context):
720 def itermaps(self, context):
720 separator = self._start
721 separator = self._start
721 for key, value in sorted(self._vars.iteritems()):
722 for key, value in sorted(self._vars.iteritems()):
722 yield {'name': key,
723 yield {'name': key,
723 'value': pycompat.bytestr(value),
724 'value': pycompat.bytestr(value),
724 'separator': separator,
725 'separator': separator,
725 }
726 }
726 separator = '&'
727 separator = '&'
727
728
728 def join(self, context, mapping, sep):
729 def join(self, context, mapping, sep):
729 # could be '{separator}{name}={value|urlescape}'
730 # could be '{separator}{name}={value|urlescape}'
730 raise error.ParseError(_('not displayable without template'))
731 raise error.ParseError(_('not displayable without template'))
731
732
732 def show(self, context, mapping):
733 def show(self, context, mapping):
733 return self.join(context, '')
734 return self.join(context, '')
734
735
735 def tovalue(self, context, mapping):
736 def tovalue(self, context, mapping):
736 return self._vars
737 return self._vars
737
738
738 class wsgiui(uimod.ui):
739 class wsgiui(uimod.ui):
739 # default termwidth breaks under mod_wsgi
740 # default termwidth breaks under mod_wsgi
740 def termwidth(self):
741 def termwidth(self):
741 return 80
742 return 80
742
743
743 def getwebsubs(repo):
744 def getwebsubs(repo):
744 websubtable = []
745 websubtable = []
745 websubdefs = repo.ui.configitems('websub')
746 websubdefs = repo.ui.configitems('websub')
746 # we must maintain interhg backwards compatibility
747 # we must maintain interhg backwards compatibility
747 websubdefs += repo.ui.configitems('interhg')
748 websubdefs += repo.ui.configitems('interhg')
748 for key, pattern in websubdefs:
749 for key, pattern in websubdefs:
749 # grab the delimiter from the character after the "s"
750 # grab the delimiter from the character after the "s"
750 unesc = pattern[1:2]
751 unesc = pattern[1:2]
751 delim = re.escape(unesc)
752 delim = re.escape(unesc)
752
753
753 # identify portions of the pattern, taking care to avoid escaped
754 # identify portions of the pattern, taking care to avoid escaped
754 # delimiters. the replace format and flags are optional, but
755 # delimiters. the replace format and flags are optional, but
755 # delimiters are required.
756 # delimiters are required.
756 match = re.match(
757 match = re.match(
757 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
758 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
758 % (delim, delim, delim), pattern)
759 % (delim, delim, delim), pattern)
759 if not match:
760 if not match:
760 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
761 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
761 % (key, pattern))
762 % (key, pattern))
762 continue
763 continue
763
764
764 # we need to unescape the delimiter for regexp and format
765 # we need to unescape the delimiter for regexp and format
765 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
766 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
766 regexp = delim_re.sub(unesc, match.group(1))
767 regexp = delim_re.sub(unesc, match.group(1))
767 format = delim_re.sub(unesc, match.group(2))
768 format = delim_re.sub(unesc, match.group(2))
768
769
769 # the pattern allows for 6 regexp flags, so set them if necessary
770 # the pattern allows for 6 regexp flags, so set them if necessary
770 flagin = match.group(3)
771 flagin = match.group(3)
771 flags = 0
772 flags = 0
772 if flagin:
773 if flagin:
773 for flag in flagin.upper():
774 for flag in flagin.upper():
774 flags |= re.__dict__[flag]
775 flags |= re.__dict__[flag]
775
776
776 try:
777 try:
777 regexp = re.compile(regexp, flags)
778 regexp = re.compile(regexp, flags)
778 websubtable.append((regexp, format))
779 websubtable.append((regexp, format))
779 except re.error:
780 except re.error:
780 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
781 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
781 % (key, regexp))
782 % (key, regexp))
782 return websubtable
783 return websubtable
783
784
784 def getgraphnode(repo, ctx):
785 def getgraphnode(repo, ctx):
785 return (templatekw.getgraphnodecurrent(repo, ctx) +
786 return (templatekw.getgraphnodecurrent(repo, ctx) +
786 templatekw.getgraphnodesymbol(ctx))
787 templatekw.getgraphnodesymbol(ctx))
@@ -1,702 +1,702 b''
1 # templatefuncs.py - common template functions
1 # templatefuncs.py - common template functions
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 bin,
14 bin,
15 wdirid,
15 wdirid,
16 )
16 )
17 from . import (
17 from . import (
18 color,
18 color,
19 encoding,
19 encoding,
20 error,
20 error,
21 minirst,
21 minirst,
22 obsutil,
22 obsutil,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 revset as revsetmod,
25 revset as revsetmod,
26 revsetlang,
26 revsetlang,
27 scmutil,
27 scmutil,
28 templatefilters,
28 templatefilters,
29 templatekw,
29 templatekw,
30 templateutil,
30 templateutil,
31 util,
31 util,
32 )
32 )
33 from .utils import (
33 from .utils import (
34 dateutil,
34 dateutil,
35 stringutil,
35 stringutil,
36 )
36 )
37
37
38 evalrawexp = templateutil.evalrawexp
38 evalrawexp = templateutil.evalrawexp
39 evalwrapped = templateutil.evalwrapped
39 evalwrapped = templateutil.evalwrapped
40 evalfuncarg = templateutil.evalfuncarg
40 evalfuncarg = templateutil.evalfuncarg
41 evalboolean = templateutil.evalboolean
41 evalboolean = templateutil.evalboolean
42 evaldate = templateutil.evaldate
42 evaldate = templateutil.evaldate
43 evalinteger = templateutil.evalinteger
43 evalinteger = templateutil.evalinteger
44 evalstring = templateutil.evalstring
44 evalstring = templateutil.evalstring
45 evalstringliteral = templateutil.evalstringliteral
45 evalstringliteral = templateutil.evalstringliteral
46
46
47 # dict of template built-in functions
47 # dict of template built-in functions
48 funcs = {}
48 funcs = {}
49 templatefunc = registrar.templatefunc(funcs)
49 templatefunc = registrar.templatefunc(funcs)
50
50
51 @templatefunc('date(date[, fmt])')
51 @templatefunc('date(date[, fmt])')
52 def date(context, mapping, args):
52 def date(context, mapping, args):
53 """Format a date. See :hg:`help dates` for formatting
53 """Format a date. See :hg:`help dates` for formatting
54 strings. The default is a Unix date format, including the timezone:
54 strings. The default is a Unix date format, including the timezone:
55 "Mon Sep 04 15:13:13 2006 0700"."""
55 "Mon Sep 04 15:13:13 2006 0700"."""
56 if not (1 <= len(args) <= 2):
56 if not (1 <= len(args) <= 2):
57 # i18n: "date" is a keyword
57 # i18n: "date" is a keyword
58 raise error.ParseError(_("date expects one or two arguments"))
58 raise error.ParseError(_("date expects one or two arguments"))
59
59
60 date = evaldate(context, mapping, args[0],
60 date = evaldate(context, mapping, args[0],
61 # i18n: "date" is a keyword
61 # i18n: "date" is a keyword
62 _("date expects a date information"))
62 _("date expects a date information"))
63 fmt = None
63 fmt = None
64 if len(args) == 2:
64 if len(args) == 2:
65 fmt = evalstring(context, mapping, args[1])
65 fmt = evalstring(context, mapping, args[1])
66 if fmt is None:
66 if fmt is None:
67 return dateutil.datestr(date)
67 return dateutil.datestr(date)
68 else:
68 else:
69 return dateutil.datestr(date, fmt)
69 return dateutil.datestr(date, fmt)
70
70
71 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
71 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
72 def dict_(context, mapping, args):
72 def dict_(context, mapping, args):
73 """Construct a dict from key-value pairs. A key may be omitted if
73 """Construct a dict from key-value pairs. A key may be omitted if
74 a value expression can provide an unambiguous name."""
74 a value expression can provide an unambiguous name."""
75 data = util.sortdict()
75 data = util.sortdict()
76
76
77 for v in args['args']:
77 for v in args['args']:
78 k = templateutil.findsymbolicname(v)
78 k = templateutil.findsymbolicname(v)
79 if not k:
79 if not k:
80 raise error.ParseError(_('dict key cannot be inferred'))
80 raise error.ParseError(_('dict key cannot be inferred'))
81 if k in data or k in args['kwargs']:
81 if k in data or k in args['kwargs']:
82 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
82 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
83 data[k] = evalfuncarg(context, mapping, v)
83 data[k] = evalfuncarg(context, mapping, v)
84
84
85 data.update((k, evalfuncarg(context, mapping, v))
85 data.update((k, evalfuncarg(context, mapping, v))
86 for k, v in args['kwargs'].iteritems())
86 for k, v in args['kwargs'].iteritems())
87 return templateutil.hybriddict(data)
87 return templateutil.hybriddict(data)
88
88
89 @templatefunc('diff([includepattern [, excludepattern]])')
89 @templatefunc('diff([includepattern [, excludepattern]])')
90 def diff(context, mapping, args):
90 def diff(context, mapping, args):
91 """Show a diff, optionally
91 """Show a diff, optionally
92 specifying files to include or exclude."""
92 specifying files to include or exclude."""
93 if len(args) > 2:
93 if len(args) > 2:
94 # i18n: "diff" is a keyword
94 # i18n: "diff" is a keyword
95 raise error.ParseError(_("diff expects zero, one, or two arguments"))
95 raise error.ParseError(_("diff expects zero, one, or two arguments"))
96
96
97 def getpatterns(i):
97 def getpatterns(i):
98 if i < len(args):
98 if i < len(args):
99 s = evalstring(context, mapping, args[i]).strip()
99 s = evalstring(context, mapping, args[i]).strip()
100 if s:
100 if s:
101 return [s]
101 return [s]
102 return []
102 return []
103
103
104 ctx = context.resource(mapping, 'ctx')
104 ctx = context.resource(mapping, 'ctx')
105 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
105 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
106
106
107 return ''.join(chunks)
107 return ''.join(chunks)
108
108
109 @templatefunc('extdata(source)', argspec='source')
109 @templatefunc('extdata(source)', argspec='source')
110 def extdata(context, mapping, args):
110 def extdata(context, mapping, args):
111 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
111 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
112 if 'source' not in args:
112 if 'source' not in args:
113 # i18n: "extdata" is a keyword
113 # i18n: "extdata" is a keyword
114 raise error.ParseError(_('extdata expects one argument'))
114 raise error.ParseError(_('extdata expects one argument'))
115
115
116 source = evalstring(context, mapping, args['source'])
116 source = evalstring(context, mapping, args['source'])
117 if not source:
117 if not source:
118 sym = templateutil.findsymbolicname(args['source'])
118 sym = templateutil.findsymbolicname(args['source'])
119 if sym:
119 if sym:
120 raise error.ParseError(_('empty data source specified'),
120 raise error.ParseError(_('empty data source specified'),
121 hint=_("did you mean extdata('%s')?") % sym)
121 hint=_("did you mean extdata('%s')?") % sym)
122 else:
122 else:
123 raise error.ParseError(_('empty data source specified'))
123 raise error.ParseError(_('empty data source specified'))
124 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
124 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
125 ctx = context.resource(mapping, 'ctx')
125 ctx = context.resource(mapping, 'ctx')
126 if source in cache:
126 if source in cache:
127 data = cache[source]
127 data = cache[source]
128 else:
128 else:
129 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
129 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
130 return data.get(ctx.rev(), '')
130 return data.get(ctx.rev(), '')
131
131
132 @templatefunc('files(pattern)')
132 @templatefunc('files(pattern)')
133 def files(context, mapping, args):
133 def files(context, mapping, args):
134 """All files of the current changeset matching the pattern. See
134 """All files of the current changeset matching the pattern. See
135 :hg:`help patterns`."""
135 :hg:`help patterns`."""
136 if not len(args) == 1:
136 if not len(args) == 1:
137 # i18n: "files" is a keyword
137 # i18n: "files" is a keyword
138 raise error.ParseError(_("files expects one argument"))
138 raise error.ParseError(_("files expects one argument"))
139
139
140 raw = evalstring(context, mapping, args[0])
140 raw = evalstring(context, mapping, args[0])
141 ctx = context.resource(mapping, 'ctx')
141 ctx = context.resource(mapping, 'ctx')
142 m = ctx.match([raw])
142 m = ctx.match([raw])
143 files = list(ctx.matches(m))
143 files = list(ctx.matches(m))
144 return templateutil.compatlist(context, mapping, "file", files)
144 return templateutil.compatlist(context, mapping, "file", files)
145
145
146 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
146 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
147 def fill(context, mapping, args):
147 def fill(context, mapping, args):
148 """Fill many
148 """Fill many
149 paragraphs with optional indentation. See the "fill" filter."""
149 paragraphs with optional indentation. See the "fill" filter."""
150 if not (1 <= len(args) <= 4):
150 if not (1 <= len(args) <= 4):
151 # i18n: "fill" is a keyword
151 # i18n: "fill" is a keyword
152 raise error.ParseError(_("fill expects one to four arguments"))
152 raise error.ParseError(_("fill expects one to four arguments"))
153
153
154 text = evalstring(context, mapping, args[0])
154 text = evalstring(context, mapping, args[0])
155 width = 76
155 width = 76
156 initindent = ''
156 initindent = ''
157 hangindent = ''
157 hangindent = ''
158 if 2 <= len(args) <= 4:
158 if 2 <= len(args) <= 4:
159 width = evalinteger(context, mapping, args[1],
159 width = evalinteger(context, mapping, args[1],
160 # i18n: "fill" is a keyword
160 # i18n: "fill" is a keyword
161 _("fill expects an integer width"))
161 _("fill expects an integer width"))
162 try:
162 try:
163 initindent = evalstring(context, mapping, args[2])
163 initindent = evalstring(context, mapping, args[2])
164 hangindent = evalstring(context, mapping, args[3])
164 hangindent = evalstring(context, mapping, args[3])
165 except IndexError:
165 except IndexError:
166 pass
166 pass
167
167
168 return templatefilters.fill(text, width, initindent, hangindent)
168 return templatefilters.fill(text, width, initindent, hangindent)
169
169
170 @templatefunc('formatnode(node)')
170 @templatefunc('formatnode(node)')
171 def formatnode(context, mapping, args):
171 def formatnode(context, mapping, args):
172 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
172 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
173 if len(args) != 1:
173 if len(args) != 1:
174 # i18n: "formatnode" is a keyword
174 # i18n: "formatnode" is a keyword
175 raise error.ParseError(_("formatnode expects one argument"))
175 raise error.ParseError(_("formatnode expects one argument"))
176
176
177 ui = context.resource(mapping, 'ui')
177 ui = context.resource(mapping, 'ui')
178 node = evalstring(context, mapping, args[0])
178 node = evalstring(context, mapping, args[0])
179 if ui.debugflag:
179 if ui.debugflag:
180 return node
180 return node
181 return templatefilters.short(node)
181 return templatefilters.short(node)
182
182
183 @templatefunc('mailmap(author)')
183 @templatefunc('mailmap(author)')
184 def mailmap(context, mapping, args):
184 def mailmap(context, mapping, args):
185 """Return the author, updated according to the value
185 """Return the author, updated according to the value
186 set in the .mailmap file"""
186 set in the .mailmap file"""
187 if len(args) != 1:
187 if len(args) != 1:
188 raise error.ParseError(_("mailmap expects one argument"))
188 raise error.ParseError(_("mailmap expects one argument"))
189
189
190 author = evalstring(context, mapping, args[0])
190 author = evalstring(context, mapping, args[0])
191
191
192 cache = context.resource(mapping, 'cache')
192 cache = context.resource(mapping, 'cache')
193 repo = context.resource(mapping, 'repo')
193 repo = context.resource(mapping, 'repo')
194
194
195 if 'mailmap' not in cache:
195 if 'mailmap' not in cache:
196 data = repo.wvfs.tryread('.mailmap')
196 data = repo.wvfs.tryread('.mailmap')
197 cache['mailmap'] = stringutil.parsemailmap(data)
197 cache['mailmap'] = stringutil.parsemailmap(data)
198
198
199 return stringutil.mapname(cache['mailmap'], author)
199 return stringutil.mapname(cache['mailmap'], author)
200
200
201 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
201 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
202 argspec='text width fillchar left')
202 argspec='text width fillchar left')
203 def pad(context, mapping, args):
203 def pad(context, mapping, args):
204 """Pad text with a
204 """Pad text with a
205 fill character."""
205 fill character."""
206 if 'text' not in args or 'width' not in args:
206 if 'text' not in args or 'width' not in args:
207 # i18n: "pad" is a keyword
207 # i18n: "pad" is a keyword
208 raise error.ParseError(_("pad() expects two to four arguments"))
208 raise error.ParseError(_("pad() expects two to four arguments"))
209
209
210 width = evalinteger(context, mapping, args['width'],
210 width = evalinteger(context, mapping, args['width'],
211 # i18n: "pad" is a keyword
211 # i18n: "pad" is a keyword
212 _("pad() expects an integer width"))
212 _("pad() expects an integer width"))
213
213
214 text = evalstring(context, mapping, args['text'])
214 text = evalstring(context, mapping, args['text'])
215
215
216 left = False
216 left = False
217 fillchar = ' '
217 fillchar = ' '
218 if 'fillchar' in args:
218 if 'fillchar' in args:
219 fillchar = evalstring(context, mapping, args['fillchar'])
219 fillchar = evalstring(context, mapping, args['fillchar'])
220 if len(color.stripeffects(fillchar)) != 1:
220 if len(color.stripeffects(fillchar)) != 1:
221 # i18n: "pad" is a keyword
221 # i18n: "pad" is a keyword
222 raise error.ParseError(_("pad() expects a single fill character"))
222 raise error.ParseError(_("pad() expects a single fill character"))
223 if 'left' in args:
223 if 'left' in args:
224 left = evalboolean(context, mapping, args['left'])
224 left = evalboolean(context, mapping, args['left'])
225
225
226 fillwidth = width - encoding.colwidth(color.stripeffects(text))
226 fillwidth = width - encoding.colwidth(color.stripeffects(text))
227 if fillwidth <= 0:
227 if fillwidth <= 0:
228 return text
228 return text
229 if left:
229 if left:
230 return fillchar * fillwidth + text
230 return fillchar * fillwidth + text
231 else:
231 else:
232 return text + fillchar * fillwidth
232 return text + fillchar * fillwidth
233
233
234 @templatefunc('indent(text, indentchars[, firstline])')
234 @templatefunc('indent(text, indentchars[, firstline])')
235 def indent(context, mapping, args):
235 def indent(context, mapping, args):
236 """Indents all non-empty lines
236 """Indents all non-empty lines
237 with the characters given in the indentchars string. An optional
237 with the characters given in the indentchars string. An optional
238 third parameter will override the indent for the first line only
238 third parameter will override the indent for the first line only
239 if present."""
239 if present."""
240 if not (2 <= len(args) <= 3):
240 if not (2 <= len(args) <= 3):
241 # i18n: "indent" is a keyword
241 # i18n: "indent" is a keyword
242 raise error.ParseError(_("indent() expects two or three arguments"))
242 raise error.ParseError(_("indent() expects two or three arguments"))
243
243
244 text = evalstring(context, mapping, args[0])
244 text = evalstring(context, mapping, args[0])
245 indent = evalstring(context, mapping, args[1])
245 indent = evalstring(context, mapping, args[1])
246
246
247 if len(args) == 3:
247 if len(args) == 3:
248 firstline = evalstring(context, mapping, args[2])
248 firstline = evalstring(context, mapping, args[2])
249 else:
249 else:
250 firstline = indent
250 firstline = indent
251
251
252 # the indent function doesn't indent the first line, so we do it here
252 # the indent function doesn't indent the first line, so we do it here
253 return templatefilters.indent(firstline + text, indent)
253 return templatefilters.indent(firstline + text, indent)
254
254
255 @templatefunc('get(dict, key)')
255 @templatefunc('get(dict, key)')
256 def get(context, mapping, args):
256 def get(context, mapping, args):
257 """Get an attribute/key from an object. Some keywords
257 """Get an attribute/key from an object. Some keywords
258 are complex types. This function allows you to obtain the value of an
258 are complex types. This function allows you to obtain the value of an
259 attribute on these types."""
259 attribute on these types."""
260 if len(args) != 2:
260 if len(args) != 2:
261 # i18n: "get" is a keyword
261 # i18n: "get" is a keyword
262 raise error.ParseError(_("get() expects two arguments"))
262 raise error.ParseError(_("get() expects two arguments"))
263
263
264 dictarg = evalwrapped(context, mapping, args[0])
264 dictarg = evalwrapped(context, mapping, args[0])
265 key = evalfuncarg(context, mapping, args[1])
265 key = evalrawexp(context, mapping, args[1])
266 try:
266 try:
267 return dictarg.getmember(context, mapping, key)
267 return dictarg.getmember(context, mapping, key)
268 except error.ParseError as err:
268 except error.ParseError as err:
269 # i18n: "get" is a keyword
269 # i18n: "get" is a keyword
270 hint = _("get() expects a dict as first argument")
270 hint = _("get() expects a dict as first argument")
271 raise error.ParseError(bytes(err), hint=hint)
271 raise error.ParseError(bytes(err), hint=hint)
272
272
273 @templatefunc('if(expr, then[, else])')
273 @templatefunc('if(expr, then[, else])')
274 def if_(context, mapping, args):
274 def if_(context, mapping, args):
275 """Conditionally execute based on the result of
275 """Conditionally execute based on the result of
276 an expression."""
276 an expression."""
277 if not (2 <= len(args) <= 3):
277 if not (2 <= len(args) <= 3):
278 # i18n: "if" is a keyword
278 # i18n: "if" is a keyword
279 raise error.ParseError(_("if expects two or three arguments"))
279 raise error.ParseError(_("if expects two or three arguments"))
280
280
281 test = evalboolean(context, mapping, args[0])
281 test = evalboolean(context, mapping, args[0])
282 if test:
282 if test:
283 return evalrawexp(context, mapping, args[1])
283 return evalrawexp(context, mapping, args[1])
284 elif len(args) == 3:
284 elif len(args) == 3:
285 return evalrawexp(context, mapping, args[2])
285 return evalrawexp(context, mapping, args[2])
286
286
287 @templatefunc('ifcontains(needle, haystack, then[, else])')
287 @templatefunc('ifcontains(needle, haystack, then[, else])')
288 def ifcontains(context, mapping, args):
288 def ifcontains(context, mapping, args):
289 """Conditionally execute based
289 """Conditionally execute based
290 on whether the item "needle" is in "haystack"."""
290 on whether the item "needle" is in "haystack"."""
291 if not (3 <= len(args) <= 4):
291 if not (3 <= len(args) <= 4):
292 # i18n: "ifcontains" is a keyword
292 # i18n: "ifcontains" is a keyword
293 raise error.ParseError(_("ifcontains expects three or four arguments"))
293 raise error.ParseError(_("ifcontains expects three or four arguments"))
294
294
295 haystack = evalfuncarg(context, mapping, args[1])
295 haystack = evalfuncarg(context, mapping, args[1])
296 keytype = getattr(haystack, 'keytype', None)
296 keytype = getattr(haystack, 'keytype', None)
297 try:
297 try:
298 needle = evalrawexp(context, mapping, args[0])
298 needle = evalrawexp(context, mapping, args[0])
299 needle = templateutil.unwrapastype(context, mapping, needle,
299 needle = templateutil.unwrapastype(context, mapping, needle,
300 keytype or bytes)
300 keytype or bytes)
301 found = (needle in haystack)
301 found = (needle in haystack)
302 except error.ParseError:
302 except error.ParseError:
303 found = False
303 found = False
304
304
305 if found:
305 if found:
306 return evalrawexp(context, mapping, args[2])
306 return evalrawexp(context, mapping, args[2])
307 elif len(args) == 4:
307 elif len(args) == 4:
308 return evalrawexp(context, mapping, args[3])
308 return evalrawexp(context, mapping, args[3])
309
309
310 @templatefunc('ifeq(expr1, expr2, then[, else])')
310 @templatefunc('ifeq(expr1, expr2, then[, else])')
311 def ifeq(context, mapping, args):
311 def ifeq(context, mapping, args):
312 """Conditionally execute based on
312 """Conditionally execute based on
313 whether 2 items are equivalent."""
313 whether 2 items are equivalent."""
314 if not (3 <= len(args) <= 4):
314 if not (3 <= len(args) <= 4):
315 # i18n: "ifeq" is a keyword
315 # i18n: "ifeq" is a keyword
316 raise error.ParseError(_("ifeq expects three or four arguments"))
316 raise error.ParseError(_("ifeq expects three or four arguments"))
317
317
318 test = evalstring(context, mapping, args[0])
318 test = evalstring(context, mapping, args[0])
319 match = evalstring(context, mapping, args[1])
319 match = evalstring(context, mapping, args[1])
320 if test == match:
320 if test == match:
321 return evalrawexp(context, mapping, args[2])
321 return evalrawexp(context, mapping, args[2])
322 elif len(args) == 4:
322 elif len(args) == 4:
323 return evalrawexp(context, mapping, args[3])
323 return evalrawexp(context, mapping, args[3])
324
324
325 @templatefunc('join(list, sep)')
325 @templatefunc('join(list, sep)')
326 def join(context, mapping, args):
326 def join(context, mapping, args):
327 """Join items in a list with a delimiter."""
327 """Join items in a list with a delimiter."""
328 if not (1 <= len(args) <= 2):
328 if not (1 <= len(args) <= 2):
329 # i18n: "join" is a keyword
329 # i18n: "join" is a keyword
330 raise error.ParseError(_("join expects one or two arguments"))
330 raise error.ParseError(_("join expects one or two arguments"))
331
331
332 joinset = evalwrapped(context, mapping, args[0])
332 joinset = evalwrapped(context, mapping, args[0])
333 joiner = " "
333 joiner = " "
334 if len(args) > 1:
334 if len(args) > 1:
335 joiner = evalstring(context, mapping, args[1])
335 joiner = evalstring(context, mapping, args[1])
336 return joinset.join(context, mapping, joiner)
336 return joinset.join(context, mapping, joiner)
337
337
338 @templatefunc('label(label, expr)')
338 @templatefunc('label(label, expr)')
339 def label(context, mapping, args):
339 def label(context, mapping, args):
340 """Apply a label to generated content. Content with
340 """Apply a label to generated content. Content with
341 a label applied can result in additional post-processing, such as
341 a label applied can result in additional post-processing, such as
342 automatic colorization."""
342 automatic colorization."""
343 if len(args) != 2:
343 if len(args) != 2:
344 # i18n: "label" is a keyword
344 # i18n: "label" is a keyword
345 raise error.ParseError(_("label expects two arguments"))
345 raise error.ParseError(_("label expects two arguments"))
346
346
347 ui = context.resource(mapping, 'ui')
347 ui = context.resource(mapping, 'ui')
348 thing = evalstring(context, mapping, args[1])
348 thing = evalstring(context, mapping, args[1])
349 # preserve unknown symbol as literal so effects like 'red', 'bold',
349 # preserve unknown symbol as literal so effects like 'red', 'bold',
350 # etc. don't need to be quoted
350 # etc. don't need to be quoted
351 label = evalstringliteral(context, mapping, args[0])
351 label = evalstringliteral(context, mapping, args[0])
352
352
353 return ui.label(thing, label)
353 return ui.label(thing, label)
354
354
355 @templatefunc('latesttag([pattern])')
355 @templatefunc('latesttag([pattern])')
356 def latesttag(context, mapping, args):
356 def latesttag(context, mapping, args):
357 """The global tags matching the given pattern on the
357 """The global tags matching the given pattern on the
358 most recent globally tagged ancestor of this changeset.
358 most recent globally tagged ancestor of this changeset.
359 If no such tags exist, the "{tag}" template resolves to
359 If no such tags exist, the "{tag}" template resolves to
360 the string "null". See :hg:`help revisions.patterns` for the pattern
360 the string "null". See :hg:`help revisions.patterns` for the pattern
361 syntax.
361 syntax.
362 """
362 """
363 if len(args) > 1:
363 if len(args) > 1:
364 # i18n: "latesttag" is a keyword
364 # i18n: "latesttag" is a keyword
365 raise error.ParseError(_("latesttag expects at most one argument"))
365 raise error.ParseError(_("latesttag expects at most one argument"))
366
366
367 pattern = None
367 pattern = None
368 if len(args) == 1:
368 if len(args) == 1:
369 pattern = evalstring(context, mapping, args[0])
369 pattern = evalstring(context, mapping, args[0])
370 return templatekw.showlatesttags(context, mapping, pattern)
370 return templatekw.showlatesttags(context, mapping, pattern)
371
371
372 @templatefunc('localdate(date[, tz])')
372 @templatefunc('localdate(date[, tz])')
373 def localdate(context, mapping, args):
373 def localdate(context, mapping, args):
374 """Converts a date to the specified timezone.
374 """Converts a date to the specified timezone.
375 The default is local date."""
375 The default is local date."""
376 if not (1 <= len(args) <= 2):
376 if not (1 <= len(args) <= 2):
377 # i18n: "localdate" is a keyword
377 # i18n: "localdate" is a keyword
378 raise error.ParseError(_("localdate expects one or two arguments"))
378 raise error.ParseError(_("localdate expects one or two arguments"))
379
379
380 date = evaldate(context, mapping, args[0],
380 date = evaldate(context, mapping, args[0],
381 # i18n: "localdate" is a keyword
381 # i18n: "localdate" is a keyword
382 _("localdate expects a date information"))
382 _("localdate expects a date information"))
383 if len(args) >= 2:
383 if len(args) >= 2:
384 tzoffset = None
384 tzoffset = None
385 tz = evalfuncarg(context, mapping, args[1])
385 tz = evalfuncarg(context, mapping, args[1])
386 if isinstance(tz, bytes):
386 if isinstance(tz, bytes):
387 tzoffset, remainder = dateutil.parsetimezone(tz)
387 tzoffset, remainder = dateutil.parsetimezone(tz)
388 if remainder:
388 if remainder:
389 tzoffset = None
389 tzoffset = None
390 if tzoffset is None:
390 if tzoffset is None:
391 try:
391 try:
392 tzoffset = int(tz)
392 tzoffset = int(tz)
393 except (TypeError, ValueError):
393 except (TypeError, ValueError):
394 # i18n: "localdate" is a keyword
394 # i18n: "localdate" is a keyword
395 raise error.ParseError(_("localdate expects a timezone"))
395 raise error.ParseError(_("localdate expects a timezone"))
396 else:
396 else:
397 tzoffset = dateutil.makedate()[1]
397 tzoffset = dateutil.makedate()[1]
398 return (date[0], tzoffset)
398 return (date[0], tzoffset)
399
399
400 @templatefunc('max(iterable)')
400 @templatefunc('max(iterable)')
401 def max_(context, mapping, args, **kwargs):
401 def max_(context, mapping, args, **kwargs):
402 """Return the max of an iterable"""
402 """Return the max of an iterable"""
403 if len(args) != 1:
403 if len(args) != 1:
404 # i18n: "max" is a keyword
404 # i18n: "max" is a keyword
405 raise error.ParseError(_("max expects one argument"))
405 raise error.ParseError(_("max expects one argument"))
406
406
407 iterable = evalfuncarg(context, mapping, args[0])
407 iterable = evalfuncarg(context, mapping, args[0])
408 try:
408 try:
409 x = max(pycompat.maybebytestr(iterable))
409 x = max(pycompat.maybebytestr(iterable))
410 except (TypeError, ValueError):
410 except (TypeError, ValueError):
411 # i18n: "max" is a keyword
411 # i18n: "max" is a keyword
412 raise error.ParseError(_("max first argument should be an iterable"))
412 raise error.ParseError(_("max first argument should be an iterable"))
413 return templateutil.wraphybridvalue(iterable, x, x)
413 return templateutil.wraphybridvalue(iterable, x, x)
414
414
415 @templatefunc('min(iterable)')
415 @templatefunc('min(iterable)')
416 def min_(context, mapping, args, **kwargs):
416 def min_(context, mapping, args, **kwargs):
417 """Return the min of an iterable"""
417 """Return the min of an iterable"""
418 if len(args) != 1:
418 if len(args) != 1:
419 # i18n: "min" is a keyword
419 # i18n: "min" is a keyword
420 raise error.ParseError(_("min expects one argument"))
420 raise error.ParseError(_("min expects one argument"))
421
421
422 iterable = evalfuncarg(context, mapping, args[0])
422 iterable = evalfuncarg(context, mapping, args[0])
423 try:
423 try:
424 x = min(pycompat.maybebytestr(iterable))
424 x = min(pycompat.maybebytestr(iterable))
425 except (TypeError, ValueError):
425 except (TypeError, ValueError):
426 # i18n: "min" is a keyword
426 # i18n: "min" is a keyword
427 raise error.ParseError(_("min first argument should be an iterable"))
427 raise error.ParseError(_("min first argument should be an iterable"))
428 return templateutil.wraphybridvalue(iterable, x, x)
428 return templateutil.wraphybridvalue(iterable, x, x)
429
429
430 @templatefunc('mod(a, b)')
430 @templatefunc('mod(a, b)')
431 def mod(context, mapping, args):
431 def mod(context, mapping, args):
432 """Calculate a mod b such that a / b + a mod b == a"""
432 """Calculate a mod b such that a / b + a mod b == a"""
433 if not len(args) == 2:
433 if not len(args) == 2:
434 # i18n: "mod" is a keyword
434 # i18n: "mod" is a keyword
435 raise error.ParseError(_("mod expects two arguments"))
435 raise error.ParseError(_("mod expects two arguments"))
436
436
437 func = lambda a, b: a % b
437 func = lambda a, b: a % b
438 return templateutil.runarithmetic(context, mapping,
438 return templateutil.runarithmetic(context, mapping,
439 (func, args[0], args[1]))
439 (func, args[0], args[1]))
440
440
441 @templatefunc('obsfateoperations(markers)')
441 @templatefunc('obsfateoperations(markers)')
442 def obsfateoperations(context, mapping, args):
442 def obsfateoperations(context, mapping, args):
443 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
443 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
444 if len(args) != 1:
444 if len(args) != 1:
445 # i18n: "obsfateoperations" is a keyword
445 # i18n: "obsfateoperations" is a keyword
446 raise error.ParseError(_("obsfateoperations expects one argument"))
446 raise error.ParseError(_("obsfateoperations expects one argument"))
447
447
448 markers = evalfuncarg(context, mapping, args[0])
448 markers = evalfuncarg(context, mapping, args[0])
449
449
450 try:
450 try:
451 data = obsutil.markersoperations(markers)
451 data = obsutil.markersoperations(markers)
452 return templateutil.hybridlist(data, name='operation')
452 return templateutil.hybridlist(data, name='operation')
453 except (TypeError, KeyError):
453 except (TypeError, KeyError):
454 # i18n: "obsfateoperations" is a keyword
454 # i18n: "obsfateoperations" is a keyword
455 errmsg = _("obsfateoperations first argument should be an iterable")
455 errmsg = _("obsfateoperations first argument should be an iterable")
456 raise error.ParseError(errmsg)
456 raise error.ParseError(errmsg)
457
457
458 @templatefunc('obsfatedate(markers)')
458 @templatefunc('obsfatedate(markers)')
459 def obsfatedate(context, mapping, args):
459 def obsfatedate(context, mapping, args):
460 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
460 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
461 if len(args) != 1:
461 if len(args) != 1:
462 # i18n: "obsfatedate" is a keyword
462 # i18n: "obsfatedate" is a keyword
463 raise error.ParseError(_("obsfatedate expects one argument"))
463 raise error.ParseError(_("obsfatedate expects one argument"))
464
464
465 markers = evalfuncarg(context, mapping, args[0])
465 markers = evalfuncarg(context, mapping, args[0])
466
466
467 try:
467 try:
468 data = obsutil.markersdates(markers)
468 data = obsutil.markersdates(markers)
469 return templateutil.hybridlist(data, name='date', fmt='%d %d')
469 return templateutil.hybridlist(data, name='date', fmt='%d %d')
470 except (TypeError, KeyError):
470 except (TypeError, KeyError):
471 # i18n: "obsfatedate" is a keyword
471 # i18n: "obsfatedate" is a keyword
472 errmsg = _("obsfatedate first argument should be an iterable")
472 errmsg = _("obsfatedate first argument should be an iterable")
473 raise error.ParseError(errmsg)
473 raise error.ParseError(errmsg)
474
474
475 @templatefunc('obsfateusers(markers)')
475 @templatefunc('obsfateusers(markers)')
476 def obsfateusers(context, mapping, args):
476 def obsfateusers(context, mapping, args):
477 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
477 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
478 if len(args) != 1:
478 if len(args) != 1:
479 # i18n: "obsfateusers" is a keyword
479 # i18n: "obsfateusers" is a keyword
480 raise error.ParseError(_("obsfateusers expects one argument"))
480 raise error.ParseError(_("obsfateusers expects one argument"))
481
481
482 markers = evalfuncarg(context, mapping, args[0])
482 markers = evalfuncarg(context, mapping, args[0])
483
483
484 try:
484 try:
485 data = obsutil.markersusers(markers)
485 data = obsutil.markersusers(markers)
486 return templateutil.hybridlist(data, name='user')
486 return templateutil.hybridlist(data, name='user')
487 except (TypeError, KeyError, ValueError):
487 except (TypeError, KeyError, ValueError):
488 # i18n: "obsfateusers" is a keyword
488 # i18n: "obsfateusers" is a keyword
489 msg = _("obsfateusers first argument should be an iterable of "
489 msg = _("obsfateusers first argument should be an iterable of "
490 "obsmakers")
490 "obsmakers")
491 raise error.ParseError(msg)
491 raise error.ParseError(msg)
492
492
493 @templatefunc('obsfateverb(successors, markers)')
493 @templatefunc('obsfateverb(successors, markers)')
494 def obsfateverb(context, mapping, args):
494 def obsfateverb(context, mapping, args):
495 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
495 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
496 if len(args) != 2:
496 if len(args) != 2:
497 # i18n: "obsfateverb" is a keyword
497 # i18n: "obsfateverb" is a keyword
498 raise error.ParseError(_("obsfateverb expects two arguments"))
498 raise error.ParseError(_("obsfateverb expects two arguments"))
499
499
500 successors = evalfuncarg(context, mapping, args[0])
500 successors = evalfuncarg(context, mapping, args[0])
501 markers = evalfuncarg(context, mapping, args[1])
501 markers = evalfuncarg(context, mapping, args[1])
502
502
503 try:
503 try:
504 return obsutil.obsfateverb(successors, markers)
504 return obsutil.obsfateverb(successors, markers)
505 except TypeError:
505 except TypeError:
506 # i18n: "obsfateverb" is a keyword
506 # i18n: "obsfateverb" is a keyword
507 errmsg = _("obsfateverb first argument should be countable")
507 errmsg = _("obsfateverb first argument should be countable")
508 raise error.ParseError(errmsg)
508 raise error.ParseError(errmsg)
509
509
510 @templatefunc('relpath(path)')
510 @templatefunc('relpath(path)')
511 def relpath(context, mapping, args):
511 def relpath(context, mapping, args):
512 """Convert a repository-absolute path into a filesystem path relative to
512 """Convert a repository-absolute path into a filesystem path relative to
513 the current working directory."""
513 the current working directory."""
514 if len(args) != 1:
514 if len(args) != 1:
515 # i18n: "relpath" is a keyword
515 # i18n: "relpath" is a keyword
516 raise error.ParseError(_("relpath expects one argument"))
516 raise error.ParseError(_("relpath expects one argument"))
517
517
518 repo = context.resource(mapping, 'ctx').repo()
518 repo = context.resource(mapping, 'ctx').repo()
519 path = evalstring(context, mapping, args[0])
519 path = evalstring(context, mapping, args[0])
520 return repo.pathto(path)
520 return repo.pathto(path)
521
521
522 @templatefunc('revset(query[, formatargs...])')
522 @templatefunc('revset(query[, formatargs...])')
523 def revset(context, mapping, args):
523 def revset(context, mapping, args):
524 """Execute a revision set query. See
524 """Execute a revision set query. See
525 :hg:`help revset`."""
525 :hg:`help revset`."""
526 if not len(args) > 0:
526 if not len(args) > 0:
527 # i18n: "revset" is a keyword
527 # i18n: "revset" is a keyword
528 raise error.ParseError(_("revset expects one or more arguments"))
528 raise error.ParseError(_("revset expects one or more arguments"))
529
529
530 raw = evalstring(context, mapping, args[0])
530 raw = evalstring(context, mapping, args[0])
531 ctx = context.resource(mapping, 'ctx')
531 ctx = context.resource(mapping, 'ctx')
532 repo = ctx.repo()
532 repo = ctx.repo()
533
533
534 def query(expr):
534 def query(expr):
535 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
535 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
536 return m(repo)
536 return m(repo)
537
537
538 if len(args) > 1:
538 if len(args) > 1:
539 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
539 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
540 revs = query(revsetlang.formatspec(raw, *formatargs))
540 revs = query(revsetlang.formatspec(raw, *formatargs))
541 revs = list(revs)
541 revs = list(revs)
542 else:
542 else:
543 cache = context.resource(mapping, 'cache')
543 cache = context.resource(mapping, 'cache')
544 revsetcache = cache.setdefault("revsetcache", {})
544 revsetcache = cache.setdefault("revsetcache", {})
545 if raw in revsetcache:
545 if raw in revsetcache:
546 revs = revsetcache[raw]
546 revs = revsetcache[raw]
547 else:
547 else:
548 revs = query(raw)
548 revs = query(raw)
549 revs = list(revs)
549 revs = list(revs)
550 revsetcache[raw] = revs
550 revsetcache[raw] = revs
551 return templatekw.showrevslist(context, mapping, "revision", revs)
551 return templatekw.showrevslist(context, mapping, "revision", revs)
552
552
553 @templatefunc('rstdoc(text, style)')
553 @templatefunc('rstdoc(text, style)')
554 def rstdoc(context, mapping, args):
554 def rstdoc(context, mapping, args):
555 """Format reStructuredText."""
555 """Format reStructuredText."""
556 if len(args) != 2:
556 if len(args) != 2:
557 # i18n: "rstdoc" is a keyword
557 # i18n: "rstdoc" is a keyword
558 raise error.ParseError(_("rstdoc expects two arguments"))
558 raise error.ParseError(_("rstdoc expects two arguments"))
559
559
560 text = evalstring(context, mapping, args[0])
560 text = evalstring(context, mapping, args[0])
561 style = evalstring(context, mapping, args[1])
561 style = evalstring(context, mapping, args[1])
562
562
563 return minirst.format(text, style=style, keep=['verbose'])[0]
563 return minirst.format(text, style=style, keep=['verbose'])[0]
564
564
565 @templatefunc('separate(sep, args...)', argspec='sep *args')
565 @templatefunc('separate(sep, args...)', argspec='sep *args')
566 def separate(context, mapping, args):
566 def separate(context, mapping, args):
567 """Add a separator between non-empty arguments."""
567 """Add a separator between non-empty arguments."""
568 if 'sep' not in args:
568 if 'sep' not in args:
569 # i18n: "separate" is a keyword
569 # i18n: "separate" is a keyword
570 raise error.ParseError(_("separate expects at least one argument"))
570 raise error.ParseError(_("separate expects at least one argument"))
571
571
572 sep = evalstring(context, mapping, args['sep'])
572 sep = evalstring(context, mapping, args['sep'])
573 first = True
573 first = True
574 for arg in args['args']:
574 for arg in args['args']:
575 argstr = evalstring(context, mapping, arg)
575 argstr = evalstring(context, mapping, arg)
576 if not argstr:
576 if not argstr:
577 continue
577 continue
578 if first:
578 if first:
579 first = False
579 first = False
580 else:
580 else:
581 yield sep
581 yield sep
582 yield argstr
582 yield argstr
583
583
584 @templatefunc('shortest(node, minlength=4)')
584 @templatefunc('shortest(node, minlength=4)')
585 def shortest(context, mapping, args):
585 def shortest(context, mapping, args):
586 """Obtain the shortest representation of
586 """Obtain the shortest representation of
587 a node."""
587 a node."""
588 if not (1 <= len(args) <= 2):
588 if not (1 <= len(args) <= 2):
589 # i18n: "shortest" is a keyword
589 # i18n: "shortest" is a keyword
590 raise error.ParseError(_("shortest() expects one or two arguments"))
590 raise error.ParseError(_("shortest() expects one or two arguments"))
591
591
592 hexnode = evalstring(context, mapping, args[0])
592 hexnode = evalstring(context, mapping, args[0])
593
593
594 minlength = 4
594 minlength = 4
595 if len(args) > 1:
595 if len(args) > 1:
596 minlength = evalinteger(context, mapping, args[1],
596 minlength = evalinteger(context, mapping, args[1],
597 # i18n: "shortest" is a keyword
597 # i18n: "shortest" is a keyword
598 _("shortest() expects an integer minlength"))
598 _("shortest() expects an integer minlength"))
599
599
600 repo = context.resource(mapping, 'ctx')._repo
600 repo = context.resource(mapping, 'ctx')._repo
601 if len(hexnode) > 40:
601 if len(hexnode) > 40:
602 return hexnode
602 return hexnode
603 elif len(hexnode) == 40:
603 elif len(hexnode) == 40:
604 try:
604 try:
605 node = bin(hexnode)
605 node = bin(hexnode)
606 except TypeError:
606 except TypeError:
607 return hexnode
607 return hexnode
608 else:
608 else:
609 try:
609 try:
610 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
610 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
611 except error.WdirUnsupported:
611 except error.WdirUnsupported:
612 node = wdirid
612 node = wdirid
613 except error.LookupError:
613 except error.LookupError:
614 return hexnode
614 return hexnode
615 if not node:
615 if not node:
616 return hexnode
616 return hexnode
617 try:
617 try:
618 return scmutil.shortesthexnodeidprefix(repo, node, minlength)
618 return scmutil.shortesthexnodeidprefix(repo, node, minlength)
619 except error.RepoLookupError:
619 except error.RepoLookupError:
620 return hexnode
620 return hexnode
621
621
622 @templatefunc('strip(text[, chars])')
622 @templatefunc('strip(text[, chars])')
623 def strip(context, mapping, args):
623 def strip(context, mapping, args):
624 """Strip characters from a string. By default,
624 """Strip characters from a string. By default,
625 strips all leading and trailing whitespace."""
625 strips all leading and trailing whitespace."""
626 if not (1 <= len(args) <= 2):
626 if not (1 <= len(args) <= 2):
627 # i18n: "strip" is a keyword
627 # i18n: "strip" is a keyword
628 raise error.ParseError(_("strip expects one or two arguments"))
628 raise error.ParseError(_("strip expects one or two arguments"))
629
629
630 text = evalstring(context, mapping, args[0])
630 text = evalstring(context, mapping, args[0])
631 if len(args) == 2:
631 if len(args) == 2:
632 chars = evalstring(context, mapping, args[1])
632 chars = evalstring(context, mapping, args[1])
633 return text.strip(chars)
633 return text.strip(chars)
634 return text.strip()
634 return text.strip()
635
635
636 @templatefunc('sub(pattern, replacement, expression)')
636 @templatefunc('sub(pattern, replacement, expression)')
637 def sub(context, mapping, args):
637 def sub(context, mapping, args):
638 """Perform text substitution
638 """Perform text substitution
639 using regular expressions."""
639 using regular expressions."""
640 if len(args) != 3:
640 if len(args) != 3:
641 # i18n: "sub" is a keyword
641 # i18n: "sub" is a keyword
642 raise error.ParseError(_("sub expects three arguments"))
642 raise error.ParseError(_("sub expects three arguments"))
643
643
644 pat = evalstring(context, mapping, args[0])
644 pat = evalstring(context, mapping, args[0])
645 rpl = evalstring(context, mapping, args[1])
645 rpl = evalstring(context, mapping, args[1])
646 src = evalstring(context, mapping, args[2])
646 src = evalstring(context, mapping, args[2])
647 try:
647 try:
648 patre = re.compile(pat)
648 patre = re.compile(pat)
649 except re.error:
649 except re.error:
650 # i18n: "sub" is a keyword
650 # i18n: "sub" is a keyword
651 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
651 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
652 try:
652 try:
653 yield patre.sub(rpl, src)
653 yield patre.sub(rpl, src)
654 except re.error:
654 except re.error:
655 # i18n: "sub" is a keyword
655 # i18n: "sub" is a keyword
656 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
656 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
657
657
658 @templatefunc('startswith(pattern, text)')
658 @templatefunc('startswith(pattern, text)')
659 def startswith(context, mapping, args):
659 def startswith(context, mapping, args):
660 """Returns the value from the "text" argument
660 """Returns the value from the "text" argument
661 if it begins with the content from the "pattern" argument."""
661 if it begins with the content from the "pattern" argument."""
662 if len(args) != 2:
662 if len(args) != 2:
663 # i18n: "startswith" is a keyword
663 # i18n: "startswith" is a keyword
664 raise error.ParseError(_("startswith expects two arguments"))
664 raise error.ParseError(_("startswith expects two arguments"))
665
665
666 patn = evalstring(context, mapping, args[0])
666 patn = evalstring(context, mapping, args[0])
667 text = evalstring(context, mapping, args[1])
667 text = evalstring(context, mapping, args[1])
668 if text.startswith(patn):
668 if text.startswith(patn):
669 return text
669 return text
670 return ''
670 return ''
671
671
672 @templatefunc('word(number, text[, separator])')
672 @templatefunc('word(number, text[, separator])')
673 def word(context, mapping, args):
673 def word(context, mapping, args):
674 """Return the nth word from a string."""
674 """Return the nth word from a string."""
675 if not (2 <= len(args) <= 3):
675 if not (2 <= len(args) <= 3):
676 # i18n: "word" is a keyword
676 # i18n: "word" is a keyword
677 raise error.ParseError(_("word expects two or three arguments, got %d")
677 raise error.ParseError(_("word expects two or three arguments, got %d")
678 % len(args))
678 % len(args))
679
679
680 num = evalinteger(context, mapping, args[0],
680 num = evalinteger(context, mapping, args[0],
681 # i18n: "word" is a keyword
681 # i18n: "word" is a keyword
682 _("word expects an integer index"))
682 _("word expects an integer index"))
683 text = evalstring(context, mapping, args[1])
683 text = evalstring(context, mapping, args[1])
684 if len(args) == 3:
684 if len(args) == 3:
685 splitter = evalstring(context, mapping, args[2])
685 splitter = evalstring(context, mapping, args[2])
686 else:
686 else:
687 splitter = None
687 splitter = None
688
688
689 tokens = text.split(splitter)
689 tokens = text.split(splitter)
690 if num >= len(tokens) or num < -len(tokens):
690 if num >= len(tokens) or num < -len(tokens):
691 return ''
691 return ''
692 else:
692 else:
693 return tokens[num]
693 return tokens[num]
694
694
695 def loadfunction(ui, extname, registrarobj):
695 def loadfunction(ui, extname, registrarobj):
696 """Load template function from specified registrarobj
696 """Load template function from specified registrarobj
697 """
697 """
698 for name, func in registrarobj._table.iteritems():
698 for name, func in registrarobj._table.iteritems():
699 funcs[name] = func
699 funcs[name] = func
700
700
701 # tell hggettext to extract docstrings from these functions:
701 # tell hggettext to extract docstrings from these functions:
702 i18nfunctions = funcs.values()
702 i18nfunctions = funcs.values()
@@ -1,738 +1,740 b''
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import abc
10 import abc
11 import types
11 import types
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 )
18 )
19 from .utils import (
19 from .utils import (
20 dateutil,
20 dateutil,
21 stringutil,
21 stringutil,
22 )
22 )
23
23
24 class ResourceUnavailable(error.Abort):
24 class ResourceUnavailable(error.Abort):
25 pass
25 pass
26
26
27 class TemplateNotFound(error.Abort):
27 class TemplateNotFound(error.Abort):
28 pass
28 pass
29
29
30 class wrapped(object):
30 class wrapped(object):
31 """Object requiring extra conversion prior to displaying or processing
31 """Object requiring extra conversion prior to displaying or processing
32 as value
32 as value
33
33
34 Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain the inner
34 Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain the inner
35 object.
35 object.
36 """
36 """
37
37
38 __metaclass__ = abc.ABCMeta
38 __metaclass__ = abc.ABCMeta
39
39
40 @abc.abstractmethod
40 @abc.abstractmethod
41 def getmember(self, context, mapping, key):
41 def getmember(self, context, mapping, key):
42 """Return a member item for the specified key
42 """Return a member item for the specified key
43
43
44 The key argument may be a wrapped object.
44 A returned object may be either a wrapped object or a pure value
45 A returned object may be either a wrapped object or a pure value
45 depending on the self type.
46 depending on the self type.
46 """
47 """
47
48
48 @abc.abstractmethod
49 @abc.abstractmethod
49 def itermaps(self, context):
50 def itermaps(self, context):
50 """Yield each template mapping"""
51 """Yield each template mapping"""
51
52
52 @abc.abstractmethod
53 @abc.abstractmethod
53 def join(self, context, mapping, sep):
54 def join(self, context, mapping, sep):
54 """Join items with the separator; Returns a bytes or (possibly nested)
55 """Join items with the separator; Returns a bytes or (possibly nested)
55 generator of bytes
56 generator of bytes
56
57
57 A pre-configured template may be rendered per item if this container
58 A pre-configured template may be rendered per item if this container
58 holds unprintable items.
59 holds unprintable items.
59 """
60 """
60
61
61 @abc.abstractmethod
62 @abc.abstractmethod
62 def show(self, context, mapping):
63 def show(self, context, mapping):
63 """Return a bytes or (possibly nested) generator of bytes representing
64 """Return a bytes or (possibly nested) generator of bytes representing
64 the underlying object
65 the underlying object
65
66
66 A pre-configured template may be rendered if the underlying object is
67 A pre-configured template may be rendered if the underlying object is
67 not printable.
68 not printable.
68 """
69 """
69
70
70 @abc.abstractmethod
71 @abc.abstractmethod
71 def tovalue(self, context, mapping):
72 def tovalue(self, context, mapping):
72 """Move the inner value object out or create a value representation
73 """Move the inner value object out or create a value representation
73
74
74 A returned value must be serializable by templaterfilters.json().
75 A returned value must be serializable by templaterfilters.json().
75 """
76 """
76
77
77 class wrappedbytes(wrapped):
78 class wrappedbytes(wrapped):
78 """Wrapper for byte string"""
79 """Wrapper for byte string"""
79
80
80 def __init__(self, value):
81 def __init__(self, value):
81 self._value = value
82 self._value = value
82
83
83 def getmember(self, context, mapping, key):
84 def getmember(self, context, mapping, key):
84 raise error.ParseError(_('%r is not a dictionary')
85 raise error.ParseError(_('%r is not a dictionary')
85 % pycompat.bytestr(self._value))
86 % pycompat.bytestr(self._value))
86
87
87 def itermaps(self, context):
88 def itermaps(self, context):
88 raise error.ParseError(_('%r is not iterable of mappings')
89 raise error.ParseError(_('%r is not iterable of mappings')
89 % pycompat.bytestr(self._value))
90 % pycompat.bytestr(self._value))
90
91
91 def join(self, context, mapping, sep):
92 def join(self, context, mapping, sep):
92 return joinitems(pycompat.iterbytestr(self._value), sep)
93 return joinitems(pycompat.iterbytestr(self._value), sep)
93
94
94 def show(self, context, mapping):
95 def show(self, context, mapping):
95 return self._value
96 return self._value
96
97
97 def tovalue(self, context, mapping):
98 def tovalue(self, context, mapping):
98 return self._value
99 return self._value
99
100
100 class wrappedvalue(wrapped):
101 class wrappedvalue(wrapped):
101 """Generic wrapper for pure non-list/dict/bytes value"""
102 """Generic wrapper for pure non-list/dict/bytes value"""
102
103
103 def __init__(self, value):
104 def __init__(self, value):
104 self._value = value
105 self._value = value
105
106
106 def getmember(self, context, mapping, key):
107 def getmember(self, context, mapping, key):
107 raise error.ParseError(_('%r is not a dictionary') % self._value)
108 raise error.ParseError(_('%r is not a dictionary') % self._value)
108
109
109 def itermaps(self, context):
110 def itermaps(self, context):
110 raise error.ParseError(_('%r is not iterable of mappings')
111 raise error.ParseError(_('%r is not iterable of mappings')
111 % self._value)
112 % self._value)
112
113
113 def join(self, context, mapping, sep):
114 def join(self, context, mapping, sep):
114 raise error.ParseError(_('%r is not iterable') % self._value)
115 raise error.ParseError(_('%r is not iterable') % self._value)
115
116
116 def show(self, context, mapping):
117 def show(self, context, mapping):
117 return pycompat.bytestr(self._value)
118 return pycompat.bytestr(self._value)
118
119
119 def tovalue(self, context, mapping):
120 def tovalue(self, context, mapping):
120 return self._value
121 return self._value
121
122
122 # stub for representing a date type; may be a real date type that can
123 # stub for representing a date type; may be a real date type that can
123 # provide a readable string value
124 # provide a readable string value
124 class date(object):
125 class date(object):
125 pass
126 pass
126
127
127 class hybrid(wrapped):
128 class hybrid(wrapped):
128 """Wrapper for list or dict to support legacy template
129 """Wrapper for list or dict to support legacy template
129
130
130 This class allows us to handle both:
131 This class allows us to handle both:
131 - "{files}" (legacy command-line-specific list hack) and
132 - "{files}" (legacy command-line-specific list hack) and
132 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
133 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
133 and to access raw values:
134 and to access raw values:
134 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
135 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
135 - "{get(extras, key)}"
136 - "{get(extras, key)}"
136 - "{files|json}"
137 - "{files|json}"
137 """
138 """
138
139
139 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
140 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
140 self._gen = gen # generator or function returning generator
141 self._gen = gen # generator or function returning generator
141 self._values = values
142 self._values = values
142 self._makemap = makemap
143 self._makemap = makemap
143 self._joinfmt = joinfmt
144 self._joinfmt = joinfmt
144 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
145 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
145
146
146 def getmember(self, context, mapping, key):
147 def getmember(self, context, mapping, key):
147 # TODO: maybe split hybrid list/dict types?
148 # TODO: maybe split hybrid list/dict types?
148 if not util.safehasattr(self._values, 'get'):
149 if not util.safehasattr(self._values, 'get'):
149 raise error.ParseError(_('not a dictionary'))
150 raise error.ParseError(_('not a dictionary'))
151 key = unwrapastype(context, mapping, key, self.keytype)
150 return self._wrapvalue(key, self._values.get(key))
152 return self._wrapvalue(key, self._values.get(key))
151
153
152 def _wrapvalue(self, key, val):
154 def _wrapvalue(self, key, val):
153 if val is None:
155 if val is None:
154 return
156 return
155 return wraphybridvalue(self, key, val)
157 return wraphybridvalue(self, key, val)
156
158
157 def itermaps(self, context):
159 def itermaps(self, context):
158 makemap = self._makemap
160 makemap = self._makemap
159 for x in self._values:
161 for x in self._values:
160 yield makemap(x)
162 yield makemap(x)
161
163
162 def join(self, context, mapping, sep):
164 def join(self, context, mapping, sep):
163 # TODO: switch gen to (context, mapping) API?
165 # TODO: switch gen to (context, mapping) API?
164 return joinitems((self._joinfmt(x) for x in self._values), sep)
166 return joinitems((self._joinfmt(x) for x in self._values), sep)
165
167
166 def show(self, context, mapping):
168 def show(self, context, mapping):
167 # TODO: switch gen to (context, mapping) API?
169 # TODO: switch gen to (context, mapping) API?
168 gen = self._gen
170 gen = self._gen
169 if gen is None:
171 if gen is None:
170 return self.join(context, mapping, ' ')
172 return self.join(context, mapping, ' ')
171 if callable(gen):
173 if callable(gen):
172 return gen()
174 return gen()
173 return gen
175 return gen
174
176
175 def tovalue(self, context, mapping):
177 def tovalue(self, context, mapping):
176 # TODO: return self._values and get rid of proxy methods
178 # TODO: return self._values and get rid of proxy methods
177 return self
179 return self
178
180
179 def __contains__(self, x):
181 def __contains__(self, x):
180 return x in self._values
182 return x in self._values
181 def __getitem__(self, key):
183 def __getitem__(self, key):
182 return self._values[key]
184 return self._values[key]
183 def __len__(self):
185 def __len__(self):
184 return len(self._values)
186 return len(self._values)
185 def __iter__(self):
187 def __iter__(self):
186 return iter(self._values)
188 return iter(self._values)
187 def __getattr__(self, name):
189 def __getattr__(self, name):
188 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
190 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
189 r'itervalues', r'keys', r'values'):
191 r'itervalues', r'keys', r'values'):
190 raise AttributeError(name)
192 raise AttributeError(name)
191 return getattr(self._values, name)
193 return getattr(self._values, name)
192
194
193 class mappable(wrapped):
195 class mappable(wrapped):
194 """Wrapper for non-list/dict object to support map operation
196 """Wrapper for non-list/dict object to support map operation
195
197
196 This class allows us to handle both:
198 This class allows us to handle both:
197 - "{manifest}"
199 - "{manifest}"
198 - "{manifest % '{rev}:{node}'}"
200 - "{manifest % '{rev}:{node}'}"
199 - "{manifest.rev}"
201 - "{manifest.rev}"
200
202
201 Unlike a hybrid, this does not simulate the behavior of the underling
203 Unlike a hybrid, this does not simulate the behavior of the underling
202 value.
204 value.
203 """
205 """
204
206
205 def __init__(self, gen, key, value, makemap):
207 def __init__(self, gen, key, value, makemap):
206 self._gen = gen # generator or function returning generator
208 self._gen = gen # generator or function returning generator
207 self._key = key
209 self._key = key
208 self._value = value # may be generator of strings
210 self._value = value # may be generator of strings
209 self._makemap = makemap
211 self._makemap = makemap
210
212
211 def tomap(self):
213 def tomap(self):
212 return self._makemap(self._key)
214 return self._makemap(self._key)
213
215
214 def getmember(self, context, mapping, key):
216 def getmember(self, context, mapping, key):
215 w = makewrapped(context, mapping, self._value)
217 w = makewrapped(context, mapping, self._value)
216 return w.getmember(context, mapping, key)
218 return w.getmember(context, mapping, key)
217
219
218 def itermaps(self, context):
220 def itermaps(self, context):
219 yield self.tomap()
221 yield self.tomap()
220
222
221 def join(self, context, mapping, sep):
223 def join(self, context, mapping, sep):
222 w = makewrapped(context, mapping, self._value)
224 w = makewrapped(context, mapping, self._value)
223 return w.join(context, mapping, sep)
225 return w.join(context, mapping, sep)
224
226
225 def show(self, context, mapping):
227 def show(self, context, mapping):
226 # TODO: switch gen to (context, mapping) API?
228 # TODO: switch gen to (context, mapping) API?
227 gen = self._gen
229 gen = self._gen
228 if gen is None:
230 if gen is None:
229 return pycompat.bytestr(self._value)
231 return pycompat.bytestr(self._value)
230 if callable(gen):
232 if callable(gen):
231 return gen()
233 return gen()
232 return gen
234 return gen
233
235
234 def tovalue(self, context, mapping):
236 def tovalue(self, context, mapping):
235 return _unthunk(context, mapping, self._value)
237 return _unthunk(context, mapping, self._value)
236
238
237 class _mappingsequence(wrapped):
239 class _mappingsequence(wrapped):
238 """Wrapper for sequence of template mappings
240 """Wrapper for sequence of template mappings
239
241
240 This represents an inner template structure (i.e. a list of dicts),
242 This represents an inner template structure (i.e. a list of dicts),
241 which can also be rendered by the specified named/literal template.
243 which can also be rendered by the specified named/literal template.
242
244
243 Template mappings may be nested.
245 Template mappings may be nested.
244 """
246 """
245
247
246 def __init__(self, name=None, tmpl=None, sep=''):
248 def __init__(self, name=None, tmpl=None, sep=''):
247 if name is not None and tmpl is not None:
249 if name is not None and tmpl is not None:
248 raise error.ProgrammingError('name and tmpl are mutually exclusive')
250 raise error.ProgrammingError('name and tmpl are mutually exclusive')
249 self._name = name
251 self._name = name
250 self._tmpl = tmpl
252 self._tmpl = tmpl
251 self._defaultsep = sep
253 self._defaultsep = sep
252
254
253 def getmember(self, context, mapping, key):
255 def getmember(self, context, mapping, key):
254 raise error.ParseError(_('not a dictionary'))
256 raise error.ParseError(_('not a dictionary'))
255
257
256 def join(self, context, mapping, sep):
258 def join(self, context, mapping, sep):
257 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
259 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
258 if self._name:
260 if self._name:
259 itemiter = (context.process(self._name, m) for m in mapsiter)
261 itemiter = (context.process(self._name, m) for m in mapsiter)
260 elif self._tmpl:
262 elif self._tmpl:
261 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
263 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
262 else:
264 else:
263 raise error.ParseError(_('not displayable without template'))
265 raise error.ParseError(_('not displayable without template'))
264 return joinitems(itemiter, sep)
266 return joinitems(itemiter, sep)
265
267
266 def show(self, context, mapping):
268 def show(self, context, mapping):
267 return self.join(context, mapping, self._defaultsep)
269 return self.join(context, mapping, self._defaultsep)
268
270
269 def tovalue(self, context, mapping):
271 def tovalue(self, context, mapping):
270 knownres = context.knownresourcekeys()
272 knownres = context.knownresourcekeys()
271 items = []
273 items = []
272 for nm in self.itermaps(context):
274 for nm in self.itermaps(context):
273 # drop internal resources (recursively) which shouldn't be displayed
275 # drop internal resources (recursively) which shouldn't be displayed
274 lm = context.overlaymap(mapping, nm)
276 lm = context.overlaymap(mapping, nm)
275 items.append({k: unwrapvalue(context, lm, v)
277 items.append({k: unwrapvalue(context, lm, v)
276 for k, v in nm.iteritems() if k not in knownres})
278 for k, v in nm.iteritems() if k not in knownres})
277 return items
279 return items
278
280
279 class mappinggenerator(_mappingsequence):
281 class mappinggenerator(_mappingsequence):
280 """Wrapper for generator of template mappings
282 """Wrapper for generator of template mappings
281
283
282 The function ``make(context, *args)`` should return a generator of
284 The function ``make(context, *args)`` should return a generator of
283 mapping dicts.
285 mapping dicts.
284 """
286 """
285
287
286 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
288 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
287 super(mappinggenerator, self).__init__(name, tmpl, sep)
289 super(mappinggenerator, self).__init__(name, tmpl, sep)
288 self._make = make
290 self._make = make
289 self._args = args
291 self._args = args
290
292
291 def itermaps(self, context):
293 def itermaps(self, context):
292 return self._make(context, *self._args)
294 return self._make(context, *self._args)
293
295
294 class mappinglist(_mappingsequence):
296 class mappinglist(_mappingsequence):
295 """Wrapper for list of template mappings"""
297 """Wrapper for list of template mappings"""
296
298
297 def __init__(self, mappings, name=None, tmpl=None, sep=''):
299 def __init__(self, mappings, name=None, tmpl=None, sep=''):
298 super(mappinglist, self).__init__(name, tmpl, sep)
300 super(mappinglist, self).__init__(name, tmpl, sep)
299 self._mappings = mappings
301 self._mappings = mappings
300
302
301 def itermaps(self, context):
303 def itermaps(self, context):
302 return iter(self._mappings)
304 return iter(self._mappings)
303
305
304 class mappedgenerator(wrapped):
306 class mappedgenerator(wrapped):
305 """Wrapper for generator of strings which acts as a list
307 """Wrapper for generator of strings which acts as a list
306
308
307 The function ``make(context, *args)`` should return a generator of
309 The function ``make(context, *args)`` should return a generator of
308 byte strings, or a generator of (possibly nested) generators of byte
310 byte strings, or a generator of (possibly nested) generators of byte
309 strings (i.e. a generator for a list of byte strings.)
311 strings (i.e. a generator for a list of byte strings.)
310 """
312 """
311
313
312 def __init__(self, make, args=()):
314 def __init__(self, make, args=()):
313 self._make = make
315 self._make = make
314 self._args = args
316 self._args = args
315
317
316 def _gen(self, context):
318 def _gen(self, context):
317 return self._make(context, *self._args)
319 return self._make(context, *self._args)
318
320
319 def getmember(self, context, mapping, key):
321 def getmember(self, context, mapping, key):
320 raise error.ParseError(_('not a dictionary'))
322 raise error.ParseError(_('not a dictionary'))
321
323
322 def itermaps(self, context):
324 def itermaps(self, context):
323 raise error.ParseError(_('list of strings is not mappable'))
325 raise error.ParseError(_('list of strings is not mappable'))
324
326
325 def join(self, context, mapping, sep):
327 def join(self, context, mapping, sep):
326 return joinitems(self._gen(context), sep)
328 return joinitems(self._gen(context), sep)
327
329
328 def show(self, context, mapping):
330 def show(self, context, mapping):
329 return self.join(context, mapping, '')
331 return self.join(context, mapping, '')
330
332
331 def tovalue(self, context, mapping):
333 def tovalue(self, context, mapping):
332 return [stringify(context, mapping, x) for x in self._gen(context)]
334 return [stringify(context, mapping, x) for x in self._gen(context)]
333
335
334 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
336 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
335 """Wrap data to support both dict-like and string-like operations"""
337 """Wrap data to support both dict-like and string-like operations"""
336 prefmt = pycompat.identity
338 prefmt = pycompat.identity
337 if fmt is None:
339 if fmt is None:
338 fmt = '%s=%s'
340 fmt = '%s=%s'
339 prefmt = pycompat.bytestr
341 prefmt = pycompat.bytestr
340 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
342 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
341 lambda k: fmt % (prefmt(k), prefmt(data[k])))
343 lambda k: fmt % (prefmt(k), prefmt(data[k])))
342
344
343 def hybridlist(data, name, fmt=None, gen=None):
345 def hybridlist(data, name, fmt=None, gen=None):
344 """Wrap data to support both list-like and string-like operations"""
346 """Wrap data to support both list-like and string-like operations"""
345 prefmt = pycompat.identity
347 prefmt = pycompat.identity
346 if fmt is None:
348 if fmt is None:
347 fmt = '%s'
349 fmt = '%s'
348 prefmt = pycompat.bytestr
350 prefmt = pycompat.bytestr
349 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
351 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
350
352
351 def unwraphybrid(context, mapping, thing):
353 def unwraphybrid(context, mapping, thing):
352 """Return an object which can be stringified possibly by using a legacy
354 """Return an object which can be stringified possibly by using a legacy
353 template"""
355 template"""
354 if not isinstance(thing, wrapped):
356 if not isinstance(thing, wrapped):
355 return thing
357 return thing
356 return thing.show(context, mapping)
358 return thing.show(context, mapping)
357
359
358 def wraphybridvalue(container, key, value):
360 def wraphybridvalue(container, key, value):
359 """Wrap an element of hybrid container to be mappable
361 """Wrap an element of hybrid container to be mappable
360
362
361 The key is passed to the makemap function of the given container, which
363 The key is passed to the makemap function of the given container, which
362 should be an item generated by iter(container).
364 should be an item generated by iter(container).
363 """
365 """
364 makemap = getattr(container, '_makemap', None)
366 makemap = getattr(container, '_makemap', None)
365 if makemap is None:
367 if makemap is None:
366 return value
368 return value
367 if util.safehasattr(value, '_makemap'):
369 if util.safehasattr(value, '_makemap'):
368 # a nested hybrid list/dict, which has its own way of map operation
370 # a nested hybrid list/dict, which has its own way of map operation
369 return value
371 return value
370 return mappable(None, key, value, makemap)
372 return mappable(None, key, value, makemap)
371
373
372 def compatdict(context, mapping, name, data, key='key', value='value',
374 def compatdict(context, mapping, name, data, key='key', value='value',
373 fmt=None, plural=None, separator=' '):
375 fmt=None, plural=None, separator=' '):
374 """Wrap data like hybriddict(), but also supports old-style list template
376 """Wrap data like hybriddict(), but also supports old-style list template
375
377
376 This exists for backward compatibility with the old-style template. Use
378 This exists for backward compatibility with the old-style template. Use
377 hybriddict() for new template keywords.
379 hybriddict() for new template keywords.
378 """
380 """
379 c = [{key: k, value: v} for k, v in data.iteritems()]
381 c = [{key: k, value: v} for k, v in data.iteritems()]
380 f = _showcompatlist(context, mapping, name, c, plural, separator)
382 f = _showcompatlist(context, mapping, name, c, plural, separator)
381 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
383 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
382
384
383 def compatlist(context, mapping, name, data, element=None, fmt=None,
385 def compatlist(context, mapping, name, data, element=None, fmt=None,
384 plural=None, separator=' '):
386 plural=None, separator=' '):
385 """Wrap data like hybridlist(), but also supports old-style list template
387 """Wrap data like hybridlist(), but also supports old-style list template
386
388
387 This exists for backward compatibility with the old-style template. Use
389 This exists for backward compatibility with the old-style template. Use
388 hybridlist() for new template keywords.
390 hybridlist() for new template keywords.
389 """
391 """
390 f = _showcompatlist(context, mapping, name, data, plural, separator)
392 f = _showcompatlist(context, mapping, name, data, plural, separator)
391 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
393 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
392
394
393 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
395 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
394 """Return a generator that renders old-style list template
396 """Return a generator that renders old-style list template
395
397
396 name is name of key in template map.
398 name is name of key in template map.
397 values is list of strings or dicts.
399 values is list of strings or dicts.
398 plural is plural of name, if not simply name + 's'.
400 plural is plural of name, if not simply name + 's'.
399 separator is used to join values as a string
401 separator is used to join values as a string
400
402
401 expansion works like this, given name 'foo'.
403 expansion works like this, given name 'foo'.
402
404
403 if values is empty, expand 'no_foos'.
405 if values is empty, expand 'no_foos'.
404
406
405 if 'foo' not in template map, return values as a string,
407 if 'foo' not in template map, return values as a string,
406 joined by 'separator'.
408 joined by 'separator'.
407
409
408 expand 'start_foos'.
410 expand 'start_foos'.
409
411
410 for each value, expand 'foo'. if 'last_foo' in template
412 for each value, expand 'foo'. if 'last_foo' in template
411 map, expand it instead of 'foo' for last key.
413 map, expand it instead of 'foo' for last key.
412
414
413 expand 'end_foos'.
415 expand 'end_foos'.
414 """
416 """
415 if not plural:
417 if not plural:
416 plural = name + 's'
418 plural = name + 's'
417 if not values:
419 if not values:
418 noname = 'no_' + plural
420 noname = 'no_' + plural
419 if context.preload(noname):
421 if context.preload(noname):
420 yield context.process(noname, mapping)
422 yield context.process(noname, mapping)
421 return
423 return
422 if not context.preload(name):
424 if not context.preload(name):
423 if isinstance(values[0], bytes):
425 if isinstance(values[0], bytes):
424 yield separator.join(values)
426 yield separator.join(values)
425 else:
427 else:
426 for v in values:
428 for v in values:
427 r = dict(v)
429 r = dict(v)
428 r.update(mapping)
430 r.update(mapping)
429 yield r
431 yield r
430 return
432 return
431 startname = 'start_' + plural
433 startname = 'start_' + plural
432 if context.preload(startname):
434 if context.preload(startname):
433 yield context.process(startname, mapping)
435 yield context.process(startname, mapping)
434 def one(v, tag=name):
436 def one(v, tag=name):
435 vmapping = {}
437 vmapping = {}
436 try:
438 try:
437 vmapping.update(v)
439 vmapping.update(v)
438 # Python 2 raises ValueError if the type of v is wrong. Python
440 # Python 2 raises ValueError if the type of v is wrong. Python
439 # 3 raises TypeError.
441 # 3 raises TypeError.
440 except (AttributeError, TypeError, ValueError):
442 except (AttributeError, TypeError, ValueError):
441 try:
443 try:
442 # Python 2 raises ValueError trying to destructure an e.g.
444 # Python 2 raises ValueError trying to destructure an e.g.
443 # bytes. Python 3 raises TypeError.
445 # bytes. Python 3 raises TypeError.
444 for a, b in v:
446 for a, b in v:
445 vmapping[a] = b
447 vmapping[a] = b
446 except (TypeError, ValueError):
448 except (TypeError, ValueError):
447 vmapping[name] = v
449 vmapping[name] = v
448 vmapping = context.overlaymap(mapping, vmapping)
450 vmapping = context.overlaymap(mapping, vmapping)
449 return context.process(tag, vmapping)
451 return context.process(tag, vmapping)
450 lastname = 'last_' + name
452 lastname = 'last_' + name
451 if context.preload(lastname):
453 if context.preload(lastname):
452 last = values.pop()
454 last = values.pop()
453 else:
455 else:
454 last = None
456 last = None
455 for v in values:
457 for v in values:
456 yield one(v)
458 yield one(v)
457 if last is not None:
459 if last is not None:
458 yield one(last, tag=lastname)
460 yield one(last, tag=lastname)
459 endname = 'end_' + plural
461 endname = 'end_' + plural
460 if context.preload(endname):
462 if context.preload(endname):
461 yield context.process(endname, mapping)
463 yield context.process(endname, mapping)
462
464
463 def flatten(context, mapping, thing):
465 def flatten(context, mapping, thing):
464 """Yield a single stream from a possibly nested set of iterators"""
466 """Yield a single stream from a possibly nested set of iterators"""
465 thing = unwraphybrid(context, mapping, thing)
467 thing = unwraphybrid(context, mapping, thing)
466 if isinstance(thing, bytes):
468 if isinstance(thing, bytes):
467 yield thing
469 yield thing
468 elif isinstance(thing, str):
470 elif isinstance(thing, str):
469 # We can only hit this on Python 3, and it's here to guard
471 # We can only hit this on Python 3, and it's here to guard
470 # against infinite recursion.
472 # against infinite recursion.
471 raise error.ProgrammingError('Mercurial IO including templates is done'
473 raise error.ProgrammingError('Mercurial IO including templates is done'
472 ' with bytes, not strings, got %r' % thing)
474 ' with bytes, not strings, got %r' % thing)
473 elif thing is None:
475 elif thing is None:
474 pass
476 pass
475 elif not util.safehasattr(thing, '__iter__'):
477 elif not util.safehasattr(thing, '__iter__'):
476 yield pycompat.bytestr(thing)
478 yield pycompat.bytestr(thing)
477 else:
479 else:
478 for i in thing:
480 for i in thing:
479 i = unwraphybrid(context, mapping, i)
481 i = unwraphybrid(context, mapping, i)
480 if isinstance(i, bytes):
482 if isinstance(i, bytes):
481 yield i
483 yield i
482 elif i is None:
484 elif i is None:
483 pass
485 pass
484 elif not util.safehasattr(i, '__iter__'):
486 elif not util.safehasattr(i, '__iter__'):
485 yield pycompat.bytestr(i)
487 yield pycompat.bytestr(i)
486 else:
488 else:
487 for j in flatten(context, mapping, i):
489 for j in flatten(context, mapping, i):
488 yield j
490 yield j
489
491
490 def stringify(context, mapping, thing):
492 def stringify(context, mapping, thing):
491 """Turn values into bytes by converting into text and concatenating them"""
493 """Turn values into bytes by converting into text and concatenating them"""
492 if isinstance(thing, bytes):
494 if isinstance(thing, bytes):
493 return thing # retain localstr to be round-tripped
495 return thing # retain localstr to be round-tripped
494 return b''.join(flatten(context, mapping, thing))
496 return b''.join(flatten(context, mapping, thing))
495
497
496 def findsymbolicname(arg):
498 def findsymbolicname(arg):
497 """Find symbolic name for the given compiled expression; returns None
499 """Find symbolic name for the given compiled expression; returns None
498 if nothing found reliably"""
500 if nothing found reliably"""
499 while True:
501 while True:
500 func, data = arg
502 func, data = arg
501 if func is runsymbol:
503 if func is runsymbol:
502 return data
504 return data
503 elif func is runfilter:
505 elif func is runfilter:
504 arg = data[0]
506 arg = data[0]
505 else:
507 else:
506 return None
508 return None
507
509
508 def _unthunk(context, mapping, thing):
510 def _unthunk(context, mapping, thing):
509 """Evaluate a lazy byte string into value"""
511 """Evaluate a lazy byte string into value"""
510 if not isinstance(thing, types.GeneratorType):
512 if not isinstance(thing, types.GeneratorType):
511 return thing
513 return thing
512 return stringify(context, mapping, thing)
514 return stringify(context, mapping, thing)
513
515
514 def evalrawexp(context, mapping, arg):
516 def evalrawexp(context, mapping, arg):
515 """Evaluate given argument as a bare template object which may require
517 """Evaluate given argument as a bare template object which may require
516 further processing (such as folding generator of strings)"""
518 further processing (such as folding generator of strings)"""
517 func, data = arg
519 func, data = arg
518 return func(context, mapping, data)
520 return func(context, mapping, data)
519
521
520 def evalwrapped(context, mapping, arg):
522 def evalwrapped(context, mapping, arg):
521 """Evaluate given argument to wrapped object"""
523 """Evaluate given argument to wrapped object"""
522 thing = evalrawexp(context, mapping, arg)
524 thing = evalrawexp(context, mapping, arg)
523 return makewrapped(context, mapping, thing)
525 return makewrapped(context, mapping, thing)
524
526
525 def makewrapped(context, mapping, thing):
527 def makewrapped(context, mapping, thing):
526 """Lift object to a wrapped type"""
528 """Lift object to a wrapped type"""
527 if isinstance(thing, wrapped):
529 if isinstance(thing, wrapped):
528 return thing
530 return thing
529 thing = _unthunk(context, mapping, thing)
531 thing = _unthunk(context, mapping, thing)
530 if isinstance(thing, bytes):
532 if isinstance(thing, bytes):
531 return wrappedbytes(thing)
533 return wrappedbytes(thing)
532 return wrappedvalue(thing)
534 return wrappedvalue(thing)
533
535
534 def evalfuncarg(context, mapping, arg):
536 def evalfuncarg(context, mapping, arg):
535 """Evaluate given argument as value type"""
537 """Evaluate given argument as value type"""
536 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
538 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
537
539
538 def unwrapvalue(context, mapping, thing):
540 def unwrapvalue(context, mapping, thing):
539 """Move the inner value object out of the wrapper"""
541 """Move the inner value object out of the wrapper"""
540 if isinstance(thing, wrapped):
542 if isinstance(thing, wrapped):
541 return thing.tovalue(context, mapping)
543 return thing.tovalue(context, mapping)
542 # evalrawexp() may return string, generator of strings or arbitrary object
544 # evalrawexp() may return string, generator of strings or arbitrary object
543 # such as date tuple, but filter does not want generator.
545 # such as date tuple, but filter does not want generator.
544 return _unthunk(context, mapping, thing)
546 return _unthunk(context, mapping, thing)
545
547
546 def evalboolean(context, mapping, arg):
548 def evalboolean(context, mapping, arg):
547 """Evaluate given argument as boolean, but also takes boolean literals"""
549 """Evaluate given argument as boolean, but also takes boolean literals"""
548 func, data = arg
550 func, data = arg
549 if func is runsymbol:
551 if func is runsymbol:
550 thing = func(context, mapping, data, default=None)
552 thing = func(context, mapping, data, default=None)
551 if thing is None:
553 if thing is None:
552 # not a template keyword, takes as a boolean literal
554 # not a template keyword, takes as a boolean literal
553 thing = stringutil.parsebool(data)
555 thing = stringutil.parsebool(data)
554 else:
556 else:
555 thing = func(context, mapping, data)
557 thing = func(context, mapping, data)
556 if isinstance(thing, wrapped):
558 if isinstance(thing, wrapped):
557 thing = thing.tovalue(context, mapping)
559 thing = thing.tovalue(context, mapping)
558 if isinstance(thing, bool):
560 if isinstance(thing, bool):
559 return thing
561 return thing
560 # other objects are evaluated as strings, which means 0 is True, but
562 # other objects are evaluated as strings, which means 0 is True, but
561 # empty dict/list should be False as they are expected to be ''
563 # empty dict/list should be False as they are expected to be ''
562 return bool(stringify(context, mapping, thing))
564 return bool(stringify(context, mapping, thing))
563
565
564 def evaldate(context, mapping, arg, err=None):
566 def evaldate(context, mapping, arg, err=None):
565 """Evaluate given argument as a date tuple or a date string; returns
567 """Evaluate given argument as a date tuple or a date string; returns
566 a (unixtime, offset) tuple"""
568 a (unixtime, offset) tuple"""
567 thing = evalrawexp(context, mapping, arg)
569 thing = evalrawexp(context, mapping, arg)
568 return unwrapdate(context, mapping, thing, err)
570 return unwrapdate(context, mapping, thing, err)
569
571
570 def unwrapdate(context, mapping, thing, err=None):
572 def unwrapdate(context, mapping, thing, err=None):
571 thing = unwrapvalue(context, mapping, thing)
573 thing = unwrapvalue(context, mapping, thing)
572 try:
574 try:
573 return dateutil.parsedate(thing)
575 return dateutil.parsedate(thing)
574 except AttributeError:
576 except AttributeError:
575 raise error.ParseError(err or _('not a date tuple nor a string'))
577 raise error.ParseError(err or _('not a date tuple nor a string'))
576 except error.ParseError:
578 except error.ParseError:
577 if not err:
579 if not err:
578 raise
580 raise
579 raise error.ParseError(err)
581 raise error.ParseError(err)
580
582
581 def evalinteger(context, mapping, arg, err=None):
583 def evalinteger(context, mapping, arg, err=None):
582 thing = evalrawexp(context, mapping, arg)
584 thing = evalrawexp(context, mapping, arg)
583 return unwrapinteger(context, mapping, thing, err)
585 return unwrapinteger(context, mapping, thing, err)
584
586
585 def unwrapinteger(context, mapping, thing, err=None):
587 def unwrapinteger(context, mapping, thing, err=None):
586 thing = unwrapvalue(context, mapping, thing)
588 thing = unwrapvalue(context, mapping, thing)
587 try:
589 try:
588 return int(thing)
590 return int(thing)
589 except (TypeError, ValueError):
591 except (TypeError, ValueError):
590 raise error.ParseError(err or _('not an integer'))
592 raise error.ParseError(err or _('not an integer'))
591
593
592 def evalstring(context, mapping, arg):
594 def evalstring(context, mapping, arg):
593 return stringify(context, mapping, evalrawexp(context, mapping, arg))
595 return stringify(context, mapping, evalrawexp(context, mapping, arg))
594
596
595 def evalstringliteral(context, mapping, arg):
597 def evalstringliteral(context, mapping, arg):
596 """Evaluate given argument as string template, but returns symbol name
598 """Evaluate given argument as string template, but returns symbol name
597 if it is unknown"""
599 if it is unknown"""
598 func, data = arg
600 func, data = arg
599 if func is runsymbol:
601 if func is runsymbol:
600 thing = func(context, mapping, data, default=data)
602 thing = func(context, mapping, data, default=data)
601 else:
603 else:
602 thing = func(context, mapping, data)
604 thing = func(context, mapping, data)
603 return stringify(context, mapping, thing)
605 return stringify(context, mapping, thing)
604
606
605 _unwrapfuncbytype = {
607 _unwrapfuncbytype = {
606 None: unwrapvalue,
608 None: unwrapvalue,
607 bytes: stringify,
609 bytes: stringify,
608 date: unwrapdate,
610 date: unwrapdate,
609 int: unwrapinteger,
611 int: unwrapinteger,
610 }
612 }
611
613
612 def unwrapastype(context, mapping, thing, typ):
614 def unwrapastype(context, mapping, thing, typ):
613 """Move the inner value object out of the wrapper and coerce its type"""
615 """Move the inner value object out of the wrapper and coerce its type"""
614 try:
616 try:
615 f = _unwrapfuncbytype[typ]
617 f = _unwrapfuncbytype[typ]
616 except KeyError:
618 except KeyError:
617 raise error.ProgrammingError('invalid type specified: %r' % typ)
619 raise error.ProgrammingError('invalid type specified: %r' % typ)
618 return f(context, mapping, thing)
620 return f(context, mapping, thing)
619
621
620 def runinteger(context, mapping, data):
622 def runinteger(context, mapping, data):
621 return int(data)
623 return int(data)
622
624
623 def runstring(context, mapping, data):
625 def runstring(context, mapping, data):
624 return data
626 return data
625
627
626 def _recursivesymbolblocker(key):
628 def _recursivesymbolblocker(key):
627 def showrecursion(**args):
629 def showrecursion(**args):
628 raise error.Abort(_("recursive reference '%s' in template") % key)
630 raise error.Abort(_("recursive reference '%s' in template") % key)
629 return showrecursion
631 return showrecursion
630
632
631 def runsymbol(context, mapping, key, default=''):
633 def runsymbol(context, mapping, key, default=''):
632 v = context.symbol(mapping, key)
634 v = context.symbol(mapping, key)
633 if v is None:
635 if v is None:
634 # put poison to cut recursion. we can't move this to parsing phase
636 # put poison to cut recursion. we can't move this to parsing phase
635 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
637 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
636 safemapping = mapping.copy()
638 safemapping = mapping.copy()
637 safemapping[key] = _recursivesymbolblocker(key)
639 safemapping[key] = _recursivesymbolblocker(key)
638 try:
640 try:
639 v = context.process(key, safemapping)
641 v = context.process(key, safemapping)
640 except TemplateNotFound:
642 except TemplateNotFound:
641 v = default
643 v = default
642 if callable(v) and getattr(v, '_requires', None) is None:
644 if callable(v) and getattr(v, '_requires', None) is None:
643 # old templatekw: expand all keywords and resources
645 # old templatekw: expand all keywords and resources
644 # (TODO: deprecate this after porting web template keywords to new API)
646 # (TODO: deprecate this after porting web template keywords to new API)
645 props = {k: context._resources.lookup(context, mapping, k)
647 props = {k: context._resources.lookup(context, mapping, k)
646 for k in context._resources.knownkeys()}
648 for k in context._resources.knownkeys()}
647 # pass context to _showcompatlist() through templatekw._showlist()
649 # pass context to _showcompatlist() through templatekw._showlist()
648 props['templ'] = context
650 props['templ'] = context
649 props.update(mapping)
651 props.update(mapping)
650 return v(**pycompat.strkwargs(props))
652 return v(**pycompat.strkwargs(props))
651 if callable(v):
653 if callable(v):
652 # new templatekw
654 # new templatekw
653 try:
655 try:
654 return v(context, mapping)
656 return v(context, mapping)
655 except ResourceUnavailable:
657 except ResourceUnavailable:
656 # unsupported keyword is mapped to empty just like unknown keyword
658 # unsupported keyword is mapped to empty just like unknown keyword
657 return None
659 return None
658 return v
660 return v
659
661
660 def runtemplate(context, mapping, template):
662 def runtemplate(context, mapping, template):
661 for arg in template:
663 for arg in template:
662 yield evalrawexp(context, mapping, arg)
664 yield evalrawexp(context, mapping, arg)
663
665
664 def runfilter(context, mapping, data):
666 def runfilter(context, mapping, data):
665 arg, filt = data
667 arg, filt = data
666 thing = evalrawexp(context, mapping, arg)
668 thing = evalrawexp(context, mapping, arg)
667 intype = getattr(filt, '_intype', None)
669 intype = getattr(filt, '_intype', None)
668 try:
670 try:
669 thing = unwrapastype(context, mapping, thing, intype)
671 thing = unwrapastype(context, mapping, thing, intype)
670 return filt(thing)
672 return filt(thing)
671 except error.ParseError as e:
673 except error.ParseError as e:
672 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
674 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
673
675
674 def _formatfiltererror(arg, filt):
676 def _formatfiltererror(arg, filt):
675 fn = pycompat.sysbytes(filt.__name__)
677 fn = pycompat.sysbytes(filt.__name__)
676 sym = findsymbolicname(arg)
678 sym = findsymbolicname(arg)
677 if not sym:
679 if not sym:
678 return _("incompatible use of template filter '%s'") % fn
680 return _("incompatible use of template filter '%s'") % fn
679 return (_("template filter '%s' is not compatible with keyword '%s'")
681 return (_("template filter '%s' is not compatible with keyword '%s'")
680 % (fn, sym))
682 % (fn, sym))
681
683
682 def _iteroverlaymaps(context, origmapping, newmappings):
684 def _iteroverlaymaps(context, origmapping, newmappings):
683 """Generate combined mappings from the original mapping and an iterable
685 """Generate combined mappings from the original mapping and an iterable
684 of partial mappings to override the original"""
686 of partial mappings to override the original"""
685 for i, nm in enumerate(newmappings):
687 for i, nm in enumerate(newmappings):
686 lm = context.overlaymap(origmapping, nm)
688 lm = context.overlaymap(origmapping, nm)
687 lm['index'] = i
689 lm['index'] = i
688 yield lm
690 yield lm
689
691
690 def _applymap(context, mapping, d, targ):
692 def _applymap(context, mapping, d, targ):
691 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
693 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
692 yield evalrawexp(context, lm, targ)
694 yield evalrawexp(context, lm, targ)
693
695
694 def runmap(context, mapping, data):
696 def runmap(context, mapping, data):
695 darg, targ = data
697 darg, targ = data
696 d = evalwrapped(context, mapping, darg)
698 d = evalwrapped(context, mapping, darg)
697 return mappedgenerator(_applymap, args=(mapping, d, targ))
699 return mappedgenerator(_applymap, args=(mapping, d, targ))
698
700
699 def runmember(context, mapping, data):
701 def runmember(context, mapping, data):
700 darg, memb = data
702 darg, memb = data
701 d = evalwrapped(context, mapping, darg)
703 d = evalwrapped(context, mapping, darg)
702 if util.safehasattr(d, 'tomap'):
704 if util.safehasattr(d, 'tomap'):
703 lm = context.overlaymap(mapping, d.tomap())
705 lm = context.overlaymap(mapping, d.tomap())
704 return runsymbol(context, lm, memb)
706 return runsymbol(context, lm, memb)
705 try:
707 try:
706 return d.getmember(context, mapping, memb)
708 return d.getmember(context, mapping, memb)
707 except error.ParseError as err:
709 except error.ParseError as err:
708 sym = findsymbolicname(darg)
710 sym = findsymbolicname(darg)
709 if not sym:
711 if not sym:
710 raise
712 raise
711 hint = _("keyword '%s' does not support member operation") % sym
713 hint = _("keyword '%s' does not support member operation") % sym
712 raise error.ParseError(bytes(err), hint=hint)
714 raise error.ParseError(bytes(err), hint=hint)
713
715
714 def runnegate(context, mapping, data):
716 def runnegate(context, mapping, data):
715 data = evalinteger(context, mapping, data,
717 data = evalinteger(context, mapping, data,
716 _('negation needs an integer argument'))
718 _('negation needs an integer argument'))
717 return -data
719 return -data
718
720
719 def runarithmetic(context, mapping, data):
721 def runarithmetic(context, mapping, data):
720 func, left, right = data
722 func, left, right = data
721 left = evalinteger(context, mapping, left,
723 left = evalinteger(context, mapping, left,
722 _('arithmetic only defined on integers'))
724 _('arithmetic only defined on integers'))
723 right = evalinteger(context, mapping, right,
725 right = evalinteger(context, mapping, right,
724 _('arithmetic only defined on integers'))
726 _('arithmetic only defined on integers'))
725 try:
727 try:
726 return func(left, right)
728 return func(left, right)
727 except ZeroDivisionError:
729 except ZeroDivisionError:
728 raise error.Abort(_('division by zero is not defined'))
730 raise error.Abort(_('division by zero is not defined'))
729
731
730 def joinitems(itemiter, sep):
732 def joinitems(itemiter, sep):
731 """Join items with the separator; Returns generator of bytes"""
733 """Join items with the separator; Returns generator of bytes"""
732 first = True
734 first = True
733 for x in itemiter:
735 for x in itemiter:
734 if first:
736 if first:
735 first = False
737 first = False
736 elif sep:
738 elif sep:
737 yield sep
739 yield sep
738 yield x
740 yield x
General Comments 0
You need to be logged in to leave comments. Login now