webcommands.py
1598 lines
| 46.5 KiB
| text/x-python
|
PythonLexer
|
r5591 | # | ||
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||||
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r5591 | |||
|
r27046 | from __future__ import absolute_import | ||
import copy | ||||
import mimetypes | ||||
import os | ||||
import re | ||||
from ..i18n import _ | ||||
r37928 | from ..node import hex, short | |||
|
r27046 | |||
from .common import ( | ||||
ErrorResponse, | ||||
HTTP_FORBIDDEN, | ||||
HTTP_NOT_FOUND, | ||||
get_contact, | ||||
paritygen, | ||||
staticfile, | ||||
) | ||||
from .. import ( | ||||
archival, | ||||
|
r32904 | dagop, | ||
|
r27046 | encoding, | ||
error, | ||||
graphmod, | ||||
|
r34810 | pycompat, | ||
|
r27046 | revset, | ||
|
r31024 | revsetlang, | ||
|
r27046 | scmutil, | ||
|
r31023 | smartset, | ||
|
r27046 | templater, | ||
|
r37418 | templateutil, | ||
|
r37102 | ) | ||
|
r43346 | from ..utils import stringutil | ||
|
r27046 | |||
|
r43346 | from . import webutil | ||
|
r5591 | |||
|
r24076 | __all__ = [] | ||
|
r24077 | commands = {} | ||
|
r24076 | |||
|
r43346 | |||
|
r24076 | class webcommand(object): | ||
"""Decorator used to register a web command handler. | ||||
The decorator takes as its positional arguments the name/path the | ||||
command should be accessible under. | ||||
|
r36886 | When called, functions receive as arguments a ``requestcontext``, | ||
``wsgirequest``, and a templater instance for generatoring output. | ||||
The functions should populate the ``rctx.res`` object with details | ||||
about the HTTP response. | ||||
|
r36896 | The function returns a generator to be consumed by the WSGI application. | ||
For most commands, this should be the result from | ||||
|
r36899 | ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()`` | ||
to render a template. | ||||
|
r36886 | |||
|
r24076 | Usage: | ||
|
r5963 | |||
|
r24076 | @webcommand('mycommand') | ||
|
r36903 | def mycommand(web): | ||
|
r24076 | pass | ||
""" | ||||
|
r5963 | |||
|
r24076 | def __init__(self, name): | ||
self.name = name | ||||
def __call__(self, func): | ||||
__all__.append(self.name) | ||||
|
r24077 | commands[self.name] = func | ||
|
r24076 | return func | ||
|
r43346 | |||
|
r43347 | @webcommand(b'log') | ||
|
r36903 | def log(web): | ||
|
r24087 | """ | ||
/log[/{revision}[/{path}]] | ||||
-------------------------- | ||||
Show repository or file history. | ||||
For URLs of the form ``/log/{revision}``, a list of changesets starting at | ||||
the specified changeset identifier is shown. If ``{revision}`` is not | ||||
defined, the default is ``tip``. This form is equivalent to the | ||||
``changelog`` handler. | ||||
For URLs of the form ``/log/{revision}/{file}``, the history for a specific | ||||
file will be shown. This form is equivalent to the ``filelog`` handler. | ||||
""" | ||||
|
r43347 | if web.req.qsparams.get(b'file'): | ||
|
r36903 | return filelog(web) | ||
|
r5591 | else: | ||
|
r36903 | return changelog(web) | ||
|
r5591 | |||
|
r43346 | |||
|
r43347 | @webcommand(b'rawfile') | ||
|
r36903 | def rawfile(web): | ||
|
r43347 | guessmime = web.configbool(b'web', b'guessmime') | ||
|
r15004 | |||
|
r43347 | path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b'')) | ||
|
r5890 | if not path: | ||
|
r36903 | return manifest(web) | ||
|
r5890 | |||
try: | ||||
|
r36902 | fctx = webutil.filectx(web.repo, web.req) | ||
|
r25660 | except error.LookupError as inst: | ||
|
r6368 | try: | ||
|
r36903 | return manifest(web) | ||
|
r6368 | except ErrorResponse: | ||
raise inst | ||||
|
r5890 | |||
path = fctx.path() | ||||
text = fctx.data() | ||||
|
r43347 | mt = b'application/binary' | ||
|
r15004 | if guessmime: | ||
|
r40194 | mt = mimetypes.guess_type(pycompat.fsdecode(path))[0] | ||
|
r15004 | if mt is None: | ||
|
r37102 | if stringutil.binary(text): | ||
|
r43347 | mt = b'application/binary' | ||
|
r24306 | else: | ||
|
r43347 | mt = b'text/plain' | ||
|
r40194 | else: | ||
mt = pycompat.sysbytes(mt) | ||||
|
r43347 | if mt.startswith(b'text/'): | ||
mt += b'; charset="%s"' % encoding.encoding | ||||
|
r5890 | |||
|
r43347 | web.res.headers[b'Content-Type'] = mt | ||
|
r43346 | filename = ( | ||
|
r43347 | path.rpartition(b'/')[-1].replace(b'\\', b'\\\\').replace(b'"', b'\\"') | ||
|
r43346 | ) | ||
|
r43347 | web.res.headers[b'Content-Disposition'] = ( | ||
b'inline; filename="%s"' % filename | ||||
) | ||||
|
r36887 | web.res.setbodybytes(text) | ||
|
r36896 | return web.res.sendresponse() | ||
|
r5890 | |||
|
r43346 | |||
|
r36903 | def _filerevision(web, fctx): | ||
|
r6393 | f = fctx.path() | ||
text = fctx.data() | ||||
parity = paritygen(web.stripecount) | ||||
|
r39820 | ishead = fctx.filenode() in fctx.filelog().heads() | ||
|
r6393 | |||
|
r37102 | if stringutil.binary(text): | ||
|
r40194 | mt = pycompat.sysbytes( | ||
mimetypes.guess_type(pycompat.fsdecode(f))[0] | ||||
|
r43346 | or r'application/octet-stream' | ||
) | ||||
|
r43347 | text = b'(binary:%s)' % mt | ||
|
r6393 | |||
|
r38071 | def lines(context): | ||
|
r9136 | for lineno, t in enumerate(text.splitlines(True)): | ||
|
r43346 | yield { | ||
|
r43347 | b"line": t, | ||
b"lineid": b"l%d" % (lineno + 1), | ||||
b"linenumber": b"% 6d" % (lineno + 1), | ||||
b"parity": next(parity), | ||||
|
r43346 | } | ||
|
r6393 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'filerevision', | ||
|
r36887 | file=f, | ||
path=webutil.up(f), | ||||
|
r38071 | text=templateutil.mappinggenerator(lines), | ||
|
r36902 | symrev=webutil.symrevorshortnode(web.req, fctx), | ||
|
r36887 | rename=webutil.renamelink(fctx), | ||
permissions=fctx.manifest().flags(f), | ||||
ishead=int(ishead), | ||||
|
r43346 | **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)) | ||
) | ||||
|
r6393 | |||
|
r43347 | @webcommand(b'file') | ||
|
r36903 | def file(web): | ||
|
r24088 | """ | ||
/file/{revision}[/{path}] | ||||
------------------------- | ||||
Show information about a directory or file in the repository. | ||||
Info about the ``path`` given as a URL parameter will be rendered. | ||||
If ``path`` is a directory, information about the entries in that | ||||
directory will be rendered. This form is equivalent to the ``manifest`` | ||||
handler. | ||||
If ``path`` is a file, information about that file will be shown via | ||||
the ``filerevision`` template. | ||||
If ``path`` is not defined, information about the root directory will | ||||
be rendered. | ||||
""" | ||||
|
r43347 | if web.req.qsparams.get(b'style') == b'raw': | ||
|
r36903 | return rawfile(web) | ||
|
r36888 | |||
|
r43347 | path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b'')) | ||
|
r6853 | if not path: | ||
|
r36903 | return manifest(web) | ||
|
r6853 | try: | ||
|
r36903 | return _filerevision(web, webutil.filectx(web.repo, web.req)) | ||
|
r25660 | except error.LookupError as inst: | ||
|
r5591 | try: | ||
|
r36903 | return manifest(web) | ||
|
r6853 | except ErrorResponse: | ||
raise inst | ||||
|
r5591 | |||
|
r43346 | |||
|
r36900 | def _search(web): | ||
|
r43347 | MODE_REVISION = b'rev' | ||
MODE_KEYWORD = b'keyword' | ||||
MODE_REVSET = b'revset' | ||||
|
r10247 | |||
|
r19633 | def revsearch(ctx): | ||
yield ctx | ||||
|
r19632 | def keywordsearch(query): | ||
|
r15727 | lower = encoding.lower | ||
qw = lower(query).split() | ||||
|
r6393 | |||
def revgen(): | ||||
|
r18497 | cl = web.repo.changelog | ||
|
r38806 | for i in pycompat.xrange(len(web.repo) - 1, 0, -100): | ||
|
r6393 | l = [] | ||
|
r19491 | for j in cl.revs(max(0, i - 99), i): | ||
|
r6747 | ctx = web.repo[j] | ||
|
r6393 | l.append(ctx) | ||
l.reverse() | ||||
for e in l: | ||||
yield e | ||||
for ctx in revgen(): | ||||
miss = 0 | ||||
for q in qw: | ||||
|
r43346 | if not ( | ||
q in lower(ctx.user()) | ||||
or q in lower(ctx.description()) | ||||
|
r43347 | or q in lower(b" ".join(ctx.files())) | ||
|
r43346 | ): | ||
|
r6393 | miss = 1 | ||
break | ||||
if miss: | ||||
continue | ||||
|
r19533 | yield ctx | ||
|
r19722 | def revsetsearch(revs): | ||
for r in revs: | ||||
yield web.repo[r] | ||||
|
r19631 | searchfuncs = { | ||
|
r43347 | MODE_REVISION: (revsearch, b'exact revision search'), | ||
MODE_KEYWORD: (keywordsearch, b'literal keyword search'), | ||||
MODE_REVSET: (revsetsearch, b'revset expression search'), | ||||
|
r19631 | } | ||
|
r19632 | def getsearchmode(query): | ||
|
r19633 | try: | ||
|
r37405 | ctx = scmutil.revsymbol(web.repo, query) | ||
|
r19633 | except (error.RepoError, error.LookupError): | ||
|
r19722 | # query is not an exact revision pointer, need to | ||
|
r19951 | # decide if it's a revset expression or keywords | ||
|
r19722 | pass | ||
|
r19633 | else: | ||
|
r19656 | return MODE_REVISION, ctx | ||
|
r19631 | |||
|
r43347 | revdef = b'reverse(%s)' % query | ||
|
r19722 | try: | ||
|
r31024 | tree = revsetlang.parse(revdef) | ||
|
r27009 | except error.ParseError: | ||
|
r19722 | # can't parse to a revset tree | ||
return MODE_KEYWORD, query | ||||
|
r31024 | if revsetlang.depth(tree) <= 2: | ||
|
r19722 | # no revset syntax used | ||
return MODE_KEYWORD, query | ||||
|
r43346 | if any( | ||
|
r43347 | (token, (value or b'')[:3]) == (b'string', b're:') | ||
|
r43346 | for token, value, pos in revsetlang.tokenize(revdef) | ||
): | ||||
|
r19722 | return MODE_KEYWORD, query | ||
|
r31024 | funcsused = revsetlang.funcsused(tree) | ||
|
r19722 | if not funcsused.issubset(revset.safesymbols): | ||
return MODE_KEYWORD, query | ||||
try: | ||||
|
r43346 | mfunc = revset.match( | ||
web.repo.ui, revdef, lookup=revset.lookupfn(web.repo) | ||||
) | ||||
|
r24114 | revs = mfunc(web.repo) | ||
|
r19722 | return MODE_REVSET, revs | ||
# ParseError: wrongly placed tokens, wrongs arguments, etc | ||||
# RepoLookupError: no such revision, e.g. in 'revision:' | ||||
# Abort: bookmark/tag not exists | ||||
# LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo | ||||
|
r43346 | except ( | ||
error.ParseError, | ||||
error.RepoLookupError, | ||||
error.Abort, | ||||
LookupError, | ||||
): | ||||
|
r19722 | return MODE_KEYWORD, query | ||
|
r37418 | def changelist(context): | ||
|
r19533 | count = 0 | ||
|
r19765 | for ctx in searchfunc[0](funcarg): | ||
|
r6659 | count += 1 | ||
|
r39830 | n = scmutil.binnode(ctx) | ||
|
r43347 | showtags = webutil.showtag(web.repo, b'changelogtag', n) | ||
|
r37973 | files = webutil.listfilediffs(ctx.files(), n, web.maxfiles) | ||
|
r6393 | |||
|
r37037 | lm = webutil.commonentry(web.repo, ctx) | ||
|
r43346 | lm.update( | ||
{ | ||||
|
r43347 | b'parity': next(parity), | ||
b'changelogtag': showtags, | ||||
b'files': files, | ||||
|
r43346 | } | ||
) | ||||
|
r37418 | yield lm | ||
|
r6393 | |||
|
r10247 | if count >= revcount: | ||
|
r6393 | break | ||
|
r43347 | query = web.req.qsparams[b'rev'] | ||
|
r19418 | revcount = web.maxchanges | ||
|
r43347 | if b'revcount' in web.req.qsparams: | ||
|
r20092 | try: | ||
|
r43347 | revcount = int(web.req.qsparams.get(b'revcount', revcount)) | ||
|
r20092 | revcount = max(revcount, 1) | ||
|
r43347 | web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount | ||
|
r20092 | except ValueError: | ||
pass | ||||
|
r19418 | |||
|
r43347 | lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||
lessvars[b'revcount'] = max(revcount // 2, 1) | ||||
lessvars[b'rev'] = query | ||||
morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||||
morevars[b'revcount'] = revcount * 2 | ||||
morevars[b'rev'] = query | ||||
|
r19418 | |||
|
r19632 | mode, funcarg = getsearchmode(query) | ||
|
r19768 | |||
|
r43347 | if b'forcekw' in web.req.qsparams: | ||
showforcekw = b'' | ||||
|
r19768 | showunforcekw = searchfuncs[mode][1] | ||
mode = MODE_KEYWORD | ||||
funcarg = query | ||||
else: | ||||
if mode != MODE_KEYWORD: | ||||
showforcekw = searchfuncs[MODE_KEYWORD][1] | ||||
else: | ||||
|
r43347 | showforcekw = b'' | ||
showunforcekw = b'' | ||||
|
r19768 | |||
|
r19631 | searchfunc = searchfuncs[mode] | ||
|
r43347 | tip = web.repo[b'tip'] | ||
|
r6393 | parity = paritygen(web.stripecount) | ||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'search', | ||
|
r36887 | query=query, | ||
node=tip.hex(), | ||||
|
r43347 | symrev=b'tip', | ||
entries=templateutil.mappinggenerator(changelist, name=b'searchentry'), | ||||
archives=web.archivelist(b'tip'), | ||||
|
r36887 | morevars=morevars, | ||
lessvars=lessvars, | ||||
modedesc=searchfunc[1], | ||||
showforcekw=showforcekw, | ||||
|
r43346 | showunforcekw=showunforcekw, | ||
) | ||||
|
r6393 | |||
|
r43347 | @webcommand(b'changelog') | ||
|
r36903 | def changelog(web, shortlog=False): | ||
|
r24089 | """ | ||
/changelog[/{revision}] | ||||
----------------------- | ||||
Show information about multiple changesets. | ||||
If the optional ``revision`` URL argument is absent, information about | ||||
all changesets starting at ``tip`` will be rendered. If the ``revision`` | ||||
argument is present, changesets will be shown starting from the specified | ||||
revision. | ||||
If ``revision`` is absent, the ``rev`` query string argument may be | ||||
defined. This will perform a search for changesets. | ||||
The argument for ``rev`` can be a single revision, a revision set, | ||||
or a literal keyword to search for in changeset data (equivalent to | ||||
|
r24867 | :hg:`log -k`). | ||
|
r24089 | |||
The ``revcount`` query string argument defines the maximum numbers of | ||||
changesets to render. | ||||
For non-searches, the ``changelog`` template will be rendered. | ||||
""" | ||||
|
r10247 | |||
|
r43347 | query = b'' | ||
if b'node' in web.req.qsparams: | ||||
|
r36902 | ctx = webutil.changectx(web.repo, web.req) | ||
symrev = webutil.symrevorshortnode(web.req, ctx) | ||||
|
r43347 | elif b'rev' in web.req.qsparams: | ||
|
r36900 | return _search(web) | ||
|
r5591 | else: | ||
|
r43347 | ctx = web.repo[b'tip'] | ||
symrev = b'tip' | ||||
|
r6393 | |||
|
r39419 | def changelist(maxcount): | ||
|
r18427 | revs = [] | ||
|
r19486 | if pos != -1: | ||
revs = web.repo.changelog.revs(pos, 0) | ||||
|
r23745 | |||
|
r39419 | for entry in webutil.changelistentries(web, revs, maxcount, parity): | ||
|
r23745 | yield entry | ||
|
r6393 | |||
|
r24306 | if shortlog: | ||
revcount = web.maxshortchanges | ||||
else: | ||||
revcount = web.maxchanges | ||||
|
r43347 | if b'revcount' in web.req.qsparams: | ||
|
r20092 | try: | ||
|
r43347 | revcount = int(web.req.qsparams.get(b'revcount', revcount)) | ||
|
r20092 | revcount = max(revcount, 1) | ||
|
r43347 | web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount | ||
|
r20092 | except ValueError: | ||
pass | ||||
|
r10246 | |||
|
r43347 | lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||
lessvars[b'revcount'] = max(revcount // 2, 1) | ||||
morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||||
morevars[b'revcount'] = revcount * 2 | ||||
|
r10246 | |||
|
r12059 | count = len(web.repo) | ||
|
r6393 | pos = ctx.rev() | ||
|
r19486 | parity = paritygen(web.stripecount) | ||
|
r6393 | |||
|
r18409 | changenav = webutil.revnav(web.repo).gen(pos, revcount, count) | ||
|
r6393 | |||
|
r39419 | entries = list(changelist(revcount + 1)) | ||
|
r19737 | latestentry = entries[:1] | ||
|
r19738 | if len(entries) > revcount: | ||
nextentry = entries[-1:] | ||||
entries = entries[:-1] | ||||
else: | ||||
nextentry = [] | ||||
|
r19737 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'shortlog' if shortlog else b'changelog', | ||
|
r36887 | changenav=changenav, | ||
node=ctx.hex(), | ||||
rev=pos, | ||||
symrev=symrev, | ||||
changesets=count, | ||||
|
r38072 | entries=templateutil.mappinglist(entries), | ||
latestentry=templateutil.mappinglist(latestentry), | ||||
nextentry=templateutil.mappinglist(nextentry), | ||||
|
r43347 | archives=web.archivelist(b'tip'), | ||
|
r36887 | revcount=revcount, | ||
morevars=morevars, | ||||
lessvars=lessvars, | ||||
|
r43346 | query=query, | ||
) | ||||
|
r5591 | |||
|
r43347 | @webcommand(b'shortlog') | ||
|
r36903 | def shortlog(web): | ||
|
r24086 | """ | ||
/shortlog | ||||
--------- | ||||
Show basic information about a set of changesets. | ||||
This accepts the same parameters as the ``changelog`` handler. The only | ||||
difference is the ``shortlog`` template will be rendered instead of the | ||||
``changelog`` template. | ||||
""" | ||||
|
r36903 | return changelog(web, shortlog=True) | ||
|
r5591 | |||
|
r43346 | |||
|
r43347 | @webcommand(b'changeset') | ||
|
r36903 | def changeset(web): | ||
|
r24085 | """ | ||
/changeset[/{revision}] | ||||
----------------------- | ||||
Show information about a single changeset. | ||||
A URL path argument is the changeset identifier to show. See ``hg help | ||||
revisions`` for possible values. If not defined, the ``tip`` changeset | ||||
will be shown. | ||||
The ``changeset`` template is rendered. Contents of the ``changesettag``, | ||||
``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many | ||||
templates related to diffs may all be used to produce the output. | ||||
""" | ||||
|
r36902 | ctx = webutil.changectx(web.repo, web.req) | ||
|
r36899 | |||
|
r43347 | return web.sendtemplate(b'changeset', **webutil.changesetentry(web, ctx)) | ||
|
r43346 | |||
|
r5591 | |||
|
r43347 | rev = webcommand(b'rev')(changeset) | ||
|
r5591 | |||
|
r43346 | |||
|
r16448 | def decodepath(path): | ||
"""Hook for mapping a path in the repository to a path in the | ||||
working copy. | ||||
Extensions (e.g., largefiles) can override this to remap files in | ||||
the virtual file system presented by the manifest command below.""" | ||||
return path | ||||
|
r43346 | |||
|
r43347 | @webcommand(b'manifest') | ||
|
r36903 | def manifest(web): | ||
|
r24090 | """ | ||
/manifest[/{revision}[/{path}]] | ||||
------------------------------- | ||||
Show information about a directory. | ||||
|
r24868 | If the URL path arguments are omitted, information about the root | ||
|
r24090 | directory for the ``tip`` changeset will be shown. | ||
Because this handler can only show information for directories, it | ||||
is recommended to use the ``file`` handler instead, as it can handle both | ||||
directories and files. | ||||
The ``manifest`` template will be rendered for this handler. | ||||
""" | ||||
|
r43347 | if b'node' in web.req.qsparams: | ||
|
r36902 | ctx = webutil.changectx(web.repo, web.req) | ||
symrev = webutil.symrevorshortnode(web.req, ctx) | ||||
r25602 | else: | |||
|
r43347 | ctx = web.repo[b'tip'] | ||
symrev = b'tip' | ||||
path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b'')) | ||||
|
r6393 | mf = ctx.manifest() | ||
|
r39830 | node = scmutil.binnode(ctx) | ||
|
r6393 | |||
files = {} | ||||
|
r7305 | dirs = {} | ||
|
r6393 | parity = paritygen(web.stripecount) | ||
|
r43347 | if path and path[-1:] != b"/": | ||
path += b"/" | ||||
|
r6393 | l = len(path) | ||
|
r43347 | abspath = b"/" + path | ||
|
r6393 | |||
|
r16448 | for full, n in mf.iteritems(): | ||
# the virtual path (working copy path) used for the full | ||||
# (repository) path | ||||
f = decodepath(full) | ||||
|
r6393 | if f[:l] != path: | ||
continue | ||||
remain = f[l:] | ||||
|
r43347 | elements = remain.split(b'/') | ||
|
r7305 | if len(elements) == 1: | ||
|
r16448 | files[remain] = full | ||
|
r7305 | else: | ||
|
r43346 | h = dirs # need to retain ref to dirs (root) | ||
|
r7305 | for elem in elements[0:-1]: | ||
if elem not in h: | ||||
h[elem] = {} | ||||
h = h[elem] | ||||
if len(h) > 1: | ||||
break | ||||
|
r43346 | h[None] = None # denotes files present | ||
|
r6393 | |||
|
r7565 | if mf and not files and not dirs: | ||
|
r43347 | raise ErrorResponse(HTTP_NOT_FOUND, b'path not found: ' + path) | ||
|
r6393 | |||
|
r38073 | def filelist(context): | ||
|
r8209 | for f in sorted(files): | ||
|
r7305 | full = files[f] | ||
|
r6393 | |||
fctx = ctx.filectx(full) | ||||
|
r43346 | yield { | ||
|
r43347 | b"file": full, | ||
b"parity": next(parity), | ||||
b"basename": f, | ||||
b"date": fctx.date(), | ||||
b"size": fctx.size(), | ||||
b"permissions": mf.flags(full), | ||||
|
r43346 | } | ||
|
r6393 | |||
|
r38073 | def dirlist(context): | ||
|
r8209 | for d in sorted(dirs): | ||
|
r6393 | |||
|
r7305 | emptydirs = [] | ||
h = dirs[d] | ||||
while isinstance(h, dict) and len(h) == 1: | ||||
|
r36288 | k, v = next(iter(h.items())) | ||
|
r7305 | if v: | ||
emptydirs.append(k) | ||||
h = v | ||||
|
r43347 | path = b"%s%s" % (abspath, d) | ||
|
r43346 | yield { | ||
|
r43347 | b"parity": next(parity), | ||
b"path": path, | ||||
b"emptydirs": b"/".join(emptydirs), | ||||
b"basename": d, | ||||
|
r43346 | } | ||
|
r6393 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'manifest', | ||
|
r36887 | symrev=symrev, | ||
path=abspath, | ||||
up=webutil.up(abspath), | ||||
upparity=next(parity), | ||||
|
r38073 | fentries=templateutil.mappinggenerator(filelist), | ||
dentries=templateutil.mappinggenerator(dirlist), | ||||
|
r36887 | archives=web.archivelist(hex(node)), | ||
|
r43346 | **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)) | ||
) | ||||
|
r5591 | |||
|
r43347 | @webcommand(b'tags') | ||
|
r36903 | def tags(web): | ||
|
r24084 | """ | ||
/tags | ||||
----- | ||||
Show information about tags. | ||||
No arguments are accepted. | ||||
The ``tags`` template is rendered. | ||||
""" | ||||
|
r18029 | i = list(reversed(web.repo.tagslist())) | ||
|
r6393 | parity = paritygen(web.stripecount) | ||
|
r38074 | def entries(context, notip, latestonly): | ||
|
r18402 | t = i | ||
if notip: | ||||
|
r43347 | t = [(k, n) for k, n in i if k != b"tip"] | ||
|
r18402 | if latestonly: | ||
t = t[:1] | ||||
for k, n in t: | ||||
|
r43346 | yield { | ||
|
r43347 | b"parity": next(parity), | ||
b"tag": k, | ||||
b"date": web.repo[n].date(), | ||||
b"node": hex(n), | ||||
|
r43346 | } | ||
|
r6393 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'tags', | ||
|
r36887 | node=hex(web.repo.changelog.tip()), | ||
|
r38074 | entries=templateutil.mappinggenerator(entries, args=(False, False)), | ||
|
r43346 | entriesnotip=templateutil.mappinggenerator(entries, args=(True, False)), | ||
latestentry=templateutil.mappinggenerator(entries, args=(True, True)), | ||||
) | ||||
|
r5591 | |||
|
r43347 | @webcommand(b'bookmarks') | ||
|
r36903 | def bookmarks(web): | ||
|
r24083 | """ | ||
/bookmarks | ||||
---------- | ||||
Show information about bookmarks. | ||||
No arguments are accepted. | ||||
The ``bookmarks`` template is rendered. | ||||
""" | ||||
|
r18478 | i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo] | ||
r28711 | sortkey = lambda b: (web.repo[b[1]].rev(), b[0]) | |||
i = sorted(i, key=sortkey, reverse=True) | ||||
|
r13597 | parity = paritygen(web.stripecount) | ||
|
r38149 | def entries(context, latestonly): | ||
r28710 | t = i | |||
|
r18402 | if latestonly: | ||
r28710 | t = i[:1] | |||
|
r18402 | for k, n in t: | ||
|
r43346 | yield { | ||
|
r43347 | b"parity": next(parity), | ||
b"bookmark": k, | ||||
b"date": web.repo[n].date(), | ||||
b"node": hex(n), | ||||
|
r43346 | } | ||
|
r13597 | |||
r28712 | if i: | |||
latestrev = i[0][1] | ||||
else: | ||||
latestrev = -1 | ||||
|
r38150 | lastdate = web.repo[latestrev].date() | ||
r28712 | ||||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'bookmarks', | ||
|
r36887 | node=hex(web.repo.changelog.tip()), | ||
|
r43347 | lastchange=templateutil.mappinglist([{b'date': lastdate}]), | ||
|
r38149 | entries=templateutil.mappinggenerator(entries, args=(False,)), | ||
|
r43346 | latestentry=templateutil.mappinggenerator(entries, args=(True,)), | ||
) | ||||
|
r13597 | |||
|
r43347 | @webcommand(b'branches') | ||
|
r36903 | def branches(web): | ||
|
r24082 | """ | ||
/branches | ||||
--------- | ||||
Show information about branches. | ||||
All known branches are contained in the output, even closed branches. | ||||
No arguments are accepted. | ||||
The ``branches`` template is rendered. | ||||
""" | ||||
r26129 | entries = webutil.branchentries(web.repo, web.stripecount) | |||
latestentry = webutil.branchentries(web.repo, web.stripecount, 1) | ||||
|
r36887 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'branches', | ||
|
r36887 | node=hex(web.repo.changelog.tip()), | ||
entries=entries, | ||||
|
r43346 | latestentry=latestentry, | ||
) | ||||
|
r8352 | |||
|
r43347 | @webcommand(b'summary') | ||
|
r36903 | def summary(web): | ||
|
r24091 | """ | ||
/summary | ||||
-------- | ||||
Show a summary of repository state. | ||||
Information about the latest changesets, bookmarks, tags, and branches | ||||
is captured by this handler. | ||||
The ``summary`` template is rendered. | ||||
""" | ||||
|
r17261 | i = reversed(web.repo.tagslist()) | ||
|
r6393 | |||
|
r37419 | def tagentries(context): | ||
|
r6393 | parity = paritygen(web.stripecount) | ||
count = 0 | ||||
for k, n in i: | ||||
|
r43347 | if k == b"tip": # skip tip | ||
|
r6393 | continue | ||
|
r6659 | count += 1 | ||
|
r43346 | if count > 10: # limit to 10 tags | ||
|
r6393 | break | ||
|
r37419 | yield { | ||
|
r43347 | b'parity': next(parity), | ||
b'tag': k, | ||||
b'node': hex(n), | ||||
b'date': web.repo[n].date(), | ||||
|
r37419 | } | ||
|
r6393 | |||
|
r38151 | def bookmarks(context): | ||
|
r13924 | parity = paritygen(web.stripecount) | ||
|
r18563 | marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo] | ||
r28711 | sortkey = lambda b: (web.repo[b[1]].rev(), b[0]) | |||
marks = sorted(marks, key=sortkey, reverse=True) | ||||
for k, n in marks[:10]: # limit to 10 bookmarks | ||||
|
r43346 | yield { | ||
|
r43347 | b'parity': next(parity), | ||
b'bookmark': k, | ||||
b'date': web.repo[n].date(), | ||||
b'node': hex(n), | ||||
|
r43346 | } | ||
|
r13924 | |||
|
r37419 | def changelist(context): | ||
|
r10282 | parity = paritygen(web.stripecount, offset=start - end) | ||
|
r43346 | l = [] # build a list in forward order for efficiency | ||
|
r18563 | revs = [] | ||
if start < end: | ||||
revs = web.repo.changelog.revs(start, end - 1) | ||||
for i in revs: | ||||
|
r6747 | ctx = web.repo[i] | ||
|
r37037 | lm = webutil.commonentry(web.repo, ctx) | ||
|
r43347 | lm[b'parity'] = next(parity) | ||
|
r37419 | l.append(lm) | ||
|
r6393 | |||
|
r29382 | for entry in reversed(l): | ||
yield entry | ||||
|
r6393 | |||
|
r43347 | tip = web.repo[b'tip'] | ||
|
r12059 | count = len(web.repo) | ||
|
r6393 | start = max(0, count - web.maxchanges) | ||
end = min(count, start + web.maxchanges) | ||||
|
r43347 | desc = web.config(b"web", b"description") | ||
|
r34235 | if not desc: | ||
|
r43347 | desc = b'unknown' | ||
labels = web.configlist(b'web', b'labels') | ||||
|
r36887 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'summary', | ||
|
r36887 | desc=desc, | ||
|
r43347 | owner=get_contact(web.config) or b'unknown', | ||
|
r36887 | lastchange=tip.date(), | ||
|
r43347 | tags=templateutil.mappinggenerator(tagentries, name=b'tagentry'), | ||
|
r38151 | bookmarks=templateutil.mappinggenerator(bookmarks), | ||
|
r36887 | branches=webutil.branchentries(web.repo, web.stripecount, 10), | ||
|
r43346 | shortlog=templateutil.mappinggenerator( | ||
|
r43347 | changelist, name=b'shortlogentry' | ||
|
r43346 | ), | ||
|
r36887 | node=tip.hex(), | ||
|
r43347 | symrev=b'tip', | ||
archives=web.archivelist(b'tip'), | ||||
labels=templateutil.hybridlist(labels, name=b'label'), | ||||
|
r43346 | ) | ||
|
r5591 | |||
|
r43347 | @webcommand(b'filediff') | ||
|
r36903 | def filediff(web): | ||
|
r24092 | """ | ||
/diff/{revision}/{path} | ||||
----------------------- | ||||
Show how a file changed in a particular commit. | ||||
The ``filediff`` template is rendered. | ||||
|
r26781 | This handler is registered under both the ``/diff`` and ``/filediff`` | ||
|
r24092 | paths. ``/diff`` is used in modern code. | ||
""" | ||||
|
r7183 | fctx, ctx = None, None | ||
try: | ||||
|
r36902 | fctx = webutil.filectx(web.repo, web.req) | ||
|
r7280 | except LookupError: | ||
|
r36902 | ctx = webutil.changectx(web.repo, web.req) | ||
|
r43347 | path = webutil.cleanpath(web.repo, web.req.qsparams[b'file']) | ||
|
r7183 | if path not in ctx.files(): | ||
raise | ||||
if fctx is not None: | ||||
path = fctx.path() | ||||
|
r16722 | ctx = fctx.changectx() | ||
|
r31082 | basectx = ctx.p1() | ||
|
r6393 | |||
|
r43347 | style = web.config(b'web', b'style') | ||
if b'style' in web.req.qsparams: | ||||
style = web.req.qsparams[b'style'] | ||||
|
r9402 | |||
|
r36901 | diffs = webutil.diffs(web, ctx, basectx, [path], style) | ||
r27160 | if fctx is not None: | |||
|
r24306 | rename = webutil.renamelink(fctx) | ||
ctx = fctx | ||||
else: | ||||
|
r37921 | rename = templateutil.mappinglist([]) | ||
|
r24306 | ctx = ctx | ||
|
r36887 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'filediff', | ||
|
r36887 | file=path, | ||
|
r36902 | symrev=webutil.symrevorshortnode(web.req, ctx), | ||
|
r36887 | rename=rename, | ||
diff=diffs, | ||||
|
r43346 | **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)) | ||
) | ||||
|
r5591 | |||
|
r43347 | diff = webcommand(b'diff')(filediff) | ||
|
r5591 | |||
|
r43346 | |||
|
r43347 | @webcommand(b'comparison') | ||
|
r36903 | def comparison(web): | ||
|
r24093 | """ | ||
/comparison/{revision}/{path} | ||||
----------------------------- | ||||
Show a comparison between the old and new versions of a file from changes | ||||
made on a particular revision. | ||||
This is similar to the ``diff`` handler. However, this form features | ||||
a split or side-by-side diff rather than a unified diff. | ||||
The ``context`` query string argument can be used to control the lines of | ||||
context in the diff. | ||||
The ``filecomparison`` template is rendered. | ||||
""" | ||||
|
r36902 | ctx = webutil.changectx(web.repo, web.req) | ||
|
r43347 | if b'file' not in web.req.qsparams: | ||
raise ErrorResponse(HTTP_NOT_FOUND, b'file not given') | ||||
path = webutil.cleanpath(web.repo, web.req.qsparams[b'file']) | ||||
|
r17202 | |||
|
r43347 | parsecontext = lambda v: v == b'full' and -1 or int(v) | ||
if b'context' in web.req.qsparams: | ||||
context = parsecontext(web.req.qsparams[b'context']) | ||||
|
r17202 | else: | ||
|
r43347 | context = parsecontext(web.config(b'web', b'comparisoncontext')) | ||
|
r17202 | |||
|
r17302 | def filelines(f): | ||
|
r32136 | if f.isbinary(): | ||
|
r40194 | mt = pycompat.sysbytes( | ||
mimetypes.guess_type(pycompat.fsdecode(f.path()))[0] | ||||
|
r43346 | or r'application/octet-stream' | ||
) | ||||
|
r43347 | return [_(b'(binary file %s, hash: %s)') % (mt, hex(f.filenode()))] | ||
|
r17302 | return f.data().splitlines() | ||
r27158 | fctx = None | |||
|
r21123 | parent = ctx.p1() | ||
leftrev = parent.rev() | ||||
leftnode = parent.node() | ||||
rightrev = ctx.rev() | ||||
|
r39830 | rightnode = scmutil.binnode(ctx) | ||
|
r17302 | if path in ctx: | ||
fctx = ctx[path] | ||||
rightlines = filelines(fctx) | ||||
|
r21121 | if path not in parent: | ||
|
r17302 | leftlines = () | ||
else: | ||||
|
r21121 | pfctx = parent[path] | ||
|
r17302 | leftlines = filelines(pfctx) | ||
else: | ||||
rightlines = () | ||||
|
r41442 | pfctx = ctx.p1()[path] | ||
r27158 | leftlines = filelines(pfctx) | |||
|
r17302 | |||
|
r38013 | comparison = webutil.compare(context, leftlines, rightlines) | ||
r27158 | if fctx is not None: | |||
r27159 | rename = webutil.renamelink(fctx) | |||
r27158 | ctx = fctx | |||
else: | ||||
|
r37921 | rename = templateutil.mappinglist([]) | ||
r27158 | ctx = ctx | |||
|
r36887 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'filecomparison', | ||
|
r36887 | file=path, | ||
|
r36902 | symrev=webutil.symrevorshortnode(web.req, ctx), | ||
|
r36887 | rename=rename, | ||
leftrev=leftrev, | ||||
leftnode=hex(leftnode), | ||||
rightrev=rightrev, | ||||
rightnode=hex(rightnode), | ||||
comparison=comparison, | ||||
|
r43346 | **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)) | ||
) | ||||
|
r17202 | |||
|
r43347 | @webcommand(b'annotate') | ||
|
r36903 | def annotate(web): | ||
|
r24094 | """ | ||
/annotate/{revision}/{path} | ||||
--------------------------- | ||||
Show changeset information for each line in a file. | ||||
|
r34391 | The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and | ||
``ignoreblanklines`` query string arguments have the same meaning as | ||||
|
r34404 | their ``[annotate]`` config equivalents. It uses the hgrc boolean | ||
parsing logic to interpret the value. e.g. ``0`` and ``false`` are | ||||
false and ``1`` and ``true`` are true. If not defined, the server | ||||
default settings are used. | ||||
|
r34391 | |||
|
r24094 | The ``fileannotate`` template is rendered. | ||
""" | ||||
|
r36902 | fctx = webutil.filectx(web.repo, web.req) | ||
|
r6393 | f = fctx.path() | ||
parity = paritygen(web.stripecount) | ||||
|
r39820 | ishead = fctx.filenode() in fctx.filelog().heads() | ||
|
r6393 | |||
|
r30298 | # parents() is called once per line and several lines likely belong to | ||
# same revision. So it is worth caching. | ||||
# TODO there are still redundant operations within basefilectx.parents() | ||||
# and from the fctx.annotate() call itself that could be cached. | ||||
parentscache = {} | ||||
|
r43346 | |||
|
r38153 | def parents(context, f): | ||
|
r30298 | rev = f.rev() | ||
if rev not in parentscache: | ||||
parentscache[rev] = [] | ||||
for p in f.parents(): | ||||
entry = { | ||||
|
r43347 | b'node': p.hex(), | ||
b'rev': p.rev(), | ||||
|
r30298 | } | ||
parentscache[rev].append(entry) | ||||
for p in parentscache[rev]: | ||||
yield p | ||||
|
r29522 | |||
|
r38152 | def annotate(context): | ||
|
r32136 | if fctx.isbinary(): | ||
|
r40194 | mt = pycompat.sysbytes( | ||
mimetypes.guess_type(pycompat.fsdecode(fctx.path()))[0] | ||||
|
r43346 | or r'application/octet-stream' | ||
) | ||||
lines = [ | ||||
dagop.annotateline( | ||||
fctx=fctx.filectx(fctx.filerev()), | ||||
lineno=1, | ||||
|
r43347 | text=b'(binary:%s)' % mt, | ||
|
r43346 | ) | ||
] | ||||
|
r6393 | else: | ||
|
r36902 | lines = webutil.annotate(web.req, fctx, web.repo.ui) | ||
|
r30081 | |||
|
r29388 | previousrev = None | ||
r29572 | blockparitygen = paritygen(1) | |||
|
r37084 | for lineno, aline in enumerate(lines): | ||
|
r34433 | f = aline.fctx | ||
|
r29388 | rev = f.rev() | ||
r29572 | if rev != previousrev: | |||
blockhead = True | ||||
blockparity = next(blockparitygen) | ||||
else: | ||||
blockhead = None | ||||
|
r29388 | previousrev = rev | ||
|
r43346 | yield { | ||
|
r43347 | b"parity": next(parity), | ||
b"node": f.hex(), | ||||
b"rev": rev, | ||||
b"author": f.user(), | ||||
b"parents": templateutil.mappinggenerator(parents, args=(f,)), | ||||
b"desc": f.description(), | ||||
b"extra": f.extra(), | ||||
b"file": f.path(), | ||||
b"blockhead": blockhead, | ||||
b"blockparity": blockparity, | ||||
b"targetline": aline.lineno, | ||||
b"line": aline.text, | ||||
b"lineno": lineno + 1, | ||||
b"lineid": b"l%d" % (lineno + 1), | ||||
b"linenumber": b"% 6d" % (lineno + 1), | ||||
b"revdate": f.date(), | ||||
|
r43346 | } | ||
|
r6393 | |||
|
r43347 | diffopts = webutil.difffeatureopts(web.req, web.repo.ui, b'annotate') | ||
|
r34392 | diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults} | ||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'fileannotate', | ||
|
r36887 | file=f, | ||
|
r38152 | annotate=templateutil.mappinggenerator(annotate), | ||
|
r36887 | path=webutil.up(f), | ||
|
r36902 | symrev=webutil.symrevorshortnode(web.req, fctx), | ||
|
r36887 | rename=webutil.renamelink(fctx), | ||
permissions=fctx.manifest().flags(f), | ||||
ishead=int(ishead), | ||||
|
r38154 | diffopts=templateutil.hybriddict(diffopts), | ||
|
r43346 | **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)) | ||
) | ||||
|
r5591 | |||
|
r43347 | @webcommand(b'filelog') | ||
|
r36903 | def filelog(web): | ||
|
r24095 | """ | ||
/filelog/{revision}/{path} | ||||
-------------------------- | ||||
Show information about the history of a file in the repository. | ||||
The ``revcount`` query string argument can be defined to control the | ||||
maximum number of entries to show. | ||||
The ``filelog`` template will be rendered. | ||||
""" | ||||
|
r7300 | |||
try: | ||||
|
r36902 | fctx = webutil.filectx(web.repo, web.req) | ||
|
r7300 | f = fctx.path() | ||
fl = fctx.filelog() | ||||
|
r7633 | except error.LookupError: | ||
|
r43347 | f = webutil.cleanpath(web.repo, web.req.qsparams[b'file']) | ||
|
r7300 | fl = web.repo.file(f) | ||
numrevs = len(fl) | ||||
|
r43346 | if not numrevs: # file doesn't exist at all | ||
|
r7300 | raise | ||
|
r36902 | rev = webutil.changectx(web.repo, web.req).rev() | ||
|
r7361 | first = fl.linkrev(0) | ||
|
r43346 | if rev < first: # current rev is from before file existed | ||
|
r7300 | raise | ||
frev = numrevs - 1 | ||||
|
r7361 | while fl.linkrev(frev) > rev: | ||
|
r7300 | frev -= 1 | ||
|
r7361 | fctx = web.repo.filectx(f, fl.linkrev(frev)) | ||
|
r7300 | |||
|
r10246 | revcount = web.maxshortchanges | ||
|
r43347 | if b'revcount' in web.req.qsparams: | ||
|
r20092 | try: | ||
|
r43347 | revcount = int(web.req.qsparams.get(b'revcount', revcount)) | ||
|
r20092 | revcount = max(revcount, 1) | ||
|
r43347 | web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount | ||
|
r20092 | except ValueError: | ||
pass | ||||
|
r10246 | |||
|
r36902 | lrange = webutil.linerange(web.req) | ||
|
r31665 | |||
|
r43347 | lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||
lessvars[b'revcount'] = max(revcount // 2, 1) | ||||
morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||||
morevars[b'revcount'] = revcount * 2 | ||||
|
r10246 | |||
|
r43347 | patch = b'patch' in web.req.qsparams | ||
|
r31661 | if patch: | ||
|
r43347 | lessvars[b'patch'] = morevars[b'patch'] = web.req.qsparams[b'patch'] | ||
descend = b'descend' in web.req.qsparams | ||||
|
r31939 | if descend: | ||
|
r43347 | lessvars[b'descend'] = morevars[b'descend'] = web.req.qsparams[ | ||
b'descend' | ||||
] | ||||
|
r31661 | |||
|
r7300 | count = fctx.filerev() + 1 | ||
|
r43346 | start = max(0, count - revcount) # first rev on this page | ||
end = min(count, start + revcount) # last rev on this page | ||||
|
r30825 | parity = paritygen(web.stripecount, offset=start - end) | ||
|
r6393 | |||
|
r30816 | repo = web.repo | ||
|
r37381 | filelog = fctx.filelog() | ||
|
r43346 | revs = [ | ||
filerev | ||||
for filerev in filelog.revs(start, end - 1) | ||||
if filelog.linkrev(filerev) in repo | ||||
] | ||||
|
r30816 | entries = [] | ||
|
r31661 | |||
|
r43347 | diffstyle = web.config(b'web', b'style') | ||
if b'style' in web.req.qsparams: | ||||
diffstyle = web.req.qsparams[b'style'] | ||||
|
r31661 | |||
|
r31667 | def diff(fctx, linerange=None): | ||
|
r31661 | ctx = fctx.changectx() | ||
basectx = ctx.p1() | ||||
path = fctx.path() | ||||
|
r43346 | return webutil.diffs( | ||
web, | ||||
ctx, | ||||
basectx, | ||||
[path], | ||||
diffstyle, | ||||
linerange=linerange, | ||||
|
r43347 | lineidprefix=b'%s-' % ctx.hex()[:12], | ||
|
r43346 | ) | ||
|
r31661 | |||
|
r31665 | linerange = None | ||
if lrange is not None: | ||||
linerange = webutil.formatlinerange(*lrange) | ||||
# deactivate numeric nav links when linerange is specified as this | ||||
# would required a dedicated "revnav" class | ||||
|
r37716 | nav = templateutil.mappinglist([]) | ||
|
r31939 | if descend: | ||
|
r32904 | it = dagop.blockdescendants(fctx, *lrange) | ||
|
r31939 | else: | ||
|
r32904 | it = dagop.blockancestors(fctx, *lrange) | ||
|
r31939 | for i, (c, lr) in enumerate(it, 1): | ||
|
r31665 | diffs = None | ||
if patch: | ||||
|
r31667 | diffs = diff(c, linerange=lr) | ||
|
r31665 | # follow renames accross filtered (not in range) revisions | ||
path = c.path() | ||||
|
r38155 | lm = webutil.commonentry(repo, c) | ||
|
r43346 | lm.update( | ||
{ | ||||
|
r43347 | b'parity': next(parity), | ||
b'filerev': c.rev(), | ||||
b'file': path, | ||||
b'diff': diffs, | ||||
b'linerange': webutil.formatlinerange(*lr), | ||||
b'rename': templateutil.mappinglist([]), | ||||
|
r43346 | } | ||
) | ||||
|
r38155 | entries.append(lm) | ||
|
r31665 | if i == revcount: | ||
break | ||||
|
r43347 | lessvars[b'linerange'] = webutil.formatlinerange(*lrange) | ||
morevars[b'linerange'] = lessvars[b'linerange'] | ||||
|
r31665 | else: | ||
for i in revs: | ||||
iterfctx = fctx.filectx(i) | ||||
diffs = None | ||||
if patch: | ||||
diffs = diff(iterfctx) | ||||
|
r38155 | lm = webutil.commonentry(repo, iterfctx) | ||
|
r43346 | lm.update( | ||
{ | ||||
|
r43347 | b'parity': next(parity), | ||
b'filerev': i, | ||||
b'file': f, | ||||
b'diff': diffs, | ||||
b'rename': webutil.renamelink(iterfctx), | ||||
|
r43346 | } | ||
) | ||||
|
r38155 | entries.append(lm) | ||
|
r31665 | entries.reverse() | ||
revnav = webutil.filerevnav(web.repo, fctx.path()) | ||||
nav = revnav.gen(end - 1, revcount, count) | ||||
|
r6393 | |||
|
r20022 | latestentry = entries[:1] | ||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'filelog', | ||
|
r36886 | file=f, | ||
nav=nav, | ||||
|
r36902 | symrev=webutil.symrevorshortnode(web.req, fctx), | ||
|
r38156 | entries=templateutil.mappinglist(entries), | ||
|
r36886 | descend=descend, | ||
patch=patch, | ||||
|
r38156 | latestentry=templateutil.mappinglist(latestentry), | ||
|
r36886 | linerange=linerange, | ||
revcount=revcount, | ||||
morevars=morevars, | ||||
lessvars=lessvars, | ||||
|
r43346 | **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)) | ||
) | ||||
|
r5591 | |||
|
r43347 | @webcommand(b'archive') | ||
|
r36903 | def archive(web): | ||
|
r24096 | """ | ||
/archive/{revision}.{format}[/{path}] | ||||
------------------------------------- | ||||
Obtain an archive of repository content. | ||||
The content and type of the archive is defined by a URL path parameter. | ||||
``format`` is the file extension of the archive type to be generated. e.g. | ||||
``zip`` or ``tar.bz2``. Not all archive types may be allowed by your | ||||
server configuration. | ||||
The optional ``path`` URL parameter controls content to include in the | ||||
archive. If omitted, every file in the specified revision is present in the | ||||
archive. If included, only the specified file or contents of the specified | ||||
directory will be included in the archive. | ||||
No template is used for this handler. Raw, binary content is generated. | ||||
""" | ||||
|
r43347 | type_ = web.req.qsparams.get(b'type') | ||
allowed = web.configlist(b"web", b"allow-archive") | ||||
key = web.req.qsparams[b'node'] | ||||
|
r6393 | |||
|
r37530 | if type_ not in webutil.archivespecs: | ||
|
r43347 | msg = b'Unsupported archive type: %s' % stringutil.pprint(type_) | ||
|
r6393 | raise ErrorResponse(HTTP_NOT_FOUND, msg) | ||
|
r43347 | if not ((type_ in allowed or web.configbool(b"web", b"allow" + type_))): | ||
msg = b'Archive type not allowed: %s' % type_ | ||||
|
r7029 | raise ErrorResponse(HTTP_FORBIDDEN, msg) | ||
|
r43347 | reponame = re.sub(br"\W+", b"-", os.path.basename(web.reponame)) | ||
|
r6393 | cnode = web.repo.lookup(key) | ||
arch_version = key | ||||
|
r43347 | if cnode == key or key == b'tip': | ||
|
r6393 | arch_version = short(cnode) | ||
|
r43347 | name = b"%s-%s" % (reponame, arch_version) | ||
|
r18771 | |||
|
r36902 | ctx = webutil.changectx(web.repo, web.req) | ||
|
r18771 | pats = [] | ||
|
r34085 | match = scmutil.match(ctx, []) | ||
|
r43347 | file = web.req.qsparams.get(b'file') | ||
|
r18771 | if file: | ||
|
r43347 | pats = [b'path:' + file] | ||
match = scmutil.match(ctx, pats, default=b'path') | ||||
|
r18968 | if pats: | ||
|
r34085 | files = [f for f in ctx.manifest().keys() if match(f)] | ||
|
r18968 | if not files: | ||
|
r43346 | raise ErrorResponse( | ||
|
r43347 | HTTP_NOT_FOUND, b'file(s) not found: %s' % file | ||
|
r43346 | ) | ||
|
r18771 | |||
|
r37530 | mimetype, artype, extension, encoding = webutil.archivespecs[type_] | ||
|
r36892 | |||
|
r43347 | web.res.headers[b'Content-Type'] = mimetype | ||
web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % ( | ||||
|
r43346 | name, | ||
extension, | ||||
) | ||||
|
r36892 | |||
|
r6393 | if encoding: | ||
|
r43347 | web.res.headers[b'Content-Encoding'] = encoding | ||
|
r17933 | |||
|
r36892 | web.res.setbodywillwrite() | ||
|
r36995 | if list(web.res.sendresponse()): | ||
|
r43346 | raise error.ProgrammingError( | ||
|
r43347 | b'sendresponse() should not emit data ' b'if writing later' | ||
|
r43346 | ) | ||
|
r36892 | |||
bodyfh = web.res.getbodyfile() | ||||
|
r36891 | |||
|
r43346 | archival.archive( | ||
web.repo, | ||||
bodyfh, | ||||
cnode, | ||||
artype, | ||||
prefix=name, | ||||
match=match, | ||||
|
r43347 | subrepos=web.configbool(b"web", b"archivesubrepos"), | ||
|
r43346 | ) | ||
|
r6393 | |||
|
r36896 | return [] | ||
|
r5591 | |||
|
r43346 | |||
|
r43347 | @webcommand(b'static') | ||
|
r36903 | def static(web): | ||
|
r43347 | fname = web.req.qsparams[b'file'] | ||
|
r5591 | # a repo owner may set web.static in .hg/hgrc to get any file | ||
# readable by the user running the CGI script | ||||
|
r43347 | static = web.config(b"web", b"static", untrusted=False) | ||
|
r7107 | if not static: | ||
|
r22634 | tp = web.templatepath or templater.templatepaths() | ||
|
r7107 | if isinstance(tp, str): | ||
tp = [tp] | ||||
|
r43347 | static = [os.path.join(p, b'static') for p in tp] | ||
|
r36889 | |||
staticfile(static, fname, web.res) | ||||
|
r36896 | return web.res.sendresponse() | ||
|
r6691 | |||
|
r43346 | |||
|
r43347 | @webcommand(b'graph') | ||
|
r36903 | def graph(web): | ||
|
r24097 | """ | ||
/graph[/{revision}] | ||||
------------------- | ||||
Show information about the graphical topology of the repository. | ||||
Information rendered by this handler can be used to create visual | ||||
representations of repository topology. | ||||
r35412 | The ``revision`` URL parameter controls the starting changeset. If it's | |||
absent, the default is ``tip``. | ||||
|
r24097 | |||
The ``revcount`` query string argument can define the number of changesets | ||||
to show information for. | ||||
r35412 | The ``graphtop`` query string argument can specify the starting changeset | |||
for producing ``jsdata`` variable that is used for rendering graph in | ||||
JavaScript. By default it has the same value as ``revision``. | ||||
|
r24097 | This handler will render the ``graph`` template. | ||
""" | ||||
|
r10245 | |||
|
r43347 | if b'node' in web.req.qsparams: | ||
|
r36902 | ctx = webutil.changectx(web.repo, web.req) | ||
symrev = webutil.symrevorshortnode(web.req, ctx) | ||||
r25602 | else: | |||
|
r43347 | ctx = web.repo[b'tip'] | ||
symrev = b'tip' | ||||
|
r17318 | rev = ctx.rev() | ||
|
r6691 | bg_height = 39 | ||
|
r10245 | revcount = web.maxshortchanges | ||
|
r43347 | if b'revcount' in web.req.qsparams: | ||
|
r20092 | try: | ||
|
r43347 | revcount = int(web.req.qsparams.get(b'revcount', revcount)) | ||
|
r20092 | revcount = max(revcount, 1) | ||
|
r43347 | web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount | ||
|
r20092 | except ValueError: | ||
pass | ||||
|
r7345 | |||
|
r43347 | lessvars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||
lessvars[b'revcount'] = max(revcount // 2, 1) | ||||
morevars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||||
morevars[b'revcount'] = revcount * 2 | ||||
|
r7345 | |||
|
r43347 | graphtop = web.req.qsparams.get(b'graphtop', ctx.hex()) | ||
graphvars = copy.copy(web.tmpl.defaults[b'sessionvars']) | ||||
graphvars[b'graphtop'] = graphtop | ||||
r35410 | ||||
|
r17318 | count = len(web.repo) | ||
pos = rev | ||||
uprev = min(max(0, count - 1), rev + revcount) | ||||
|
r6691 | downrev = max(0, rev - revcount) | ||
|
r18409 | changenav = webutil.revnav(web.repo).gen(pos, revcount, count) | ||
|
r6691 | |||
|
r18428 | tree = [] | ||
r35410 | nextentry = [] | |||
lastrev = 0 | ||||
|
r19487 | if pos != -1: | ||
allrevs = web.repo.changelog.revs(pos, 0) | ||||
revs = [] | ||||
for i in allrevs: | ||||
revs.append(i) | ||||
r35410 | if len(revs) >= revcount + 1: | |||
|
r19487 | break | ||
r35410 | if len(revs) > revcount: | |||
nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])] | ||||
revs = revs[:-1] | ||||
lastrev = revs[-1] | ||||
|
r20761 | # We have to feed a baseset to dagwalker as it is expecting smartset | ||
# object. This does not have a big impact on hgweb performance itself | ||||
# since hgweb graphing code is not itself lazy yet. | ||||
|
r31023 | dag = graphmod.dagwalker(web.repo, smartset.baseset(revs)) | ||
|
r20761 | # As we said one line above... not lazy. | ||
|
r43346 | tree = list( | ||
item | ||||
for item in graphmod.colored(dag, web.repo) | ||||
if item[1] == graphmod.CHANGESET | ||||
) | ||||
|
r16773 | |||
r35410 | def fulltree(): | |||
pos = web.repo[graphtop].rev() | ||||
tree = [] | ||||
if pos != -1: | ||||
revs = web.repo.changelog.revs(pos, lastrev) | ||||
dag = graphmod.dagwalker(web.repo, smartset.baseset(revs)) | ||||
|
r43346 | tree = list( | ||
item | ||||
for item in graphmod.colored(dag, web.repo) | ||||
if item[1] == graphmod.CHANGESET | ||||
) | ||||
r35410 | return tree | |||
|
r38209 | def jsdata(context): | ||
for (id, type, ctx, vtx, edges) in fulltree(): | ||||
|
r43346 | yield { | ||
|
r43347 | b'node': pycompat.bytestr(ctx), | ||
b'graphnode': webutil.getgraphnode(web.repo, ctx), | ||||
b'vertex': vtx, | ||||
b'edges': edges, | ||||
|
r43346 | } | ||
|
r16773 | |||
|
r38210 | def nodes(context): | ||
r35548 | parity = paritygen(web.stripecount) | |||
r35409 | for row, (id, type, ctx, vtx, edges) in enumerate(tree): | |||
entry = webutil.commonentry(web.repo, ctx) | ||||
|
r43346 | edgedata = [ | ||
{ | ||||
|
r43347 | b'col': edge[0], | ||
b'nextcol': edge[1], | ||||
b'color': (edge[2] - 1) % 6 + 1, | ||||
b'width': edge[3], | ||||
b'bcolor': edge[4], | ||||
|
r43346 | } | ||
for edge in edges | ||||
] | ||||
|
r16773 | |||
|
r43346 | entry.update( | ||
{ | ||||
|
r43347 | b'col': vtx[0], | ||
b'color': (vtx[1] - 1) % 6 + 1, | ||||
b'parity': next(parity), | ||||
b'edges': templateutil.mappinglist(edgedata), | ||||
b'row': row, | ||||
b'nextrow': row + 1, | ||||
|
r43346 | } | ||
) | ||||
r35095 | ||||
r35409 | yield entry | |||
|
r16773 | |||
rows = len(tree) | ||||
|
r6691 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'graph', | ||
|
r36887 | rev=rev, | ||
symrev=symrev, | ||||
revcount=revcount, | ||||
uprev=uprev, | ||||
lessvars=lessvars, | ||||
morevars=morevars, | ||||
downrev=downrev, | ||||
graphvars=graphvars, | ||||
rows=rows, | ||||
bg_height=bg_height, | ||||
changesets=count, | ||||
|
r38208 | nextentry=templateutil.mappinglist(nextentry), | ||
|
r38209 | jsdata=templateutil.mappinggenerator(jsdata), | ||
|
r38210 | nodes=templateutil.mappinggenerator(nodes), | ||
|
r36887 | node=ctx.hex(), | ||
|
r43347 | archives=web.archivelist(b'tip'), | ||
|
r43346 | changenav=changenav, | ||
) | ||||
|
r12666 | |||
def _getdoc(e): | ||||
doc = e[0].__doc__ | ||||
if doc: | ||||
|
r43347 | doc = _(doc).partition(b'\n')[0] | ||
|
r12666 | else: | ||
|
r43347 | doc = _(b'(no help text available)') | ||
|
r12666 | return doc | ||
|
r43346 | |||
|
r43347 | @webcommand(b'help') | ||
|
r36903 | def help(web): | ||
|
r24081 | """ | ||
/help[/{topic}] | ||||
--------------- | ||||
Render help documentation. | ||||
This web command is roughly equivalent to :hg:`help`. If a ``topic`` | ||||
is defined, that help topic will be rendered. If not, an index of | ||||
available help topics will be rendered. | ||||
The ``help`` template will be rendered when requesting help for a topic. | ||||
``helptopics`` will be rendered for the index of help topics. | ||||
""" | ||||
|
r27046 | from .. import commands, help as helpmod # avoid cycle | ||
|
r12666 | |||
|
r43347 | topicname = web.req.qsparams.get(b'node') | ||
|
r12666 | if not topicname: | ||
|
r43346 | |||
|
r38212 | def topics(context): | ||
|
r40328 | for h in helpmod.helptable: | ||
entries, summary, _doc = h[0:3] | ||||
|
r43347 | yield {b'topic': entries[0], b'summary': summary} | ||
|
r12666 | |||
early, other = [], [] | ||||
|
r43347 | primary = lambda s: s.partition(b'|')[0] | ||
|
r12666 | for c, e in commands.table.iteritems(): | ||
doc = _getdoc(e) | ||||
|
r43347 | if b'DEPRECATED' in doc or c.startswith(b'debug'): | ||
|
r12666 | continue | ||
cmd = primary(c) | ||||
|
r40331 | if getattr(e[0], 'helpbasic', False): | ||
early.append((cmd, doc)) | ||||
|
r12666 | else: | ||
other.append((cmd, doc)) | ||||
early.sort() | ||||
other.sort() | ||||
|
r38213 | def earlycommands(context): | ||
|
r12666 | for c, doc in early: | ||
|
r43347 | yield {b'topic': c, b'summary': doc} | ||
|
r12666 | |||
|
r38213 | def othercommands(context): | ||
|
r12666 | for c, doc in other: | ||
|
r43347 | yield {b'topic': c, b'summary': doc} | ||
|
r12666 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'helptopics', | ||
|
r38212 | topics=templateutil.mappinggenerator(topics), | ||
|
r38213 | earlycommands=templateutil.mappinggenerator(earlycommands), | ||
othercommands=templateutil.mappinggenerator(othercommands), | ||||
|
r43347 | title=b'Index', | ||
|
r43346 | ) | ||
|
r12666 | |||
|
r27581 | # Render an index of sub-topics. | ||
if topicname in helpmod.subtopics: | ||||
topics = [] | ||||
for entries, summary, _doc in helpmod.subtopics[topicname]: | ||||
|
r43346 | topics.append( | ||
{ | ||||
|
r43347 | b'topic': b'%s.%s' % (topicname, entries[0]), | ||
b'basename': entries[0], | ||||
b'summary': summary, | ||||
|
r43346 | } | ||
) | ||||
|
r27581 | |||
|
r36899 | return web.sendtemplate( | ||
|
r43347 | b'helptopics', | ||
|
r38212 | topics=templateutil.mappinglist(topics), | ||
|
r36887 | title=topicname, | ||
|
r43346 | subindex=True, | ||
) | ||||
|
r27581 | |||
|
r30559 | u = webutil.wsgiui.load() | ||
|
r17146 | u.verbose = True | ||
|
r27582 | |||
# Render a page from a sub-topic. | ||||
|
r43347 | if b'.' in topicname: | ||
|
r27582 | # TODO implement support for rendering sections, like | ||
# `hg help` works. | ||||
|
r43347 | topic, subtopic = topicname.split(b'.', 1) | ||
|
r27582 | if topic not in helpmod.subtopics: | ||
raise ErrorResponse(HTTP_NOT_FOUND) | ||||
else: | ||||
topic = topicname | ||||
subtopic = None | ||||
|
r12666 | try: | ||
|
r32567 | doc = helpmod.help_(u, commands, topic, subtopic=subtopic) | ||
|
r36262 | except error.Abort: | ||
|
r12666 | raise ErrorResponse(HTTP_NOT_FOUND) | ||
|
r36887 | |||
|
r43347 | return web.sendtemplate(b'help', topic=topicname, doc=doc) | ||
|
r43346 | |||
|
r24859 | |||
# tell hggettext to extract docstrings from these functions: | ||||
i18nfunctions = commands.values() | ||||