mercurial/hgweb/webcommands.py
changeset 43076 2372284d9457
parent 41397 0bd56c291359
child 43077 687b865b95ad
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()