##// END OF EJS Templates
templater: drop support for old style keywords (API)...
Matt Harbison -
r42524:832c59d1 default
parent child Browse files
Show More
@@ -1,813 +1,807
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 diffutil,
28 diffutil,
29 error,
29 error,
30 match,
30 match,
31 mdiff,
31 mdiff,
32 obsutil,
32 obsutil,
33 patch,
33 patch,
34 pathutil,
34 pathutil,
35 pycompat,
35 pycompat,
36 scmutil,
36 scmutil,
37 templatefilters,
37 templatefilters,
38 templatekw,
38 templatekw,
39 templateutil,
39 templateutil,
40 ui as uimod,
40 ui as uimod,
41 util,
41 util,
42 )
42 )
43
43
44 from ..utils import (
44 from ..utils import (
45 stringutil,
45 stringutil,
46 )
46 )
47
47
48 archivespecs = util.sortdict((
48 archivespecs = util.sortdict((
49 ('zip', ('application/zip', 'zip', '.zip', None)),
49 ('zip', ('application/zip', 'zip', '.zip', None)),
50 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
50 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
51 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
51 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
52 ))
52 ))
53
53
54 def archivelist(ui, nodeid, url=None):
54 def archivelist(ui, nodeid, url=None):
55 allowed = ui.configlist('web', 'allow-archive', untrusted=True)
55 allowed = ui.configlist('web', 'allow-archive', untrusted=True)
56 archives = []
56 archives = []
57
57
58 for typ, spec in archivespecs.iteritems():
58 for typ, spec in archivespecs.iteritems():
59 if typ in allowed or ui.configbool('web', 'allow' + typ,
59 if typ in allowed or ui.configbool('web', 'allow' + typ,
60 untrusted=True):
60 untrusted=True):
61 archives.append({
61 archives.append({
62 'type': typ,
62 'type': typ,
63 'extension': spec[2],
63 'extension': spec[2],
64 'node': nodeid,
64 'node': nodeid,
65 'url': url,
65 'url': url,
66 })
66 })
67
67
68 return templateutil.mappinglist(archives)
68 return templateutil.mappinglist(archives)
69
69
70 def up(p):
70 def up(p):
71 if p[0:1] != "/":
71 if p[0:1] != "/":
72 p = "/" + p
72 p = "/" + p
73 if p[-1:] == "/":
73 if p[-1:] == "/":
74 p = p[:-1]
74 p = p[:-1]
75 up = os.path.dirname(p)
75 up = os.path.dirname(p)
76 if up == "/":
76 if up == "/":
77 return "/"
77 return "/"
78 return up + "/"
78 return up + "/"
79
79
80 def _navseq(step, firststep=None):
80 def _navseq(step, firststep=None):
81 if firststep:
81 if firststep:
82 yield firststep
82 yield firststep
83 if firststep >= 20 and firststep <= 40:
83 if firststep >= 20 and firststep <= 40:
84 firststep = 50
84 firststep = 50
85 yield firststep
85 yield firststep
86 assert step > 0
86 assert step > 0
87 assert firststep > 0
87 assert firststep > 0
88 while step <= firststep:
88 while step <= firststep:
89 step *= 10
89 step *= 10
90 while True:
90 while True:
91 yield 1 * step
91 yield 1 * step
92 yield 3 * step
92 yield 3 * step
93 step *= 10
93 step *= 10
94
94
95 class revnav(object):
95 class revnav(object):
96
96
97 def __init__(self, repo):
97 def __init__(self, repo):
98 """Navigation generation object
98 """Navigation generation object
99
99
100 :repo: repo object we generate nav for
100 :repo: repo object we generate nav for
101 """
101 """
102 # used for hex generation
102 # used for hex generation
103 self._revlog = repo.changelog
103 self._revlog = repo.changelog
104
104
105 def __nonzero__(self):
105 def __nonzero__(self):
106 """return True if any revision to navigate over"""
106 """return True if any revision to navigate over"""
107 return self._first() is not None
107 return self._first() is not None
108
108
109 __bool__ = __nonzero__
109 __bool__ = __nonzero__
110
110
111 def _first(self):
111 def _first(self):
112 """return the minimum non-filtered changeset or None"""
112 """return the minimum non-filtered changeset or None"""
113 try:
113 try:
114 return next(iter(self._revlog))
114 return next(iter(self._revlog))
115 except StopIteration:
115 except StopIteration:
116 return None
116 return None
117
117
118 def hex(self, rev):
118 def hex(self, rev):
119 return hex(self._revlog.node(rev))
119 return hex(self._revlog.node(rev))
120
120
121 def gen(self, pos, pagelen, limit):
121 def gen(self, pos, pagelen, limit):
122 """computes label and revision id for navigation link
122 """computes label and revision id for navigation link
123
123
124 :pos: is the revision relative to which we generate navigation.
124 :pos: is the revision relative to which we generate navigation.
125 :pagelen: the size of each navigation page
125 :pagelen: the size of each navigation page
126 :limit: how far shall we link
126 :limit: how far shall we link
127
127
128 The return is:
128 The return is:
129 - a single element mappinglist
129 - a single element mappinglist
130 - containing a dictionary with a `before` and `after` key
130 - containing a dictionary with a `before` and `after` key
131 - values are dictionaries with `label` and `node` keys
131 - values are dictionaries with `label` and `node` keys
132 """
132 """
133 if not self:
133 if not self:
134 # empty repo
134 # empty repo
135 return templateutil.mappinglist([
135 return templateutil.mappinglist([
136 {'before': templateutil.mappinglist([]),
136 {'before': templateutil.mappinglist([]),
137 'after': templateutil.mappinglist([])},
137 'after': templateutil.mappinglist([])},
138 ])
138 ])
139
139
140 targets = []
140 targets = []
141 for f in _navseq(1, pagelen):
141 for f in _navseq(1, pagelen):
142 if f > limit:
142 if f > limit:
143 break
143 break
144 targets.append(pos + f)
144 targets.append(pos + f)
145 targets.append(pos - f)
145 targets.append(pos - f)
146 targets.sort()
146 targets.sort()
147
147
148 first = self._first()
148 first = self._first()
149 navbefore = [{'label': '(%i)' % first, 'node': self.hex(first)}]
149 navbefore = [{'label': '(%i)' % first, 'node': self.hex(first)}]
150 navafter = []
150 navafter = []
151 for rev in targets:
151 for rev in targets:
152 if rev not in self._revlog:
152 if rev not in self._revlog:
153 continue
153 continue
154 if pos < rev < limit:
154 if pos < rev < limit:
155 navafter.append({'label': '+%d' % abs(rev - pos),
155 navafter.append({'label': '+%d' % abs(rev - pos),
156 'node': self.hex(rev)})
156 'node': self.hex(rev)})
157 if 0 < rev < pos:
157 if 0 < rev < pos:
158 navbefore.append({'label': '-%d' % abs(rev - pos),
158 navbefore.append({'label': '-%d' % abs(rev - pos),
159 'node': self.hex(rev)})
159 'node': self.hex(rev)})
160
160
161 navafter.append({'label': 'tip', 'node': 'tip'})
161 navafter.append({'label': 'tip', 'node': 'tip'})
162
162
163 # TODO: maybe this can be a scalar object supporting tomap()
163 # TODO: maybe this can be a scalar object supporting tomap()
164 return templateutil.mappinglist([
164 return templateutil.mappinglist([
165 {'before': templateutil.mappinglist(navbefore),
165 {'before': templateutil.mappinglist(navbefore),
166 'after': templateutil.mappinglist(navafter)},
166 'after': templateutil.mappinglist(navafter)},
167 ])
167 ])
168
168
169 class filerevnav(revnav):
169 class filerevnav(revnav):
170
170
171 def __init__(self, repo, path):
171 def __init__(self, repo, path):
172 """Navigation generation object
172 """Navigation generation object
173
173
174 :repo: repo object we generate nav for
174 :repo: repo object we generate nav for
175 :path: path of the file we generate nav for
175 :path: path of the file we generate nav for
176 """
176 """
177 # used for iteration
177 # used for iteration
178 self._changelog = repo.unfiltered().changelog
178 self._changelog = repo.unfiltered().changelog
179 # used for hex generation
179 # used for hex generation
180 self._revlog = repo.file(path)
180 self._revlog = repo.file(path)
181
181
182 def hex(self, rev):
182 def hex(self, rev):
183 return hex(self._changelog.node(self._revlog.linkrev(rev)))
183 return hex(self._changelog.node(self._revlog.linkrev(rev)))
184
184
185 # TODO: maybe this can be a wrapper class for changectx/filectx list, which
185 # TODO: maybe this can be a wrapper class for changectx/filectx list, which
186 # yields {'ctx': ctx}
186 # yields {'ctx': ctx}
187 def _ctxsgen(context, ctxs):
187 def _ctxsgen(context, ctxs):
188 for s in ctxs:
188 for s in ctxs:
189 d = {
189 d = {
190 'node': s.hex(),
190 'node': s.hex(),
191 'rev': s.rev(),
191 'rev': s.rev(),
192 'user': s.user(),
192 'user': s.user(),
193 'date': s.date(),
193 'date': s.date(),
194 'description': s.description(),
194 'description': s.description(),
195 'branch': s.branch(),
195 'branch': s.branch(),
196 }
196 }
197 if util.safehasattr(s, 'path'):
197 if util.safehasattr(s, 'path'):
198 d['file'] = s.path()
198 d['file'] = s.path()
199 yield d
199 yield d
200
200
201 def _siblings(siblings=None, hiderev=None):
201 def _siblings(siblings=None, hiderev=None):
202 if siblings is None:
202 if siblings is None:
203 siblings = []
203 siblings = []
204 siblings = [s for s in siblings if s.node() != nullid]
204 siblings = [s for s in siblings if s.node() != nullid]
205 if len(siblings) == 1 and siblings[0].rev() == hiderev:
205 if len(siblings) == 1 and siblings[0].rev() == hiderev:
206 siblings = []
206 siblings = []
207 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,))
207 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,))
208
208
209 def difffeatureopts(req, ui, section):
209 def difffeatureopts(req, ui, section):
210 diffopts = diffutil.difffeatureopts(ui, untrusted=True,
210 diffopts = diffutil.difffeatureopts(ui, untrusted=True,
211 section=section, whitespace=True)
211 section=section, whitespace=True)
212
212
213 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
213 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
214 v = req.qsparams.get(k)
214 v = req.qsparams.get(k)
215 if v is not None:
215 if v is not None:
216 v = stringutil.parsebool(v)
216 v = stringutil.parsebool(v)
217 setattr(diffopts, k, v if v is not None else True)
217 setattr(diffopts, k, v if v is not None else True)
218
218
219 return diffopts
219 return diffopts
220
220
221 def annotate(req, fctx, ui):
221 def annotate(req, fctx, ui):
222 diffopts = difffeatureopts(req, ui, 'annotate')
222 diffopts = difffeatureopts(req, ui, 'annotate')
223 return fctx.annotate(follow=True, diffopts=diffopts)
223 return fctx.annotate(follow=True, diffopts=diffopts)
224
224
225 def parents(ctx, hide=None):
225 def parents(ctx, hide=None):
226 if isinstance(ctx, context.basefilectx):
226 if isinstance(ctx, context.basefilectx):
227 introrev = ctx.introrev()
227 introrev = ctx.introrev()
228 if ctx.changectx().rev() != introrev:
228 if ctx.changectx().rev() != introrev:
229 return _siblings([ctx.repo()[introrev]], hide)
229 return _siblings([ctx.repo()[introrev]], hide)
230 return _siblings(ctx.parents(), hide)
230 return _siblings(ctx.parents(), hide)
231
231
232 def children(ctx, hide=None):
232 def children(ctx, hide=None):
233 return _siblings(ctx.children(), hide)
233 return _siblings(ctx.children(), hide)
234
234
235 def renamelink(fctx):
235 def renamelink(fctx):
236 r = fctx.renamed()
236 r = fctx.renamed()
237 if r:
237 if r:
238 return templateutil.mappinglist([{'file': r[0], 'node': hex(r[1])}])
238 return templateutil.mappinglist([{'file': r[0], 'node': hex(r[1])}])
239 return templateutil.mappinglist([])
239 return templateutil.mappinglist([])
240
240
241 def nodetagsdict(repo, node):
241 def nodetagsdict(repo, node):
242 return templateutil.hybridlist(repo.nodetags(node), name='name')
242 return templateutil.hybridlist(repo.nodetags(node), name='name')
243
243
244 def nodebookmarksdict(repo, node):
244 def nodebookmarksdict(repo, node):
245 return templateutil.hybridlist(repo.nodebookmarks(node), name='name')
245 return templateutil.hybridlist(repo.nodebookmarks(node), name='name')
246
246
247 def nodebranchdict(repo, ctx):
247 def nodebranchdict(repo, ctx):
248 branches = []
248 branches = []
249 branch = ctx.branch()
249 branch = ctx.branch()
250 # If this is an empty repo, ctx.node() == nullid,
250 # If this is an empty repo, ctx.node() == nullid,
251 # ctx.branch() == 'default'.
251 # ctx.branch() == 'default'.
252 try:
252 try:
253 branchnode = repo.branchtip(branch)
253 branchnode = repo.branchtip(branch)
254 except error.RepoLookupError:
254 except error.RepoLookupError:
255 branchnode = None
255 branchnode = None
256 if branchnode == ctx.node():
256 if branchnode == ctx.node():
257 branches.append(branch)
257 branches.append(branch)
258 return templateutil.hybridlist(branches, name='name')
258 return templateutil.hybridlist(branches, name='name')
259
259
260 def nodeinbranch(repo, ctx):
260 def nodeinbranch(repo, ctx):
261 branches = []
261 branches = []
262 branch = ctx.branch()
262 branch = ctx.branch()
263 try:
263 try:
264 branchnode = repo.branchtip(branch)
264 branchnode = repo.branchtip(branch)
265 except error.RepoLookupError:
265 except error.RepoLookupError:
266 branchnode = None
266 branchnode = None
267 if branch != 'default' and branchnode != ctx.node():
267 if branch != 'default' and branchnode != ctx.node():
268 branches.append(branch)
268 branches.append(branch)
269 return templateutil.hybridlist(branches, name='name')
269 return templateutil.hybridlist(branches, name='name')
270
270
271 def nodebranchnodefault(ctx):
271 def nodebranchnodefault(ctx):
272 branches = []
272 branches = []
273 branch = ctx.branch()
273 branch = ctx.branch()
274 if branch != 'default':
274 if branch != 'default':
275 branches.append(branch)
275 branches.append(branch)
276 return templateutil.hybridlist(branches, name='name')
276 return templateutil.hybridlist(branches, name='name')
277
277
278 def _nodenamesgen(context, f, node, name):
278 def _nodenamesgen(context, f, node, name):
279 for t in f(node):
279 for t in f(node):
280 yield {name: t}
280 yield {name: t}
281
281
282 def showtag(repo, t1, node=nullid):
282 def showtag(repo, t1, node=nullid):
283 args = (repo.nodetags, node, 'tag')
283 args = (repo.nodetags, node, 'tag')
284 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
284 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
285
285
286 def showbookmark(repo, t1, node=nullid):
286 def showbookmark(repo, t1, node=nullid):
287 args = (repo.nodebookmarks, node, 'bookmark')
287 args = (repo.nodebookmarks, node, 'bookmark')
288 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
288 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1)
289
289
290 def branchentries(repo, stripecount, limit=0):
290 def branchentries(repo, stripecount, limit=0):
291 tips = []
291 tips = []
292 heads = repo.heads()
292 heads = repo.heads()
293 parity = paritygen(stripecount)
293 parity = paritygen(stripecount)
294 sortkey = lambda item: (not item[1], item[0].rev())
294 sortkey = lambda item: (not item[1], item[0].rev())
295
295
296 def entries(context):
296 def entries(context):
297 count = 0
297 count = 0
298 if not tips:
298 if not tips:
299 for tag, hs, tip, closed in repo.branchmap().iterbranches():
299 for tag, hs, tip, closed in repo.branchmap().iterbranches():
300 tips.append((repo[tip], closed))
300 tips.append((repo[tip], closed))
301 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
301 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
302 if limit > 0 and count >= limit:
302 if limit > 0 and count >= limit:
303 return
303 return
304 count += 1
304 count += 1
305 if closed:
305 if closed:
306 status = 'closed'
306 status = 'closed'
307 elif ctx.node() not in heads:
307 elif ctx.node() not in heads:
308 status = 'inactive'
308 status = 'inactive'
309 else:
309 else:
310 status = 'open'
310 status = 'open'
311 yield {
311 yield {
312 'parity': next(parity),
312 'parity': next(parity),
313 'branch': ctx.branch(),
313 'branch': ctx.branch(),
314 'status': status,
314 'status': status,
315 'node': ctx.hex(),
315 'node': ctx.hex(),
316 'date': ctx.date()
316 'date': ctx.date()
317 }
317 }
318
318
319 return templateutil.mappinggenerator(entries)
319 return templateutil.mappinggenerator(entries)
320
320
321 def cleanpath(repo, path):
321 def cleanpath(repo, path):
322 path = path.lstrip('/')
322 path = path.lstrip('/')
323 auditor = pathutil.pathauditor(repo.root, realfs=False)
323 auditor = pathutil.pathauditor(repo.root, realfs=False)
324 return pathutil.canonpath(repo.root, '', path, auditor=auditor)
324 return pathutil.canonpath(repo.root, '', path, auditor=auditor)
325
325
326 def changectx(repo, req):
326 def changectx(repo, req):
327 changeid = "tip"
327 changeid = "tip"
328 if 'node' in req.qsparams:
328 if 'node' in req.qsparams:
329 changeid = req.qsparams['node']
329 changeid = req.qsparams['node']
330 ipos = changeid.find(':')
330 ipos = changeid.find(':')
331 if ipos != -1:
331 if ipos != -1:
332 changeid = changeid[(ipos + 1):]
332 changeid = changeid[(ipos + 1):]
333
333
334 return scmutil.revsymbol(repo, changeid)
334 return scmutil.revsymbol(repo, changeid)
335
335
336 def basechangectx(repo, req):
336 def basechangectx(repo, req):
337 if 'node' in req.qsparams:
337 if 'node' in req.qsparams:
338 changeid = req.qsparams['node']
338 changeid = req.qsparams['node']
339 ipos = changeid.find(':')
339 ipos = changeid.find(':')
340 if ipos != -1:
340 if ipos != -1:
341 changeid = changeid[:ipos]
341 changeid = changeid[:ipos]
342 return scmutil.revsymbol(repo, changeid)
342 return scmutil.revsymbol(repo, changeid)
343
343
344 return None
344 return None
345
345
346 def filectx(repo, req):
346 def filectx(repo, req):
347 if 'file' not in req.qsparams:
347 if 'file' not in req.qsparams:
348 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
348 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
349 path = cleanpath(repo, req.qsparams['file'])
349 path = cleanpath(repo, req.qsparams['file'])
350 if 'node' in req.qsparams:
350 if 'node' in req.qsparams:
351 changeid = req.qsparams['node']
351 changeid = req.qsparams['node']
352 elif 'filenode' in req.qsparams:
352 elif 'filenode' in req.qsparams:
353 changeid = req.qsparams['filenode']
353 changeid = req.qsparams['filenode']
354 else:
354 else:
355 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
355 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
356 try:
356 try:
357 fctx = scmutil.revsymbol(repo, changeid)[path]
357 fctx = scmutil.revsymbol(repo, changeid)[path]
358 except error.RepoError:
358 except error.RepoError:
359 fctx = repo.filectx(path, fileid=changeid)
359 fctx = repo.filectx(path, fileid=changeid)
360
360
361 return fctx
361 return fctx
362
362
363 def linerange(req):
363 def linerange(req):
364 linerange = req.qsparams.getall('linerange')
364 linerange = req.qsparams.getall('linerange')
365 if not linerange:
365 if not linerange:
366 return None
366 return None
367 if len(linerange) > 1:
367 if len(linerange) > 1:
368 raise ErrorResponse(HTTP_BAD_REQUEST,
368 raise ErrorResponse(HTTP_BAD_REQUEST,
369 'redundant linerange parameter')
369 'redundant linerange parameter')
370 try:
370 try:
371 fromline, toline = map(int, linerange[0].split(':', 1))
371 fromline, toline = map(int, linerange[0].split(':', 1))
372 except ValueError:
372 except ValueError:
373 raise ErrorResponse(HTTP_BAD_REQUEST,
373 raise ErrorResponse(HTTP_BAD_REQUEST,
374 'invalid linerange parameter')
374 'invalid linerange parameter')
375 try:
375 try:
376 return util.processlinerange(fromline, toline)
376 return util.processlinerange(fromline, toline)
377 except error.ParseError as exc:
377 except error.ParseError as exc:
378 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
378 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
379
379
380 def formatlinerange(fromline, toline):
380 def formatlinerange(fromline, toline):
381 return '%d:%d' % (fromline + 1, toline)
381 return '%d:%d' % (fromline + 1, toline)
382
382
383 def _succsandmarkersgen(context, mapping):
383 def _succsandmarkersgen(context, mapping):
384 repo = context.resource(mapping, 'repo')
384 repo = context.resource(mapping, 'repo')
385 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
385 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
386 for item in itemmappings.tovalue(context, mapping):
386 for item in itemmappings.tovalue(context, mapping):
387 item['successors'] = _siblings(repo[successor]
387 item['successors'] = _siblings(repo[successor]
388 for successor in item['successors'])
388 for successor in item['successors'])
389 yield item
389 yield item
390
390
391 def succsandmarkers(context, mapping):
391 def succsandmarkers(context, mapping):
392 return templateutil.mappinggenerator(_succsandmarkersgen, args=(mapping,))
392 return templateutil.mappinggenerator(_succsandmarkersgen, args=(mapping,))
393
393
394 # teach templater succsandmarkers is switched to (context, mapping) API
394 # teach templater succsandmarkers is switched to (context, mapping) API
395 succsandmarkers._requires = {'repo', 'ctx'}
395 succsandmarkers._requires = {'repo', 'ctx'}
396
396
397 def _whyunstablegen(context, mapping):
397 def _whyunstablegen(context, mapping):
398 repo = context.resource(mapping, 'repo')
398 repo = context.resource(mapping, 'repo')
399 ctx = context.resource(mapping, 'ctx')
399 ctx = context.resource(mapping, 'ctx')
400
400
401 entries = obsutil.whyunstable(repo, ctx)
401 entries = obsutil.whyunstable(repo, ctx)
402 for entry in entries:
402 for entry in entries:
403 if entry.get('divergentnodes'):
403 if entry.get('divergentnodes'):
404 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
404 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
405 yield entry
405 yield entry
406
406
407 def whyunstable(context, mapping):
407 def whyunstable(context, mapping):
408 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,))
408 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,))
409
409
410 whyunstable._requires = {'repo', 'ctx'}
410 whyunstable._requires = {'repo', 'ctx'}
411
411
412 # helper to mark a function as a new-style template keyword; can be removed
413 # once old-style function gets unsupported and new-style becomes the default
414 def _kwfunc(f):
415 f._requires = ()
416 return f
417
418 def commonentry(repo, ctx):
412 def commonentry(repo, ctx):
419 node = scmutil.binnode(ctx)
413 node = scmutil.binnode(ctx)
420 return {
414 return {
421 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
415 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
422 # filectx, but I'm not pretty sure if that would always work because
416 # filectx, but I'm not pretty sure if that would always work because
423 # fctx.parents() != fctx.changectx.parents() for example.
417 # fctx.parents() != fctx.changectx.parents() for example.
424 'ctx': ctx,
418 'ctx': ctx,
425 'rev': ctx.rev(),
419 'rev': ctx.rev(),
426 'node': hex(node),
420 'node': hex(node),
427 'author': ctx.user(),
421 'author': ctx.user(),
428 'desc': ctx.description(),
422 'desc': ctx.description(),
429 'date': ctx.date(),
423 'date': ctx.date(),
430 'extra': ctx.extra(),
424 'extra': ctx.extra(),
431 'phase': ctx.phasestr(),
425 'phase': ctx.phasestr(),
432 'obsolete': ctx.obsolete(),
426 'obsolete': ctx.obsolete(),
433 'succsandmarkers': succsandmarkers,
427 'succsandmarkers': succsandmarkers,
434 'instabilities': templateutil.hybridlist(ctx.instabilities(),
428 'instabilities': templateutil.hybridlist(ctx.instabilities(),
435 name='instability'),
429 name='instability'),
436 'whyunstable': whyunstable,
430 'whyunstable': whyunstable,
437 'branch': nodebranchnodefault(ctx),
431 'branch': nodebranchnodefault(ctx),
438 'inbranch': nodeinbranch(repo, ctx),
432 'inbranch': nodeinbranch(repo, ctx),
439 'branches': nodebranchdict(repo, ctx),
433 'branches': nodebranchdict(repo, ctx),
440 'tags': nodetagsdict(repo, node),
434 'tags': nodetagsdict(repo, node),
441 'bookmarks': nodebookmarksdict(repo, node),
435 'bookmarks': nodebookmarksdict(repo, node),
442 'parent': _kwfunc(lambda context, mapping: parents(ctx)),
436 'parent': lambda context, mapping: parents(ctx),
443 'child': _kwfunc(lambda context, mapping: children(ctx)),
437 'child': lambda context, mapping: children(ctx),
444 }
438 }
445
439
446 def changelistentry(web, ctx):
440 def changelistentry(web, ctx):
447 '''Obtain a dictionary to be used for entries in a changelist.
441 '''Obtain a dictionary to be used for entries in a changelist.
448
442
449 This function is called when producing items for the "entries" list passed
443 This function is called when producing items for the "entries" list passed
450 to the "shortlog" and "changelog" templates.
444 to the "shortlog" and "changelog" templates.
451 '''
445 '''
452 repo = web.repo
446 repo = web.repo
453 rev = ctx.rev()
447 rev = ctx.rev()
454 n = scmutil.binnode(ctx)
448 n = scmutil.binnode(ctx)
455 showtags = showtag(repo, 'changelogtag', n)
449 showtags = showtag(repo, 'changelogtag', n)
456 files = listfilediffs(ctx.files(), n, web.maxfiles)
450 files = listfilediffs(ctx.files(), n, web.maxfiles)
457
451
458 entry = commonentry(repo, ctx)
452 entry = commonentry(repo, ctx)
459 entry.update({
453 entry.update({
460 'allparents': _kwfunc(lambda context, mapping: parents(ctx)),
454 'allparents': lambda context, mapping: parents(ctx),
461 'parent': _kwfunc(lambda context, mapping: parents(ctx, rev - 1)),
455 'parent': lambda context, mapping: parents(ctx, rev - 1),
462 'child': _kwfunc(lambda context, mapping: children(ctx, rev + 1)),
456 'child': lambda context, mapping: children(ctx, rev + 1),
463 'changelogtag': showtags,
457 'changelogtag': showtags,
464 'files': files,
458 'files': files,
465 })
459 })
466 return entry
460 return entry
467
461
468 def changelistentries(web, revs, maxcount, parityfn):
462 def changelistentries(web, revs, maxcount, parityfn):
469 """Emit up to N records for an iterable of revisions."""
463 """Emit up to N records for an iterable of revisions."""
470 repo = web.repo
464 repo = web.repo
471
465
472 count = 0
466 count = 0
473 for rev in revs:
467 for rev in revs:
474 if count >= maxcount:
468 if count >= maxcount:
475 break
469 break
476
470
477 count += 1
471 count += 1
478
472
479 entry = changelistentry(web, repo[rev])
473 entry = changelistentry(web, repo[rev])
480 entry['parity'] = next(parityfn)
474 entry['parity'] = next(parityfn)
481
475
482 yield entry
476 yield entry
483
477
484 def symrevorshortnode(req, ctx):
478 def symrevorshortnode(req, ctx):
485 if 'node' in req.qsparams:
479 if 'node' in req.qsparams:
486 return templatefilters.revescape(req.qsparams['node'])
480 return templatefilters.revescape(req.qsparams['node'])
487 else:
481 else:
488 return short(scmutil.binnode(ctx))
482 return short(scmutil.binnode(ctx))
489
483
490 def _listfilesgen(context, ctx, stripecount):
484 def _listfilesgen(context, ctx, stripecount):
491 parity = paritygen(stripecount)
485 parity = paritygen(stripecount)
492 for blockno, f in enumerate(ctx.files()):
486 for blockno, f in enumerate(ctx.files()):
493 template = 'filenodelink' if f in ctx else 'filenolink'
487 template = 'filenodelink' if f in ctx else 'filenolink'
494 yield context.process(template, {
488 yield context.process(template, {
495 'node': ctx.hex(),
489 'node': ctx.hex(),
496 'file': f,
490 'file': f,
497 'blockno': blockno + 1,
491 'blockno': blockno + 1,
498 'parity': next(parity),
492 'parity': next(parity),
499 })
493 })
500
494
501 def changesetentry(web, ctx):
495 def changesetentry(web, ctx):
502 '''Obtain a dictionary to be used to render the "changeset" template.'''
496 '''Obtain a dictionary to be used to render the "changeset" template.'''
503
497
504 showtags = showtag(web.repo, 'changesettag', scmutil.binnode(ctx))
498 showtags = showtag(web.repo, 'changesettag', scmutil.binnode(ctx))
505 showbookmarks = showbookmark(web.repo, 'changesetbookmark',
499 showbookmarks = showbookmark(web.repo, 'changesetbookmark',
506 scmutil.binnode(ctx))
500 scmutil.binnode(ctx))
507 showbranch = nodebranchnodefault(ctx)
501 showbranch = nodebranchnodefault(ctx)
508
502
509 basectx = basechangectx(web.repo, web.req)
503 basectx = basechangectx(web.repo, web.req)
510 if basectx is None:
504 if basectx is None:
511 basectx = ctx.p1()
505 basectx = ctx.p1()
512
506
513 style = web.config('web', 'style')
507 style = web.config('web', 'style')
514 if 'style' in web.req.qsparams:
508 if 'style' in web.req.qsparams:
515 style = web.req.qsparams['style']
509 style = web.req.qsparams['style']
516
510
517 diff = diffs(web, ctx, basectx, None, style)
511 diff = diffs(web, ctx, basectx, None, style)
518
512
519 parity = paritygen(web.stripecount)
513 parity = paritygen(web.stripecount)
520 diffstatsgen = diffstatgen(web.repo.ui, ctx, basectx)
514 diffstatsgen = diffstatgen(web.repo.ui, ctx, basectx)
521 diffstats = diffstat(ctx, diffstatsgen, parity)
515 diffstats = diffstat(ctx, diffstatsgen, parity)
522
516
523 return dict(
517 return dict(
524 diff=diff,
518 diff=diff,
525 symrev=symrevorshortnode(web.req, ctx),
519 symrev=symrevorshortnode(web.req, ctx),
526 basenode=basectx.hex(),
520 basenode=basectx.hex(),
527 changesettag=showtags,
521 changesettag=showtags,
528 changesetbookmark=showbookmarks,
522 changesetbookmark=showbookmarks,
529 changesetbranch=showbranch,
523 changesetbranch=showbranch,
530 files=templateutil.mappedgenerator(_listfilesgen,
524 files=templateutil.mappedgenerator(_listfilesgen,
531 args=(ctx, web.stripecount)),
525 args=(ctx, web.stripecount)),
532 diffsummary=_kwfunc(lambda context, mapping: diffsummary(diffstatsgen)),
526 diffsummary=lambda context, mapping: diffsummary(diffstatsgen),
533 diffstat=diffstats,
527 diffstat=diffstats,
534 archives=web.archivelist(ctx.hex()),
528 archives=web.archivelist(ctx.hex()),
535 **pycompat.strkwargs(commonentry(web.repo, ctx)))
529 **pycompat.strkwargs(commonentry(web.repo, ctx)))
536
530
537 def _listfilediffsgen(context, files, node, max):
531 def _listfilediffsgen(context, files, node, max):
538 for f in files[:max]:
532 for f in files[:max]:
539 yield context.process('filedifflink', {'node': hex(node), 'file': f})
533 yield context.process('filedifflink', {'node': hex(node), 'file': f})
540 if len(files) > max:
534 if len(files) > max:
541 yield context.process('fileellipses', {})
535 yield context.process('fileellipses', {})
542
536
543 def listfilediffs(files, node, max):
537 def listfilediffs(files, node, max):
544 return templateutil.mappedgenerator(_listfilediffsgen,
538 return templateutil.mappedgenerator(_listfilediffsgen,
545 args=(files, node, max))
539 args=(files, node, max))
546
540
547 def _prettyprintdifflines(context, lines, blockno, lineidprefix):
541 def _prettyprintdifflines(context, lines, blockno, lineidprefix):
548 for lineno, l in enumerate(lines, 1):
542 for lineno, l in enumerate(lines, 1):
549 difflineno = "%d.%d" % (blockno, lineno)
543 difflineno = "%d.%d" % (blockno, lineno)
550 if l.startswith('+'):
544 if l.startswith('+'):
551 ltype = "difflineplus"
545 ltype = "difflineplus"
552 elif l.startswith('-'):
546 elif l.startswith('-'):
553 ltype = "difflineminus"
547 ltype = "difflineminus"
554 elif l.startswith('@'):
548 elif l.startswith('@'):
555 ltype = "difflineat"
549 ltype = "difflineat"
556 else:
550 else:
557 ltype = "diffline"
551 ltype = "diffline"
558 yield context.process(ltype, {
552 yield context.process(ltype, {
559 'line': l,
553 'line': l,
560 'lineno': lineno,
554 'lineno': lineno,
561 'lineid': lineidprefix + "l%s" % difflineno,
555 'lineid': lineidprefix + "l%s" % difflineno,
562 'linenumber': "% 8s" % difflineno,
556 'linenumber': "% 8s" % difflineno,
563 })
557 })
564
558
565 def _diffsgen(context, repo, ctx, basectx, files, style, stripecount,
559 def _diffsgen(context, repo, ctx, basectx, files, style, stripecount,
566 linerange, lineidprefix):
560 linerange, lineidprefix):
567 if files:
561 if files:
568 m = match.exact(files)
562 m = match.exact(files)
569 else:
563 else:
570 m = match.always()
564 m = match.always()
571
565
572 diffopts = patch.diffopts(repo.ui, untrusted=True)
566 diffopts = patch.diffopts(repo.ui, untrusted=True)
573 parity = paritygen(stripecount)
567 parity = paritygen(stripecount)
574
568
575 diffhunks = patch.diffhunks(repo, basectx, ctx, m, opts=diffopts)
569 diffhunks = patch.diffhunks(repo, basectx, ctx, m, opts=diffopts)
576 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
570 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
577 if style != 'raw':
571 if style != 'raw':
578 header = header[1:]
572 header = header[1:]
579 lines = [h + '\n' for h in header]
573 lines = [h + '\n' for h in header]
580 for hunkrange, hunklines in hunks:
574 for hunkrange, hunklines in hunks:
581 if linerange is not None and hunkrange is not None:
575 if linerange is not None and hunkrange is not None:
582 s1, l1, s2, l2 = hunkrange
576 s1, l1, s2, l2 = hunkrange
583 if not mdiff.hunkinrange((s2, l2), linerange):
577 if not mdiff.hunkinrange((s2, l2), linerange):
584 continue
578 continue
585 lines.extend(hunklines)
579 lines.extend(hunklines)
586 if lines:
580 if lines:
587 l = templateutil.mappedgenerator(_prettyprintdifflines,
581 l = templateutil.mappedgenerator(_prettyprintdifflines,
588 args=(lines, blockno,
582 args=(lines, blockno,
589 lineidprefix))
583 lineidprefix))
590 yield {
584 yield {
591 'parity': next(parity),
585 'parity': next(parity),
592 'blockno': blockno,
586 'blockno': blockno,
593 'lines': l,
587 'lines': l,
594 }
588 }
595
589
596 def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=''):
590 def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=''):
597 args = (web.repo, ctx, basectx, files, style, web.stripecount,
591 args = (web.repo, ctx, basectx, files, style, web.stripecount,
598 linerange, lineidprefix)
592 linerange, lineidprefix)
599 return templateutil.mappinggenerator(_diffsgen, args=args, name='diffblock')
593 return templateutil.mappinggenerator(_diffsgen, args=args, name='diffblock')
600
594
601 def _compline(type, leftlineno, leftline, rightlineno, rightline):
595 def _compline(type, leftlineno, leftline, rightlineno, rightline):
602 lineid = leftlineno and ("l%d" % leftlineno) or ''
596 lineid = leftlineno and ("l%d" % leftlineno) or ''
603 lineid += rightlineno and ("r%d" % rightlineno) or ''
597 lineid += rightlineno and ("r%d" % rightlineno) or ''
604 llno = '%d' % leftlineno if leftlineno else ''
598 llno = '%d' % leftlineno if leftlineno else ''
605 rlno = '%d' % rightlineno if rightlineno else ''
599 rlno = '%d' % rightlineno if rightlineno else ''
606 return {
600 return {
607 'type': type,
601 'type': type,
608 'lineid': lineid,
602 'lineid': lineid,
609 'leftlineno': leftlineno,
603 'leftlineno': leftlineno,
610 'leftlinenumber': "% 6s" % llno,
604 'leftlinenumber': "% 6s" % llno,
611 'leftline': leftline or '',
605 'leftline': leftline or '',
612 'rightlineno': rightlineno,
606 'rightlineno': rightlineno,
613 'rightlinenumber': "% 6s" % rlno,
607 'rightlinenumber': "% 6s" % rlno,
614 'rightline': rightline or '',
608 'rightline': rightline or '',
615 }
609 }
616
610
617 def _getcompblockgen(context, leftlines, rightlines, opcodes):
611 def _getcompblockgen(context, leftlines, rightlines, opcodes):
618 for type, llo, lhi, rlo, rhi in opcodes:
612 for type, llo, lhi, rlo, rhi in opcodes:
619 type = pycompat.sysbytes(type)
613 type = pycompat.sysbytes(type)
620 len1 = lhi - llo
614 len1 = lhi - llo
621 len2 = rhi - rlo
615 len2 = rhi - rlo
622 count = min(len1, len2)
616 count = min(len1, len2)
623 for i in pycompat.xrange(count):
617 for i in pycompat.xrange(count):
624 yield _compline(type=type,
618 yield _compline(type=type,
625 leftlineno=llo + i + 1,
619 leftlineno=llo + i + 1,
626 leftline=leftlines[llo + i],
620 leftline=leftlines[llo + i],
627 rightlineno=rlo + i + 1,
621 rightlineno=rlo + i + 1,
628 rightline=rightlines[rlo + i])
622 rightline=rightlines[rlo + i])
629 if len1 > len2:
623 if len1 > len2:
630 for i in pycompat.xrange(llo + count, lhi):
624 for i in pycompat.xrange(llo + count, lhi):
631 yield _compline(type=type,
625 yield _compline(type=type,
632 leftlineno=i + 1,
626 leftlineno=i + 1,
633 leftline=leftlines[i],
627 leftline=leftlines[i],
634 rightlineno=None,
628 rightlineno=None,
635 rightline=None)
629 rightline=None)
636 elif len2 > len1:
630 elif len2 > len1:
637 for i in pycompat.xrange(rlo + count, rhi):
631 for i in pycompat.xrange(rlo + count, rhi):
638 yield _compline(type=type,
632 yield _compline(type=type,
639 leftlineno=None,
633 leftlineno=None,
640 leftline=None,
634 leftline=None,
641 rightlineno=i + 1,
635 rightlineno=i + 1,
642 rightline=rightlines[i])
636 rightline=rightlines[i])
643
637
644 def _getcompblock(leftlines, rightlines, opcodes):
638 def _getcompblock(leftlines, rightlines, opcodes):
645 args = (leftlines, rightlines, opcodes)
639 args = (leftlines, rightlines, opcodes)
646 return templateutil.mappinggenerator(_getcompblockgen, args=args,
640 return templateutil.mappinggenerator(_getcompblockgen, args=args,
647 name='comparisonline')
641 name='comparisonline')
648
642
649 def _comparegen(context, contextnum, leftlines, rightlines):
643 def _comparegen(context, contextnum, leftlines, rightlines):
650 '''Generator function that provides side-by-side comparison data.'''
644 '''Generator function that provides side-by-side comparison data.'''
651 s = difflib.SequenceMatcher(None, leftlines, rightlines)
645 s = difflib.SequenceMatcher(None, leftlines, rightlines)
652 if contextnum < 0:
646 if contextnum < 0:
653 l = _getcompblock(leftlines, rightlines, s.get_opcodes())
647 l = _getcompblock(leftlines, rightlines, s.get_opcodes())
654 yield {'lines': l}
648 yield {'lines': l}
655 else:
649 else:
656 for oc in s.get_grouped_opcodes(n=contextnum):
650 for oc in s.get_grouped_opcodes(n=contextnum):
657 l = _getcompblock(leftlines, rightlines, oc)
651 l = _getcompblock(leftlines, rightlines, oc)
658 yield {'lines': l}
652 yield {'lines': l}
659
653
660 def compare(contextnum, leftlines, rightlines):
654 def compare(contextnum, leftlines, rightlines):
661 args = (contextnum, leftlines, rightlines)
655 args = (contextnum, leftlines, rightlines)
662 return templateutil.mappinggenerator(_comparegen, args=args,
656 return templateutil.mappinggenerator(_comparegen, args=args,
663 name='comparisonblock')
657 name='comparisonblock')
664
658
665 def diffstatgen(ui, ctx, basectx):
659 def diffstatgen(ui, ctx, basectx):
666 '''Generator function that provides the diffstat data.'''
660 '''Generator function that provides the diffstat data.'''
667
661
668 diffopts = patch.diffopts(ui, {'noprefix': False})
662 diffopts = patch.diffopts(ui, {'noprefix': False})
669 stats = patch.diffstatdata(
663 stats = patch.diffstatdata(
670 util.iterlines(ctx.diff(basectx, opts=diffopts)))
664 util.iterlines(ctx.diff(basectx, opts=diffopts)))
671 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
665 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
672 while True:
666 while True:
673 yield stats, maxname, maxtotal, addtotal, removetotal, binary
667 yield stats, maxname, maxtotal, addtotal, removetotal, binary
674
668
675 def diffsummary(statgen):
669 def diffsummary(statgen):
676 '''Return a short summary of the diff.'''
670 '''Return a short summary of the diff.'''
677
671
678 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
672 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
679 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
673 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
680 len(stats), addtotal, removetotal)
674 len(stats), addtotal, removetotal)
681
675
682 def _diffstattmplgen(context, ctx, statgen, parity):
676 def _diffstattmplgen(context, ctx, statgen, parity):
683 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
677 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
684 files = ctx.files()
678 files = ctx.files()
685
679
686 def pct(i):
680 def pct(i):
687 if maxtotal == 0:
681 if maxtotal == 0:
688 return 0
682 return 0
689 return (float(i) / maxtotal) * 100
683 return (float(i) / maxtotal) * 100
690
684
691 fileno = 0
685 fileno = 0
692 for filename, adds, removes, isbinary in stats:
686 for filename, adds, removes, isbinary in stats:
693 template = 'diffstatlink' if filename in files else 'diffstatnolink'
687 template = 'diffstatlink' if filename in files else 'diffstatnolink'
694 total = adds + removes
688 total = adds + removes
695 fileno += 1
689 fileno += 1
696 yield context.process(template, {
690 yield context.process(template, {
697 'node': ctx.hex(),
691 'node': ctx.hex(),
698 'file': filename,
692 'file': filename,
699 'fileno': fileno,
693 'fileno': fileno,
700 'total': total,
694 'total': total,
701 'addpct': pct(adds),
695 'addpct': pct(adds),
702 'removepct': pct(removes),
696 'removepct': pct(removes),
703 'parity': next(parity),
697 'parity': next(parity),
704 })
698 })
705
699
706 def diffstat(ctx, statgen, parity):
700 def diffstat(ctx, statgen, parity):
707 '''Return a diffstat template for each file in the diff.'''
701 '''Return a diffstat template for each file in the diff.'''
708 args = (ctx, statgen, parity)
702 args = (ctx, statgen, parity)
709 return templateutil.mappedgenerator(_diffstattmplgen, args=args)
703 return templateutil.mappedgenerator(_diffstattmplgen, args=args)
710
704
711 class sessionvars(templateutil.wrapped):
705 class sessionvars(templateutil.wrapped):
712 def __init__(self, vars, start='?'):
706 def __init__(self, vars, start='?'):
713 self._start = start
707 self._start = start
714 self._vars = vars
708 self._vars = vars
715
709
716 def __getitem__(self, key):
710 def __getitem__(self, key):
717 return self._vars[key]
711 return self._vars[key]
718
712
719 def __setitem__(self, key, value):
713 def __setitem__(self, key, value):
720 self._vars[key] = value
714 self._vars[key] = value
721
715
722 def __copy__(self):
716 def __copy__(self):
723 return sessionvars(copy.copy(self._vars), self._start)
717 return sessionvars(copy.copy(self._vars), self._start)
724
718
725 def contains(self, context, mapping, item):
719 def contains(self, context, mapping, item):
726 item = templateutil.unwrapvalue(context, mapping, item)
720 item = templateutil.unwrapvalue(context, mapping, item)
727 return item in self._vars
721 return item in self._vars
728
722
729 def getmember(self, context, mapping, key):
723 def getmember(self, context, mapping, key):
730 key = templateutil.unwrapvalue(context, mapping, key)
724 key = templateutil.unwrapvalue(context, mapping, key)
731 return self._vars.get(key)
725 return self._vars.get(key)
732
726
733 def getmin(self, context, mapping):
727 def getmin(self, context, mapping):
734 raise error.ParseError(_('not comparable'))
728 raise error.ParseError(_('not comparable'))
735
729
736 def getmax(self, context, mapping):
730 def getmax(self, context, mapping):
737 raise error.ParseError(_('not comparable'))
731 raise error.ParseError(_('not comparable'))
738
732
739 def filter(self, context, mapping, select):
733 def filter(self, context, mapping, select):
740 # implement if necessary
734 # implement if necessary
741 raise error.ParseError(_('not filterable'))
735 raise error.ParseError(_('not filterable'))
742
736
743 def itermaps(self, context):
737 def itermaps(self, context):
744 separator = self._start
738 separator = self._start
745 for key, value in sorted(self._vars.iteritems()):
739 for key, value in sorted(self._vars.iteritems()):
746 yield {'name': key,
740 yield {'name': key,
747 'value': pycompat.bytestr(value),
741 'value': pycompat.bytestr(value),
748 'separator': separator,
742 'separator': separator,
749 }
743 }
750 separator = '&'
744 separator = '&'
751
745
752 def join(self, context, mapping, sep):
746 def join(self, context, mapping, sep):
753 # could be '{separator}{name}={value|urlescape}'
747 # could be '{separator}{name}={value|urlescape}'
754 raise error.ParseError(_('not displayable without template'))
748 raise error.ParseError(_('not displayable without template'))
755
749
756 def show(self, context, mapping):
750 def show(self, context, mapping):
757 return self.join(context, '')
751 return self.join(context, '')
758
752
759 def tobool(self, context, mapping):
753 def tobool(self, context, mapping):
760 return bool(self._vars)
754 return bool(self._vars)
761
755
762 def tovalue(self, context, mapping):
756 def tovalue(self, context, mapping):
763 return self._vars
757 return self._vars
764
758
765 class wsgiui(uimod.ui):
759 class wsgiui(uimod.ui):
766 # default termwidth breaks under mod_wsgi
760 # default termwidth breaks under mod_wsgi
767 def termwidth(self):
761 def termwidth(self):
768 return 80
762 return 80
769
763
770 def getwebsubs(repo):
764 def getwebsubs(repo):
771 websubtable = []
765 websubtable = []
772 websubdefs = repo.ui.configitems('websub')
766 websubdefs = repo.ui.configitems('websub')
773 # we must maintain interhg backwards compatibility
767 # we must maintain interhg backwards compatibility
774 websubdefs += repo.ui.configitems('interhg')
768 websubdefs += repo.ui.configitems('interhg')
775 for key, pattern in websubdefs:
769 for key, pattern in websubdefs:
776 # grab the delimiter from the character after the "s"
770 # grab the delimiter from the character after the "s"
777 unesc = pattern[1:2]
771 unesc = pattern[1:2]
778 delim = stringutil.reescape(unesc)
772 delim = stringutil.reescape(unesc)
779
773
780 # identify portions of the pattern, taking care to avoid escaped
774 # identify portions of the pattern, taking care to avoid escaped
781 # delimiters. the replace format and flags are optional, but
775 # delimiters. the replace format and flags are optional, but
782 # delimiters are required.
776 # delimiters are required.
783 match = re.match(
777 match = re.match(
784 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
778 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
785 % (delim, delim, delim), pattern)
779 % (delim, delim, delim), pattern)
786 if not match:
780 if not match:
787 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
781 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
788 % (key, pattern))
782 % (key, pattern))
789 continue
783 continue
790
784
791 # we need to unescape the delimiter for regexp and format
785 # we need to unescape the delimiter for regexp and format
792 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
786 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
793 regexp = delim_re.sub(unesc, match.group(1))
787 regexp = delim_re.sub(unesc, match.group(1))
794 format = delim_re.sub(unesc, match.group(2))
788 format = delim_re.sub(unesc, match.group(2))
795
789
796 # the pattern allows for 6 regexp flags, so set them if necessary
790 # the pattern allows for 6 regexp flags, so set them if necessary
797 flagin = match.group(3)
791 flagin = match.group(3)
798 flags = 0
792 flags = 0
799 if flagin:
793 if flagin:
800 for flag in flagin.upper():
794 for flag in flagin.upper():
801 flags |= re.__dict__[flag]
795 flags |= re.__dict__[flag]
802
796
803 try:
797 try:
804 regexp = re.compile(regexp, flags)
798 regexp = re.compile(regexp, flags)
805 websubtable.append((regexp, format))
799 websubtable.append((regexp, format))
806 except re.error:
800 except re.error:
807 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
801 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
808 % (key, regexp))
802 % (key, regexp))
809 return websubtable
803 return websubtable
810
804
811 def getgraphnode(repo, ctx):
805 def getgraphnode(repo, ctx):
812 return (templatekw.getgraphnodecurrent(repo, ctx) +
806 return (templatekw.getgraphnodecurrent(repo, ctx) +
813 templatekw.getgraphnodesymbol(ctx))
807 templatekw.getgraphnodesymbol(ctx))
@@ -1,510 +1,503
1 # registrar.py - utilities to register function for specific purpose
1 # registrar.py - utilities to register function for specific purpose
2 #
2 #
3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from . import (
10 from . import (
11 configitems,
11 configitems,
12 error,
12 error,
13 pycompat,
13 pycompat,
14 util,
14 util,
15 )
15 )
16
16
17 # unlike the other registered items, config options are neither functions or
17 # unlike the other registered items, config options are neither functions or
18 # classes. Registering the option is just small function call.
18 # classes. Registering the option is just small function call.
19 #
19 #
20 # We still add the official API to the registrar module for consistency with
20 # We still add the official API to the registrar module for consistency with
21 # the other items extensions want might to register.
21 # the other items extensions want might to register.
22 configitem = configitems.getitemregister
22 configitem = configitems.getitemregister
23
23
24 class _funcregistrarbase(object):
24 class _funcregistrarbase(object):
25 """Base of decorator to register a function for specific purpose
25 """Base of decorator to register a function for specific purpose
26
26
27 This decorator stores decorated functions into own dict 'table'.
27 This decorator stores decorated functions into own dict 'table'.
28
28
29 The least derived class can be defined by overriding 'formatdoc',
29 The least derived class can be defined by overriding 'formatdoc',
30 for example::
30 for example::
31
31
32 class keyword(_funcregistrarbase):
32 class keyword(_funcregistrarbase):
33 _docformat = ":%s: %s"
33 _docformat = ":%s: %s"
34
34
35 This should be used as below:
35 This should be used as below:
36
36
37 keyword = registrar.keyword()
37 keyword = registrar.keyword()
38
38
39 @keyword('bar')
39 @keyword('bar')
40 def barfunc(*args, **kwargs):
40 def barfunc(*args, **kwargs):
41 '''Explanation of bar keyword ....
41 '''Explanation of bar keyword ....
42 '''
42 '''
43 pass
43 pass
44
44
45 In this case:
45 In this case:
46
46
47 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
47 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
48 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
48 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
49 """
49 """
50 def __init__(self, table=None):
50 def __init__(self, table=None):
51 if table is None:
51 if table is None:
52 self._table = {}
52 self._table = {}
53 else:
53 else:
54 self._table = table
54 self._table = table
55
55
56 def __call__(self, decl, *args, **kwargs):
56 def __call__(self, decl, *args, **kwargs):
57 return lambda func: self._doregister(func, decl, *args, **kwargs)
57 return lambda func: self._doregister(func, decl, *args, **kwargs)
58
58
59 def _doregister(self, func, decl, *args, **kwargs):
59 def _doregister(self, func, decl, *args, **kwargs):
60 name = self._getname(decl)
60 name = self._getname(decl)
61
61
62 if name in self._table:
62 if name in self._table:
63 msg = 'duplicate registration for name: "%s"' % name
63 msg = 'duplicate registration for name: "%s"' % name
64 raise error.ProgrammingError(msg)
64 raise error.ProgrammingError(msg)
65
65
66 if func.__doc__ and not util.safehasattr(func, '_origdoc'):
66 if func.__doc__ and not util.safehasattr(func, '_origdoc'):
67 doc = pycompat.sysbytes(func.__doc__).strip()
67 doc = pycompat.sysbytes(func.__doc__).strip()
68 func._origdoc = doc
68 func._origdoc = doc
69 func.__doc__ = pycompat.sysstr(self._formatdoc(decl, doc))
69 func.__doc__ = pycompat.sysstr(self._formatdoc(decl, doc))
70
70
71 self._table[name] = func
71 self._table[name] = func
72 self._extrasetup(name, func, *args, **kwargs)
72 self._extrasetup(name, func, *args, **kwargs)
73
73
74 return func
74 return func
75
75
76 def _merge(self, registrarbase):
76 def _merge(self, registrarbase):
77 """Merge the entries of the given registrar object into this one.
77 """Merge the entries of the given registrar object into this one.
78
78
79 The other registrar object must not contain any entries already in the
79 The other registrar object must not contain any entries already in the
80 current one, or a ProgrammmingError is raised. Additionally, the types
80 current one, or a ProgrammmingError is raised. Additionally, the types
81 of the two registrars must match.
81 of the two registrars must match.
82 """
82 """
83 if not isinstance(registrarbase, type(self)):
83 if not isinstance(registrarbase, type(self)):
84 msg = "cannot merge different types of registrar"
84 msg = "cannot merge different types of registrar"
85 raise error.ProgrammingError(msg)
85 raise error.ProgrammingError(msg)
86
86
87 dups = set(registrarbase._table).intersection(self._table)
87 dups = set(registrarbase._table).intersection(self._table)
88
88
89 if dups:
89 if dups:
90 msg = 'duplicate registration for names: "%s"' % '", "'.join(dups)
90 msg = 'duplicate registration for names: "%s"' % '", "'.join(dups)
91 raise error.ProgrammingError(msg)
91 raise error.ProgrammingError(msg)
92
92
93 self._table.update(registrarbase._table)
93 self._table.update(registrarbase._table)
94
94
95 def _parsefuncdecl(self, decl):
95 def _parsefuncdecl(self, decl):
96 """Parse function declaration and return the name of function in it
96 """Parse function declaration and return the name of function in it
97 """
97 """
98 i = decl.find('(')
98 i = decl.find('(')
99 if i >= 0:
99 if i >= 0:
100 return decl[:i]
100 return decl[:i]
101 else:
101 else:
102 return decl
102 return decl
103
103
104 def _getname(self, decl):
104 def _getname(self, decl):
105 """Return the name of the registered function from decl
105 """Return the name of the registered function from decl
106
106
107 Derived class should override this, if it allows more
107 Derived class should override this, if it allows more
108 descriptive 'decl' string than just a name.
108 descriptive 'decl' string than just a name.
109 """
109 """
110 return decl
110 return decl
111
111
112 _docformat = None
112 _docformat = None
113
113
114 def _formatdoc(self, decl, doc):
114 def _formatdoc(self, decl, doc):
115 """Return formatted document of the registered function for help
115 """Return formatted document of the registered function for help
116
116
117 'doc' is '__doc__.strip()' of the registered function.
117 'doc' is '__doc__.strip()' of the registered function.
118 """
118 """
119 return self._docformat % (decl, doc)
119 return self._docformat % (decl, doc)
120
120
121 def _extrasetup(self, name, func):
121 def _extrasetup(self, name, func):
122 """Execute exra setup for registered function, if needed
122 """Execute exra setup for registered function, if needed
123 """
123 """
124
124
125 class command(_funcregistrarbase):
125 class command(_funcregistrarbase):
126 """Decorator to register a command function to table
126 """Decorator to register a command function to table
127
127
128 This class receives a command table as its argument. The table should
128 This class receives a command table as its argument. The table should
129 be a dict.
129 be a dict.
130
130
131 The created object can be used as a decorator for adding commands to
131 The created object can be used as a decorator for adding commands to
132 that command table. This accepts multiple arguments to define a command.
132 that command table. This accepts multiple arguments to define a command.
133
133
134 The first argument is the command name (as bytes).
134 The first argument is the command name (as bytes).
135
135
136 The `options` keyword argument is an iterable of tuples defining command
136 The `options` keyword argument is an iterable of tuples defining command
137 arguments. See ``mercurial.fancyopts.fancyopts()`` for the format of each
137 arguments. See ``mercurial.fancyopts.fancyopts()`` for the format of each
138 tuple.
138 tuple.
139
139
140 The `synopsis` argument defines a short, one line summary of how to use the
140 The `synopsis` argument defines a short, one line summary of how to use the
141 command. This shows up in the help output.
141 command. This shows up in the help output.
142
142
143 There are three arguments that control what repository (if any) is found
143 There are three arguments that control what repository (if any) is found
144 and passed to the decorated function: `norepo`, `optionalrepo`, and
144 and passed to the decorated function: `norepo`, `optionalrepo`, and
145 `inferrepo`.
145 `inferrepo`.
146
146
147 The `norepo` argument defines whether the command does not require a
147 The `norepo` argument defines whether the command does not require a
148 local repository. Most commands operate against a repository, thus the
148 local repository. Most commands operate against a repository, thus the
149 default is False. When True, no repository will be passed.
149 default is False. When True, no repository will be passed.
150
150
151 The `optionalrepo` argument defines whether the command optionally requires
151 The `optionalrepo` argument defines whether the command optionally requires
152 a local repository. If no repository can be found, None will be passed
152 a local repository. If no repository can be found, None will be passed
153 to the decorated function.
153 to the decorated function.
154
154
155 The `inferrepo` argument defines whether to try to find a repository from
155 The `inferrepo` argument defines whether to try to find a repository from
156 the command line arguments. If True, arguments will be examined for
156 the command line arguments. If True, arguments will be examined for
157 potential repository locations. See ``findrepo()``. If a repository is
157 potential repository locations. See ``findrepo()``. If a repository is
158 found, it will be used and passed to the decorated function.
158 found, it will be used and passed to the decorated function.
159
159
160 The `intents` argument defines a set of intended actions or capabilities
160 The `intents` argument defines a set of intended actions or capabilities
161 the command is taking. These intents can be used to affect the construction
161 the command is taking. These intents can be used to affect the construction
162 of the repository object passed to the command. For example, commands
162 of the repository object passed to the command. For example, commands
163 declaring that they are read-only could receive a repository that doesn't
163 declaring that they are read-only could receive a repository that doesn't
164 have any methods allowing repository mutation. Other intents could be used
164 have any methods allowing repository mutation. Other intents could be used
165 to prevent the command from running if the requested intent could not be
165 to prevent the command from running if the requested intent could not be
166 fulfilled.
166 fulfilled.
167
167
168 If `helpcategory` is set (usually to one of the constants in the help
168 If `helpcategory` is set (usually to one of the constants in the help
169 module), the command will be displayed under that category in the help's
169 module), the command will be displayed under that category in the help's
170 list of commands.
170 list of commands.
171
171
172 The following intents are defined:
172 The following intents are defined:
173
173
174 readonly
174 readonly
175 The command is read-only
175 The command is read-only
176
176
177 The signature of the decorated function looks like this:
177 The signature of the decorated function looks like this:
178 def cmd(ui[, repo] [, <args>] [, <options>])
178 def cmd(ui[, repo] [, <args>] [, <options>])
179
179
180 `repo` is required if `norepo` is False.
180 `repo` is required if `norepo` is False.
181 `<args>` are positional args (or `*args`) arguments, of non-option
181 `<args>` are positional args (or `*args`) arguments, of non-option
182 arguments from the command line.
182 arguments from the command line.
183 `<options>` are keyword arguments (or `**options`) of option arguments
183 `<options>` are keyword arguments (or `**options`) of option arguments
184 from the command line.
184 from the command line.
185
185
186 See the WritingExtensions and MercurialApi documentation for more exhaustive
186 See the WritingExtensions and MercurialApi documentation for more exhaustive
187 descriptions and examples.
187 descriptions and examples.
188 """
188 """
189
189
190 # Command categories for grouping them in help output.
190 # Command categories for grouping them in help output.
191 # These can also be specified for aliases, like:
191 # These can also be specified for aliases, like:
192 # [alias]
192 # [alias]
193 # myalias = something
193 # myalias = something
194 # myalias:category = repo
194 # myalias:category = repo
195 CATEGORY_REPO_CREATION = 'repo'
195 CATEGORY_REPO_CREATION = 'repo'
196 CATEGORY_REMOTE_REPO_MANAGEMENT = 'remote'
196 CATEGORY_REMOTE_REPO_MANAGEMENT = 'remote'
197 CATEGORY_COMMITTING = 'commit'
197 CATEGORY_COMMITTING = 'commit'
198 CATEGORY_CHANGE_MANAGEMENT = 'management'
198 CATEGORY_CHANGE_MANAGEMENT = 'management'
199 CATEGORY_CHANGE_ORGANIZATION = 'organization'
199 CATEGORY_CHANGE_ORGANIZATION = 'organization'
200 CATEGORY_FILE_CONTENTS = 'files'
200 CATEGORY_FILE_CONTENTS = 'files'
201 CATEGORY_CHANGE_NAVIGATION = 'navigation'
201 CATEGORY_CHANGE_NAVIGATION = 'navigation'
202 CATEGORY_WORKING_DIRECTORY = 'wdir'
202 CATEGORY_WORKING_DIRECTORY = 'wdir'
203 CATEGORY_IMPORT_EXPORT = 'import'
203 CATEGORY_IMPORT_EXPORT = 'import'
204 CATEGORY_MAINTENANCE = 'maintenance'
204 CATEGORY_MAINTENANCE = 'maintenance'
205 CATEGORY_HELP = 'help'
205 CATEGORY_HELP = 'help'
206 CATEGORY_MISC = 'misc'
206 CATEGORY_MISC = 'misc'
207 CATEGORY_NONE = 'none'
207 CATEGORY_NONE = 'none'
208
208
209 def _doregister(self, func, name, options=(), synopsis=None,
209 def _doregister(self, func, name, options=(), synopsis=None,
210 norepo=False, optionalrepo=False, inferrepo=False,
210 norepo=False, optionalrepo=False, inferrepo=False,
211 intents=None, helpcategory=None, helpbasic=False):
211 intents=None, helpcategory=None, helpbasic=False):
212 func.norepo = norepo
212 func.norepo = norepo
213 func.optionalrepo = optionalrepo
213 func.optionalrepo = optionalrepo
214 func.inferrepo = inferrepo
214 func.inferrepo = inferrepo
215 func.intents = intents or set()
215 func.intents = intents or set()
216 func.helpcategory = helpcategory
216 func.helpcategory = helpcategory
217 func.helpbasic = helpbasic
217 func.helpbasic = helpbasic
218 if synopsis:
218 if synopsis:
219 self._table[name] = func, list(options), synopsis
219 self._table[name] = func, list(options), synopsis
220 else:
220 else:
221 self._table[name] = func, list(options)
221 self._table[name] = func, list(options)
222 return func
222 return func
223
223
224 INTENT_READONLY = b'readonly'
224 INTENT_READONLY = b'readonly'
225
225
226 class revsetpredicate(_funcregistrarbase):
226 class revsetpredicate(_funcregistrarbase):
227 """Decorator to register revset predicate
227 """Decorator to register revset predicate
228
228
229 Usage::
229 Usage::
230
230
231 revsetpredicate = registrar.revsetpredicate()
231 revsetpredicate = registrar.revsetpredicate()
232
232
233 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
233 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
234 def mypredicatefunc(repo, subset, x):
234 def mypredicatefunc(repo, subset, x):
235 '''Explanation of this revset predicate ....
235 '''Explanation of this revset predicate ....
236 '''
236 '''
237 pass
237 pass
238
238
239 The first string argument is used also in online help.
239 The first string argument is used also in online help.
240
240
241 Optional argument 'safe' indicates whether a predicate is safe for
241 Optional argument 'safe' indicates whether a predicate is safe for
242 DoS attack (False by default).
242 DoS attack (False by default).
243
243
244 Optional argument 'takeorder' indicates whether a predicate function
244 Optional argument 'takeorder' indicates whether a predicate function
245 takes ordering policy as the last argument.
245 takes ordering policy as the last argument.
246
246
247 Optional argument 'weight' indicates the estimated run-time cost, useful
247 Optional argument 'weight' indicates the estimated run-time cost, useful
248 for static optimization, default is 1. Higher weight means more expensive.
248 for static optimization, default is 1. Higher weight means more expensive.
249 Usually, revsets that are fast and return only one revision has a weight of
249 Usually, revsets that are fast and return only one revision has a weight of
250 0.5 (ex. a symbol); revsets with O(changelog) complexity and read only the
250 0.5 (ex. a symbol); revsets with O(changelog) complexity and read only the
251 changelog have weight 10 (ex. author); revsets reading manifest deltas have
251 changelog have weight 10 (ex. author); revsets reading manifest deltas have
252 weight 30 (ex. adds); revset reading manifest contents have weight 100
252 weight 30 (ex. adds); revset reading manifest contents have weight 100
253 (ex. contains). Note: those values are flexible. If the revset has a
253 (ex. contains). Note: those values are flexible. If the revset has a
254 same big-O time complexity as 'contains', but with a smaller constant, it
254 same big-O time complexity as 'contains', but with a smaller constant, it
255 might have a weight of 90.
255 might have a weight of 90.
256
256
257 'revsetpredicate' instance in example above can be used to
257 'revsetpredicate' instance in example above can be used to
258 decorate multiple functions.
258 decorate multiple functions.
259
259
260 Decorated functions are registered automatically at loading
260 Decorated functions are registered automatically at loading
261 extension, if an instance named as 'revsetpredicate' is used for
261 extension, if an instance named as 'revsetpredicate' is used for
262 decorating in extension.
262 decorating in extension.
263
263
264 Otherwise, explicit 'revset.loadpredicate()' is needed.
264 Otherwise, explicit 'revset.loadpredicate()' is needed.
265 """
265 """
266 _getname = _funcregistrarbase._parsefuncdecl
266 _getname = _funcregistrarbase._parsefuncdecl
267 _docformat = "``%s``\n %s"
267 _docformat = "``%s``\n %s"
268
268
269 def _extrasetup(self, name, func, safe=False, takeorder=False, weight=1):
269 def _extrasetup(self, name, func, safe=False, takeorder=False, weight=1):
270 func._safe = safe
270 func._safe = safe
271 func._takeorder = takeorder
271 func._takeorder = takeorder
272 func._weight = weight
272 func._weight = weight
273
273
274 class filesetpredicate(_funcregistrarbase):
274 class filesetpredicate(_funcregistrarbase):
275 """Decorator to register fileset predicate
275 """Decorator to register fileset predicate
276
276
277 Usage::
277 Usage::
278
278
279 filesetpredicate = registrar.filesetpredicate()
279 filesetpredicate = registrar.filesetpredicate()
280
280
281 @filesetpredicate('mypredicate()')
281 @filesetpredicate('mypredicate()')
282 def mypredicatefunc(mctx, x):
282 def mypredicatefunc(mctx, x):
283 '''Explanation of this fileset predicate ....
283 '''Explanation of this fileset predicate ....
284 '''
284 '''
285 pass
285 pass
286
286
287 The first string argument is used also in online help.
287 The first string argument is used also in online help.
288
288
289 Optional argument 'callstatus' indicates whether a predicate
289 Optional argument 'callstatus' indicates whether a predicate
290 implies 'matchctx.status()' at runtime or not (False, by
290 implies 'matchctx.status()' at runtime or not (False, by
291 default).
291 default).
292
292
293 Optional argument 'weight' indicates the estimated run-time cost, useful
293 Optional argument 'weight' indicates the estimated run-time cost, useful
294 for static optimization, default is 1. Higher weight means more expensive.
294 for static optimization, default is 1. Higher weight means more expensive.
295 There are predefined weights in the 'filesetlang' module.
295 There are predefined weights in the 'filesetlang' module.
296
296
297 ====== =============================================================
297 ====== =============================================================
298 Weight Description and examples
298 Weight Description and examples
299 ====== =============================================================
299 ====== =============================================================
300 0.5 basic match patterns (e.g. a symbol)
300 0.5 basic match patterns (e.g. a symbol)
301 10 computing status (e.g. added()) or accessing a few files
301 10 computing status (e.g. added()) or accessing a few files
302 30 reading file content for each (e.g. grep())
302 30 reading file content for each (e.g. grep())
303 50 scanning working directory (ignored())
303 50 scanning working directory (ignored())
304 ====== =============================================================
304 ====== =============================================================
305
305
306 'filesetpredicate' instance in example above can be used to
306 'filesetpredicate' instance in example above can be used to
307 decorate multiple functions.
307 decorate multiple functions.
308
308
309 Decorated functions are registered automatically at loading
309 Decorated functions are registered automatically at loading
310 extension, if an instance named as 'filesetpredicate' is used for
310 extension, if an instance named as 'filesetpredicate' is used for
311 decorating in extension.
311 decorating in extension.
312
312
313 Otherwise, explicit 'fileset.loadpredicate()' is needed.
313 Otherwise, explicit 'fileset.loadpredicate()' is needed.
314 """
314 """
315 _getname = _funcregistrarbase._parsefuncdecl
315 _getname = _funcregistrarbase._parsefuncdecl
316 _docformat = "``%s``\n %s"
316 _docformat = "``%s``\n %s"
317
317
318 def _extrasetup(self, name, func, callstatus=False, weight=1):
318 def _extrasetup(self, name, func, callstatus=False, weight=1):
319 func._callstatus = callstatus
319 func._callstatus = callstatus
320 func._weight = weight
320 func._weight = weight
321
321
322 class _templateregistrarbase(_funcregistrarbase):
322 class _templateregistrarbase(_funcregistrarbase):
323 """Base of decorator to register functions as template specific one
323 """Base of decorator to register functions as template specific one
324 """
324 """
325 _docformat = ":%s: %s"
325 _docformat = ":%s: %s"
326
326
327 class templatekeyword(_templateregistrarbase):
327 class templatekeyword(_templateregistrarbase):
328 """Decorator to register template keyword
328 """Decorator to register template keyword
329
329
330 Usage::
330 Usage::
331
331
332 templatekeyword = registrar.templatekeyword()
332 templatekeyword = registrar.templatekeyword()
333
333
334 # new API (since Mercurial 4.6)
334 # new API (since Mercurial 4.6)
335 @templatekeyword('mykeyword', requires={'repo', 'ctx'})
335 @templatekeyword('mykeyword', requires={'repo', 'ctx'})
336 def mykeywordfunc(context, mapping):
336 def mykeywordfunc(context, mapping):
337 '''Explanation of this template keyword ....
337 '''Explanation of this template keyword ....
338 '''
338 '''
339 pass
339 pass
340
340
341 # old API (DEPRECATED)
342 @templatekeyword('mykeyword')
343 def mykeywordfunc(repo, ctx, templ, cache, revcache, **args):
344 '''Explanation of this template keyword ....
345 '''
346 pass
347
348 The first string argument is used also in online help.
341 The first string argument is used also in online help.
349
342
350 Optional argument 'requires' should be a collection of resource names
343 Optional argument 'requires' should be a collection of resource names
351 which the template keyword depends on. This also serves as a flag to
344 which the template keyword depends on. This also serves as a flag to
352 switch to the new API. If 'requires' is unspecified, all template
345 switch to the new API. If 'requires' is unspecified, all template
353 keywords and resources are expanded to the function arguments.
346 keywords and resources are expanded to the function arguments.
354
347
355 'templatekeyword' instance in example above can be used to
348 'templatekeyword' instance in example above can be used to
356 decorate multiple functions.
349 decorate multiple functions.
357
350
358 Decorated functions are registered automatically at loading
351 Decorated functions are registered automatically at loading
359 extension, if an instance named as 'templatekeyword' is used for
352 extension, if an instance named as 'templatekeyword' is used for
360 decorating in extension.
353 decorating in extension.
361
354
362 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
355 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
363 """
356 """
364
357
365 def _extrasetup(self, name, func, requires=None):
358 def _extrasetup(self, name, func, requires=None):
366 func._requires = requires
359 func._requires = requires
367
360
368 class templatefilter(_templateregistrarbase):
361 class templatefilter(_templateregistrarbase):
369 """Decorator to register template filer
362 """Decorator to register template filer
370
363
371 Usage::
364 Usage::
372
365
373 templatefilter = registrar.templatefilter()
366 templatefilter = registrar.templatefilter()
374
367
375 @templatefilter('myfilter', intype=bytes)
368 @templatefilter('myfilter', intype=bytes)
376 def myfilterfunc(text):
369 def myfilterfunc(text):
377 '''Explanation of this template filter ....
370 '''Explanation of this template filter ....
378 '''
371 '''
379 pass
372 pass
380
373
381 The first string argument is used also in online help.
374 The first string argument is used also in online help.
382
375
383 Optional argument 'intype' defines the type of the input argument,
376 Optional argument 'intype' defines the type of the input argument,
384 which should be (bytes, int, templateutil.date, or None for any.)
377 which should be (bytes, int, templateutil.date, or None for any.)
385
378
386 'templatefilter' instance in example above can be used to
379 'templatefilter' instance in example above can be used to
387 decorate multiple functions.
380 decorate multiple functions.
388
381
389 Decorated functions are registered automatically at loading
382 Decorated functions are registered automatically at loading
390 extension, if an instance named as 'templatefilter' is used for
383 extension, if an instance named as 'templatefilter' is used for
391 decorating in extension.
384 decorating in extension.
392
385
393 Otherwise, explicit 'templatefilters.loadkeyword()' is needed.
386 Otherwise, explicit 'templatefilters.loadkeyword()' is needed.
394 """
387 """
395
388
396 def _extrasetup(self, name, func, intype=None):
389 def _extrasetup(self, name, func, intype=None):
397 func._intype = intype
390 func._intype = intype
398
391
399 class templatefunc(_templateregistrarbase):
392 class templatefunc(_templateregistrarbase):
400 """Decorator to register template function
393 """Decorator to register template function
401
394
402 Usage::
395 Usage::
403
396
404 templatefunc = registrar.templatefunc()
397 templatefunc = registrar.templatefunc()
405
398
406 @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3',
399 @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3',
407 requires={'ctx'})
400 requires={'ctx'})
408 def myfuncfunc(context, mapping, args):
401 def myfuncfunc(context, mapping, args):
409 '''Explanation of this template function ....
402 '''Explanation of this template function ....
410 '''
403 '''
411 pass
404 pass
412
405
413 The first string argument is used also in online help.
406 The first string argument is used also in online help.
414
407
415 If optional 'argspec' is defined, the function will receive 'args' as
408 If optional 'argspec' is defined, the function will receive 'args' as
416 a dict of named arguments. Otherwise 'args' is a list of positional
409 a dict of named arguments. Otherwise 'args' is a list of positional
417 arguments.
410 arguments.
418
411
419 Optional argument 'requires' should be a collection of resource names
412 Optional argument 'requires' should be a collection of resource names
420 which the template function depends on.
413 which the template function depends on.
421
414
422 'templatefunc' instance in example above can be used to
415 'templatefunc' instance in example above can be used to
423 decorate multiple functions.
416 decorate multiple functions.
424
417
425 Decorated functions are registered automatically at loading
418 Decorated functions are registered automatically at loading
426 extension, if an instance named as 'templatefunc' is used for
419 extension, if an instance named as 'templatefunc' is used for
427 decorating in extension.
420 decorating in extension.
428
421
429 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
422 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
430 """
423 """
431 _getname = _funcregistrarbase._parsefuncdecl
424 _getname = _funcregistrarbase._parsefuncdecl
432
425
433 def _extrasetup(self, name, func, argspec=None, requires=()):
426 def _extrasetup(self, name, func, argspec=None, requires=()):
434 func._argspec = argspec
427 func._argspec = argspec
435 func._requires = requires
428 func._requires = requires
436
429
437 class internalmerge(_funcregistrarbase):
430 class internalmerge(_funcregistrarbase):
438 """Decorator to register in-process merge tool
431 """Decorator to register in-process merge tool
439
432
440 Usage::
433 Usage::
441
434
442 internalmerge = registrar.internalmerge()
435 internalmerge = registrar.internalmerge()
443
436
444 @internalmerge('mymerge', internalmerge.mergeonly,
437 @internalmerge('mymerge', internalmerge.mergeonly,
445 onfailure=None, precheck=None,
438 onfailure=None, precheck=None,
446 binary=False, symlink=False):
439 binary=False, symlink=False):
447 def mymergefunc(repo, mynode, orig, fcd, fco, fca,
440 def mymergefunc(repo, mynode, orig, fcd, fco, fca,
448 toolconf, files, labels=None):
441 toolconf, files, labels=None):
449 '''Explanation of this internal merge tool ....
442 '''Explanation of this internal merge tool ....
450 '''
443 '''
451 return 1, False # means "conflicted", "no deletion needed"
444 return 1, False # means "conflicted", "no deletion needed"
452
445
453 The first string argument is used to compose actual merge tool name,
446 The first string argument is used to compose actual merge tool name,
454 ":name" and "internal:name" (the latter is historical one).
447 ":name" and "internal:name" (the latter is historical one).
455
448
456 The second argument is one of merge types below:
449 The second argument is one of merge types below:
457
450
458 ========== ======== ======== =========
451 ========== ======== ======== =========
459 merge type precheck premerge fullmerge
452 merge type precheck premerge fullmerge
460 ========== ======== ======== =========
453 ========== ======== ======== =========
461 nomerge x x x
454 nomerge x x x
462 mergeonly o x o
455 mergeonly o x o
463 fullmerge o o o
456 fullmerge o o o
464 ========== ======== ======== =========
457 ========== ======== ======== =========
465
458
466 Optional argument 'onfailure' is the format of warning message
459 Optional argument 'onfailure' is the format of warning message
467 to be used at failure of merging (target filename is specified
460 to be used at failure of merging (target filename is specified
468 at formatting). Or, None or so, if warning message should be
461 at formatting). Or, None or so, if warning message should be
469 suppressed.
462 suppressed.
470
463
471 Optional argument 'precheck' is the function to be used
464 Optional argument 'precheck' is the function to be used
472 before actual invocation of internal merge tool itself.
465 before actual invocation of internal merge tool itself.
473 It takes as same arguments as internal merge tool does, other than
466 It takes as same arguments as internal merge tool does, other than
474 'files' and 'labels'. If it returns false value, merging is aborted
467 'files' and 'labels'. If it returns false value, merging is aborted
475 immediately (and file is marked as "unresolved").
468 immediately (and file is marked as "unresolved").
476
469
477 Optional argument 'binary' is a binary files capability of internal
470 Optional argument 'binary' is a binary files capability of internal
478 merge tool. 'nomerge' merge type implies binary=True.
471 merge tool. 'nomerge' merge type implies binary=True.
479
472
480 Optional argument 'symlink' is a symlinks capability of inetrnal
473 Optional argument 'symlink' is a symlinks capability of inetrnal
481 merge function. 'nomerge' merge type implies symlink=True.
474 merge function. 'nomerge' merge type implies symlink=True.
482
475
483 'internalmerge' instance in example above can be used to
476 'internalmerge' instance in example above can be used to
484 decorate multiple functions.
477 decorate multiple functions.
485
478
486 Decorated functions are registered automatically at loading
479 Decorated functions are registered automatically at loading
487 extension, if an instance named as 'internalmerge' is used for
480 extension, if an instance named as 'internalmerge' is used for
488 decorating in extension.
481 decorating in extension.
489
482
490 Otherwise, explicit 'filemerge.loadinternalmerge()' is needed.
483 Otherwise, explicit 'filemerge.loadinternalmerge()' is needed.
491 """
484 """
492 _docformat = "``:%s``\n %s"
485 _docformat = "``:%s``\n %s"
493
486
494 # merge type definitions:
487 # merge type definitions:
495 nomerge = None
488 nomerge = None
496 mergeonly = 'mergeonly' # just the full merge, no premerge
489 mergeonly = 'mergeonly' # just the full merge, no premerge
497 fullmerge = 'fullmerge' # both premerge and merge
490 fullmerge = 'fullmerge' # both premerge and merge
498
491
499 def _extrasetup(self, name, func, mergetype,
492 def _extrasetup(self, name, func, mergetype,
500 onfailure=None, precheck=None,
493 onfailure=None, precheck=None,
501 binary=False, symlink=False):
494 binary=False, symlink=False):
502 func.mergetype = mergetype
495 func.mergetype = mergetype
503 func.onfailure = onfailure
496 func.onfailure = onfailure
504 func.precheck = precheck
497 func.precheck = precheck
505
498
506 binarycap = binary or mergetype == self.nomerge
499 binarycap = binary or mergetype == self.nomerge
507 symlinkcap = symlink or mergetype == self.nomerge
500 symlinkcap = symlink or mergetype == self.nomerge
508
501
509 # actual capabilities, which this internal merge tool has
502 # actual capabilities, which this internal merge tool has
510 func.capabilities = {"binary": binarycap, "symlink": symlinkcap}
503 func.capabilities = {"binary": binarycap, "symlink": symlinkcap}
@@ -1,999 +1,985
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() or unwrapastype() to obtain the inner object.
34 Use unwrapvalue() or unwrapastype() to obtain the inner object.
35 """
35 """
36
36
37 __metaclass__ = abc.ABCMeta
37 __metaclass__ = abc.ABCMeta
38
38
39 @abc.abstractmethod
39 @abc.abstractmethod
40 def contains(self, context, mapping, item):
40 def contains(self, context, mapping, item):
41 """Test if the specified item is in self
41 """Test if the specified item is in self
42
42
43 The item argument may be a wrapped object.
43 The item argument may be a wrapped object.
44 """
44 """
45
45
46 @abc.abstractmethod
46 @abc.abstractmethod
47 def getmember(self, context, mapping, key):
47 def getmember(self, context, mapping, key):
48 """Return a member item for the specified key
48 """Return a member item for the specified key
49
49
50 The key argument may be a wrapped object.
50 The key argument may be a wrapped object.
51 A returned object may be either a wrapped object or a pure value
51 A returned object may be either a wrapped object or a pure value
52 depending on the self type.
52 depending on the self type.
53 """
53 """
54
54
55 @abc.abstractmethod
55 @abc.abstractmethod
56 def getmin(self, context, mapping):
56 def getmin(self, context, mapping):
57 """Return the smallest item, which may be either a wrapped or a pure
57 """Return the smallest item, which may be either a wrapped or a pure
58 value depending on the self type"""
58 value depending on the self type"""
59
59
60 @abc.abstractmethod
60 @abc.abstractmethod
61 def getmax(self, context, mapping):
61 def getmax(self, context, mapping):
62 """Return the largest item, which may be either a wrapped or a pure
62 """Return the largest item, which may be either a wrapped or a pure
63 value depending on the self type"""
63 value depending on the self type"""
64
64
65 @abc.abstractmethod
65 @abc.abstractmethod
66 def filter(self, context, mapping, select):
66 def filter(self, context, mapping, select):
67 """Return new container of the same type which includes only the
67 """Return new container of the same type which includes only the
68 selected elements
68 selected elements
69
69
70 select() takes each item as a wrapped object and returns True/False.
70 select() takes each item as a wrapped object and returns True/False.
71 """
71 """
72
72
73 @abc.abstractmethod
73 @abc.abstractmethod
74 def itermaps(self, context):
74 def itermaps(self, context):
75 """Yield each template mapping"""
75 """Yield each template mapping"""
76
76
77 @abc.abstractmethod
77 @abc.abstractmethod
78 def join(self, context, mapping, sep):
78 def join(self, context, mapping, sep):
79 """Join items with the separator; Returns a bytes or (possibly nested)
79 """Join items with the separator; Returns a bytes or (possibly nested)
80 generator of bytes
80 generator of bytes
81
81
82 A pre-configured template may be rendered per item if this container
82 A pre-configured template may be rendered per item if this container
83 holds unprintable items.
83 holds unprintable items.
84 """
84 """
85
85
86 @abc.abstractmethod
86 @abc.abstractmethod
87 def show(self, context, mapping):
87 def show(self, context, mapping):
88 """Return a bytes or (possibly nested) generator of bytes representing
88 """Return a bytes or (possibly nested) generator of bytes representing
89 the underlying object
89 the underlying object
90
90
91 A pre-configured template may be rendered if the underlying object is
91 A pre-configured template may be rendered if the underlying object is
92 not printable.
92 not printable.
93 """
93 """
94
94
95 @abc.abstractmethod
95 @abc.abstractmethod
96 def tobool(self, context, mapping):
96 def tobool(self, context, mapping):
97 """Return a boolean representation of the inner value"""
97 """Return a boolean representation of the inner value"""
98
98
99 @abc.abstractmethod
99 @abc.abstractmethod
100 def tovalue(self, context, mapping):
100 def tovalue(self, context, mapping):
101 """Move the inner value object out or create a value representation
101 """Move the inner value object out or create a value representation
102
102
103 A returned value must be serializable by templaterfilters.json().
103 A returned value must be serializable by templaterfilters.json().
104 """
104 """
105
105
106 class mappable(object):
106 class mappable(object):
107 """Object which can be converted to a single template mapping"""
107 """Object which can be converted to a single template mapping"""
108
108
109 def itermaps(self, context):
109 def itermaps(self, context):
110 yield self.tomap(context)
110 yield self.tomap(context)
111
111
112 @abc.abstractmethod
112 @abc.abstractmethod
113 def tomap(self, context):
113 def tomap(self, context):
114 """Create a single template mapping representing this"""
114 """Create a single template mapping representing this"""
115
115
116 class wrappedbytes(wrapped):
116 class wrappedbytes(wrapped):
117 """Wrapper for byte string"""
117 """Wrapper for byte string"""
118
118
119 def __init__(self, value):
119 def __init__(self, value):
120 self._value = value
120 self._value = value
121
121
122 def contains(self, context, mapping, item):
122 def contains(self, context, mapping, item):
123 item = stringify(context, mapping, item)
123 item = stringify(context, mapping, item)
124 return item in self._value
124 return item in self._value
125
125
126 def getmember(self, context, mapping, key):
126 def getmember(self, context, mapping, key):
127 raise error.ParseError(_('%r is not a dictionary')
127 raise error.ParseError(_('%r is not a dictionary')
128 % pycompat.bytestr(self._value))
128 % pycompat.bytestr(self._value))
129
129
130 def getmin(self, context, mapping):
130 def getmin(self, context, mapping):
131 return self._getby(context, mapping, min)
131 return self._getby(context, mapping, min)
132
132
133 def getmax(self, context, mapping):
133 def getmax(self, context, mapping):
134 return self._getby(context, mapping, max)
134 return self._getby(context, mapping, max)
135
135
136 def _getby(self, context, mapping, func):
136 def _getby(self, context, mapping, func):
137 if not self._value:
137 if not self._value:
138 raise error.ParseError(_('empty string'))
138 raise error.ParseError(_('empty string'))
139 return func(pycompat.iterbytestr(self._value))
139 return func(pycompat.iterbytestr(self._value))
140
140
141 def filter(self, context, mapping, select):
141 def filter(self, context, mapping, select):
142 raise error.ParseError(_('%r is not filterable')
142 raise error.ParseError(_('%r is not filterable')
143 % pycompat.bytestr(self._value))
143 % pycompat.bytestr(self._value))
144
144
145 def itermaps(self, context):
145 def itermaps(self, context):
146 raise error.ParseError(_('%r is not iterable of mappings')
146 raise error.ParseError(_('%r is not iterable of mappings')
147 % pycompat.bytestr(self._value))
147 % pycompat.bytestr(self._value))
148
148
149 def join(self, context, mapping, sep):
149 def join(self, context, mapping, sep):
150 return joinitems(pycompat.iterbytestr(self._value), sep)
150 return joinitems(pycompat.iterbytestr(self._value), sep)
151
151
152 def show(self, context, mapping):
152 def show(self, context, mapping):
153 return self._value
153 return self._value
154
154
155 def tobool(self, context, mapping):
155 def tobool(self, context, mapping):
156 return bool(self._value)
156 return bool(self._value)
157
157
158 def tovalue(self, context, mapping):
158 def tovalue(self, context, mapping):
159 return self._value
159 return self._value
160
160
161 class wrappedvalue(wrapped):
161 class wrappedvalue(wrapped):
162 """Generic wrapper for pure non-list/dict/bytes value"""
162 """Generic wrapper for pure non-list/dict/bytes value"""
163
163
164 def __init__(self, value):
164 def __init__(self, value):
165 self._value = value
165 self._value = value
166
166
167 def contains(self, context, mapping, item):
167 def contains(self, context, mapping, item):
168 raise error.ParseError(_("%r is not iterable") % self._value)
168 raise error.ParseError(_("%r is not iterable") % self._value)
169
169
170 def getmember(self, context, mapping, key):
170 def getmember(self, context, mapping, key):
171 raise error.ParseError(_('%r is not a dictionary') % self._value)
171 raise error.ParseError(_('%r is not a dictionary') % self._value)
172
172
173 def getmin(self, context, mapping):
173 def getmin(self, context, mapping):
174 raise error.ParseError(_("%r is not iterable") % self._value)
174 raise error.ParseError(_("%r is not iterable") % self._value)
175
175
176 def getmax(self, context, mapping):
176 def getmax(self, context, mapping):
177 raise error.ParseError(_("%r is not iterable") % self._value)
177 raise error.ParseError(_("%r is not iterable") % self._value)
178
178
179 def filter(self, context, mapping, select):
179 def filter(self, context, mapping, select):
180 raise error.ParseError(_("%r is not iterable") % self._value)
180 raise error.ParseError(_("%r is not iterable") % self._value)
181
181
182 def itermaps(self, context):
182 def itermaps(self, context):
183 raise error.ParseError(_('%r is not iterable of mappings')
183 raise error.ParseError(_('%r is not iterable of mappings')
184 % self._value)
184 % self._value)
185
185
186 def join(self, context, mapping, sep):
186 def join(self, context, mapping, sep):
187 raise error.ParseError(_('%r is not iterable') % self._value)
187 raise error.ParseError(_('%r is not iterable') % self._value)
188
188
189 def show(self, context, mapping):
189 def show(self, context, mapping):
190 if self._value is None:
190 if self._value is None:
191 return b''
191 return b''
192 return pycompat.bytestr(self._value)
192 return pycompat.bytestr(self._value)
193
193
194 def tobool(self, context, mapping):
194 def tobool(self, context, mapping):
195 if self._value is None:
195 if self._value is None:
196 return False
196 return False
197 if isinstance(self._value, bool):
197 if isinstance(self._value, bool):
198 return self._value
198 return self._value
199 # otherwise evaluate as string, which means 0 is True
199 # otherwise evaluate as string, which means 0 is True
200 return bool(pycompat.bytestr(self._value))
200 return bool(pycompat.bytestr(self._value))
201
201
202 def tovalue(self, context, mapping):
202 def tovalue(self, context, mapping):
203 return self._value
203 return self._value
204
204
205 class date(mappable, wrapped):
205 class date(mappable, wrapped):
206 """Wrapper for date tuple"""
206 """Wrapper for date tuple"""
207
207
208 def __init__(self, value, showfmt='%d %d'):
208 def __init__(self, value, showfmt='%d %d'):
209 # value may be (float, int), but public interface shouldn't support
209 # value may be (float, int), but public interface shouldn't support
210 # floating-point timestamp
210 # floating-point timestamp
211 self._unixtime, self._tzoffset = map(int, value)
211 self._unixtime, self._tzoffset = map(int, value)
212 self._showfmt = showfmt
212 self._showfmt = showfmt
213
213
214 def contains(self, context, mapping, item):
214 def contains(self, context, mapping, item):
215 raise error.ParseError(_('date is not iterable'))
215 raise error.ParseError(_('date is not iterable'))
216
216
217 def getmember(self, context, mapping, key):
217 def getmember(self, context, mapping, key):
218 raise error.ParseError(_('date is not a dictionary'))
218 raise error.ParseError(_('date is not a dictionary'))
219
219
220 def getmin(self, context, mapping):
220 def getmin(self, context, mapping):
221 raise error.ParseError(_('date is not iterable'))
221 raise error.ParseError(_('date is not iterable'))
222
222
223 def getmax(self, context, mapping):
223 def getmax(self, context, mapping):
224 raise error.ParseError(_('date is not iterable'))
224 raise error.ParseError(_('date is not iterable'))
225
225
226 def filter(self, context, mapping, select):
226 def filter(self, context, mapping, select):
227 raise error.ParseError(_('date is not iterable'))
227 raise error.ParseError(_('date is not iterable'))
228
228
229 def join(self, context, mapping, sep):
229 def join(self, context, mapping, sep):
230 raise error.ParseError(_("date is not iterable"))
230 raise error.ParseError(_("date is not iterable"))
231
231
232 def show(self, context, mapping):
232 def show(self, context, mapping):
233 return self._showfmt % (self._unixtime, self._tzoffset)
233 return self._showfmt % (self._unixtime, self._tzoffset)
234
234
235 def tomap(self, context):
235 def tomap(self, context):
236 return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
236 return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
237
237
238 def tobool(self, context, mapping):
238 def tobool(self, context, mapping):
239 return True
239 return True
240
240
241 def tovalue(self, context, mapping):
241 def tovalue(self, context, mapping):
242 return (self._unixtime, self._tzoffset)
242 return (self._unixtime, self._tzoffset)
243
243
244 class hybrid(wrapped):
244 class hybrid(wrapped):
245 """Wrapper for list or dict to support legacy template
245 """Wrapper for list or dict to support legacy template
246
246
247 This class allows us to handle both:
247 This class allows us to handle both:
248 - "{files}" (legacy command-line-specific list hack) and
248 - "{files}" (legacy command-line-specific list hack) and
249 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
249 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
250 and to access raw values:
250 and to access raw values:
251 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
251 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
252 - "{get(extras, key)}"
252 - "{get(extras, key)}"
253 - "{files|json}"
253 - "{files|json}"
254 """
254 """
255
255
256 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
256 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
257 self._gen = gen # generator or function returning generator
257 self._gen = gen # generator or function returning generator
258 self._values = values
258 self._values = values
259 self._makemap = makemap
259 self._makemap = makemap
260 self._joinfmt = joinfmt
260 self._joinfmt = joinfmt
261 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
261 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
262
262
263 def contains(self, context, mapping, item):
263 def contains(self, context, mapping, item):
264 item = unwrapastype(context, mapping, item, self._keytype)
264 item = unwrapastype(context, mapping, item, self._keytype)
265 return item in self._values
265 return item in self._values
266
266
267 def getmember(self, context, mapping, key):
267 def getmember(self, context, mapping, key):
268 # TODO: maybe split hybrid list/dict types?
268 # TODO: maybe split hybrid list/dict types?
269 if not util.safehasattr(self._values, 'get'):
269 if not util.safehasattr(self._values, 'get'):
270 raise error.ParseError(_('not a dictionary'))
270 raise error.ParseError(_('not a dictionary'))
271 key = unwrapastype(context, mapping, key, self._keytype)
271 key = unwrapastype(context, mapping, key, self._keytype)
272 return self._wrapvalue(key, self._values.get(key))
272 return self._wrapvalue(key, self._values.get(key))
273
273
274 def getmin(self, context, mapping):
274 def getmin(self, context, mapping):
275 return self._getby(context, mapping, min)
275 return self._getby(context, mapping, min)
276
276
277 def getmax(self, context, mapping):
277 def getmax(self, context, mapping):
278 return self._getby(context, mapping, max)
278 return self._getby(context, mapping, max)
279
279
280 def _getby(self, context, mapping, func):
280 def _getby(self, context, mapping, func):
281 if not self._values:
281 if not self._values:
282 raise error.ParseError(_('empty sequence'))
282 raise error.ParseError(_('empty sequence'))
283 val = func(self._values)
283 val = func(self._values)
284 return self._wrapvalue(val, val)
284 return self._wrapvalue(val, val)
285
285
286 def _wrapvalue(self, key, val):
286 def _wrapvalue(self, key, val):
287 if val is None:
287 if val is None:
288 return
288 return
289 if util.safehasattr(val, '_makemap'):
289 if util.safehasattr(val, '_makemap'):
290 # a nested hybrid list/dict, which has its own way of map operation
290 # a nested hybrid list/dict, which has its own way of map operation
291 return val
291 return val
292 return hybriditem(None, key, val, self._makemap)
292 return hybriditem(None, key, val, self._makemap)
293
293
294 def filter(self, context, mapping, select):
294 def filter(self, context, mapping, select):
295 if util.safehasattr(self._values, 'get'):
295 if util.safehasattr(self._values, 'get'):
296 values = {k: v for k, v in self._values.iteritems()
296 values = {k: v for k, v in self._values.iteritems()
297 if select(self._wrapvalue(k, v))}
297 if select(self._wrapvalue(k, v))}
298 else:
298 else:
299 values = [v for v in self._values if select(self._wrapvalue(v, v))]
299 values = [v for v in self._values if select(self._wrapvalue(v, v))]
300 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
300 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
301
301
302 def itermaps(self, context):
302 def itermaps(self, context):
303 makemap = self._makemap
303 makemap = self._makemap
304 for x in self._values:
304 for x in self._values:
305 yield makemap(x)
305 yield makemap(x)
306
306
307 def join(self, context, mapping, sep):
307 def join(self, context, mapping, sep):
308 # TODO: switch gen to (context, mapping) API?
308 # TODO: switch gen to (context, mapping) API?
309 return joinitems((self._joinfmt(x) for x in self._values), sep)
309 return joinitems((self._joinfmt(x) for x in self._values), sep)
310
310
311 def show(self, context, mapping):
311 def show(self, context, mapping):
312 # TODO: switch gen to (context, mapping) API?
312 # TODO: switch gen to (context, mapping) API?
313 gen = self._gen
313 gen = self._gen
314 if gen is None:
314 if gen is None:
315 return self.join(context, mapping, ' ')
315 return self.join(context, mapping, ' ')
316 if callable(gen):
316 if callable(gen):
317 return gen()
317 return gen()
318 return gen
318 return gen
319
319
320 def tobool(self, context, mapping):
320 def tobool(self, context, mapping):
321 return bool(self._values)
321 return bool(self._values)
322
322
323 def tovalue(self, context, mapping):
323 def tovalue(self, context, mapping):
324 # TODO: make it non-recursive for trivial lists/dicts
324 # TODO: make it non-recursive for trivial lists/dicts
325 xs = self._values
325 xs = self._values
326 if util.safehasattr(xs, 'get'):
326 if util.safehasattr(xs, 'get'):
327 return {k: unwrapvalue(context, mapping, v)
327 return {k: unwrapvalue(context, mapping, v)
328 for k, v in xs.iteritems()}
328 for k, v in xs.iteritems()}
329 return [unwrapvalue(context, mapping, x) for x in xs]
329 return [unwrapvalue(context, mapping, x) for x in xs]
330
330
331 class hybriditem(mappable, wrapped):
331 class hybriditem(mappable, wrapped):
332 """Wrapper for non-list/dict object to support map operation
332 """Wrapper for non-list/dict object to support map operation
333
333
334 This class allows us to handle both:
334 This class allows us to handle both:
335 - "{manifest}"
335 - "{manifest}"
336 - "{manifest % '{rev}:{node}'}"
336 - "{manifest % '{rev}:{node}'}"
337 - "{manifest.rev}"
337 - "{manifest.rev}"
338 """
338 """
339
339
340 def __init__(self, gen, key, value, makemap):
340 def __init__(self, gen, key, value, makemap):
341 self._gen = gen # generator or function returning generator
341 self._gen = gen # generator or function returning generator
342 self._key = key
342 self._key = key
343 self._value = value # may be generator of strings
343 self._value = value # may be generator of strings
344 self._makemap = makemap
344 self._makemap = makemap
345
345
346 def tomap(self, context):
346 def tomap(self, context):
347 return self._makemap(self._key)
347 return self._makemap(self._key)
348
348
349 def contains(self, context, mapping, item):
349 def contains(self, context, mapping, item):
350 w = makewrapped(context, mapping, self._value)
350 w = makewrapped(context, mapping, self._value)
351 return w.contains(context, mapping, item)
351 return w.contains(context, mapping, item)
352
352
353 def getmember(self, context, mapping, key):
353 def getmember(self, context, mapping, key):
354 w = makewrapped(context, mapping, self._value)
354 w = makewrapped(context, mapping, self._value)
355 return w.getmember(context, mapping, key)
355 return w.getmember(context, mapping, key)
356
356
357 def getmin(self, context, mapping):
357 def getmin(self, context, mapping):
358 w = makewrapped(context, mapping, self._value)
358 w = makewrapped(context, mapping, self._value)
359 return w.getmin(context, mapping)
359 return w.getmin(context, mapping)
360
360
361 def getmax(self, context, mapping):
361 def getmax(self, context, mapping):
362 w = makewrapped(context, mapping, self._value)
362 w = makewrapped(context, mapping, self._value)
363 return w.getmax(context, mapping)
363 return w.getmax(context, mapping)
364
364
365 def filter(self, context, mapping, select):
365 def filter(self, context, mapping, select):
366 w = makewrapped(context, mapping, self._value)
366 w = makewrapped(context, mapping, self._value)
367 return w.filter(context, mapping, select)
367 return w.filter(context, mapping, select)
368
368
369 def join(self, context, mapping, sep):
369 def join(self, context, mapping, sep):
370 w = makewrapped(context, mapping, self._value)
370 w = makewrapped(context, mapping, self._value)
371 return w.join(context, mapping, sep)
371 return w.join(context, mapping, sep)
372
372
373 def show(self, context, mapping):
373 def show(self, context, mapping):
374 # TODO: switch gen to (context, mapping) API?
374 # TODO: switch gen to (context, mapping) API?
375 gen = self._gen
375 gen = self._gen
376 if gen is None:
376 if gen is None:
377 return pycompat.bytestr(self._value)
377 return pycompat.bytestr(self._value)
378 if callable(gen):
378 if callable(gen):
379 return gen()
379 return gen()
380 return gen
380 return gen
381
381
382 def tobool(self, context, mapping):
382 def tobool(self, context, mapping):
383 w = makewrapped(context, mapping, self._value)
383 w = makewrapped(context, mapping, self._value)
384 return w.tobool(context, mapping)
384 return w.tobool(context, mapping)
385
385
386 def tovalue(self, context, mapping):
386 def tovalue(self, context, mapping):
387 return _unthunk(context, mapping, self._value)
387 return _unthunk(context, mapping, self._value)
388
388
389 class _mappingsequence(wrapped):
389 class _mappingsequence(wrapped):
390 """Wrapper for sequence of template mappings
390 """Wrapper for sequence of template mappings
391
391
392 This represents an inner template structure (i.e. a list of dicts),
392 This represents an inner template structure (i.e. a list of dicts),
393 which can also be rendered by the specified named/literal template.
393 which can also be rendered by the specified named/literal template.
394
394
395 Template mappings may be nested.
395 Template mappings may be nested.
396 """
396 """
397
397
398 def __init__(self, name=None, tmpl=None, sep=''):
398 def __init__(self, name=None, tmpl=None, sep=''):
399 if name is not None and tmpl is not None:
399 if name is not None and tmpl is not None:
400 raise error.ProgrammingError('name and tmpl are mutually exclusive')
400 raise error.ProgrammingError('name and tmpl are mutually exclusive')
401 self._name = name
401 self._name = name
402 self._tmpl = tmpl
402 self._tmpl = tmpl
403 self._defaultsep = sep
403 self._defaultsep = sep
404
404
405 def contains(self, context, mapping, item):
405 def contains(self, context, mapping, item):
406 raise error.ParseError(_('not comparable'))
406 raise error.ParseError(_('not comparable'))
407
407
408 def getmember(self, context, mapping, key):
408 def getmember(self, context, mapping, key):
409 raise error.ParseError(_('not a dictionary'))
409 raise error.ParseError(_('not a dictionary'))
410
410
411 def getmin(self, context, mapping):
411 def getmin(self, context, mapping):
412 raise error.ParseError(_('not comparable'))
412 raise error.ParseError(_('not comparable'))
413
413
414 def getmax(self, context, mapping):
414 def getmax(self, context, mapping):
415 raise error.ParseError(_('not comparable'))
415 raise error.ParseError(_('not comparable'))
416
416
417 def filter(self, context, mapping, select):
417 def filter(self, context, mapping, select):
418 # implement if necessary; we'll need a wrapped type for a mapping dict
418 # implement if necessary; we'll need a wrapped type for a mapping dict
419 raise error.ParseError(_('not filterable without template'))
419 raise error.ParseError(_('not filterable without template'))
420
420
421 def join(self, context, mapping, sep):
421 def join(self, context, mapping, sep):
422 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
422 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
423 if self._name:
423 if self._name:
424 itemiter = (context.process(self._name, m) for m in mapsiter)
424 itemiter = (context.process(self._name, m) for m in mapsiter)
425 elif self._tmpl:
425 elif self._tmpl:
426 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
426 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
427 else:
427 else:
428 raise error.ParseError(_('not displayable without template'))
428 raise error.ParseError(_('not displayable without template'))
429 return joinitems(itemiter, sep)
429 return joinitems(itemiter, sep)
430
430
431 def show(self, context, mapping):
431 def show(self, context, mapping):
432 return self.join(context, mapping, self._defaultsep)
432 return self.join(context, mapping, self._defaultsep)
433
433
434 def tovalue(self, context, mapping):
434 def tovalue(self, context, mapping):
435 knownres = context.knownresourcekeys()
435 knownres = context.knownresourcekeys()
436 items = []
436 items = []
437 for nm in self.itermaps(context):
437 for nm in self.itermaps(context):
438 # drop internal resources (recursively) which shouldn't be displayed
438 # drop internal resources (recursively) which shouldn't be displayed
439 lm = context.overlaymap(mapping, nm)
439 lm = context.overlaymap(mapping, nm)
440 items.append({k: unwrapvalue(context, lm, v)
440 items.append({k: unwrapvalue(context, lm, v)
441 for k, v in nm.iteritems() if k not in knownres})
441 for k, v in nm.iteritems() if k not in knownres})
442 return items
442 return items
443
443
444 class mappinggenerator(_mappingsequence):
444 class mappinggenerator(_mappingsequence):
445 """Wrapper for generator of template mappings
445 """Wrapper for generator of template mappings
446
446
447 The function ``make(context, *args)`` should return a generator of
447 The function ``make(context, *args)`` should return a generator of
448 mapping dicts.
448 mapping dicts.
449 """
449 """
450
450
451 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
451 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
452 super(mappinggenerator, self).__init__(name, tmpl, sep)
452 super(mappinggenerator, self).__init__(name, tmpl, sep)
453 self._make = make
453 self._make = make
454 self._args = args
454 self._args = args
455
455
456 def itermaps(self, context):
456 def itermaps(self, context):
457 return self._make(context, *self._args)
457 return self._make(context, *self._args)
458
458
459 def tobool(self, context, mapping):
459 def tobool(self, context, mapping):
460 return _nonempty(self.itermaps(context))
460 return _nonempty(self.itermaps(context))
461
461
462 class mappinglist(_mappingsequence):
462 class mappinglist(_mappingsequence):
463 """Wrapper for list of template mappings"""
463 """Wrapper for list of template mappings"""
464
464
465 def __init__(self, mappings, name=None, tmpl=None, sep=''):
465 def __init__(self, mappings, name=None, tmpl=None, sep=''):
466 super(mappinglist, self).__init__(name, tmpl, sep)
466 super(mappinglist, self).__init__(name, tmpl, sep)
467 self._mappings = mappings
467 self._mappings = mappings
468
468
469 def itermaps(self, context):
469 def itermaps(self, context):
470 return iter(self._mappings)
470 return iter(self._mappings)
471
471
472 def tobool(self, context, mapping):
472 def tobool(self, context, mapping):
473 return bool(self._mappings)
473 return bool(self._mappings)
474
474
475 class mappingdict(mappable, _mappingsequence):
475 class mappingdict(mappable, _mappingsequence):
476 """Wrapper for a single template mapping
476 """Wrapper for a single template mapping
477
477
478 This isn't a sequence in a way that the underlying dict won't be iterated
478 This isn't a sequence in a way that the underlying dict won't be iterated
479 as a dict, but shares most of the _mappingsequence functions.
479 as a dict, but shares most of the _mappingsequence functions.
480 """
480 """
481
481
482 def __init__(self, mapping, name=None, tmpl=None):
482 def __init__(self, mapping, name=None, tmpl=None):
483 super(mappingdict, self).__init__(name, tmpl)
483 super(mappingdict, self).__init__(name, tmpl)
484 self._mapping = mapping
484 self._mapping = mapping
485
485
486 def tomap(self, context):
486 def tomap(self, context):
487 return self._mapping
487 return self._mapping
488
488
489 def tobool(self, context, mapping):
489 def tobool(self, context, mapping):
490 # no idea when a template mapping should be considered an empty, but
490 # no idea when a template mapping should be considered an empty, but
491 # a mapping dict should have at least one item in practice, so always
491 # a mapping dict should have at least one item in practice, so always
492 # mark this as non-empty.
492 # mark this as non-empty.
493 return True
493 return True
494
494
495 def tovalue(self, context, mapping):
495 def tovalue(self, context, mapping):
496 return super(mappingdict, self).tovalue(context, mapping)[0]
496 return super(mappingdict, self).tovalue(context, mapping)[0]
497
497
498 class mappingnone(wrappedvalue):
498 class mappingnone(wrappedvalue):
499 """Wrapper for None, but supports map operation
499 """Wrapper for None, but supports map operation
500
500
501 This represents None of Optional[mappable]. It's similar to
501 This represents None of Optional[mappable]. It's similar to
502 mapplinglist([]), but the underlying value is not [], but None.
502 mapplinglist([]), but the underlying value is not [], but None.
503 """
503 """
504
504
505 def __init__(self):
505 def __init__(self):
506 super(mappingnone, self).__init__(None)
506 super(mappingnone, self).__init__(None)
507
507
508 def itermaps(self, context):
508 def itermaps(self, context):
509 return iter([])
509 return iter([])
510
510
511 class mappedgenerator(wrapped):
511 class mappedgenerator(wrapped):
512 """Wrapper for generator of strings which acts as a list
512 """Wrapper for generator of strings which acts as a list
513
513
514 The function ``make(context, *args)`` should return a generator of
514 The function ``make(context, *args)`` should return a generator of
515 byte strings, or a generator of (possibly nested) generators of byte
515 byte strings, or a generator of (possibly nested) generators of byte
516 strings (i.e. a generator for a list of byte strings.)
516 strings (i.e. a generator for a list of byte strings.)
517 """
517 """
518
518
519 def __init__(self, make, args=()):
519 def __init__(self, make, args=()):
520 self._make = make
520 self._make = make
521 self._args = args
521 self._args = args
522
522
523 def contains(self, context, mapping, item):
523 def contains(self, context, mapping, item):
524 item = stringify(context, mapping, item)
524 item = stringify(context, mapping, item)
525 return item in self.tovalue(context, mapping)
525 return item in self.tovalue(context, mapping)
526
526
527 def _gen(self, context):
527 def _gen(self, context):
528 return self._make(context, *self._args)
528 return self._make(context, *self._args)
529
529
530 def getmember(self, context, mapping, key):
530 def getmember(self, context, mapping, key):
531 raise error.ParseError(_('not a dictionary'))
531 raise error.ParseError(_('not a dictionary'))
532
532
533 def getmin(self, context, mapping):
533 def getmin(self, context, mapping):
534 return self._getby(context, mapping, min)
534 return self._getby(context, mapping, min)
535
535
536 def getmax(self, context, mapping):
536 def getmax(self, context, mapping):
537 return self._getby(context, mapping, max)
537 return self._getby(context, mapping, max)
538
538
539 def _getby(self, context, mapping, func):
539 def _getby(self, context, mapping, func):
540 xs = self.tovalue(context, mapping)
540 xs = self.tovalue(context, mapping)
541 if not xs:
541 if not xs:
542 raise error.ParseError(_('empty sequence'))
542 raise error.ParseError(_('empty sequence'))
543 return func(xs)
543 return func(xs)
544
544
545 @staticmethod
545 @staticmethod
546 def _filteredgen(context, mapping, make, args, select):
546 def _filteredgen(context, mapping, make, args, select):
547 for x in make(context, *args):
547 for x in make(context, *args):
548 s = stringify(context, mapping, x)
548 s = stringify(context, mapping, x)
549 if select(wrappedbytes(s)):
549 if select(wrappedbytes(s)):
550 yield s
550 yield s
551
551
552 def filter(self, context, mapping, select):
552 def filter(self, context, mapping, select):
553 args = (mapping, self._make, self._args, select)
553 args = (mapping, self._make, self._args, select)
554 return mappedgenerator(self._filteredgen, args)
554 return mappedgenerator(self._filteredgen, args)
555
555
556 def itermaps(self, context):
556 def itermaps(self, context):
557 raise error.ParseError(_('list of strings is not mappable'))
557 raise error.ParseError(_('list of strings is not mappable'))
558
558
559 def join(self, context, mapping, sep):
559 def join(self, context, mapping, sep):
560 return joinitems(self._gen(context), sep)
560 return joinitems(self._gen(context), sep)
561
561
562 def show(self, context, mapping):
562 def show(self, context, mapping):
563 return self.join(context, mapping, '')
563 return self.join(context, mapping, '')
564
564
565 def tobool(self, context, mapping):
565 def tobool(self, context, mapping):
566 return _nonempty(self._gen(context))
566 return _nonempty(self._gen(context))
567
567
568 def tovalue(self, context, mapping):
568 def tovalue(self, context, mapping):
569 return [stringify(context, mapping, x) for x in self._gen(context)]
569 return [stringify(context, mapping, x) for x in self._gen(context)]
570
570
571 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
571 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
572 """Wrap data to support both dict-like and string-like operations"""
572 """Wrap data to support both dict-like and string-like operations"""
573 prefmt = pycompat.identity
573 prefmt = pycompat.identity
574 if fmt is None:
574 if fmt is None:
575 fmt = '%s=%s'
575 fmt = '%s=%s'
576 prefmt = pycompat.bytestr
576 prefmt = pycompat.bytestr
577 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
577 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
578 lambda k: fmt % (prefmt(k), prefmt(data[k])))
578 lambda k: fmt % (prefmt(k), prefmt(data[k])))
579
579
580 def hybridlist(data, name, fmt=None, gen=None):
580 def hybridlist(data, name, fmt=None, gen=None):
581 """Wrap data to support both list-like and string-like operations"""
581 """Wrap data to support both list-like and string-like operations"""
582 prefmt = pycompat.identity
582 prefmt = pycompat.identity
583 if fmt is None:
583 if fmt is None:
584 fmt = '%s'
584 fmt = '%s'
585 prefmt = pycompat.bytestr
585 prefmt = pycompat.bytestr
586 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
586 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
587
587
588 def compatdict(context, mapping, name, data, key='key', value='value',
588 def compatdict(context, mapping, name, data, key='key', value='value',
589 fmt=None, plural=None, separator=' '):
589 fmt=None, plural=None, separator=' '):
590 """Wrap data like hybriddict(), but also supports old-style list template
590 """Wrap data like hybriddict(), but also supports old-style list template
591
591
592 This exists for backward compatibility with the old-style template. Use
592 This exists for backward compatibility with the old-style template. Use
593 hybriddict() for new template keywords.
593 hybriddict() for new template keywords.
594 """
594 """
595 c = [{key: k, value: v} for k, v in data.iteritems()]
595 c = [{key: k, value: v} for k, v in data.iteritems()]
596 f = _showcompatlist(context, mapping, name, c, plural, separator)
596 f = _showcompatlist(context, mapping, name, c, plural, separator)
597 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
597 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
598
598
599 def compatlist(context, mapping, name, data, element=None, fmt=None,
599 def compatlist(context, mapping, name, data, element=None, fmt=None,
600 plural=None, separator=' '):
600 plural=None, separator=' '):
601 """Wrap data like hybridlist(), but also supports old-style list template
601 """Wrap data like hybridlist(), but also supports old-style list template
602
602
603 This exists for backward compatibility with the old-style template. Use
603 This exists for backward compatibility with the old-style template. Use
604 hybridlist() for new template keywords.
604 hybridlist() for new template keywords.
605 """
605 """
606 f = _showcompatlist(context, mapping, name, data, plural, separator)
606 f = _showcompatlist(context, mapping, name, data, plural, separator)
607 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
607 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
608
608
609 def compatfilecopiesdict(context, mapping, name, copies):
609 def compatfilecopiesdict(context, mapping, name, copies):
610 """Wrap list of (dest, source) file names to support old-style list
610 """Wrap list of (dest, source) file names to support old-style list
611 template and field names
611 template and field names
612
612
613 This exists for backward compatibility. Use hybriddict for new template
613 This exists for backward compatibility. Use hybriddict for new template
614 keywords.
614 keywords.
615 """
615 """
616 # no need to provide {path} to old-style list template
616 # no need to provide {path} to old-style list template
617 c = [{'name': k, 'source': v} for k, v in copies]
617 c = [{'name': k, 'source': v} for k, v in copies]
618 f = _showcompatlist(context, mapping, name, c, plural='file_copies')
618 f = _showcompatlist(context, mapping, name, c, plural='file_copies')
619 copies = util.sortdict(copies)
619 copies = util.sortdict(copies)
620 return hybrid(f, copies,
620 return hybrid(f, copies,
621 lambda k: {'name': k, 'path': k, 'source': copies[k]},
621 lambda k: {'name': k, 'path': k, 'source': copies[k]},
622 lambda k: '%s (%s)' % (k, copies[k]))
622 lambda k: '%s (%s)' % (k, copies[k]))
623
623
624 def compatfileslist(context, mapping, name, files):
624 def compatfileslist(context, mapping, name, files):
625 """Wrap list of file names to support old-style list template and field
625 """Wrap list of file names to support old-style list template and field
626 names
626 names
627
627
628 This exists for backward compatibility. Use hybridlist for new template
628 This exists for backward compatibility. Use hybridlist for new template
629 keywords.
629 keywords.
630 """
630 """
631 f = _showcompatlist(context, mapping, name, files)
631 f = _showcompatlist(context, mapping, name, files)
632 return hybrid(f, files, lambda x: {'file': x, 'path': x},
632 return hybrid(f, files, lambda x: {'file': x, 'path': x},
633 pycompat.identity)
633 pycompat.identity)
634
634
635 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
635 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
636 """Return a generator that renders old-style list template
636 """Return a generator that renders old-style list template
637
637
638 name is name of key in template map.
638 name is name of key in template map.
639 values is list of strings or dicts.
639 values is list of strings or dicts.
640 plural is plural of name, if not simply name + 's'.
640 plural is plural of name, if not simply name + 's'.
641 separator is used to join values as a string
641 separator is used to join values as a string
642
642
643 expansion works like this, given name 'foo'.
643 expansion works like this, given name 'foo'.
644
644
645 if values is empty, expand 'no_foos'.
645 if values is empty, expand 'no_foos'.
646
646
647 if 'foo' not in template map, return values as a string,
647 if 'foo' not in template map, return values as a string,
648 joined by 'separator'.
648 joined by 'separator'.
649
649
650 expand 'start_foos'.
650 expand 'start_foos'.
651
651
652 for each value, expand 'foo'. if 'last_foo' in template
652 for each value, expand 'foo'. if 'last_foo' in template
653 map, expand it instead of 'foo' for last key.
653 map, expand it instead of 'foo' for last key.
654
654
655 expand 'end_foos'.
655 expand 'end_foos'.
656 """
656 """
657 if not plural:
657 if not plural:
658 plural = name + 's'
658 plural = name + 's'
659 if not values:
659 if not values:
660 noname = 'no_' + plural
660 noname = 'no_' + plural
661 if context.preload(noname):
661 if context.preload(noname):
662 yield context.process(noname, mapping)
662 yield context.process(noname, mapping)
663 return
663 return
664 if not context.preload(name):
664 if not context.preload(name):
665 if isinstance(values[0], bytes):
665 if isinstance(values[0], bytes):
666 yield separator.join(values)
666 yield separator.join(values)
667 else:
667 else:
668 for v in values:
668 for v in values:
669 r = dict(v)
669 r = dict(v)
670 r.update(mapping)
670 r.update(mapping)
671 yield r
671 yield r
672 return
672 return
673 startname = 'start_' + plural
673 startname = 'start_' + plural
674 if context.preload(startname):
674 if context.preload(startname):
675 yield context.process(startname, mapping)
675 yield context.process(startname, mapping)
676 def one(v, tag=name):
676 def one(v, tag=name):
677 vmapping = {}
677 vmapping = {}
678 try:
678 try:
679 vmapping.update(v)
679 vmapping.update(v)
680 # Python 2 raises ValueError if the type of v is wrong. Python
680 # Python 2 raises ValueError if the type of v is wrong. Python
681 # 3 raises TypeError.
681 # 3 raises TypeError.
682 except (AttributeError, TypeError, ValueError):
682 except (AttributeError, TypeError, ValueError):
683 try:
683 try:
684 # Python 2 raises ValueError trying to destructure an e.g.
684 # Python 2 raises ValueError trying to destructure an e.g.
685 # bytes. Python 3 raises TypeError.
685 # bytes. Python 3 raises TypeError.
686 for a, b in v:
686 for a, b in v:
687 vmapping[a] = b
687 vmapping[a] = b
688 except (TypeError, ValueError):
688 except (TypeError, ValueError):
689 vmapping[name] = v
689 vmapping[name] = v
690 vmapping = context.overlaymap(mapping, vmapping)
690 vmapping = context.overlaymap(mapping, vmapping)
691 return context.process(tag, vmapping)
691 return context.process(tag, vmapping)
692 lastname = 'last_' + name
692 lastname = 'last_' + name
693 if context.preload(lastname):
693 if context.preload(lastname):
694 last = values.pop()
694 last = values.pop()
695 else:
695 else:
696 last = None
696 last = None
697 for v in values:
697 for v in values:
698 yield one(v)
698 yield one(v)
699 if last is not None:
699 if last is not None:
700 yield one(last, tag=lastname)
700 yield one(last, tag=lastname)
701 endname = 'end_' + plural
701 endname = 'end_' + plural
702 if context.preload(endname):
702 if context.preload(endname):
703 yield context.process(endname, mapping)
703 yield context.process(endname, mapping)
704
704
705 def flatten(context, mapping, thing):
705 def flatten(context, mapping, thing):
706 """Yield a single stream from a possibly nested set of iterators"""
706 """Yield a single stream from a possibly nested set of iterators"""
707 if isinstance(thing, wrapped):
707 if isinstance(thing, wrapped):
708 thing = thing.show(context, mapping)
708 thing = thing.show(context, mapping)
709 if isinstance(thing, bytes):
709 if isinstance(thing, bytes):
710 yield thing
710 yield thing
711 elif isinstance(thing, str):
711 elif isinstance(thing, str):
712 # We can only hit this on Python 3, and it's here to guard
712 # We can only hit this on Python 3, and it's here to guard
713 # against infinite recursion.
713 # against infinite recursion.
714 raise error.ProgrammingError('Mercurial IO including templates is done'
714 raise error.ProgrammingError('Mercurial IO including templates is done'
715 ' with bytes, not strings, got %r' % thing)
715 ' with bytes, not strings, got %r' % thing)
716 elif thing is None:
716 elif thing is None:
717 pass
717 pass
718 elif not util.safehasattr(thing, '__iter__'):
718 elif not util.safehasattr(thing, '__iter__'):
719 yield pycompat.bytestr(thing)
719 yield pycompat.bytestr(thing)
720 else:
720 else:
721 for i in thing:
721 for i in thing:
722 if isinstance(i, wrapped):
722 if isinstance(i, wrapped):
723 i = i.show(context, mapping)
723 i = i.show(context, mapping)
724 if isinstance(i, bytes):
724 if isinstance(i, bytes):
725 yield i
725 yield i
726 elif i is None:
726 elif i is None:
727 pass
727 pass
728 elif not util.safehasattr(i, '__iter__'):
728 elif not util.safehasattr(i, '__iter__'):
729 yield pycompat.bytestr(i)
729 yield pycompat.bytestr(i)
730 else:
730 else:
731 for j in flatten(context, mapping, i):
731 for j in flatten(context, mapping, i):
732 yield j
732 yield j
733
733
734 def stringify(context, mapping, thing):
734 def stringify(context, mapping, thing):
735 """Turn values into bytes by converting into text and concatenating them"""
735 """Turn values into bytes by converting into text and concatenating them"""
736 if isinstance(thing, bytes):
736 if isinstance(thing, bytes):
737 return thing # retain localstr to be round-tripped
737 return thing # retain localstr to be round-tripped
738 return b''.join(flatten(context, mapping, thing))
738 return b''.join(flatten(context, mapping, thing))
739
739
740 def findsymbolicname(arg):
740 def findsymbolicname(arg):
741 """Find symbolic name for the given compiled expression; returns None
741 """Find symbolic name for the given compiled expression; returns None
742 if nothing found reliably"""
742 if nothing found reliably"""
743 while True:
743 while True:
744 func, data = arg
744 func, data = arg
745 if func is runsymbol:
745 if func is runsymbol:
746 return data
746 return data
747 elif func is runfilter:
747 elif func is runfilter:
748 arg = data[0]
748 arg = data[0]
749 else:
749 else:
750 return None
750 return None
751
751
752 def _nonempty(xiter):
752 def _nonempty(xiter):
753 try:
753 try:
754 next(xiter)
754 next(xiter)
755 return True
755 return True
756 except StopIteration:
756 except StopIteration:
757 return False
757 return False
758
758
759 def _unthunk(context, mapping, thing):
759 def _unthunk(context, mapping, thing):
760 """Evaluate a lazy byte string into value"""
760 """Evaluate a lazy byte string into value"""
761 if not isinstance(thing, types.GeneratorType):
761 if not isinstance(thing, types.GeneratorType):
762 return thing
762 return thing
763 return stringify(context, mapping, thing)
763 return stringify(context, mapping, thing)
764
764
765 def evalrawexp(context, mapping, arg):
765 def evalrawexp(context, mapping, arg):
766 """Evaluate given argument as a bare template object which may require
766 """Evaluate given argument as a bare template object which may require
767 further processing (such as folding generator of strings)"""
767 further processing (such as folding generator of strings)"""
768 func, data = arg
768 func, data = arg
769 return func(context, mapping, data)
769 return func(context, mapping, data)
770
770
771 def evalwrapped(context, mapping, arg):
771 def evalwrapped(context, mapping, arg):
772 """Evaluate given argument to wrapped object"""
772 """Evaluate given argument to wrapped object"""
773 thing = evalrawexp(context, mapping, arg)
773 thing = evalrawexp(context, mapping, arg)
774 return makewrapped(context, mapping, thing)
774 return makewrapped(context, mapping, thing)
775
775
776 def makewrapped(context, mapping, thing):
776 def makewrapped(context, mapping, thing):
777 """Lift object to a wrapped type"""
777 """Lift object to a wrapped type"""
778 if isinstance(thing, wrapped):
778 if isinstance(thing, wrapped):
779 return thing
779 return thing
780 thing = _unthunk(context, mapping, thing)
780 thing = _unthunk(context, mapping, thing)
781 if isinstance(thing, bytes):
781 if isinstance(thing, bytes):
782 return wrappedbytes(thing)
782 return wrappedbytes(thing)
783 return wrappedvalue(thing)
783 return wrappedvalue(thing)
784
784
785 def evalfuncarg(context, mapping, arg):
785 def evalfuncarg(context, mapping, arg):
786 """Evaluate given argument as value type"""
786 """Evaluate given argument as value type"""
787 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
787 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
788
788
789 def unwrapvalue(context, mapping, thing):
789 def unwrapvalue(context, mapping, thing):
790 """Move the inner value object out of the wrapper"""
790 """Move the inner value object out of the wrapper"""
791 if isinstance(thing, wrapped):
791 if isinstance(thing, wrapped):
792 return thing.tovalue(context, mapping)
792 return thing.tovalue(context, mapping)
793 # evalrawexp() may return string, generator of strings or arbitrary object
793 # evalrawexp() may return string, generator of strings or arbitrary object
794 # such as date tuple, but filter does not want generator.
794 # such as date tuple, but filter does not want generator.
795 return _unthunk(context, mapping, thing)
795 return _unthunk(context, mapping, thing)
796
796
797 def evalboolean(context, mapping, arg):
797 def evalboolean(context, mapping, arg):
798 """Evaluate given argument as boolean, but also takes boolean literals"""
798 """Evaluate given argument as boolean, but also takes boolean literals"""
799 func, data = arg
799 func, data = arg
800 if func is runsymbol:
800 if func is runsymbol:
801 thing = func(context, mapping, data, default=None)
801 thing = func(context, mapping, data, default=None)
802 if thing is None:
802 if thing is None:
803 # not a template keyword, takes as a boolean literal
803 # not a template keyword, takes as a boolean literal
804 thing = stringutil.parsebool(data)
804 thing = stringutil.parsebool(data)
805 else:
805 else:
806 thing = func(context, mapping, data)
806 thing = func(context, mapping, data)
807 return makewrapped(context, mapping, thing).tobool(context, mapping)
807 return makewrapped(context, mapping, thing).tobool(context, mapping)
808
808
809 def evaldate(context, mapping, arg, err=None):
809 def evaldate(context, mapping, arg, err=None):
810 """Evaluate given argument as a date tuple or a date string; returns
810 """Evaluate given argument as a date tuple or a date string; returns
811 a (unixtime, offset) tuple"""
811 a (unixtime, offset) tuple"""
812 thing = evalrawexp(context, mapping, arg)
812 thing = evalrawexp(context, mapping, arg)
813 return unwrapdate(context, mapping, thing, err)
813 return unwrapdate(context, mapping, thing, err)
814
814
815 def unwrapdate(context, mapping, thing, err=None):
815 def unwrapdate(context, mapping, thing, err=None):
816 if isinstance(thing, date):
816 if isinstance(thing, date):
817 return thing.tovalue(context, mapping)
817 return thing.tovalue(context, mapping)
818 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
818 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
819 thing = unwrapvalue(context, mapping, thing)
819 thing = unwrapvalue(context, mapping, thing)
820 try:
820 try:
821 return dateutil.parsedate(thing)
821 return dateutil.parsedate(thing)
822 except AttributeError:
822 except AttributeError:
823 raise error.ParseError(err or _('not a date tuple nor a string'))
823 raise error.ParseError(err or _('not a date tuple nor a string'))
824 except error.ParseError:
824 except error.ParseError:
825 if not err:
825 if not err:
826 raise
826 raise
827 raise error.ParseError(err)
827 raise error.ParseError(err)
828
828
829 def evalinteger(context, mapping, arg, err=None):
829 def evalinteger(context, mapping, arg, err=None):
830 thing = evalrawexp(context, mapping, arg)
830 thing = evalrawexp(context, mapping, arg)
831 return unwrapinteger(context, mapping, thing, err)
831 return unwrapinteger(context, mapping, thing, err)
832
832
833 def unwrapinteger(context, mapping, thing, err=None):
833 def unwrapinteger(context, mapping, thing, err=None):
834 thing = unwrapvalue(context, mapping, thing)
834 thing = unwrapvalue(context, mapping, thing)
835 try:
835 try:
836 return int(thing)
836 return int(thing)
837 except (TypeError, ValueError):
837 except (TypeError, ValueError):
838 raise error.ParseError(err or _('not an integer'))
838 raise error.ParseError(err or _('not an integer'))
839
839
840 def evalstring(context, mapping, arg):
840 def evalstring(context, mapping, arg):
841 return stringify(context, mapping, evalrawexp(context, mapping, arg))
841 return stringify(context, mapping, evalrawexp(context, mapping, arg))
842
842
843 def evalstringliteral(context, mapping, arg):
843 def evalstringliteral(context, mapping, arg):
844 """Evaluate given argument as string template, but returns symbol name
844 """Evaluate given argument as string template, but returns symbol name
845 if it is unknown"""
845 if it is unknown"""
846 func, data = arg
846 func, data = arg
847 if func is runsymbol:
847 if func is runsymbol:
848 thing = func(context, mapping, data, default=data)
848 thing = func(context, mapping, data, default=data)
849 else:
849 else:
850 thing = func(context, mapping, data)
850 thing = func(context, mapping, data)
851 return stringify(context, mapping, thing)
851 return stringify(context, mapping, thing)
852
852
853 _unwrapfuncbytype = {
853 _unwrapfuncbytype = {
854 None: unwrapvalue,
854 None: unwrapvalue,
855 bytes: stringify,
855 bytes: stringify,
856 date: unwrapdate,
856 date: unwrapdate,
857 int: unwrapinteger,
857 int: unwrapinteger,
858 }
858 }
859
859
860 def unwrapastype(context, mapping, thing, typ):
860 def unwrapastype(context, mapping, thing, typ):
861 """Move the inner value object out of the wrapper and coerce its type"""
861 """Move the inner value object out of the wrapper and coerce its type"""
862 try:
862 try:
863 f = _unwrapfuncbytype[typ]
863 f = _unwrapfuncbytype[typ]
864 except KeyError:
864 except KeyError:
865 raise error.ProgrammingError('invalid type specified: %r' % typ)
865 raise error.ProgrammingError('invalid type specified: %r' % typ)
866 return f(context, mapping, thing)
866 return f(context, mapping, thing)
867
867
868 def runinteger(context, mapping, data):
868 def runinteger(context, mapping, data):
869 return int(data)
869 return int(data)
870
870
871 def runstring(context, mapping, data):
871 def runstring(context, mapping, data):
872 return data
872 return data
873
873
874 def _recursivesymbolblocker(key):
874 def _recursivesymbolblocker(key):
875 def showrecursion(context, mapping):
875 def showrecursion(context, mapping):
876 raise error.Abort(_("recursive reference '%s' in template") % key)
876 raise error.Abort(_("recursive reference '%s' in template") % key)
877 showrecursion._requires = () # mark as new-style templatekw
878 return showrecursion
877 return showrecursion
879
878
880 def runsymbol(context, mapping, key, default=''):
879 def runsymbol(context, mapping, key, default=''):
881 v = context.symbol(mapping, key)
880 v = context.symbol(mapping, key)
882 if v is None:
881 if v is None:
883 # put poison to cut recursion. we can't move this to parsing phase
882 # put poison to cut recursion. we can't move this to parsing phase
884 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
883 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
885 safemapping = mapping.copy()
884 safemapping = mapping.copy()
886 safemapping[key] = _recursivesymbolblocker(key)
885 safemapping[key] = _recursivesymbolblocker(key)
887 try:
886 try:
888 v = context.process(key, safemapping)
887 v = context.process(key, safemapping)
889 except TemplateNotFound:
888 except TemplateNotFound:
890 v = default
889 v = default
891 if callable(v) and getattr(v, '_requires', None) is None:
892 # old templatekw: expand all keywords and resources
893 # (TODO: drop support for old-style functions. 'f._requires = ()'
894 # can be removed.)
895 props = {k: context._resources.lookup(mapping, k)
896 for k in context._resources.knownkeys()}
897 # pass context to _showcompatlist() through templatekw._showlist()
898 props['templ'] = context
899 props.update(mapping)
900 ui = props.get('ui')
901 if ui:
902 ui.deprecwarn("old-style template keyword '%s'" % key, '4.8')
903 return v(**pycompat.strkwargs(props))
904 if callable(v):
890 if callable(v):
905 # new templatekw
891 # new templatekw
906 try:
892 try:
907 return v(context, mapping)
893 return v(context, mapping)
908 except ResourceUnavailable:
894 except ResourceUnavailable:
909 # unsupported keyword is mapped to empty just like unknown keyword
895 # unsupported keyword is mapped to empty just like unknown keyword
910 return None
896 return None
911 return v
897 return v
912
898
913 def runtemplate(context, mapping, template):
899 def runtemplate(context, mapping, template):
914 for arg in template:
900 for arg in template:
915 yield evalrawexp(context, mapping, arg)
901 yield evalrawexp(context, mapping, arg)
916
902
917 def runfilter(context, mapping, data):
903 def runfilter(context, mapping, data):
918 arg, filt = data
904 arg, filt = data
919 thing = evalrawexp(context, mapping, arg)
905 thing = evalrawexp(context, mapping, arg)
920 intype = getattr(filt, '_intype', None)
906 intype = getattr(filt, '_intype', None)
921 try:
907 try:
922 thing = unwrapastype(context, mapping, thing, intype)
908 thing = unwrapastype(context, mapping, thing, intype)
923 return filt(thing)
909 return filt(thing)
924 except error.ParseError as e:
910 except error.ParseError as e:
925 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
911 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
926
912
927 def _formatfiltererror(arg, filt):
913 def _formatfiltererror(arg, filt):
928 fn = pycompat.sysbytes(filt.__name__)
914 fn = pycompat.sysbytes(filt.__name__)
929 sym = findsymbolicname(arg)
915 sym = findsymbolicname(arg)
930 if not sym:
916 if not sym:
931 return _("incompatible use of template filter '%s'") % fn
917 return _("incompatible use of template filter '%s'") % fn
932 return (_("template filter '%s' is not compatible with keyword '%s'")
918 return (_("template filter '%s' is not compatible with keyword '%s'")
933 % (fn, sym))
919 % (fn, sym))
934
920
935 def _iteroverlaymaps(context, origmapping, newmappings):
921 def _iteroverlaymaps(context, origmapping, newmappings):
936 """Generate combined mappings from the original mapping and an iterable
922 """Generate combined mappings from the original mapping and an iterable
937 of partial mappings to override the original"""
923 of partial mappings to override the original"""
938 for i, nm in enumerate(newmappings):
924 for i, nm in enumerate(newmappings):
939 lm = context.overlaymap(origmapping, nm)
925 lm = context.overlaymap(origmapping, nm)
940 lm['index'] = i
926 lm['index'] = i
941 yield lm
927 yield lm
942
928
943 def _applymap(context, mapping, d, darg, targ):
929 def _applymap(context, mapping, d, darg, targ):
944 try:
930 try:
945 diter = d.itermaps(context)
931 diter = d.itermaps(context)
946 except error.ParseError as err:
932 except error.ParseError as err:
947 sym = findsymbolicname(darg)
933 sym = findsymbolicname(darg)
948 if not sym:
934 if not sym:
949 raise
935 raise
950 hint = _("keyword '%s' does not support map operation") % sym
936 hint = _("keyword '%s' does not support map operation") % sym
951 raise error.ParseError(bytes(err), hint=hint)
937 raise error.ParseError(bytes(err), hint=hint)
952 for lm in _iteroverlaymaps(context, mapping, diter):
938 for lm in _iteroverlaymaps(context, mapping, diter):
953 yield evalrawexp(context, lm, targ)
939 yield evalrawexp(context, lm, targ)
954
940
955 def runmap(context, mapping, data):
941 def runmap(context, mapping, data):
956 darg, targ = data
942 darg, targ = data
957 d = evalwrapped(context, mapping, darg)
943 d = evalwrapped(context, mapping, darg)
958 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
944 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
959
945
960 def runmember(context, mapping, data):
946 def runmember(context, mapping, data):
961 darg, memb = data
947 darg, memb = data
962 d = evalwrapped(context, mapping, darg)
948 d = evalwrapped(context, mapping, darg)
963 if isinstance(d, mappable):
949 if isinstance(d, mappable):
964 lm = context.overlaymap(mapping, d.tomap(context))
950 lm = context.overlaymap(mapping, d.tomap(context))
965 return runsymbol(context, lm, memb)
951 return runsymbol(context, lm, memb)
966 try:
952 try:
967 return d.getmember(context, mapping, memb)
953 return d.getmember(context, mapping, memb)
968 except error.ParseError as err:
954 except error.ParseError as err:
969 sym = findsymbolicname(darg)
955 sym = findsymbolicname(darg)
970 if not sym:
956 if not sym:
971 raise
957 raise
972 hint = _("keyword '%s' does not support member operation") % sym
958 hint = _("keyword '%s' does not support member operation") % sym
973 raise error.ParseError(bytes(err), hint=hint)
959 raise error.ParseError(bytes(err), hint=hint)
974
960
975 def runnegate(context, mapping, data):
961 def runnegate(context, mapping, data):
976 data = evalinteger(context, mapping, data,
962 data = evalinteger(context, mapping, data,
977 _('negation needs an integer argument'))
963 _('negation needs an integer argument'))
978 return -data
964 return -data
979
965
980 def runarithmetic(context, mapping, data):
966 def runarithmetic(context, mapping, data):
981 func, left, right = data
967 func, left, right = data
982 left = evalinteger(context, mapping, left,
968 left = evalinteger(context, mapping, left,
983 _('arithmetic only defined on integers'))
969 _('arithmetic only defined on integers'))
984 right = evalinteger(context, mapping, right,
970 right = evalinteger(context, mapping, right,
985 _('arithmetic only defined on integers'))
971 _('arithmetic only defined on integers'))
986 try:
972 try:
987 return func(left, right)
973 return func(left, right)
988 except ZeroDivisionError:
974 except ZeroDivisionError:
989 raise error.Abort(_('division by zero is not defined'))
975 raise error.Abort(_('division by zero is not defined'))
990
976
991 def joinitems(itemiter, sep):
977 def joinitems(itemiter, sep):
992 """Join items with the separator; Returns generator of bytes"""
978 """Join items with the separator; Returns generator of bytes"""
993 first = True
979 first = True
994 for x in itemiter:
980 for x in itemiter:
995 if first:
981 if first:
996 first = False
982 first = False
997 elif sep:
983 elif sep:
998 yield sep
984 yield sep
999 yield x
985 yield x
General Comments 0
You need to be logged in to leave comments. Login now