1# 2# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> 3# Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8from __future__ import absolute_import 9 10import copy 11import mimetypes 12import os 13import re 14 15from ..i18n import _ 16from ..node import hex, short 17from ..pycompat import getattr 18 19from .common import ( 20 ErrorResponse, 21 HTTP_FORBIDDEN, 22 HTTP_NOT_FOUND, 23 get_contact, 24 paritygen, 25 staticfile, 26) 27 28from .. import ( 29 archival, 30 dagop, 31 encoding, 32 error, 33 graphmod, 34 pycompat, 35 revset, 36 revsetlang, 37 scmutil, 38 smartset, 39 templateutil, 40) 41 42from ..utils import stringutil 43 44from . import webutil 45 46__all__ = [] 47commands = {} 48 49 50class webcommand(object): 51 """Decorator used to register a web command handler. 52 53 The decorator takes as its positional arguments the name/path the 54 command should be accessible under. 55 56 When called, functions receive as arguments a ``requestcontext``, 57 ``wsgirequest``, and a templater instance for generatoring output. 58 The functions should populate the ``rctx.res`` object with details 59 about the HTTP response. 60 61 The function returns a generator to be consumed by the WSGI application. 62 For most commands, this should be the result from 63 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()`` 64 to render a template. 65 66 Usage: 67 68 @webcommand('mycommand') 69 def mycommand(web): 70 pass 71 """ 72 73 def __init__(self, name): 74 self.name = name 75 76 def __call__(self, func): 77 __all__.append(self.name) 78 commands[self.name] = func 79 return func 80 81 82@webcommand(b'log') 83def log(web): 84 """ 85 /log[/{revision}[/{path}]] 86 -------------------------- 87 88 Show repository or file history. 89 90 For URLs of the form ``/log/{revision}``, a list of changesets starting at 91 the specified changeset identifier is shown. If ``{revision}`` is not 92 defined, the default is ``tip``. This form is equivalent to the 93 ``changelog`` handler. 94 95 For URLs of the form ``/log/{revision}/{file}``, the history for a specific 96 file will be shown. This form is equivalent to the ``filelog`` handler. 97 """ 98 99 if web.req.qsparams.get(b'file'): 100 return filelog(web) 101 else: 102 return changelog(web) 103 104 105@webcommand(b'rawfile') 106def rawfile(web): 107 guessmime = web.configbool(b'web', b'guessmime') 108 109 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b'')) 110 if not path: 111 return manifest(web) 112 113 try: 114 fctx = webutil.filectx(web.repo, web.req) 115 except error.LookupError as inst: 116 try: 117 return manifest(web) 118 except ErrorResponse: 119 raise inst 120 121 path = fctx.path() 122 text = fctx.data() 123 mt = b'application/binary' 124 if guessmime: 125 mt = mimetypes.guess_type(pycompat.fsdecode(path))[0] 126 if mt is None: 127 if stringutil.binary(text): 128 mt = b'application/binary' 129 else: 130 mt = b'text/plain' 131 else: 132 mt = pycompat.sysbytes(mt) 133 134 if mt.startswith(b'text/'): 135 mt += b'; charset="%s"' % encoding.encoding 136 137 web.res.headers[b'Content-Type'] = mt 138 filename = ( 139 path.rpartition(b'/')[-1].replace(b'\\', b'\\\\').replace(b'"', b'\\"') 140 ) 141 web.res.headers[b'Content-Disposition'] = ( 142 b'inline; filename="%s"' % filename 143 ) 144 web.res.setbodybytes(text) 145 return web.res.sendresponse() 146 147 148def _filerevision(web, fctx): 149 f = fctx.path() 150 text = fctx.data() 151 parity = paritygen(web.stripecount) 152 ishead = fctx.filenode() in fctx.filelog().heads() 153 154 if stringutil.binary(text): 155 mt = pycompat.sysbytes( 156 mimetypes.guess_type(pycompat.fsdecode(f))[0] 157 or r'application/octet-stream' 158 ) 159 text = b'(binary:%s)' % mt 160 161 def lines(context): 162 for lineno, t in enumerate(text.splitlines(True)): 163 yield { 164 b"line": t, 165 b"lineid": b"l%d" % (lineno + 1), 166 b"linenumber": b"% 6d" % (lineno + 1), 167 b"parity": next(parity), 168 } 169 170 return web.sendtemplate( 171 b'filerevision', 172 file=f, 173 path=webutil.up(f), 174 text=templateutil.mappinggenerator(lines), 175 symrev=webutil.symrevorshortnode(web.req, fctx), 176 rename=webutil.renamelink(fctx), 177 permissions=fctx.manifest().flags(f), 178 ishead=int(ishead), 179 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)) 180 ) 181 182 183@webcommand(b'file') 184def file(web): 185 """ 186 /file/{revision}[/{path}] 187 ------------------------- 188 189 Show information about a directory or file in the repository. 190 191 Info about the ``path`` given as a URL parameter will be rendered. 192 193 If ``path`` is a directory, information about the entries in that 194 directory will be rendered. This form is equivalent to the ``manifest`` 195 handler. 196 197 If ``path`` is a file, information about that file will be shown via 198 the ``filerevision`` template. 199 200 If ``path`` is not defined, information about the root directory will 201 be rendered. 202 """ 203 if web.req.qsparams.get(b'style') == b'raw': 204 return rawfile(web) 205 206 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b'')) 207 if not path: 208 return manifest(web) 209 try: 210 return _filerevision(web, webutil.filectx(web.repo, web.req)) 211 except error.LookupError as inst: 212 try: 213 return manifest(web) 214 except ErrorResponse: 215 raise inst 216 217 218def _search(web): 219 MODE_REVISION = b'rev' 220 MODE_KEYWORD = b'keyword' 221 MODE_REVSET = b'revset' 222 223 def revsearch(ctx): 224 yield ctx 225 226 def keywordsearch(query): 227 lower = encoding.lower 228 qw = lower(query).split() 229 230 def revgen(): 231 cl = web.repo.changelog 232 for i in pycompat.xrange(len(web.repo) - 1, 0, -100): 233 l = [] 234 for j in cl.revs(max(0, i - 99), i): 235 ctx = web.repo[j] 236 l.append(ctx) 237 l.reverse() 238 for e in l: 239 yield e 240 241 for ctx in revgen(): 242 miss = 0 243 for q in qw: 244 if not ( 245 q in lower(ctx.user()) 246 or q in lower(ctx.description()) 247 or q in lower(b" ".join(ctx.files())) 248 ): 249 miss = 1 250 break 251 if miss: 252 continue 253 254 yield ctx 255 256 def revsetsearch(revs): 257 for r in revs: 258 yield web.repo[r] 259 260 searchfuncs = { 261 MODE_REVISION: (revsearch, b'exact revision search'), 262 MODE_KEYWORD: (keywordsearch, b'literal keyword search'), 263 MODE_REVSET: (revsetsearch, b'revset expression search'), 264 } 265 266 def getsearchmode(query): 267 try: 268 ctx = scmutil.revsymbol(web.repo, query) 269 except (error.RepoError, error.LookupError): 270 # query is not an exact revision pointer, need to 271 # decide if it's a revset expression or keywords 272 pass 273 else: 274 return MODE_REVISION, ctx 275 276 revdef = b'reverse(%s)' % query 277 try: 278 tree = revsetlang.parse(revdef) 279 except error.ParseError: 280 # can't parse to a revset tree 281 return MODE_KEYWORD, query 282 283 if revsetlang.depth(tree) <= 2: 284 # no revset syntax used 285 return MODE_KEYWORD, query 286 287 if any( 288 (token, (value or b'')[:3]) == (b'string', b're:') 289 for token, value, pos in revsetlang.tokenize(revdef) 290 ): 291 return MODE_KEYWORD, query 292 293 funcsused = revsetlang.funcsused(tree) 294 if not funcsused.issubset(revset.safesymbols): 295 return MODE_KEYWORD, query 296 297 try: 298 mfunc = revset.match( 299 web.repo.ui, revdef, lookup=revset.lookupfn(web.repo) 300 ) 301 revs = mfunc(web.repo) 302 return MODE_REVSET, revs 303 # ParseError: wrongly placed tokens, wrongs arguments, etc 304 # RepoLookupError: no such revision, e.g. in 'revision:' 305 # Abort: bookmark/tag not exists 306 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo 307 except ( 308 error.ParseError, 309 error.RepoLookupError, 310 error.Abort, 311 LookupError, 312 ): 313 return MODE_KEYWORD, query 314 315 def changelist(context): 316 count = 0 317 318 for ctx in searchfunc[0](funcarg): 319 count += 1 320 n = scmutil.binnode(ctx) 321 showtags = webutil.showtag(web.repo, b'changelogtag', n) 322 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles) 323 324 lm = webutil.commonentry(web.repo, ctx) 325 lm.update( 326 { 327 b'parity': next(parity), 328 b'changelogtag': showtags, 329 b'files': files, 330 } 331 ) 332 yield lm 333 334 if count >= revcount: 335 break 336 337 query = web.req.qsparams[b'rev'] 338 revcount = web.maxchanges 339 if b'revcount' in web.req.qsparams: 340 try: 341 revcount = int(web.req.qsparams.get(b'revcount', revcount)) 342 revcount = max(revcount, 1) 343 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount 344 except ValueError: 345 pass 346 347 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) 348 lessvars[b'revcount'] = max(revcount // 2, 1) 349 lessvars[b'rev'] = query 350 morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) 351 morevars[b'revcount'] = revcount * 2 352 morevars[b'rev'] = query 353 354 mode, funcarg = getsearchmode(query) 355 356 if b'forcekw' in web.req.qsparams: 357 showforcekw = b'' 358 showunforcekw = searchfuncs[mode][1] 359 mode = MODE_KEYWORD 360 funcarg = query 361 else: 362 if mode != MODE_KEYWORD: 363 showforcekw = searchfuncs[MODE_KEYWORD][1] 364 else: 365 showforcekw = b'' 366 showunforcekw = b'' 367 368 searchfunc = searchfuncs[mode] 369 370 tip = web.repo[b'tip'] 371 parity = paritygen(web.stripecount) 372 373 return web.sendtemplate( 374 b'search', 375 query=query, 376 node=tip.hex(), 377 symrev=b'tip', 378 entries=templateutil.mappinggenerator(changelist, name=b'searchentry'), 379 archives=web.archivelist(b'tip'), 380 morevars=morevars, 381 lessvars=lessvars, 382 modedesc=searchfunc[1], 383 showforcekw=showforcekw, 384 showunforcekw=showunforcekw, 385 ) 386 387 388@webcommand(b'changelog') 389def changelog(web, shortlog=False): 390 """ 391 /changelog[/{revision}] 392 ----------------------- 393 394 Show information about multiple changesets. 395 396 If the optional ``revision`` URL argument is absent, information about 397 all changesets starting at ``tip`` will be rendered. If the ``revision`` 398 argument is present, changesets will be shown starting from the specified 399 revision. 400 401 If ``revision`` is absent, the ``rev`` query string argument may be 402 defined. This will perform a search for changesets. 403 404 The argument for ``rev`` can be a single revision, a revision set, 405 or a literal keyword to search for in changeset data (equivalent to 406 :hg:`log -k`). 407 408 The ``revcount`` query string argument defines the maximum numbers of 409 changesets to render. 410 411 For non-searches, the ``changelog`` template will be rendered. 412 """ 413 414 query = b'' 415 if b'node' in web.req.qsparams: 416 ctx = webutil.changectx(web.repo, web.req) 417 symrev = webutil.symrevorshortnode(web.req, ctx) 418 elif b'rev' in web.req.qsparams: 419 return _search(web) 420 else: 421 ctx = web.repo[b'tip'] 422 symrev = b'tip' 423 424 def changelist(maxcount): 425 revs = [] 426 if pos != -1: 427 revs = web.repo.changelog.revs(pos, 0) 428 429 for entry in webutil.changelistentries(web, revs, maxcount, parity): 430 yield entry 431 432 if shortlog: 433 revcount = web.maxshortchanges 434 else: 435 revcount = web.maxchanges 436 437 if b'revcount' in web.req.qsparams: 438 try: 439 revcount = int(web.req.qsparams.get(b'revcount', revcount)) 440 revcount = max(revcount, 1) 441 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount 442 except ValueError: 443 pass 444 445 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) 446 lessvars[b'revcount'] = max(revcount // 2, 1) 447 morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) 448 morevars[b'revcount'] = revcount * 2 449 450 count = len(web.repo) 451 pos = ctx.rev() 452 parity = paritygen(web.stripecount) 453 454 changenav = webutil.revnav(web.repo).gen(pos, revcount, count) 455 456 entries = list(changelist(revcount + 1)) 457 latestentry = entries[:1] 458 if len(entries) > revcount: 459 nextentry = entries[-1:] 460 entries = entries[:-1] 461 else: 462 nextentry = [] 463 464 return web.sendtemplate( 465 b'shortlog' if shortlog else b'changelog', 466 changenav=changenav, 467 node=ctx.hex(), 468 rev=pos, 469 symrev=symrev, 470 changesets=count, 471 entries=templateutil.mappinglist(entries), 472 latestentry=templateutil.mappinglist(latestentry), 473 nextentry=templateutil.mappinglist(nextentry), 474 archives=web.archivelist(b'tip'), 475 revcount=revcount, 476 morevars=morevars, 477 lessvars=lessvars, 478 query=query, 479 ) 480 481 482@webcommand(b'shortlog') 483def shortlog(web): 484 """ 485 /shortlog 486 --------- 487 488 Show basic information about a set of changesets. 489 490 This accepts the same parameters as the ``changelog`` handler. The only 491 difference is the ``shortlog`` template will be rendered instead of the 492 ``changelog`` template. 493 """ 494 return changelog(web, shortlog=True) 495 496 497@webcommand(b'changeset') 498def changeset(web): 499 """ 500 /changeset[/{revision}] 501 ----------------------- 502 503 Show information about a single changeset. 504 505 A URL path argument is the changeset identifier to show. See ``hg help 506 revisions`` for possible values. If not defined, the ``tip`` changeset 507 will be shown. 508 509 The ``changeset`` template is rendered. Contents of the ``changesettag``, 510 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many 511 templates related to diffs may all be used to produce the output. 512 """ 513 ctx = webutil.changectx(web.repo, web.req) 514 515 return web.sendtemplate(b'changeset', **webutil.changesetentry(web, ctx)) 516 517 518rev = webcommand(b'rev')(changeset) 519 520 521def decodepath(path): 522 """Hook for mapping a path in the repository to a path in the 523 working copy. 524 525 Extensions (e.g., largefiles) can override this to remap files in 526 the virtual file system presented by the manifest command below.""" 527 return path 528 529 530@webcommand(b'manifest') 531def manifest(web): 532 """ 533 /manifest[/{revision}[/{path}]] 534 ------------------------------- 535 536 Show information about a directory. 537 538 If the URL path arguments are omitted, information about the root 539 directory for the ``tip`` changeset will be shown. 540 541 Because this handler can only show information for directories, it 542 is recommended to use the ``file`` handler instead, as it can handle both 543 directories and files. 544 545 The ``manifest`` template will be rendered for this handler. 546 """ 547 if b'node' in web.req.qsparams: 548 ctx = webutil.changectx(web.repo, web.req) 549 symrev = webutil.symrevorshortnode(web.req, ctx) 550 else: 551 ctx = web.repo[b'tip'] 552 symrev = b'tip' 553 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b'')) 554 mf = ctx.manifest() 555 node = scmutil.binnode(ctx) 556 557 files = {} 558 dirs = {} 559 parity = paritygen(web.stripecount) 560 561 if path and path[-1:] != b"/": 562 path += b"/" 563 l = len(path) 564 abspath = b"/" + path 565 566 for full, n in pycompat.iteritems(mf): 567 # the virtual path (working copy path) used for the full 568 # (repository) path 569 f = decodepath(full) 570 571 if f[:l] != path: 572 continue 573 remain = f[l:] 574 elements = remain.split(b'/') 575 if len(elements) == 1: 576 files[remain] = full 577 else: 578 h = dirs # need to retain ref to dirs (root) 579 for elem in elements[0:-1]: 580 if elem not in h: 581 h[elem] = {} 582 h = h[elem] 583 if len(h) > 1: 584 break 585 h[None] = None # denotes files present 586 587 if mf and not files and not dirs: 588 raise ErrorResponse(HTTP_NOT_FOUND, b'path not found: ' + path) 589 590 def filelist(context): 591 for f in sorted(files): 592 full = files[f] 593 594 fctx = ctx.filectx(full) 595 yield { 596 b"file": full, 597 b"parity": next(parity), 598 b"basename": f, 599 b"date": fctx.date(), 600 b"size": fctx.size(), 601 b"permissions": mf.flags(full), 602 } 603 604 def dirlist(context): 605 for d in sorted(dirs): 606 607 emptydirs = [] 608 h = dirs[d] 609 while isinstance(h, dict) and len(h) == 1: 610 k, v = next(iter(h.items())) 611 if v: 612 emptydirs.append(k) 613 h = v 614 615 path = b"%s%s" % (abspath, d) 616 yield { 617 b"parity": next(parity), 618 b"path": path, 619 b"emptydirs": b"/".join(emptydirs), 620 b"basename": d, 621 } 622 623 return web.sendtemplate( 624 b'manifest', 625 symrev=symrev, 626 path=abspath, 627 up=webutil.up(abspath), 628 upparity=next(parity), 629 fentries=templateutil.mappinggenerator(filelist), 630 dentries=templateutil.mappinggenerator(dirlist), 631 archives=web.archivelist(hex(node)), 632 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)) 633 ) 634 635 636@webcommand(b'tags') 637def tags(web): 638 """ 639 /tags 640 ----- 641 642 Show information about tags. 643 644 No arguments are accepted. 645 646 The ``tags`` template is rendered. 647 """ 648 i = list(reversed(web.repo.tagslist())) 649 parity = paritygen(web.stripecount) 650 651 def entries(context, notip, latestonly): 652 t = i 653 if notip: 654 t = [(k, n) for k, n in i if k != b"tip"] 655 if latestonly: 656 t = t[:1] 657 for k, n in t: 658 yield { 659 b"parity": next(parity), 660 b"tag": k, 661 b"date": web.repo[n].date(), 662 b"node": hex(n), 663 } 664 665 return web.sendtemplate( 666 b'tags', 667 node=hex(web.repo.changelog.tip()), 668 entries=templateutil.mappinggenerator(entries, args=(False, False)), 669 entriesnotip=templateutil.mappinggenerator(entries, args=(True, False)), 670 latestentry=templateutil.mappinggenerator(entries, args=(True, True)), 671 ) 672 673 674@webcommand(b'bookmarks') 675def bookmarks(web): 676 """ 677 /bookmarks 678 ---------- 679 680 Show information about bookmarks. 681 682 No arguments are accepted. 683 684 The ``bookmarks`` template is rendered. 685 """ 686 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo] 687 sortkey = lambda b: (web.repo[b[1]].rev(), b[0]) 688 i = sorted(i, key=sortkey, reverse=True) 689 parity = paritygen(web.stripecount) 690 691 def entries(context, latestonly): 692 t = i 693 if latestonly: 694 t = i[:1] 695 for k, n in t: 696 yield { 697 b"parity": next(parity), 698 b"bookmark": k, 699 b"date": web.repo[n].date(), 700 b"node": hex(n), 701 } 702 703 if i: 704 latestrev = i[0][1] 705 else: 706 latestrev = -1 707 lastdate = web.repo[latestrev].date() 708 709 return web.sendtemplate( 710 b'bookmarks', 711 node=hex(web.repo.changelog.tip()), 712 lastchange=templateutil.mappinglist([{b'date': lastdate}]), 713 entries=templateutil.mappinggenerator(entries, args=(False,)), 714 latestentry=templateutil.mappinggenerator(entries, args=(True,)), 715 ) 716 717 718@webcommand(b'branches') 719def branches(web): 720 """ 721 /branches 722 --------- 723 724 Show information about branches. 725 726 All known branches are contained in the output, even closed branches. 727 728 No arguments are accepted. 729 730 The ``branches`` template is rendered. 731 """ 732 entries = webutil.branchentries(web.repo, web.stripecount) 733 latestentry = webutil.branchentries(web.repo, web.stripecount, 1) 734 735 return web.sendtemplate( 736 b'branches', 737 node=hex(web.repo.changelog.tip()), 738 entries=entries, 739 latestentry=latestentry, 740 ) 741 742 743@webcommand(b'summary') 744def summary(web): 745 """ 746 /summary 747 -------- 748 749 Show a summary of repository state. 750 751 Information about the latest changesets, bookmarks, tags, and branches 752 is captured by this handler. 753 754 The ``summary`` template is rendered. 755 """ 756 i = reversed(web.repo.tagslist()) 757 758 def tagentries(context): 759 parity = paritygen(web.stripecount) 760 count = 0 761 for k, n in i: 762 if k == b"tip": # skip tip 763 continue 764 765 count += 1 766 if count > 10: # limit to 10 tags 767 break 768 769 yield { 770 b'parity': next(parity), 771 b'tag': k, 772 b'node': hex(n), 773 b'date': web.repo[n].date(), 774 } 775 776 def bookmarks(context): 777 parity = paritygen(web.stripecount) 778 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo] 779 sortkey = lambda b: (web.repo[b[1]].rev(), b[0]) 780 marks = sorted(marks, key=sortkey, reverse=True) 781 for k, n in marks[:10]: # limit to 10 bookmarks 782 yield { 783 b'parity': next(parity), 784 b'bookmark': k, 785 b'date': web.repo[n].date(), 786 b'node': hex(n), 787 } 788 789 def changelist(context): 790 parity = paritygen(web.stripecount, offset=start - end) 791 l = [] # build a list in forward order for efficiency 792 revs = [] 793 if start < end: 794 revs = web.repo.changelog.revs(start, end - 1) 795 for i in revs: 796 ctx = web.repo[i] 797 lm = webutil.commonentry(web.repo, ctx) 798 lm[b'parity'] = next(parity) 799 l.append(lm) 800 801 for entry in reversed(l): 802 yield entry 803 804 tip = web.repo[b'tip'] 805 count = len(web.repo) 806 start = max(0, count - web.maxchanges) 807 end = min(count, start + web.maxchanges) 808 809 desc = web.config(b"web", b"description") 810 if not desc: 811 desc = b'unknown' 812 labels = web.configlist(b'web', b'labels') 813 814 return web.sendtemplate( 815 b'summary', 816 desc=desc, 817 owner=get_contact(web.config) or b'unknown', 818 lastchange=tip.date(), 819 tags=templateutil.mappinggenerator(tagentries, name=b'tagentry'), 820 bookmarks=templateutil.mappinggenerator(bookmarks), 821 branches=webutil.branchentries(web.repo, web.stripecount, 10), 822 shortlog=templateutil.mappinggenerator( 823 changelist, name=b'shortlogentry' 824 ), 825 node=tip.hex(), 826 symrev=b'tip', 827 archives=web.archivelist(b'tip'), 828 labels=templateutil.hybridlist(labels, name=b'label'), 829 ) 830 831 832@webcommand(b'filediff') 833def filediff(web): 834 """ 835 /diff/{revision}/{path} 836 ----------------------- 837 838 Show how a file changed in a particular commit. 839 840 The ``filediff`` template is rendered. 841 842 This handler is registered under both the ``/diff`` and ``/filediff`` 843 paths. ``/diff`` is used in modern code. 844 """ 845 fctx, ctx = None, None 846 try: 847 fctx = webutil.filectx(web.repo, web.req) 848 except LookupError: 849 ctx = webutil.changectx(web.repo, web.req) 850 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file']) 851 if path not in ctx.files(): 852 raise 853 854 if fctx is not None: 855 path = fctx.path() 856 ctx = fctx.changectx() 857 basectx = ctx.p1() 858 859 style = web.config(b'web', b'style') 860 if b'style' in web.req.qsparams: 861 style = web.req.qsparams[b'style'] 862 863 diffs = webutil.diffs(web, ctx, basectx, [path], style) 864 if fctx is not None: 865 rename = webutil.renamelink(fctx) 866 ctx = fctx 867 else: 868 rename = templateutil.mappinglist([]) 869 ctx = ctx 870 871 return web.sendtemplate( 872 b'filediff', 873 file=path, 874 symrev=webutil.symrevorshortnode(web.req, ctx), 875 rename=rename, 876 diff=diffs, 877 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)) 878 ) 879 880 881diff = webcommand(b'diff')(filediff) 882 883 884@webcommand(b'comparison') 885def comparison(web): 886 """ 887 /comparison/{revision}/{path} 888 ----------------------------- 889 890 Show a comparison between the old and new versions of a file from changes 891 made on a particular revision. 892 893 This is similar to the ``diff`` handler. However, this form features 894 a split or side-by-side diff rather than a unified diff. 895 896 The ``context`` query string argument can be used to control the lines of 897 context in the diff. 898 899 The ``filecomparison`` template is rendered. 900 """ 901 ctx = webutil.changectx(web.repo, web.req) 902 if b'file' not in web.req.qsparams: 903 raise ErrorResponse(HTTP_NOT_FOUND, b'file not given') 904 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file']) 905 906 parsecontext = lambda v: v == b'full' and -1 or int(v) 907 if b'context' in web.req.qsparams: 908 context = parsecontext(web.req.qsparams[b'context']) 909 else: 910 context = parsecontext(web.config(b'web', b'comparisoncontext')) 911 912 def filelines(f): 913 if f.isbinary(): 914 mt = pycompat.sysbytes( 915 mimetypes.guess_type(pycompat.fsdecode(f.path()))[0] 916 or r'application/octet-stream' 917 ) 918 return [_(b'(binary file %s, hash: %s)') % (mt, hex(f.filenode()))] 919 return f.data().splitlines() 920 921 fctx = None 922 parent = ctx.p1() 923 leftrev = parent.rev() 924 leftnode = parent.node() 925 rightrev = ctx.rev() 926 rightnode = scmutil.binnode(ctx) 927 if path in ctx: 928 fctx = ctx[path] 929 rightlines = filelines(fctx) 930 if path not in parent: 931 leftlines = () 932 else: 933 pfctx = parent[path] 934 leftlines = filelines(pfctx) 935 else: 936 rightlines = () 937 pfctx = ctx.p1()[path] 938 leftlines = filelines(pfctx) 939 940 comparison = webutil.compare(context, leftlines, rightlines) 941 if fctx is not None: 942 rename = webutil.renamelink(fctx) 943 ctx = fctx 944 else: 945 rename = templateutil.mappinglist([]) 946 ctx = ctx 947 948 return web.sendtemplate( 949 b'filecomparison', 950 file=path, 951 symrev=webutil.symrevorshortnode(web.req, ctx), 952 rename=rename, 953 leftrev=leftrev, 954 leftnode=hex(leftnode), 955 rightrev=rightrev, 956 rightnode=hex(rightnode), 957 comparison=comparison, 958 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)) 959 ) 960 961 962@webcommand(b'annotate') 963def annotate(web): 964 """ 965 /annotate/{revision}/{path} 966 --------------------------- 967 968 Show changeset information for each line in a file. 969 970 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and 971 ``ignoreblanklines`` query string arguments have the same meaning as 972 their ``[annotate]`` config equivalents. It uses the hgrc boolean 973 parsing logic to interpret the value. e.g. ``0`` and ``false`` are 974 false and ``1`` and ``true`` are true. If not defined, the server 975 default settings are used. 976 977 The ``fileannotate`` template is rendered. 978 """ 979 fctx = webutil.filectx(web.repo, web.req) 980 f = fctx.path() 981 parity = paritygen(web.stripecount) 982 ishead = fctx.filenode() in fctx.filelog().heads() 983 984 # parents() is called once per line and several lines likely belong to 985 # same revision. So it is worth caching. 986 # TODO there are still redundant operations within basefilectx.parents() 987 # and from the fctx.annotate() call itself that could be cached. 988 parentscache = {} 989 990 def parents(context, f): 991 rev = f.rev() 992 if rev not in parentscache: 993 parentscache[rev] = [] 994 for p in f.parents(): 995 entry = { 996 b'node': p.hex(), 997 b'rev': p.rev(), 998 } 999 parentscache[rev].append(entry) 1000 1001 for p in parentscache[rev]: 1002 yield p 1003 1004 def annotate(context): 1005 if fctx.isbinary(): 1006 mt = pycompat.sysbytes( 1007 mimetypes.guess_type(pycompat.fsdecode(fctx.path()))[0] 1008 or r'application/octet-stream' 1009 ) 1010 lines = [ 1011 dagop.annotateline( 1012 fctx=fctx.filectx(fctx.filerev()), 1013 lineno=1, 1014 text=b'(binary:%s)' % mt, 1015 ) 1016 ] 1017 else: 1018 lines = webutil.annotate(web.req, fctx, web.repo.ui) 1019 1020 previousrev = None 1021 blockparitygen = paritygen(1) 1022 for lineno, aline in enumerate(lines): 1023 f = aline.fctx 1024 rev = f.rev() 1025 if rev != previousrev: 1026 blockhead = True 1027 blockparity = next(blockparitygen) 1028 else: 1029 blockhead = None 1030 previousrev = rev 1031 yield { 1032 b"parity": next(parity), 1033 b"node": f.hex(), 1034 b"rev": rev, 1035 b"author": f.user(), 1036 b"parents": templateutil.mappinggenerator(parents, args=(f,)), 1037 b"desc": f.description(), 1038 b"extra": f.extra(), 1039 b"file": f.path(), 1040 b"blockhead": blockhead, 1041 b"blockparity": blockparity, 1042 b"targetline": aline.lineno, 1043 b"line": aline.text, 1044 b"lineno": lineno + 1, 1045 b"lineid": b"l%d" % (lineno + 1), 1046 b"linenumber": b"% 6d" % (lineno + 1), 1047 b"revdate": f.date(), 1048 } 1049 1050 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, b'annotate') 1051 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults} 1052 1053 return web.sendtemplate( 1054 b'fileannotate', 1055 file=f, 1056 annotate=templateutil.mappinggenerator(annotate), 1057 path=webutil.up(f), 1058 symrev=webutil.symrevorshortnode(web.req, fctx), 1059 rename=webutil.renamelink(fctx), 1060 permissions=fctx.manifest().flags(f), 1061 ishead=int(ishead), 1062 diffopts=templateutil.hybriddict(diffopts), 1063 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)) 1064 ) 1065 1066 1067@webcommand(b'filelog') 1068def filelog(web): 1069 """ 1070 /filelog/{revision}/{path} 1071 -------------------------- 1072 1073 Show information about the history of a file in the repository. 1074 1075 The ``revcount`` query string argument can be defined to control the 1076 maximum number of entries to show. 1077 1078 The ``filelog`` template will be rendered. 1079 """ 1080 1081 try: 1082 fctx = webutil.filectx(web.repo, web.req) 1083 f = fctx.path() 1084 fl = fctx.filelog() 1085 except error.LookupError: 1086 f = webutil.cleanpath(web.repo, web.req.qsparams[b'file']) 1087 fl = web.repo.file(f) 1088 numrevs = len(fl) 1089 if not numrevs: # file doesn't exist at all 1090 raise 1091 rev = webutil.changectx(web.repo, web.req).rev() 1092 first = fl.linkrev(0) 1093 if rev < first: # current rev is from before file existed 1094 raise 1095 frev = numrevs - 1 1096 while fl.linkrev(frev) > rev: 1097 frev -= 1 1098 fctx = web.repo.filectx(f, fl.linkrev(frev)) 1099 1100 revcount = web.maxshortchanges 1101 if b'revcount' in web.req.qsparams: 1102 try: 1103 revcount = int(web.req.qsparams.get(b'revcount', revcount)) 1104 revcount = max(revcount, 1) 1105 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount 1106 except ValueError: 1107 pass 1108 1109 lrange = webutil.linerange(web.req) 1110 1111 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) 1112 lessvars[b'revcount'] = max(revcount // 2, 1) 1113 morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) 1114 morevars[b'revcount'] = revcount * 2 1115 1116 patch = b'patch' in web.req.qsparams 1117 if patch: 1118 lessvars[b'patch'] = morevars[b'patch'] = web.req.qsparams[b'patch'] 1119 descend = b'descend' in web.req.qsparams 1120 if descend: 1121 lessvars[b'descend'] = morevars[b'descend'] = web.req.qsparams[ 1122 b'descend' 1123 ] 1124 1125 count = fctx.filerev() + 1 1126 start = max(0, count - revcount) # first rev on this page 1127 end = min(count, start + revcount) # last rev on this page 1128 parity = paritygen(web.stripecount, offset=start - end) 1129 1130 repo = web.repo 1131 filelog = fctx.filelog() 1132 revs = [ 1133 filerev 1134 for filerev in filelog.revs(start, end - 1) 1135 if filelog.linkrev(filerev) in repo 1136 ] 1137 entries = [] 1138 1139 diffstyle = web.config(b'web', b'style') 1140 if b'style' in web.req.qsparams: 1141 diffstyle = web.req.qsparams[b'style'] 1142 1143 def diff(fctx, linerange=None): 1144 ctx = fctx.changectx() 1145 basectx = ctx.p1() 1146 path = fctx.path() 1147 return webutil.diffs( 1148 web, 1149 ctx, 1150 basectx, 1151 [path], 1152 diffstyle, 1153 linerange=linerange, 1154 lineidprefix=b'%s-' % ctx.hex()[:12], 1155 ) 1156 1157 linerange = None 1158 if lrange is not None: 1159 assert lrange is not None # help pytype (!?) 1160 linerange = webutil.formatlinerange(*lrange) 1161 # deactivate numeric nav links when linerange is specified as this 1162 # would required a dedicated "revnav" class 1163 nav = templateutil.mappinglist([]) 1164 if descend: 1165 it = dagop.blockdescendants(fctx, *lrange) 1166 else: 1167 it = dagop.blockancestors(fctx, *lrange) 1168 for i, (c, lr) in enumerate(it, 1): 1169 diffs = None 1170 if patch: 1171 diffs = diff(c, linerange=lr) 1172 # follow renames accross filtered (not in range) revisions 1173 path = c.path() 1174 lm = webutil.commonentry(repo, c) 1175 lm.update( 1176 { 1177 b'parity': next(parity), 1178 b'filerev': c.rev(), 1179 b'file': path, 1180 b'diff': diffs, 1181 b'linerange': webutil.formatlinerange(*lr), 1182 b'rename': templateutil.mappinglist([]), 1183 } 1184 ) 1185 entries.append(lm) 1186 if i == revcount: 1187 break 1188 lessvars[b'linerange'] = webutil.formatlinerange(*lrange) 1189 morevars[b'linerange'] = lessvars[b'linerange'] 1190 else: 1191 for i in revs: 1192 iterfctx = fctx.filectx(i) 1193 diffs = None 1194 if patch: 1195 diffs = diff(iterfctx) 1196 lm = webutil.commonentry(repo, iterfctx) 1197 lm.update( 1198 { 1199 b'parity': next(parity), 1200 b'filerev': i, 1201 b'file': f, 1202 b'diff': diffs, 1203 b'rename': webutil.renamelink(iterfctx), 1204 } 1205 ) 1206 entries.append(lm) 1207 entries.reverse() 1208 revnav = webutil.filerevnav(web.repo, fctx.path()) 1209 nav = revnav.gen(end - 1, revcount, count) 1210 1211 latestentry = entries[:1] 1212 1213 return web.sendtemplate( 1214 b'filelog', 1215 file=f, 1216 nav=nav, 1217 symrev=webutil.symrevorshortnode(web.req, fctx), 1218 entries=templateutil.mappinglist(entries), 1219 descend=descend, 1220 patch=patch, 1221 latestentry=templateutil.mappinglist(latestentry), 1222 linerange=linerange, 1223 revcount=revcount, 1224 morevars=morevars, 1225 lessvars=lessvars, 1226 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)) 1227 ) 1228 1229 1230@webcommand(b'archive') 1231def archive(web): 1232 """ 1233 /archive/{revision}.{format}[/{path}] 1234 ------------------------------------- 1235 1236 Obtain an archive of repository content. 1237 1238 The content and type of the archive is defined by a URL path parameter. 1239 ``format`` is the file extension of the archive type to be generated. e.g. 1240 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your 1241 server configuration. 1242 1243 The optional ``path`` URL parameter controls content to include in the 1244 archive. If omitted, every file in the specified revision is present in the 1245 archive. If included, only the specified file or contents of the specified 1246 directory will be included in the archive. 1247 1248 No template is used for this handler. Raw, binary content is generated. 1249 """ 1250 1251 type_ = web.req.qsparams.get(b'type') 1252 allowed = web.configlist(b"web", b"allow-archive") 1253 key = web.req.qsparams[b'node'] 1254 1255 if type_ not in webutil.archivespecs: 1256 msg = b'Unsupported archive type: %s' % stringutil.pprint(type_) 1257 raise ErrorResponse(HTTP_NOT_FOUND, msg) 1258 1259 if not ((type_ in allowed or web.configbool(b"web", b"allow" + type_))): 1260 msg = b'Archive type not allowed: %s' % type_ 1261 raise ErrorResponse(HTTP_FORBIDDEN, msg) 1262 1263 reponame = re.sub(br"\W+", b"-", os.path.basename(web.reponame)) 1264 cnode = web.repo.lookup(key) 1265 arch_version = key 1266 if cnode == key or key == b'tip': 1267 arch_version = short(cnode) 1268 name = b"%s-%s" % (reponame, arch_version) 1269 1270 ctx = webutil.changectx(web.repo, web.req) 1271 match = scmutil.match(ctx, []) 1272 file = web.req.qsparams.get(b'file') 1273 if file: 1274 pats = [b'path:' + file] 1275 match = scmutil.match(ctx, pats, default=b'path') 1276 if pats: 1277 files = [f for f in ctx.manifest().keys() if match(f)] 1278 if not files: 1279 raise ErrorResponse( 1280 HTTP_NOT_FOUND, b'file(s) not found: %s' % file 1281 ) 1282 1283 mimetype, artype, extension, encoding = webutil.archivespecs[type_] 1284 1285 web.res.headers[b'Content-Type'] = mimetype 1286 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % ( 1287 name, 1288 extension, 1289 ) 1290 1291 if encoding: 1292 web.res.headers[b'Content-Encoding'] = encoding 1293 1294 web.res.setbodywillwrite() 1295 if list(web.res.sendresponse()): 1296 raise error.ProgrammingError( 1297 b'sendresponse() should not emit data if writing later' 1298 ) 1299 1300 bodyfh = web.res.getbodyfile() 1301 1302 archival.archive( 1303 web.repo, 1304 bodyfh, 1305 cnode, 1306 artype, 1307 prefix=name, 1308 match=match, 1309 subrepos=web.configbool(b"web", b"archivesubrepos"), 1310 ) 1311 1312 return [] 1313 1314 1315@webcommand(b'static') 1316def static(web): 1317 fname = web.req.qsparams[b'file'] 1318 # a repo owner may set web.static in .hg/hgrc to get any file 1319 # readable by the user running the CGI script 1320 static = web.config(b"web", b"static", untrusted=False) 1321 staticfile(web.templatepath, static, fname, web.res) 1322 return web.res.sendresponse() 1323 1324 1325@webcommand(b'graph') 1326def graph(web): 1327 """ 1328 /graph[/{revision}] 1329 ------------------- 1330 1331 Show information about the graphical topology of the repository. 1332 1333 Information rendered by this handler can be used to create visual 1334 representations of repository topology. 1335 1336 The ``revision`` URL parameter controls the starting changeset. If it's 1337 absent, the default is ``tip``. 1338 1339 The ``revcount`` query string argument can define the number of changesets 1340 to show information for. 1341 1342 The ``graphtop`` query string argument can specify the starting changeset 1343 for producing ``jsdata`` variable that is used for rendering graph in 1344 JavaScript. By default it has the same value as ``revision``. 1345 1346 This handler will render the ``graph`` template. 1347 """ 1348 1349 if b'node' in web.req.qsparams: 1350 ctx = webutil.changectx(web.repo, web.req) 1351 symrev = webutil.symrevorshortnode(web.req, ctx) 1352 else: 1353 ctx = web.repo[b'tip'] 1354 symrev = b'tip' 1355 rev = ctx.rev() 1356 1357 bg_height = 39 1358 revcount = web.maxshortchanges 1359 if b'revcount' in web.req.qsparams: 1360 try: 1361 revcount = int(web.req.qsparams.get(b'revcount', revcount)) 1362 revcount = max(revcount, 1) 1363 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount 1364 except ValueError: 1365 pass 1366 1367 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) 1368 lessvars[b'revcount'] = max(revcount // 2, 1) 1369 morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) 1370 morevars[b'revcount'] = revcount * 2 1371 1372 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex()) 1373 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars']) 1374 graphvars[b'graphtop'] = graphtop 1375 1376 count = len(web.repo) 1377 pos = rev 1378 1379 uprev = min(max(0, count - 1), rev + revcount) 1380 downrev = max(0, rev - revcount) 1381 changenav = webutil.revnav(web.repo).gen(pos, revcount, count) 1382 1383 tree = [] 1384 nextentry = [] 1385 lastrev = 0 1386 if pos != -1: 1387 allrevs = web.repo.changelog.revs(pos, 0) 1388 revs = [] 1389 for i in allrevs: 1390 revs.append(i) 1391 if len(revs) >= revcount + 1: 1392 break 1393 1394 if len(revs) > revcount: 1395 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])] 1396 revs = revs[:-1] 1397 1398 lastrev = revs[-1] 1399 1400 # We have to feed a baseset to dagwalker as it is expecting smartset 1401 # object. This does not have a big impact on hgweb performance itself 1402 # since hgweb graphing code is not itself lazy yet. 1403 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs)) 1404 # As we said one line above... not lazy. 1405 tree = list( 1406 item 1407 for item in graphmod.colored(dag, web.repo) 1408 if item[1] == graphmod.CHANGESET 1409 ) 1410 1411 def fulltree(): 1412 pos = web.repo[graphtop].rev() 1413 tree = [] 1414 if pos != -1: 1415 revs = web.repo.changelog.revs(pos, lastrev) 1416 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs)) 1417 tree = list( 1418 item 1419 for item in graphmod.colored(dag, web.repo) 1420 if item[1] == graphmod.CHANGESET 1421 ) 1422 return tree 1423 1424 def jsdata(context): 1425 for (id, type, ctx, vtx, edges) in fulltree(): 1426 yield { 1427 b'node': pycompat.bytestr(ctx), 1428 b'graphnode': webutil.getgraphnode(web.repo, ctx), 1429 b'vertex': vtx, 1430 b'edges': edges, 1431 } 1432 1433 def nodes(context): 1434 parity = paritygen(web.stripecount) 1435 for row, (id, type, ctx, vtx, edges) in enumerate(tree): 1436 entry = webutil.commonentry(web.repo, ctx) 1437 edgedata = [ 1438 { 1439 b'col': edge[0], 1440 b'nextcol': edge[1], 1441 b'color': (edge[2] - 1) % 6 + 1, 1442 b'width': edge[3], 1443 b'bcolor': edge[4], 1444 } 1445 for edge in edges 1446 ] 1447 1448 entry.update( 1449 { 1450 b'col': vtx[0], 1451 b'color': (vtx[1] - 1) % 6 + 1, 1452 b'parity': next(parity), 1453 b'edges': templateutil.mappinglist(edgedata), 1454 b'row': row, 1455 b'nextrow': row + 1, 1456 } 1457 ) 1458 1459 yield entry 1460 1461 rows = len(tree) 1462 1463 return web.sendtemplate( 1464 b'graph', 1465 rev=rev, 1466 symrev=symrev, 1467 revcount=revcount, 1468 uprev=uprev, 1469 lessvars=lessvars, 1470 morevars=morevars, 1471 downrev=downrev, 1472 graphvars=graphvars, 1473 rows=rows, 1474 bg_height=bg_height, 1475 changesets=count, 1476 nextentry=templateutil.mappinglist(nextentry), 1477 jsdata=templateutil.mappinggenerator(jsdata), 1478 nodes=templateutil.mappinggenerator(nodes), 1479 node=ctx.hex(), 1480 archives=web.archivelist(b'tip'), 1481 changenav=changenav, 1482 ) 1483 1484 1485def _getdoc(e): 1486 doc = e[0].__doc__ 1487 if doc: 1488 doc = _(doc).partition(b'\n')[0] 1489 else: 1490 doc = _(b'(no help text available)') 1491 return doc 1492 1493 1494@webcommand(b'help') 1495def help(web): 1496 """ 1497 /help[/{topic}] 1498 --------------- 1499 1500 Render help documentation. 1501 1502 This web command is roughly equivalent to :hg:`help`. If a ``topic`` 1503 is defined, that help topic will be rendered. If not, an index of 1504 available help topics will be rendered. 1505 1506 The ``help`` template will be rendered when requesting help for a topic. 1507 ``helptopics`` will be rendered for the index of help topics. 1508 """ 1509 from .. import commands, help as helpmod # avoid cycle 1510 1511 topicname = web.req.qsparams.get(b'node') 1512 if not topicname: 1513 1514 def topics(context): 1515 for h in helpmod.helptable: 1516 entries, summary, _doc = h[0:3] 1517 yield {b'topic': entries[0], b'summary': summary} 1518 1519 early, other = [], [] 1520 primary = lambda s: s.partition(b'|')[0] 1521 for c, e in pycompat.iteritems(commands.table): 1522 doc = _getdoc(e) 1523 if b'DEPRECATED' in doc or c.startswith(b'debug'): 1524 continue 1525 cmd = primary(c) 1526 if getattr(e[0], 'helpbasic', False): 1527 early.append((cmd, doc)) 1528 else: 1529 other.append((cmd, doc)) 1530 1531 early.sort() 1532 other.sort() 1533 1534 def earlycommands(context): 1535 for c, doc in early: 1536 yield {b'topic': c, b'summary': doc} 1537 1538 def othercommands(context): 1539 for c, doc in other: 1540 yield {b'topic': c, b'summary': doc} 1541 1542 return web.sendtemplate( 1543 b'helptopics', 1544 topics=templateutil.mappinggenerator(topics), 1545 earlycommands=templateutil.mappinggenerator(earlycommands), 1546 othercommands=templateutil.mappinggenerator(othercommands), 1547 title=b'Index', 1548 ) 1549 1550 # Render an index of sub-topics. 1551 if topicname in helpmod.subtopics: 1552 topics = [] 1553 for entries, summary, _doc in helpmod.subtopics[topicname]: 1554 topics.append( 1555 { 1556 b'topic': b'%s.%s' % (topicname, entries[0]), 1557 b'basename': entries[0], 1558 b'summary': summary, 1559 } 1560 ) 1561 1562 return web.sendtemplate( 1563 b'helptopics', 1564 topics=templateutil.mappinglist(topics), 1565 title=topicname, 1566 subindex=True, 1567 ) 1568 1569 u = webutil.wsgiui.load() 1570 u.verbose = True 1571 1572 # Render a page from a sub-topic. 1573 if b'.' in topicname: 1574 # TODO implement support for rendering sections, like 1575 # `hg help` works. 1576 topic, subtopic = topicname.split(b'.', 1) 1577 if topic not in helpmod.subtopics: 1578 raise ErrorResponse(HTTP_NOT_FOUND) 1579 else: 1580 topic = topicname 1581 subtopic = None 1582 1583 try: 1584 doc = helpmod.help_(u, commands, topic, subtopic=subtopic) 1585 except error.Abort: 1586 raise ErrorResponse(HTTP_NOT_FOUND) 1587 1588 return web.sendtemplate(b'help', topic=topicname, doc=doc) 1589 1590 1591# tell hggettext to extract docstrings from these functions: 1592i18nfunctions = commands.values() 1593