1# hgweb/webutil.py - utility library for the web interface. 2# 3# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> 4# Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> 5# 6# This software may be used and distributed according to the terms of the 7# GNU General Public License version 2 or any later version. 8 9from __future__ import absolute_import 10 11import copy 12import difflib 13import os 14import re 15 16from ..i18n import _ 17from ..node import hex, short 18from ..pycompat import setattr 19 20from .common import ( 21 ErrorResponse, 22 HTTP_BAD_REQUEST, 23 HTTP_NOT_FOUND, 24 paritygen, 25) 26 27from .. import ( 28 context, 29 diffutil, 30 error, 31 match, 32 mdiff, 33 obsutil, 34 patch, 35 pathutil, 36 pycompat, 37 scmutil, 38 templatefilters, 39 templatekw, 40 templateutil, 41 ui as uimod, 42 util, 43) 44 45from ..utils import stringutil 46 47archivespecs = util.sortdict( 48 ( 49 (b'zip', (b'application/zip', b'zip', b'.zip', None)), 50 (b'gz', (b'application/x-gzip', b'tgz', b'.tar.gz', None)), 51 (b'bz2', (b'application/x-bzip2', b'tbz2', b'.tar.bz2', None)), 52 ) 53) 54 55 56def archivelist(ui, nodeid, url=None): 57 allowed = ui.configlist(b'web', b'allow-archive', untrusted=True) 58 archives = [] 59 60 for typ, spec in pycompat.iteritems(archivespecs): 61 if typ in allowed or ui.configbool( 62 b'web', b'allow' + typ, untrusted=True 63 ): 64 archives.append( 65 { 66 b'type': typ, 67 b'extension': spec[2], 68 b'node': nodeid, 69 b'url': url, 70 } 71 ) 72 73 return templateutil.mappinglist(archives) 74 75 76def up(p): 77 if p[0:1] != b"/": 78 p = b"/" + p 79 if p[-1:] == b"/": 80 p = p[:-1] 81 up = os.path.dirname(p) 82 if up == b"/": 83 return b"/" 84 return up + b"/" 85 86 87def _navseq(step, firststep=None): 88 if firststep: 89 yield firststep 90 if firststep >= 20 and firststep <= 40: 91 firststep = 50 92 yield firststep 93 assert step > 0 94 assert firststep > 0 95 while step <= firststep: 96 step *= 10 97 while True: 98 yield 1 * step 99 yield 3 * step 100 step *= 10 101 102 103class revnav(object): 104 def __init__(self, repo): 105 """Navigation generation object 106 107 :repo: repo object we generate nav for 108 """ 109 # used for hex generation 110 self._revlog = repo.changelog 111 112 def __nonzero__(self): 113 """return True if any revision to navigate over""" 114 return self._first() is not None 115 116 __bool__ = __nonzero__ 117 118 def _first(self): 119 """return the minimum non-filtered changeset or None""" 120 try: 121 return next(iter(self._revlog)) 122 except StopIteration: 123 return None 124 125 def hex(self, rev): 126 return hex(self._revlog.node(rev)) 127 128 def gen(self, pos, pagelen, limit): 129 """computes label and revision id for navigation link 130 131 :pos: is the revision relative to which we generate navigation. 132 :pagelen: the size of each navigation page 133 :limit: how far shall we link 134 135 The return is: 136 - a single element mappinglist 137 - containing a dictionary with a `before` and `after` key 138 - values are dictionaries with `label` and `node` keys 139 """ 140 if not self: 141 # empty repo 142 return templateutil.mappinglist( 143 [ 144 { 145 b'before': templateutil.mappinglist([]), 146 b'after': templateutil.mappinglist([]), 147 }, 148 ] 149 ) 150 151 targets = [] 152 for f in _navseq(1, pagelen): 153 if f > limit: 154 break 155 targets.append(pos + f) 156 targets.append(pos - f) 157 targets.sort() 158 159 first = self._first() 160 navbefore = [{b'label': b'(%i)' % first, b'node': self.hex(first)}] 161 navafter = [] 162 for rev in targets: 163 if rev not in self._revlog: 164 continue 165 if pos < rev < limit: 166 navafter.append( 167 {b'label': b'+%d' % abs(rev - pos), b'node': self.hex(rev)} 168 ) 169 if 0 < rev < pos: 170 navbefore.append( 171 {b'label': b'-%d' % abs(rev - pos), b'node': self.hex(rev)} 172 ) 173 174 navafter.append({b'label': b'tip', b'node': b'tip'}) 175 176 # TODO: maybe this can be a scalar object supporting tomap() 177 return templateutil.mappinglist( 178 [ 179 { 180 b'before': templateutil.mappinglist(navbefore), 181 b'after': templateutil.mappinglist(navafter), 182 }, 183 ] 184 ) 185 186 187class filerevnav(revnav): 188 def __init__(self, repo, path): 189 """Navigation generation object 190 191 :repo: repo object we generate nav for 192 :path: path of the file we generate nav for 193 """ 194 # used for iteration 195 self._changelog = repo.unfiltered().changelog 196 # used for hex generation 197 self._revlog = repo.file(path) 198 199 def hex(self, rev): 200 return hex(self._changelog.node(self._revlog.linkrev(rev))) 201 202 203# TODO: maybe this can be a wrapper class for changectx/filectx list, which 204# yields {'ctx': ctx} 205def _ctxsgen(context, ctxs): 206 for s in ctxs: 207 d = { 208 b'node': s.hex(), 209 b'rev': s.rev(), 210 b'user': s.user(), 211 b'date': s.date(), 212 b'description': s.description(), 213 b'branch': s.branch(), 214 } 215 if util.safehasattr(s, b'path'): 216 d[b'file'] = s.path() 217 yield d 218 219 220def _siblings(siblings=None, hiderev=None): 221 if siblings is None: 222 siblings = [] 223 siblings = [s for s in siblings if s.node() != s.repo().nullid] 224 if len(siblings) == 1 and siblings[0].rev() == hiderev: 225 siblings = [] 226 return templateutil.mappinggenerator(_ctxsgen, args=(siblings,)) 227 228 229def difffeatureopts(req, ui, section): 230 diffopts = diffutil.difffeatureopts( 231 ui, untrusted=True, section=section, whitespace=True 232 ) 233 234 for k in ( 235 b'ignorews', 236 b'ignorewsamount', 237 b'ignorewseol', 238 b'ignoreblanklines', 239 ): 240 v = req.qsparams.get(k) 241 if v is not None: 242 v = stringutil.parsebool(v) 243 setattr(diffopts, k, v if v is not None else True) 244 245 return diffopts 246 247 248def annotate(req, fctx, ui): 249 diffopts = difffeatureopts(req, ui, b'annotate') 250 return fctx.annotate(follow=True, diffopts=diffopts) 251 252 253def parents(ctx, hide=None): 254 if isinstance(ctx, context.basefilectx): 255 introrev = ctx.introrev() 256 if ctx.changectx().rev() != introrev: 257 return _siblings([ctx.repo()[introrev]], hide) 258 return _siblings(ctx.parents(), hide) 259 260 261def children(ctx, hide=None): 262 return _siblings(ctx.children(), hide) 263 264 265def renamelink(fctx): 266 r = fctx.renamed() 267 if r: 268 return templateutil.mappinglist([{b'file': r[0], b'node': hex(r[1])}]) 269 return templateutil.mappinglist([]) 270 271 272def nodetagsdict(repo, node): 273 return templateutil.hybridlist(repo.nodetags(node), name=b'name') 274 275 276def nodebookmarksdict(repo, node): 277 return templateutil.hybridlist(repo.nodebookmarks(node), name=b'name') 278 279 280def nodebranchdict(repo, ctx): 281 branches = [] 282 branch = ctx.branch() 283 # If this is an empty repo, ctx.node() == nullid, 284 # ctx.branch() == 'default'. 285 try: 286 branchnode = repo.branchtip(branch) 287 except error.RepoLookupError: 288 branchnode = None 289 if branchnode == ctx.node(): 290 branches.append(branch) 291 return templateutil.hybridlist(branches, name=b'name') 292 293 294def nodeinbranch(repo, ctx): 295 branches = [] 296 branch = ctx.branch() 297 try: 298 branchnode = repo.branchtip(branch) 299 except error.RepoLookupError: 300 branchnode = None 301 if branch != b'default' and branchnode != ctx.node(): 302 branches.append(branch) 303 return templateutil.hybridlist(branches, name=b'name') 304 305 306def nodebranchnodefault(ctx): 307 branches = [] 308 branch = ctx.branch() 309 if branch != b'default': 310 branches.append(branch) 311 return templateutil.hybridlist(branches, name=b'name') 312 313 314def _nodenamesgen(context, f, node, name): 315 for t in f(node): 316 yield {name: t} 317 318 319def showtag(repo, t1, node=None): 320 if node is None: 321 node = repo.nullid 322 args = (repo.nodetags, node, b'tag') 323 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1) 324 325 326def showbookmark(repo, t1, node=None): 327 if node is None: 328 node = repo.nullid 329 args = (repo.nodebookmarks, node, b'bookmark') 330 return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1) 331 332 333def branchentries(repo, stripecount, limit=0): 334 tips = [] 335 heads = repo.heads() 336 parity = paritygen(stripecount) 337 sortkey = lambda item: (not item[1], item[0].rev()) 338 339 def entries(context): 340 count = 0 341 if not tips: 342 for tag, hs, tip, closed in repo.branchmap().iterbranches(): 343 tips.append((repo[tip], closed)) 344 for ctx, closed in sorted(tips, key=sortkey, reverse=True): 345 if limit > 0 and count >= limit: 346 return 347 count += 1 348 if closed: 349 status = b'closed' 350 elif ctx.node() not in heads: 351 status = b'inactive' 352 else: 353 status = b'open' 354 yield { 355 b'parity': next(parity), 356 b'branch': ctx.branch(), 357 b'status': status, 358 b'node': ctx.hex(), 359 b'date': ctx.date(), 360 } 361 362 return templateutil.mappinggenerator(entries) 363 364 365def cleanpath(repo, path): 366 path = path.lstrip(b'/') 367 auditor = pathutil.pathauditor(repo.root, realfs=False) 368 return pathutil.canonpath(repo.root, b'', path, auditor=auditor) 369 370 371def changectx(repo, req): 372 changeid = b"tip" 373 if b'node' in req.qsparams: 374 changeid = req.qsparams[b'node'] 375 ipos = changeid.find(b':') 376 if ipos != -1: 377 changeid = changeid[(ipos + 1) :] 378 379 return scmutil.revsymbol(repo, changeid) 380 381 382def basechangectx(repo, req): 383 if b'node' in req.qsparams: 384 changeid = req.qsparams[b'node'] 385 ipos = changeid.find(b':') 386 if ipos != -1: 387 changeid = changeid[:ipos] 388 return scmutil.revsymbol(repo, changeid) 389 390 return None 391 392 393def filectx(repo, req): 394 if b'file' not in req.qsparams: 395 raise ErrorResponse(HTTP_NOT_FOUND, b'file not given') 396 path = cleanpath(repo, req.qsparams[b'file']) 397 if b'node' in req.qsparams: 398 changeid = req.qsparams[b'node'] 399 elif b'filenode' in req.qsparams: 400 changeid = req.qsparams[b'filenode'] 401 else: 402 raise ErrorResponse(HTTP_NOT_FOUND, b'node or filenode not given') 403 try: 404 fctx = scmutil.revsymbol(repo, changeid)[path] 405 except error.RepoError: 406 fctx = repo.filectx(path, fileid=changeid) 407 408 return fctx 409 410 411def linerange(req): 412 linerange = req.qsparams.getall(b'linerange') 413 if not linerange: 414 return None 415 if len(linerange) > 1: 416 raise ErrorResponse(HTTP_BAD_REQUEST, b'redundant linerange parameter') 417 try: 418 fromline, toline = map(int, linerange[0].split(b':', 1)) 419 except ValueError: 420 raise ErrorResponse(HTTP_BAD_REQUEST, b'invalid linerange parameter') 421 try: 422 return util.processlinerange(fromline, toline) 423 except error.ParseError as exc: 424 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc)) 425 426 427def formatlinerange(fromline, toline): 428 return b'%d:%d' % (fromline + 1, toline) 429 430 431def _succsandmarkersgen(context, mapping): 432 repo = context.resource(mapping, b'repo') 433 itemmappings = templatekw.showsuccsandmarkers(context, mapping) 434 for item in itemmappings.tovalue(context, mapping): 435 item[b'successors'] = _siblings( 436 repo[successor] for successor in item[b'successors'] 437 ) 438 yield item 439 440 441def succsandmarkers(context, mapping): 442 return templateutil.mappinggenerator(_succsandmarkersgen, args=(mapping,)) 443 444 445# teach templater succsandmarkers is switched to (context, mapping) API 446succsandmarkers._requires = {b'repo', b'ctx'} 447 448 449def _whyunstablegen(context, mapping): 450 repo = context.resource(mapping, b'repo') 451 ctx = context.resource(mapping, b'ctx') 452 453 entries = obsutil.whyunstable(repo, ctx) 454 for entry in entries: 455 if entry.get(b'divergentnodes'): 456 entry[b'divergentnodes'] = _siblings(entry[b'divergentnodes']) 457 yield entry 458 459 460def whyunstable(context, mapping): 461 return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,)) 462 463 464whyunstable._requires = {b'repo', b'ctx'} 465 466 467def commonentry(repo, ctx): 468 node = scmutil.binnode(ctx) 469 return { 470 # TODO: perhaps ctx.changectx() should be assigned if ctx is a 471 # filectx, but I'm not pretty sure if that would always work because 472 # fctx.parents() != fctx.changectx.parents() for example. 473 b'ctx': ctx, 474 b'rev': ctx.rev(), 475 b'node': hex(node), 476 b'author': ctx.user(), 477 b'desc': ctx.description(), 478 b'date': ctx.date(), 479 b'extra': ctx.extra(), 480 b'phase': ctx.phasestr(), 481 b'obsolete': ctx.obsolete(), 482 b'succsandmarkers': succsandmarkers, 483 b'instabilities': templateutil.hybridlist( 484 ctx.instabilities(), name=b'instability' 485 ), 486 b'whyunstable': whyunstable, 487 b'branch': nodebranchnodefault(ctx), 488 b'inbranch': nodeinbranch(repo, ctx), 489 b'branches': nodebranchdict(repo, ctx), 490 b'tags': nodetagsdict(repo, node), 491 b'bookmarks': nodebookmarksdict(repo, node), 492 b'parent': lambda context, mapping: parents(ctx), 493 b'child': lambda context, mapping: children(ctx), 494 } 495 496 497def changelistentry(web, ctx): 498 """Obtain a dictionary to be used for entries in a changelist. 499 500 This function is called when producing items for the "entries" list passed 501 to the "shortlog" and "changelog" templates. 502 """ 503 repo = web.repo 504 rev = ctx.rev() 505 n = scmutil.binnode(ctx) 506 showtags = showtag(repo, b'changelogtag', n) 507 files = listfilediffs(ctx.files(), n, web.maxfiles) 508 509 entry = commonentry(repo, ctx) 510 entry.update( 511 { 512 b'allparents': lambda context, mapping: parents(ctx), 513 b'parent': lambda context, mapping: parents(ctx, rev - 1), 514 b'child': lambda context, mapping: children(ctx, rev + 1), 515 b'changelogtag': showtags, 516 b'files': files, 517 } 518 ) 519 return entry 520 521 522def changelistentries(web, revs, maxcount, parityfn): 523 """Emit up to N records for an iterable of revisions.""" 524 repo = web.repo 525 526 count = 0 527 for rev in revs: 528 if count >= maxcount: 529 break 530 531 count += 1 532 533 entry = changelistentry(web, repo[rev]) 534 entry[b'parity'] = next(parityfn) 535 536 yield entry 537 538 539def symrevorshortnode(req, ctx): 540 if b'node' in req.qsparams: 541 return templatefilters.revescape(req.qsparams[b'node']) 542 else: 543 return short(scmutil.binnode(ctx)) 544 545 546def _listfilesgen(context, ctx, stripecount): 547 parity = paritygen(stripecount) 548 filesadded = ctx.filesadded() 549 for blockno, f in enumerate(ctx.files()): 550 if f not in ctx: 551 status = b'removed' 552 elif f in filesadded: 553 status = b'added' 554 else: 555 status = b'modified' 556 template = b'filenolink' if status == b'removed' else b'filenodelink' 557 yield context.process( 558 template, 559 { 560 b'node': ctx.hex(), 561 b'file': f, 562 b'blockno': blockno + 1, 563 b'parity': next(parity), 564 b'status': status, 565 }, 566 ) 567 568 569def changesetentry(web, ctx): 570 '''Obtain a dictionary to be used to render the "changeset" template.''' 571 572 showtags = showtag(web.repo, b'changesettag', scmutil.binnode(ctx)) 573 showbookmarks = showbookmark( 574 web.repo, b'changesetbookmark', scmutil.binnode(ctx) 575 ) 576 showbranch = nodebranchnodefault(ctx) 577 578 basectx = basechangectx(web.repo, web.req) 579 if basectx is None: 580 basectx = ctx.p1() 581 582 style = web.config(b'web', b'style') 583 if b'style' in web.req.qsparams: 584 style = web.req.qsparams[b'style'] 585 586 diff = diffs(web, ctx, basectx, None, style) 587 588 parity = paritygen(web.stripecount) 589 diffstatsgen = diffstatgen(web.repo.ui, ctx, basectx) 590 diffstats = diffstat(ctx, diffstatsgen, parity) 591 592 return dict( 593 diff=diff, 594 symrev=symrevorshortnode(web.req, ctx), 595 basenode=basectx.hex(), 596 changesettag=showtags, 597 changesetbookmark=showbookmarks, 598 changesetbranch=showbranch, 599 files=templateutil.mappedgenerator( 600 _listfilesgen, args=(ctx, web.stripecount) 601 ), 602 diffsummary=lambda context, mapping: diffsummary(diffstatsgen), 603 diffstat=diffstats, 604 archives=web.archivelist(ctx.hex()), 605 **pycompat.strkwargs(commonentry(web.repo, ctx)) 606 ) 607 608 609def _listfilediffsgen(context, files, node, max): 610 for f in files[:max]: 611 yield context.process(b'filedifflink', {b'node': hex(node), b'file': f}) 612 if len(files) > max: 613 yield context.process(b'fileellipses', {}) 614 615 616def listfilediffs(files, node, max): 617 return templateutil.mappedgenerator( 618 _listfilediffsgen, args=(files, node, max) 619 ) 620 621 622def _prettyprintdifflines(context, lines, blockno, lineidprefix): 623 for lineno, l in enumerate(lines, 1): 624 difflineno = b"%d.%d" % (blockno, lineno) 625 if l.startswith(b'+'): 626 ltype = b"difflineplus" 627 elif l.startswith(b'-'): 628 ltype = b"difflineminus" 629 elif l.startswith(b'@'): 630 ltype = b"difflineat" 631 else: 632 ltype = b"diffline" 633 yield context.process( 634 ltype, 635 { 636 b'line': l, 637 b'lineno': lineno, 638 b'lineid': lineidprefix + b"l%s" % difflineno, 639 b'linenumber': b"% 8s" % difflineno, 640 }, 641 ) 642 643 644def _diffsgen( 645 context, 646 repo, 647 ctx, 648 basectx, 649 files, 650 style, 651 stripecount, 652 linerange, 653 lineidprefix, 654): 655 if files: 656 m = match.exact(files) 657 else: 658 m = match.always() 659 660 diffopts = patch.diffopts(repo.ui, untrusted=True) 661 parity = paritygen(stripecount) 662 663 diffhunks = patch.diffhunks(repo, basectx, ctx, m, opts=diffopts) 664 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1): 665 if style != b'raw': 666 header = header[1:] 667 lines = [h + b'\n' for h in header] 668 for hunkrange, hunklines in hunks: 669 if linerange is not None and hunkrange is not None: 670 s1, l1, s2, l2 = hunkrange 671 if not mdiff.hunkinrange((s2, l2), linerange): 672 continue 673 lines.extend(hunklines) 674 if lines: 675 l = templateutil.mappedgenerator( 676 _prettyprintdifflines, args=(lines, blockno, lineidprefix) 677 ) 678 yield { 679 b'parity': next(parity), 680 b'blockno': blockno, 681 b'lines': l, 682 } 683 684 685def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=b''): 686 args = ( 687 web.repo, 688 ctx, 689 basectx, 690 files, 691 style, 692 web.stripecount, 693 linerange, 694 lineidprefix, 695 ) 696 return templateutil.mappinggenerator( 697 _diffsgen, args=args, name=b'diffblock' 698 ) 699 700 701def _compline(type, leftlineno, leftline, rightlineno, rightline): 702 lineid = leftlineno and (b"l%d" % leftlineno) or b'' 703 lineid += rightlineno and (b"r%d" % rightlineno) or b'' 704 llno = b'%d' % leftlineno if leftlineno else b'' 705 rlno = b'%d' % rightlineno if rightlineno else b'' 706 return { 707 b'type': type, 708 b'lineid': lineid, 709 b'leftlineno': leftlineno, 710 b'leftlinenumber': b"% 6s" % llno, 711 b'leftline': leftline or b'', 712 b'rightlineno': rightlineno, 713 b'rightlinenumber': b"% 6s" % rlno, 714 b'rightline': rightline or b'', 715 } 716 717 718def _getcompblockgen(context, leftlines, rightlines, opcodes): 719 for type, llo, lhi, rlo, rhi in opcodes: 720 type = pycompat.sysbytes(type) 721 len1 = lhi - llo 722 len2 = rhi - rlo 723 count = min(len1, len2) 724 for i in pycompat.xrange(count): 725 yield _compline( 726 type=type, 727 leftlineno=llo + i + 1, 728 leftline=leftlines[llo + i], 729 rightlineno=rlo + i + 1, 730 rightline=rightlines[rlo + i], 731 ) 732 if len1 > len2: 733 for i in pycompat.xrange(llo + count, lhi): 734 yield _compline( 735 type=type, 736 leftlineno=i + 1, 737 leftline=leftlines[i], 738 rightlineno=None, 739 rightline=None, 740 ) 741 elif len2 > len1: 742 for i in pycompat.xrange(rlo + count, rhi): 743 yield _compline( 744 type=type, 745 leftlineno=None, 746 leftline=None, 747 rightlineno=i + 1, 748 rightline=rightlines[i], 749 ) 750 751 752def _getcompblock(leftlines, rightlines, opcodes): 753 args = (leftlines, rightlines, opcodes) 754 return templateutil.mappinggenerator( 755 _getcompblockgen, args=args, name=b'comparisonline' 756 ) 757 758 759def _comparegen(context, contextnum, leftlines, rightlines): 760 '''Generator function that provides side-by-side comparison data.''' 761 s = difflib.SequenceMatcher(None, leftlines, rightlines) 762 if contextnum < 0: 763 l = _getcompblock(leftlines, rightlines, s.get_opcodes()) 764 yield {b'lines': l} 765 else: 766 for oc in s.get_grouped_opcodes(n=contextnum): 767 l = _getcompblock(leftlines, rightlines, oc) 768 yield {b'lines': l} 769 770 771def compare(contextnum, leftlines, rightlines): 772 args = (contextnum, leftlines, rightlines) 773 return templateutil.mappinggenerator( 774 _comparegen, args=args, name=b'comparisonblock' 775 ) 776 777 778def diffstatgen(ui, ctx, basectx): 779 '''Generator function that provides the diffstat data.''' 780 781 diffopts = patch.diffopts(ui, {b'noprefix': False}) 782 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx, opts=diffopts))) 783 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats) 784 while True: 785 yield stats, maxname, maxtotal, addtotal, removetotal, binary 786 787 788def diffsummary(statgen): 789 '''Return a short summary of the diff.''' 790 791 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen) 792 return _(b' %d files changed, %d insertions(+), %d deletions(-)\n') % ( 793 len(stats), 794 addtotal, 795 removetotal, 796 ) 797 798 799def _diffstattmplgen(context, ctx, statgen, parity): 800 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen) 801 files = ctx.files() 802 803 def pct(i): 804 if maxtotal == 0: 805 return 0 806 return (float(i) / maxtotal) * 100 807 808 fileno = 0 809 for filename, adds, removes, isbinary in stats: 810 template = b'diffstatlink' if filename in files else b'diffstatnolink' 811 total = adds + removes 812 fileno += 1 813 yield context.process( 814 template, 815 { 816 b'node': ctx.hex(), 817 b'file': filename, 818 b'fileno': fileno, 819 b'total': total, 820 b'addpct': pct(adds), 821 b'removepct': pct(removes), 822 b'parity': next(parity), 823 }, 824 ) 825 826 827def diffstat(ctx, statgen, parity): 828 '''Return a diffstat template for each file in the diff.''' 829 args = (ctx, statgen, parity) 830 return templateutil.mappedgenerator(_diffstattmplgen, args=args) 831 832 833class sessionvars(templateutil.wrapped): 834 def __init__(self, vars, start=b'?'): 835 self._start = start 836 self._vars = vars 837 838 def __getitem__(self, key): 839 return self._vars[key] 840 841 def __setitem__(self, key, value): 842 self._vars[key] = value 843 844 def __copy__(self): 845 return sessionvars(copy.copy(self._vars), self._start) 846 847 def contains(self, context, mapping, item): 848 item = templateutil.unwrapvalue(context, mapping, item) 849 return item in self._vars 850 851 def getmember(self, context, mapping, key): 852 key = templateutil.unwrapvalue(context, mapping, key) 853 return self._vars.get(key) 854 855 def getmin(self, context, mapping): 856 raise error.ParseError(_(b'not comparable')) 857 858 def getmax(self, context, mapping): 859 raise error.ParseError(_(b'not comparable')) 860 861 def filter(self, context, mapping, select): 862 # implement if necessary 863 raise error.ParseError(_(b'not filterable')) 864 865 def itermaps(self, context): 866 separator = self._start 867 for key, value in sorted(pycompat.iteritems(self._vars)): 868 yield { 869 b'name': key, 870 b'value': pycompat.bytestr(value), 871 b'separator': separator, 872 } 873 separator = b'&' 874 875 def join(self, context, mapping, sep): 876 # could be '{separator}{name}={value|urlescape}' 877 raise error.ParseError(_(b'not displayable without template')) 878 879 def show(self, context, mapping): 880 return self.join(context, mapping, b'') 881 882 def tobool(self, context, mapping): 883 return bool(self._vars) 884 885 def tovalue(self, context, mapping): 886 return self._vars 887 888 889class wsgiui(uimod.ui): 890 # default termwidth breaks under mod_wsgi 891 def termwidth(self): 892 return 80 893 894 895def getwebsubs(repo): 896 websubtable = [] 897 websubdefs = repo.ui.configitems(b'websub') 898 # we must maintain interhg backwards compatibility 899 websubdefs += repo.ui.configitems(b'interhg') 900 for key, pattern in websubdefs: 901 # grab the delimiter from the character after the "s" 902 unesc = pattern[1:2] 903 delim = stringutil.reescape(unesc) 904 905 # identify portions of the pattern, taking care to avoid escaped 906 # delimiters. the replace format and flags are optional, but 907 # delimiters are required. 908 match = re.match( 909 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$' 910 % (delim, delim, delim), 911 pattern, 912 ) 913 if not match: 914 repo.ui.warn( 915 _(b"websub: invalid pattern for %s: %s\n") % (key, pattern) 916 ) 917 continue 918 919 # we need to unescape the delimiter for regexp and format 920 delim_re = re.compile(br'(?<!\\)\\%s' % delim) 921 regexp = delim_re.sub(unesc, match.group(1)) 922 format = delim_re.sub(unesc, match.group(2)) 923 924 # the pattern allows for 6 regexp flags, so set them if necessary 925 flagin = match.group(3) 926 flags = 0 927 if flagin: 928 for flag in pycompat.sysstr(flagin.upper()): 929 flags |= re.__dict__[flag] 930 931 try: 932 regexp = re.compile(regexp, flags) 933 websubtable.append((regexp, format)) 934 except re.error: 935 repo.ui.warn( 936 _(b"websub: invalid regexp for %s: %s\n") % (key, regexp) 937 ) 938 return websubtable 939 940 941def getgraphnode(repo, ctx): 942 return templatekw.getgraphnodecurrent( 943 repo, ctx, {} 944 ) + templatekw.getgraphnodesymbol(ctx) 945