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