comparison mercurial/hgweb/webcommands.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents 0bd56c291359
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
37 smartset, 37 smartset,
38 templater, 38 templater,
39 templateutil, 39 templateutil,
40 ) 40 )
41 41
42 from ..utils import ( 42 from ..utils import stringutil
43 stringutil, 43
44 ) 44 from . import webutil
45
46 from . import (
47 webutil,
48 )
49 45
50 __all__ = [] 46 __all__ = []
51 commands = {} 47 commands = {}
48
52 49
53 class webcommand(object): 50 class webcommand(object):
54 """Decorator used to register a web command handler. 51 """Decorator used to register a web command handler.
55 52
56 The decorator takes as its positional arguments the name/path the 53 The decorator takes as its positional arguments the name/path the
79 def __call__(self, func): 76 def __call__(self, func):
80 __all__.append(self.name) 77 __all__.append(self.name)
81 commands[self.name] = func 78 commands[self.name] = func
82 return func 79 return func
83 80
81
84 @webcommand('log') 82 @webcommand('log')
85 def log(web): 83 def log(web):
86 """ 84 """
87 /log[/{revision}[/{path}]] 85 /log[/{revision}[/{path}]]
88 -------------------------- 86 --------------------------
100 98
101 if web.req.qsparams.get('file'): 99 if web.req.qsparams.get('file'):
102 return filelog(web) 100 return filelog(web)
103 else: 101 else:
104 return changelog(web) 102 return changelog(web)
103
105 104
106 @webcommand('rawfile') 105 @webcommand('rawfile')
107 def rawfile(web): 106 def rawfile(web):
108 guessmime = web.configbool('web', 'guessmime') 107 guessmime = web.configbool('web', 'guessmime')
109 108
134 133
135 if mt.startswith('text/'): 134 if mt.startswith('text/'):
136 mt += '; charset="%s"' % encoding.encoding 135 mt += '; charset="%s"' % encoding.encoding
137 136
138 web.res.headers['Content-Type'] = mt 137 web.res.headers['Content-Type'] = mt
139 filename = (path.rpartition('/')[-1] 138 filename = (
140 .replace('\\', '\\\\').replace('"', '\\"')) 139 path.rpartition('/')[-1].replace('\\', '\\\\').replace('"', '\\"')
140 )
141 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename 141 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
142 web.res.setbodybytes(text) 142 web.res.setbodybytes(text)
143 return web.res.sendresponse() 143 return web.res.sendresponse()
144
144 145
145 def _filerevision(web, fctx): 146 def _filerevision(web, fctx):
146 f = fctx.path() 147 f = fctx.path()
147 text = fctx.data() 148 text = fctx.data()
148 parity = paritygen(web.stripecount) 149 parity = paritygen(web.stripecount)
149 ishead = fctx.filenode() in fctx.filelog().heads() 150 ishead = fctx.filenode() in fctx.filelog().heads()
150 151
151 if stringutil.binary(text): 152 if stringutil.binary(text):
152 mt = pycompat.sysbytes( 153 mt = pycompat.sysbytes(
153 mimetypes.guess_type(pycompat.fsdecode(f))[0] 154 mimetypes.guess_type(pycompat.fsdecode(f))[0]
154 or r'application/octet-stream') 155 or r'application/octet-stream'
156 )
155 text = '(binary:%s)' % mt 157 text = '(binary:%s)' % mt
156 158
157 def lines(context): 159 def lines(context):
158 for lineno, t in enumerate(text.splitlines(True)): 160 for lineno, t in enumerate(text.splitlines(True)):
159 yield {"line": t, 161 yield {
160 "lineid": "l%d" % (lineno + 1), 162 "line": t,
161 "linenumber": "% 6d" % (lineno + 1), 163 "lineid": "l%d" % (lineno + 1),
162 "parity": next(parity)} 164 "linenumber": "% 6d" % (lineno + 1),
165 "parity": next(parity),
166 }
163 167
164 return web.sendtemplate( 168 return web.sendtemplate(
165 'filerevision', 169 'filerevision',
166 file=f, 170 file=f,
167 path=webutil.up(f), 171 path=webutil.up(f),
168 text=templateutil.mappinggenerator(lines), 172 text=templateutil.mappinggenerator(lines),
169 symrev=webutil.symrevorshortnode(web.req, fctx), 173 symrev=webutil.symrevorshortnode(web.req, fctx),
170 rename=webutil.renamelink(fctx), 174 rename=webutil.renamelink(fctx),
171 permissions=fctx.manifest().flags(f), 175 permissions=fctx.manifest().flags(f),
172 ishead=int(ishead), 176 ishead=int(ishead),
173 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))) 177 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
178 )
179
174 180
175 @webcommand('file') 181 @webcommand('file')
176 def file(web): 182 def file(web):
177 """ 183 """
178 /file/{revision}[/{path}] 184 /file/{revision}[/{path}]
204 try: 210 try:
205 return manifest(web) 211 return manifest(web)
206 except ErrorResponse: 212 except ErrorResponse:
207 raise inst 213 raise inst
208 214
215
209 def _search(web): 216 def _search(web):
210 MODE_REVISION = 'rev' 217 MODE_REVISION = 'rev'
211 MODE_KEYWORD = 'keyword' 218 MODE_KEYWORD = 'keyword'
212 MODE_REVSET = 'revset' 219 MODE_REVSET = 'revset'
213 220
230 yield e 237 yield e
231 238
232 for ctx in revgen(): 239 for ctx in revgen():
233 miss = 0 240 miss = 0
234 for q in qw: 241 for q in qw:
235 if not (q in lower(ctx.user()) or 242 if not (
236 q in lower(ctx.description()) or 243 q in lower(ctx.user())
237 q in lower(" ".join(ctx.files()))): 244 or q in lower(ctx.description())
245 or q in lower(" ".join(ctx.files()))
246 ):
238 miss = 1 247 miss = 1
239 break 248 break
240 if miss: 249 if miss:
241 continue 250 continue
242 251
271 280
272 if revsetlang.depth(tree) <= 2: 281 if revsetlang.depth(tree) <= 2:
273 # no revset syntax used 282 # no revset syntax used
274 return MODE_KEYWORD, query 283 return MODE_KEYWORD, query
275 284
276 if any((token, (value or '')[:3]) == ('string', 're:') 285 if any(
277 for token, value, pos in revsetlang.tokenize(revdef)): 286 (token, (value or '')[:3]) == ('string', 're:')
287 for token, value, pos in revsetlang.tokenize(revdef)
288 ):
278 return MODE_KEYWORD, query 289 return MODE_KEYWORD, query
279 290
280 funcsused = revsetlang.funcsused(tree) 291 funcsused = revsetlang.funcsused(tree)
281 if not funcsused.issubset(revset.safesymbols): 292 if not funcsused.issubset(revset.safesymbols):
282 return MODE_KEYWORD, query 293 return MODE_KEYWORD, query
283 294
284 try: 295 try:
285 mfunc = revset.match(web.repo.ui, revdef, 296 mfunc = revset.match(
286 lookup=revset.lookupfn(web.repo)) 297 web.repo.ui, revdef, lookup=revset.lookupfn(web.repo)
298 )
287 revs = mfunc(web.repo) 299 revs = mfunc(web.repo)
288 return MODE_REVSET, revs 300 return MODE_REVSET, revs
289 # ParseError: wrongly placed tokens, wrongs arguments, etc 301 # ParseError: wrongly placed tokens, wrongs arguments, etc
290 # RepoLookupError: no such revision, e.g. in 'revision:' 302 # RepoLookupError: no such revision, e.g. in 'revision:'
291 # Abort: bookmark/tag not exists 303 # Abort: bookmark/tag not exists
292 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo 304 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
293 except (error.ParseError, error.RepoLookupError, error.Abort, 305 except (
294 LookupError): 306 error.ParseError,
307 error.RepoLookupError,
308 error.Abort,
309 LookupError,
310 ):
295 return MODE_KEYWORD, query 311 return MODE_KEYWORD, query
296 312
297 def changelist(context): 313 def changelist(context):
298 count = 0 314 count = 0
299 315
302 n = scmutil.binnode(ctx) 318 n = scmutil.binnode(ctx)
303 showtags = webutil.showtag(web.repo, 'changelogtag', n) 319 showtags = webutil.showtag(web.repo, 'changelogtag', n)
304 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles) 320 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles)
305 321
306 lm = webutil.commonentry(web.repo, ctx) 322 lm = webutil.commonentry(web.repo, ctx)
307 lm.update({ 323 lm.update(
308 'parity': next(parity), 324 {
309 'changelogtag': showtags, 325 'parity': next(parity),
310 'files': files, 326 'changelogtag': showtags,
311 }) 327 'files': files,
328 }
329 )
312 yield lm 330 yield lm
313 331
314 if count >= revcount: 332 if count >= revcount:
315 break 333 break
316 334
359 archives=web.archivelist('tip'), 377 archives=web.archivelist('tip'),
360 morevars=morevars, 378 morevars=morevars,
361 lessvars=lessvars, 379 lessvars=lessvars,
362 modedesc=searchfunc[1], 380 modedesc=searchfunc[1],
363 showforcekw=showforcekw, 381 showforcekw=showforcekw,
364 showunforcekw=showunforcekw) 382 showunforcekw=showunforcekw,
383 )
384
365 385
366 @webcommand('changelog') 386 @webcommand('changelog')
367 def changelog(web, shortlog=False): 387 def changelog(web, shortlog=False):
368 """ 388 """
369 /changelog[/{revision}] 389 /changelog[/{revision}]
451 nextentry=templateutil.mappinglist(nextentry), 471 nextentry=templateutil.mappinglist(nextentry),
452 archives=web.archivelist('tip'), 472 archives=web.archivelist('tip'),
453 revcount=revcount, 473 revcount=revcount,
454 morevars=morevars, 474 morevars=morevars,
455 lessvars=lessvars, 475 lessvars=lessvars,
456 query=query) 476 query=query,
477 )
478
457 479
458 @webcommand('shortlog') 480 @webcommand('shortlog')
459 def shortlog(web): 481 def shortlog(web):
460 """ 482 """
461 /shortlog 483 /shortlog
467 difference is the ``shortlog`` template will be rendered instead of the 489 difference is the ``shortlog`` template will be rendered instead of the
468 ``changelog`` template. 490 ``changelog`` template.
469 """ 491 """
470 return changelog(web, shortlog=True) 492 return changelog(web, shortlog=True)
471 493
494
472 @webcommand('changeset') 495 @webcommand('changeset')
473 def changeset(web): 496 def changeset(web):
474 """ 497 """
475 /changeset[/{revision}] 498 /changeset[/{revision}]
476 ----------------------- 499 -----------------------
485 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many 508 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
486 templates related to diffs may all be used to produce the output. 509 templates related to diffs may all be used to produce the output.
487 """ 510 """
488 ctx = webutil.changectx(web.repo, web.req) 511 ctx = webutil.changectx(web.repo, web.req)
489 512
490 return web.sendtemplate( 513 return web.sendtemplate('changeset', **webutil.changesetentry(web, ctx))
491 'changeset', 514
492 **webutil.changesetentry(web, ctx))
493 515
494 rev = webcommand('rev')(changeset) 516 rev = webcommand('rev')(changeset)
517
495 518
496 def decodepath(path): 519 def decodepath(path):
497 """Hook for mapping a path in the repository to a path in the 520 """Hook for mapping a path in the repository to a path in the
498 working copy. 521 working copy.
499 522
500 Extensions (e.g., largefiles) can override this to remap files in 523 Extensions (e.g., largefiles) can override this to remap files in
501 the virtual file system presented by the manifest command below.""" 524 the virtual file system presented by the manifest command below."""
502 return path 525 return path
526
503 527
504 @webcommand('manifest') 528 @webcommand('manifest')
505 def manifest(web): 529 def manifest(web):
506 """ 530 """
507 /manifest[/{revision}[/{path}]] 531 /manifest[/{revision}[/{path}]]
547 remain = f[l:] 571 remain = f[l:]
548 elements = remain.split('/') 572 elements = remain.split('/')
549 if len(elements) == 1: 573 if len(elements) == 1:
550 files[remain] = full 574 files[remain] = full
551 else: 575 else:
552 h = dirs # need to retain ref to dirs (root) 576 h = dirs # need to retain ref to dirs (root)
553 for elem in elements[0:-1]: 577 for elem in elements[0:-1]:
554 if elem not in h: 578 if elem not in h:
555 h[elem] = {} 579 h[elem] = {}
556 h = h[elem] 580 h = h[elem]
557 if len(h) > 1: 581 if len(h) > 1:
558 break 582 break
559 h[None] = None # denotes files present 583 h[None] = None # denotes files present
560 584
561 if mf and not files and not dirs: 585 if mf and not files and not dirs:
562 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path) 586 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
563 587
564 def filelist(context): 588 def filelist(context):
565 for f in sorted(files): 589 for f in sorted(files):
566 full = files[f] 590 full = files[f]
567 591
568 fctx = ctx.filectx(full) 592 fctx = ctx.filectx(full)
569 yield {"file": full, 593 yield {
570 "parity": next(parity), 594 "file": full,
571 "basename": f, 595 "parity": next(parity),
572 "date": fctx.date(), 596 "basename": f,
573 "size": fctx.size(), 597 "date": fctx.date(),
574 "permissions": mf.flags(full)} 598 "size": fctx.size(),
599 "permissions": mf.flags(full),
600 }
575 601
576 def dirlist(context): 602 def dirlist(context):
577 for d in sorted(dirs): 603 for d in sorted(dirs):
578 604
579 emptydirs = [] 605 emptydirs = []
583 if v: 609 if v:
584 emptydirs.append(k) 610 emptydirs.append(k)
585 h = v 611 h = v
586 612
587 path = "%s%s" % (abspath, d) 613 path = "%s%s" % (abspath, d)
588 yield {"parity": next(parity), 614 yield {
589 "path": path, 615 "parity": next(parity),
590 "emptydirs": "/".join(emptydirs), 616 "path": path,
591 "basename": d} 617 "emptydirs": "/".join(emptydirs),
618 "basename": d,
619 }
592 620
593 return web.sendtemplate( 621 return web.sendtemplate(
594 'manifest', 622 'manifest',
595 symrev=symrev, 623 symrev=symrev,
596 path=abspath, 624 path=abspath,
597 up=webutil.up(abspath), 625 up=webutil.up(abspath),
598 upparity=next(parity), 626 upparity=next(parity),
599 fentries=templateutil.mappinggenerator(filelist), 627 fentries=templateutil.mappinggenerator(filelist),
600 dentries=templateutil.mappinggenerator(dirlist), 628 dentries=templateutil.mappinggenerator(dirlist),
601 archives=web.archivelist(hex(node)), 629 archives=web.archivelist(hex(node)),
602 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))) 630 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
631 )
632
603 633
604 @webcommand('tags') 634 @webcommand('tags')
605 def tags(web): 635 def tags(web):
606 """ 636 """
607 /tags 637 /tags
621 if notip: 651 if notip:
622 t = [(k, n) for k, n in i if k != "tip"] 652 t = [(k, n) for k, n in i if k != "tip"]
623 if latestonly: 653 if latestonly:
624 t = t[:1] 654 t = t[:1]
625 for k, n in t: 655 for k, n in t:
626 yield {"parity": next(parity), 656 yield {
627 "tag": k, 657 "parity": next(parity),
628 "date": web.repo[n].date(), 658 "tag": k,
629 "node": hex(n)} 659 "date": web.repo[n].date(),
660 "node": hex(n),
661 }
630 662
631 return web.sendtemplate( 663 return web.sendtemplate(
632 'tags', 664 'tags',
633 node=hex(web.repo.changelog.tip()), 665 node=hex(web.repo.changelog.tip()),
634 entries=templateutil.mappinggenerator(entries, args=(False, False)), 666 entries=templateutil.mappinggenerator(entries, args=(False, False)),
635 entriesnotip=templateutil.mappinggenerator(entries, 667 entriesnotip=templateutil.mappinggenerator(entries, args=(True, False)),
636 args=(True, False)), 668 latestentry=templateutil.mappinggenerator(entries, args=(True, True)),
637 latestentry=templateutil.mappinggenerator(entries, args=(True, True))) 669 )
670
638 671
639 @webcommand('bookmarks') 672 @webcommand('bookmarks')
640 def bookmarks(web): 673 def bookmarks(web):
641 """ 674 """
642 /bookmarks 675 /bookmarks
656 def entries(context, latestonly): 689 def entries(context, latestonly):
657 t = i 690 t = i
658 if latestonly: 691 if latestonly:
659 t = i[:1] 692 t = i[:1]
660 for k, n in t: 693 for k, n in t:
661 yield {"parity": next(parity), 694 yield {
662 "bookmark": k, 695 "parity": next(parity),
663 "date": web.repo[n].date(), 696 "bookmark": k,
664 "node": hex(n)} 697 "date": web.repo[n].date(),
698 "node": hex(n),
699 }
665 700
666 if i: 701 if i:
667 latestrev = i[0][1] 702 latestrev = i[0][1]
668 else: 703 else:
669 latestrev = -1 704 latestrev = -1
672 return web.sendtemplate( 707 return web.sendtemplate(
673 'bookmarks', 708 'bookmarks',
674 node=hex(web.repo.changelog.tip()), 709 node=hex(web.repo.changelog.tip()),
675 lastchange=templateutil.mappinglist([{'date': lastdate}]), 710 lastchange=templateutil.mappinglist([{'date': lastdate}]),
676 entries=templateutil.mappinggenerator(entries, args=(False,)), 711 entries=templateutil.mappinggenerator(entries, args=(False,)),
677 latestentry=templateutil.mappinggenerator(entries, args=(True,))) 712 latestentry=templateutil.mappinggenerator(entries, args=(True,)),
713 )
714
678 715
679 @webcommand('branches') 716 @webcommand('branches')
680 def branches(web): 717 def branches(web):
681 """ 718 """
682 /branches 719 /branches
695 732
696 return web.sendtemplate( 733 return web.sendtemplate(
697 'branches', 734 'branches',
698 node=hex(web.repo.changelog.tip()), 735 node=hex(web.repo.changelog.tip()),
699 entries=entries, 736 entries=entries,
700 latestentry=latestentry) 737 latestentry=latestentry,
738 )
739
701 740
702 @webcommand('summary') 741 @webcommand('summary')
703 def summary(web): 742 def summary(web):
704 """ 743 """
705 /summary 744 /summary
716 755
717 def tagentries(context): 756 def tagentries(context):
718 parity = paritygen(web.stripecount) 757 parity = paritygen(web.stripecount)
719 count = 0 758 count = 0
720 for k, n in i: 759 for k, n in i:
721 if k == "tip": # skip tip 760 if k == "tip": # skip tip
722 continue 761 continue
723 762
724 count += 1 763 count += 1
725 if count > 10: # limit to 10 tags 764 if count > 10: # limit to 10 tags
726 break 765 break
727 766
728 yield { 767 yield {
729 'parity': next(parity), 768 'parity': next(parity),
730 'tag': k, 769 'tag': k,
736 parity = paritygen(web.stripecount) 775 parity = paritygen(web.stripecount)
737 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo] 776 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
738 sortkey = lambda b: (web.repo[b[1]].rev(), b[0]) 777 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
739 marks = sorted(marks, key=sortkey, reverse=True) 778 marks = sorted(marks, key=sortkey, reverse=True)
740 for k, n in marks[:10]: # limit to 10 bookmarks 779 for k, n in marks[:10]: # limit to 10 bookmarks
741 yield {'parity': next(parity), 780 yield {
742 'bookmark': k, 781 'parity': next(parity),
743 'date': web.repo[n].date(), 782 'bookmark': k,
744 'node': hex(n)} 783 'date': web.repo[n].date(),
784 'node': hex(n),
785 }
745 786
746 def changelist(context): 787 def changelist(context):
747 parity = paritygen(web.stripecount, offset=start - end) 788 parity = paritygen(web.stripecount, offset=start - end)
748 l = [] # build a list in forward order for efficiency 789 l = [] # build a list in forward order for efficiency
749 revs = [] 790 revs = []
750 if start < end: 791 if start < end:
751 revs = web.repo.changelog.revs(start, end - 1) 792 revs = web.repo.changelog.revs(start, end - 1)
752 for i in revs: 793 for i in revs:
753 ctx = web.repo[i] 794 ctx = web.repo[i]
774 owner=get_contact(web.config) or 'unknown', 815 owner=get_contact(web.config) or 'unknown',
775 lastchange=tip.date(), 816 lastchange=tip.date(),
776 tags=templateutil.mappinggenerator(tagentries, name='tagentry'), 817 tags=templateutil.mappinggenerator(tagentries, name='tagentry'),
777 bookmarks=templateutil.mappinggenerator(bookmarks), 818 bookmarks=templateutil.mappinggenerator(bookmarks),
778 branches=webutil.branchentries(web.repo, web.stripecount, 10), 819 branches=webutil.branchentries(web.repo, web.stripecount, 10),
779 shortlog=templateutil.mappinggenerator(changelist, 820 shortlog=templateutil.mappinggenerator(
780 name='shortlogentry'), 821 changelist, name='shortlogentry'
822 ),
781 node=tip.hex(), 823 node=tip.hex(),
782 symrev='tip', 824 symrev='tip',
783 archives=web.archivelist('tip'), 825 archives=web.archivelist('tip'),
784 labels=templateutil.hybridlist(labels, name='label')) 826 labels=templateutil.hybridlist(labels, name='label'),
827 )
828
785 829
786 @webcommand('filediff') 830 @webcommand('filediff')
787 def filediff(web): 831 def filediff(web):
788 """ 832 """
789 /diff/{revision}/{path} 833 /diff/{revision}/{path}
826 'filediff', 870 'filediff',
827 file=path, 871 file=path,
828 symrev=webutil.symrevorshortnode(web.req, ctx), 872 symrev=webutil.symrevorshortnode(web.req, ctx),
829 rename=rename, 873 rename=rename,
830 diff=diffs, 874 diff=diffs,
831 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))) 875 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
876 )
877
832 878
833 diff = webcommand('diff')(filediff) 879 diff = webcommand('diff')(filediff)
880
834 881
835 @webcommand('comparison') 882 @webcommand('comparison')
836 def comparison(web): 883 def comparison(web):
837 """ 884 """
838 /comparison/{revision}/{path} 885 /comparison/{revision}/{path}
862 909
863 def filelines(f): 910 def filelines(f):
864 if f.isbinary(): 911 if f.isbinary():
865 mt = pycompat.sysbytes( 912 mt = pycompat.sysbytes(
866 mimetypes.guess_type(pycompat.fsdecode(f.path()))[0] 913 mimetypes.guess_type(pycompat.fsdecode(f.path()))[0]
867 or r'application/octet-stream') 914 or r'application/octet-stream'
915 )
868 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))] 916 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
869 return f.data().splitlines() 917 return f.data().splitlines()
870 918
871 fctx = None 919 fctx = None
872 parent = ctx.p1() 920 parent = ctx.p1()
903 leftrev=leftrev, 951 leftrev=leftrev,
904 leftnode=hex(leftnode), 952 leftnode=hex(leftnode),
905 rightrev=rightrev, 953 rightrev=rightrev,
906 rightnode=hex(rightnode), 954 rightnode=hex(rightnode),
907 comparison=comparison, 955 comparison=comparison,
908 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))) 956 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
957 )
958
909 959
910 @webcommand('annotate') 960 @webcommand('annotate')
911 def annotate(web): 961 def annotate(web):
912 """ 962 """
913 /annotate/{revision}/{path} 963 /annotate/{revision}/{path}
932 # parents() is called once per line and several lines likely belong to 982 # parents() is called once per line and several lines likely belong to
933 # same revision. So it is worth caching. 983 # same revision. So it is worth caching.
934 # TODO there are still redundant operations within basefilectx.parents() 984 # TODO there are still redundant operations within basefilectx.parents()
935 # and from the fctx.annotate() call itself that could be cached. 985 # and from the fctx.annotate() call itself that could be cached.
936 parentscache = {} 986 parentscache = {}
987
937 def parents(context, f): 988 def parents(context, f):
938 rev = f.rev() 989 rev = f.rev()
939 if rev not in parentscache: 990 if rev not in parentscache:
940 parentscache[rev] = [] 991 parentscache[rev] = []
941 for p in f.parents(): 992 for p in f.parents():
950 1001
951 def annotate(context): 1002 def annotate(context):
952 if fctx.isbinary(): 1003 if fctx.isbinary():
953 mt = pycompat.sysbytes( 1004 mt = pycompat.sysbytes(
954 mimetypes.guess_type(pycompat.fsdecode(fctx.path()))[0] 1005 mimetypes.guess_type(pycompat.fsdecode(fctx.path()))[0]
955 or r'application/octet-stream') 1006 or r'application/octet-stream'
956 lines = [dagop.annotateline(fctx=fctx.filectx(fctx.filerev()), 1007 )
957 lineno=1, text='(binary:%s)' % mt)] 1008 lines = [
1009 dagop.annotateline(
1010 fctx=fctx.filectx(fctx.filerev()),
1011 lineno=1,
1012 text='(binary:%s)' % mt,
1013 )
1014 ]
958 else: 1015 else:
959 lines = webutil.annotate(web.req, fctx, web.repo.ui) 1016 lines = webutil.annotate(web.req, fctx, web.repo.ui)
960 1017
961 previousrev = None 1018 previousrev = None
962 blockparitygen = paritygen(1) 1019 blockparitygen = paritygen(1)
967 blockhead = True 1024 blockhead = True
968 blockparity = next(blockparitygen) 1025 blockparity = next(blockparitygen)
969 else: 1026 else:
970 blockhead = None 1027 blockhead = None
971 previousrev = rev 1028 previousrev = rev
972 yield {"parity": next(parity), 1029 yield {
973 "node": f.hex(), 1030 "parity": next(parity),
974 "rev": rev, 1031 "node": f.hex(),
975 "author": f.user(), 1032 "rev": rev,
976 "parents": templateutil.mappinggenerator(parents, args=(f,)), 1033 "author": f.user(),
977 "desc": f.description(), 1034 "parents": templateutil.mappinggenerator(parents, args=(f,)),
978 "extra": f.extra(), 1035 "desc": f.description(),
979 "file": f.path(), 1036 "extra": f.extra(),
980 "blockhead": blockhead, 1037 "file": f.path(),
981 "blockparity": blockparity, 1038 "blockhead": blockhead,
982 "targetline": aline.lineno, 1039 "blockparity": blockparity,
983 "line": aline.text, 1040 "targetline": aline.lineno,
984 "lineno": lineno + 1, 1041 "line": aline.text,
985 "lineid": "l%d" % (lineno + 1), 1042 "lineno": lineno + 1,
986 "linenumber": "% 6d" % (lineno + 1), 1043 "lineid": "l%d" % (lineno + 1),
987 "revdate": f.date()} 1044 "linenumber": "% 6d" % (lineno + 1),
1045 "revdate": f.date(),
1046 }
988 1047
989 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate') 1048 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate')
990 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults} 1049 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
991 1050
992 return web.sendtemplate( 1051 return web.sendtemplate(
997 symrev=webutil.symrevorshortnode(web.req, fctx), 1056 symrev=webutil.symrevorshortnode(web.req, fctx),
998 rename=webutil.renamelink(fctx), 1057 rename=webutil.renamelink(fctx),
999 permissions=fctx.manifest().flags(f), 1058 permissions=fctx.manifest().flags(f),
1000 ishead=int(ishead), 1059 ishead=int(ishead),
1001 diffopts=templateutil.hybriddict(diffopts), 1060 diffopts=templateutil.hybriddict(diffopts),
1002 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))) 1061 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1062 )
1063
1003 1064
1004 @webcommand('filelog') 1065 @webcommand('filelog')
1005 def filelog(web): 1066 def filelog(web):
1006 """ 1067 """
1007 /filelog/{revision}/{path} 1068 /filelog/{revision}/{path}
1021 fl = fctx.filelog() 1082 fl = fctx.filelog()
1022 except error.LookupError: 1083 except error.LookupError:
1023 f = webutil.cleanpath(web.repo, web.req.qsparams['file']) 1084 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1024 fl = web.repo.file(f) 1085 fl = web.repo.file(f)
1025 numrevs = len(fl) 1086 numrevs = len(fl)
1026 if not numrevs: # file doesn't exist at all 1087 if not numrevs: # file doesn't exist at all
1027 raise 1088 raise
1028 rev = webutil.changectx(web.repo, web.req).rev() 1089 rev = webutil.changectx(web.repo, web.req).rev()
1029 first = fl.linkrev(0) 1090 first = fl.linkrev(0)
1030 if rev < first: # current rev is from before file existed 1091 if rev < first: # current rev is from before file existed
1031 raise 1092 raise
1032 frev = numrevs - 1 1093 frev = numrevs - 1
1033 while fl.linkrev(frev) > rev: 1094 while fl.linkrev(frev) > rev:
1034 frev -= 1 1095 frev -= 1
1035 fctx = web.repo.filectx(f, fl.linkrev(frev)) 1096 fctx = web.repo.filectx(f, fl.linkrev(frev))
1056 descend = 'descend' in web.req.qsparams 1117 descend = 'descend' in web.req.qsparams
1057 if descend: 1118 if descend:
1058 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend'] 1119 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1059 1120
1060 count = fctx.filerev() + 1 1121 count = fctx.filerev() + 1
1061 start = max(0, count - revcount) # first rev on this page 1122 start = max(0, count - revcount) # first rev on this page
1062 end = min(count, start + revcount) # last rev on this page 1123 end = min(count, start + revcount) # last rev on this page
1063 parity = paritygen(web.stripecount, offset=start - end) 1124 parity = paritygen(web.stripecount, offset=start - end)
1064 1125
1065 repo = web.repo 1126 repo = web.repo
1066 filelog = fctx.filelog() 1127 filelog = fctx.filelog()
1067 revs = [filerev for filerev in filelog.revs(start, end - 1) 1128 revs = [
1068 if filelog.linkrev(filerev) in repo] 1129 filerev
1130 for filerev in filelog.revs(start, end - 1)
1131 if filelog.linkrev(filerev) in repo
1132 ]
1069 entries = [] 1133 entries = []
1070 1134
1071 diffstyle = web.config('web', 'style') 1135 diffstyle = web.config('web', 'style')
1072 if 'style' in web.req.qsparams: 1136 if 'style' in web.req.qsparams:
1073 diffstyle = web.req.qsparams['style'] 1137 diffstyle = web.req.qsparams['style']
1074 1138
1075 def diff(fctx, linerange=None): 1139 def diff(fctx, linerange=None):
1076 ctx = fctx.changectx() 1140 ctx = fctx.changectx()
1077 basectx = ctx.p1() 1141 basectx = ctx.p1()
1078 path = fctx.path() 1142 path = fctx.path()
1079 return webutil.diffs(web, ctx, basectx, [path], diffstyle, 1143 return webutil.diffs(
1080 linerange=linerange, 1144 web,
1081 lineidprefix='%s-' % ctx.hex()[:12]) 1145 ctx,
1146 basectx,
1147 [path],
1148 diffstyle,
1149 linerange=linerange,
1150 lineidprefix='%s-' % ctx.hex()[:12],
1151 )
1082 1152
1083 linerange = None 1153 linerange = None
1084 if lrange is not None: 1154 if lrange is not None:
1085 linerange = webutil.formatlinerange(*lrange) 1155 linerange = webutil.formatlinerange(*lrange)
1086 # deactivate numeric nav links when linerange is specified as this 1156 # deactivate numeric nav links when linerange is specified as this
1095 if patch: 1165 if patch:
1096 diffs = diff(c, linerange=lr) 1166 diffs = diff(c, linerange=lr)
1097 # follow renames accross filtered (not in range) revisions 1167 # follow renames accross filtered (not in range) revisions
1098 path = c.path() 1168 path = c.path()
1099 lm = webutil.commonentry(repo, c) 1169 lm = webutil.commonentry(repo, c)
1100 lm.update({ 1170 lm.update(
1101 'parity': next(parity), 1171 {
1102 'filerev': c.rev(), 1172 'parity': next(parity),
1103 'file': path, 1173 'filerev': c.rev(),
1104 'diff': diffs, 1174 'file': path,
1105 'linerange': webutil.formatlinerange(*lr), 1175 'diff': diffs,
1106 'rename': templateutil.mappinglist([]), 1176 'linerange': webutil.formatlinerange(*lr),
1107 }) 1177 'rename': templateutil.mappinglist([]),
1178 }
1179 )
1108 entries.append(lm) 1180 entries.append(lm)
1109 if i == revcount: 1181 if i == revcount:
1110 break 1182 break
1111 lessvars['linerange'] = webutil.formatlinerange(*lrange) 1183 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1112 morevars['linerange'] = lessvars['linerange'] 1184 morevars['linerange'] = lessvars['linerange']
1115 iterfctx = fctx.filectx(i) 1187 iterfctx = fctx.filectx(i)
1116 diffs = None 1188 diffs = None
1117 if patch: 1189 if patch:
1118 diffs = diff(iterfctx) 1190 diffs = diff(iterfctx)
1119 lm = webutil.commonentry(repo, iterfctx) 1191 lm = webutil.commonentry(repo, iterfctx)
1120 lm.update({ 1192 lm.update(
1121 'parity': next(parity), 1193 {
1122 'filerev': i, 1194 'parity': next(parity),
1123 'file': f, 1195 'filerev': i,
1124 'diff': diffs, 1196 'file': f,
1125 'rename': webutil.renamelink(iterfctx), 1197 'diff': diffs,
1126 }) 1198 'rename': webutil.renamelink(iterfctx),
1199 }
1200 )
1127 entries.append(lm) 1201 entries.append(lm)
1128 entries.reverse() 1202 entries.reverse()
1129 revnav = webutil.filerevnav(web.repo, fctx.path()) 1203 revnav = webutil.filerevnav(web.repo, fctx.path())
1130 nav = revnav.gen(end - 1, revcount, count) 1204 nav = revnav.gen(end - 1, revcount, count)
1131 1205
1142 latestentry=templateutil.mappinglist(latestentry), 1216 latestentry=templateutil.mappinglist(latestentry),
1143 linerange=linerange, 1217 linerange=linerange,
1144 revcount=revcount, 1218 revcount=revcount,
1145 morevars=morevars, 1219 morevars=morevars,
1146 lessvars=lessvars, 1220 lessvars=lessvars,
1147 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))) 1221 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1222 )
1223
1148 1224
1149 @webcommand('archive') 1225 @webcommand('archive')
1150 def archive(web): 1226 def archive(web):
1151 """ 1227 """
1152 /archive/{revision}.{format}[/{path}] 1228 /archive/{revision}.{format}[/{path}]
1173 1249
1174 if type_ not in webutil.archivespecs: 1250 if type_ not in webutil.archivespecs:
1175 msg = 'Unsupported archive type: %s' % stringutil.pprint(type_) 1251 msg = 'Unsupported archive type: %s' % stringutil.pprint(type_)
1176 raise ErrorResponse(HTTP_NOT_FOUND, msg) 1252 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1177 1253
1178 if not ((type_ in allowed or 1254 if not ((type_ in allowed or web.configbool("web", "allow" + type_))):
1179 web.configbool("web", "allow" + type_))):
1180 msg = 'Archive type not allowed: %s' % type_ 1255 msg = 'Archive type not allowed: %s' % type_
1181 raise ErrorResponse(HTTP_FORBIDDEN, msg) 1256 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1182 1257
1183 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame)) 1258 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1184 cnode = web.repo.lookup(key) 1259 cnode = web.repo.lookup(key)
1195 pats = ['path:' + file] 1270 pats = ['path:' + file]
1196 match = scmutil.match(ctx, pats, default='path') 1271 match = scmutil.match(ctx, pats, default='path')
1197 if pats: 1272 if pats:
1198 files = [f for f in ctx.manifest().keys() if match(f)] 1273 files = [f for f in ctx.manifest().keys() if match(f)]
1199 if not files: 1274 if not files:
1200 raise ErrorResponse(HTTP_NOT_FOUND, 1275 raise ErrorResponse(
1201 'file(s) not found: %s' % file) 1276 HTTP_NOT_FOUND, 'file(s) not found: %s' % file
1277 )
1202 1278
1203 mimetype, artype, extension, encoding = webutil.archivespecs[type_] 1279 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1204 1280
1205 web.res.headers['Content-Type'] = mimetype 1281 web.res.headers['Content-Type'] = mimetype
1206 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % ( 1282 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1207 name, extension) 1283 name,
1284 extension,
1285 )
1208 1286
1209 if encoding: 1287 if encoding:
1210 web.res.headers['Content-Encoding'] = encoding 1288 web.res.headers['Content-Encoding'] = encoding
1211 1289
1212 web.res.setbodywillwrite() 1290 web.res.setbodywillwrite()
1213 if list(web.res.sendresponse()): 1291 if list(web.res.sendresponse()):
1214 raise error.ProgrammingError('sendresponse() should not emit data ' 1292 raise error.ProgrammingError(
1215 'if writing later') 1293 'sendresponse() should not emit data ' 'if writing later'
1294 )
1216 1295
1217 bodyfh = web.res.getbodyfile() 1296 bodyfh = web.res.getbodyfile()
1218 1297
1219 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name, match=match, 1298 archival.archive(
1220 subrepos=web.configbool("web", "archivesubrepos")) 1299 web.repo,
1300 bodyfh,
1301 cnode,
1302 artype,
1303 prefix=name,
1304 match=match,
1305 subrepos=web.configbool("web", "archivesubrepos"),
1306 )
1221 1307
1222 return [] 1308 return []
1309
1223 1310
1224 @webcommand('static') 1311 @webcommand('static')
1225 def static(web): 1312 def static(web):
1226 fname = web.req.qsparams['file'] 1313 fname = web.req.qsparams['file']
1227 # a repo owner may set web.static in .hg/hgrc to get any file 1314 # a repo owner may set web.static in .hg/hgrc to get any file
1234 static = [os.path.join(p, 'static') for p in tp] 1321 static = [os.path.join(p, 'static') for p in tp]
1235 1322
1236 staticfile(static, fname, web.res) 1323 staticfile(static, fname, web.res)
1237 return web.res.sendresponse() 1324 return web.res.sendresponse()
1238 1325
1326
1239 @webcommand('graph') 1327 @webcommand('graph')
1240 def graph(web): 1328 def graph(web):
1241 """ 1329 """
1242 /graph[/{revision}] 1330 /graph[/{revision}]
1243 ------------------- 1331 -------------------
1314 # We have to feed a baseset to dagwalker as it is expecting smartset 1402 # We have to feed a baseset to dagwalker as it is expecting smartset
1315 # object. This does not have a big impact on hgweb performance itself 1403 # object. This does not have a big impact on hgweb performance itself
1316 # since hgweb graphing code is not itself lazy yet. 1404 # since hgweb graphing code is not itself lazy yet.
1317 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs)) 1405 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1318 # As we said one line above... not lazy. 1406 # As we said one line above... not lazy.
1319 tree = list(item for item in graphmod.colored(dag, web.repo) 1407 tree = list(
1320 if item[1] == graphmod.CHANGESET) 1408 item
1409 for item in graphmod.colored(dag, web.repo)
1410 if item[1] == graphmod.CHANGESET
1411 )
1321 1412
1322 def fulltree(): 1413 def fulltree():
1323 pos = web.repo[graphtop].rev() 1414 pos = web.repo[graphtop].rev()
1324 tree = [] 1415 tree = []
1325 if pos != -1: 1416 if pos != -1:
1326 revs = web.repo.changelog.revs(pos, lastrev) 1417 revs = web.repo.changelog.revs(pos, lastrev)
1327 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs)) 1418 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1328 tree = list(item for item in graphmod.colored(dag, web.repo) 1419 tree = list(
1329 if item[1] == graphmod.CHANGESET) 1420 item
1421 for item in graphmod.colored(dag, web.repo)
1422 if item[1] == graphmod.CHANGESET
1423 )
1330 return tree 1424 return tree
1331 1425
1332 def jsdata(context): 1426 def jsdata(context):
1333 for (id, type, ctx, vtx, edges) in fulltree(): 1427 for (id, type, ctx, vtx, edges) in fulltree():
1334 yield {'node': pycompat.bytestr(ctx), 1428 yield {
1335 'graphnode': webutil.getgraphnode(web.repo, ctx), 1429 'node': pycompat.bytestr(ctx),
1336 'vertex': vtx, 1430 'graphnode': webutil.getgraphnode(web.repo, ctx),
1337 'edges': edges} 1431 'vertex': vtx,
1432 'edges': edges,
1433 }
1338 1434
1339 def nodes(context): 1435 def nodes(context):
1340 parity = paritygen(web.stripecount) 1436 parity = paritygen(web.stripecount)
1341 for row, (id, type, ctx, vtx, edges) in enumerate(tree): 1437 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1342 entry = webutil.commonentry(web.repo, ctx) 1438 entry = webutil.commonentry(web.repo, ctx)
1343 edgedata = [{'col': edge[0], 1439 edgedata = [
1344 'nextcol': edge[1], 1440 {
1345 'color': (edge[2] - 1) % 6 + 1, 1441 'col': edge[0],
1346 'width': edge[3], 1442 'nextcol': edge[1],
1347 'bcolor': edge[4]} 1443 'color': (edge[2] - 1) % 6 + 1,
1348 for edge in edges] 1444 'width': edge[3],
1349 1445 'bcolor': edge[4],
1350 entry.update({'col': vtx[0], 1446 }
1351 'color': (vtx[1] - 1) % 6 + 1, 1447 for edge in edges
1352 'parity': next(parity), 1448 ]
1353 'edges': templateutil.mappinglist(edgedata), 1449
1354 'row': row, 1450 entry.update(
1355 'nextrow': row + 1}) 1451 {
1452 'col': vtx[0],
1453 'color': (vtx[1] - 1) % 6 + 1,
1454 'parity': next(parity),
1455 'edges': templateutil.mappinglist(edgedata),
1456 'row': row,
1457 'nextrow': row + 1,
1458 }
1459 )
1356 1460
1357 yield entry 1461 yield entry
1358 1462
1359 rows = len(tree) 1463 rows = len(tree)
1360 1464
1374 nextentry=templateutil.mappinglist(nextentry), 1478 nextentry=templateutil.mappinglist(nextentry),
1375 jsdata=templateutil.mappinggenerator(jsdata), 1479 jsdata=templateutil.mappinggenerator(jsdata),
1376 nodes=templateutil.mappinggenerator(nodes), 1480 nodes=templateutil.mappinggenerator(nodes),
1377 node=ctx.hex(), 1481 node=ctx.hex(),
1378 archives=web.archivelist('tip'), 1482 archives=web.archivelist('tip'),
1379 changenav=changenav) 1483 changenav=changenav,
1484 )
1485
1380 1486
1381 def _getdoc(e): 1487 def _getdoc(e):
1382 doc = e[0].__doc__ 1488 doc = e[0].__doc__
1383 if doc: 1489 if doc:
1384 doc = _(doc).partition('\n')[0] 1490 doc = _(doc).partition('\n')[0]
1385 else: 1491 else:
1386 doc = _('(no help text available)') 1492 doc = _('(no help text available)')
1387 return doc 1493 return doc
1388 1494
1495
1389 @webcommand('help') 1496 @webcommand('help')
1390 def help(web): 1497 def help(web):
1391 """ 1498 """
1392 /help[/{topic}] 1499 /help[/{topic}]
1393 --------------- 1500 ---------------
1403 """ 1510 """
1404 from .. import commands, help as helpmod # avoid cycle 1511 from .. import commands, help as helpmod # avoid cycle
1405 1512
1406 topicname = web.req.qsparams.get('node') 1513 topicname = web.req.qsparams.get('node')
1407 if not topicname: 1514 if not topicname:
1515
1408 def topics(context): 1516 def topics(context):
1409 for h in helpmod.helptable: 1517 for h in helpmod.helptable:
1410 entries, summary, _doc = h[0:3] 1518 entries, summary, _doc = h[0:3]
1411 yield {'topic': entries[0], 'summary': summary} 1519 yield {'topic': entries[0], 'summary': summary}
1412 1520
1436 return web.sendtemplate( 1544 return web.sendtemplate(
1437 'helptopics', 1545 'helptopics',
1438 topics=templateutil.mappinggenerator(topics), 1546 topics=templateutil.mappinggenerator(topics),
1439 earlycommands=templateutil.mappinggenerator(earlycommands), 1547 earlycommands=templateutil.mappinggenerator(earlycommands),
1440 othercommands=templateutil.mappinggenerator(othercommands), 1548 othercommands=templateutil.mappinggenerator(othercommands),
1441 title='Index') 1549 title='Index',
1550 )
1442 1551
1443 # Render an index of sub-topics. 1552 # Render an index of sub-topics.
1444 if topicname in helpmod.subtopics: 1553 if topicname in helpmod.subtopics:
1445 topics = [] 1554 topics = []
1446 for entries, summary, _doc in helpmod.subtopics[topicname]: 1555 for entries, summary, _doc in helpmod.subtopics[topicname]:
1447 topics.append({ 1556 topics.append(
1448 'topic': '%s.%s' % (topicname, entries[0]), 1557 {
1449 'basename': entries[0], 1558 'topic': '%s.%s' % (topicname, entries[0]),
1450 'summary': summary, 1559 'basename': entries[0],
1451 }) 1560 'summary': summary,
1561 }
1562 )
1452 1563
1453 return web.sendtemplate( 1564 return web.sendtemplate(
1454 'helptopics', 1565 'helptopics',
1455 topics=templateutil.mappinglist(topics), 1566 topics=templateutil.mappinglist(topics),
1456 title=topicname, 1567 title=topicname,
1457 subindex=True) 1568 subindex=True,
1569 )
1458 1570
1459 u = webutil.wsgiui.load() 1571 u = webutil.wsgiui.load()
1460 u.verbose = True 1572 u.verbose = True
1461 1573
1462 # Render a page from a sub-topic. 1574 # Render a page from a sub-topic.
1473 try: 1585 try:
1474 doc = helpmod.help_(u, commands, topic, subtopic=subtopic) 1586 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1475 except error.Abort: 1587 except error.Abort:
1476 raise ErrorResponse(HTTP_NOT_FOUND) 1588 raise ErrorResponse(HTTP_NOT_FOUND)
1477 1589
1478 return web.sendtemplate( 1590 return web.sendtemplate('help', topic=topicname, doc=doc)
1479 'help', 1591
1480 topic=topicname,
1481 doc=doc)
1482 1592
1483 # tell hggettext to extract docstrings from these functions: 1593 # tell hggettext to extract docstrings from these functions:
1484 i18nfunctions = commands.values() 1594 i18nfunctions = commands.values()