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