webutil.py
932 lines
| 26.7 KiB
| text/x-python
|
PythonLexer
|
r6392 | # hgweb/webutil.py - utility library for the web interface. | ||
# | ||||
# 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. | ||
|
r6392 | |||
|
r27046 | from __future__ import absolute_import | ||
import copy | ||||
import difflib | ||||
import os | ||||
|
r26162 | import re | ||
|
r27046 | |||
from ..i18n import _ | ||||
from ..node import hex, nullid, short | ||||
|
r43357 | from ..pycompat import setattr | ||
|
r27046 | |||
from .common import ( | ||||
ErrorResponse, | ||||
|
r31665 | HTTP_BAD_REQUEST, | ||
|
r27046 | HTTP_NOT_FOUND, | ||
paritygen, | ||||
) | ||||
from .. import ( | ||||
context, | ||||
|
r38607 | diffutil, | ||
|
r27046 | error, | ||
match, | ||||
|
r31808 | mdiff, | ||
r36973 | obsutil, | |||
|
r27046 | patch, | ||
pathutil, | ||||
|
r34808 | pycompat, | ||
|
r37351 | scmutil, | ||
|
r27046 | templatefilters, | ||
r35501 | templatekw, | |||
|
r37533 | templateutil, | ||
|
r27046 | ui as uimod, | ||
util, | ||||
) | ||||
|
r6392 | |||
|
r43346 | from ..utils import stringutil | ||
archivespecs = util.sortdict( | ||||
( | ||||
|
r43347 | (b'zip', (b'application/zip', b'zip', b'.zip', None)), | ||
(b'gz', (b'application/x-gzip', b'tgz', b'.tar.gz', None)), | ||||
(b'bz2', (b'application/x-bzip2', b'tbz2', b'.tar.bz2', None)), | ||||
|
r43346 | ) | ||
|
r37102 | ) | ||
|
r37529 | |||
|
r37532 | def archivelist(ui, nodeid, url=None): | ||
|
r43347 | allowed = ui.configlist(b'web', b'allow-archive', untrusted=True) | ||
|
r37531 | archives = [] | ||
|
r43376 | for typ, spec in pycompat.iteritems(archivespecs): | ||
|
r43346 | if typ in allowed or ui.configbool( | ||
|
r43347 | b'web', b'allow' + typ, untrusted=True | ||
|
r43346 | ): | ||
archives.append( | ||||
|
r43347 | { | ||
b'type': typ, | ||||
b'extension': spec[2], | ||||
b'node': nodeid, | ||||
b'url': url, | ||||
} | ||||
|
r43346 | ) | ||
|
r37531 | |||
|
r37533 | return templateutil.mappinglist(archives) | ||
|
r37531 | |||
|
r43346 | |||
|
r6393 | def up(p): | ||
|
r43347 | if p[0:1] != b"/": | ||
p = b"/" + p | ||||
if p[-1:] == b"/": | ||||
|
r6393 | p = p[:-1] | ||
up = os.path.dirname(p) | ||||
|
r43347 | if up == b"/": | ||
return b"/" | ||||
return up + b"/" | ||||
|
r6393 | |||
|
r43346 | |||
|
r18391 | def _navseq(step, firststep=None): | ||
if firststep: | ||||
yield firststep | ||||
if firststep >= 20 and firststep <= 40: | ||||
|
r18392 | firststep = 50 | ||
yield firststep | ||||
assert step > 0 | ||||
assert firststep > 0 | ||||
while step <= firststep: | ||||
step *= 10 | ||||
|
r18390 | while True: | ||
|
r18391 | yield 1 * step | ||
yield 3 * step | ||||
step *= 10 | ||||
|
r18389 | |||
|
r43346 | |||
|
r18403 | class revnav(object): | ||
|
r18409 | def __init__(self, repo): | ||
|
r18404 | """Navigation generation object | ||
|
r18409 | :repo: repo object we generate nav for | ||
|
r18404 | """ | ||
|
r18409 | # used for hex generation | ||
self._revlog = repo.changelog | ||||
|
r18404 | |||
|
r18406 | def __nonzero__(self): | ||
"""return True if any revision to navigate over""" | ||||
|
r19094 | return self._first() is not None | ||
|
r31476 | __bool__ = __nonzero__ | ||
|
r19094 | def _first(self): | ||
"""return the minimum non-filtered changeset or None""" | ||||
try: | ||||
|
r29216 | return next(iter(self._revlog)) | ||
|
r19094 | except StopIteration: | ||
return None | ||||
|
r18406 | |||
|
r18405 | def hex(self, rev): | ||
|
r18409 | return hex(self._revlog.node(rev)) | ||
|
r18405 | |||
|
r18404 | def gen(self, pos, pagelen, limit): | ||
|
r18403 | """computes label and revision id for navigation link | ||
|
r18320 | |||
|
r18403 | :pos: is the revision relative to which we generate navigation. | ||
:pagelen: the size of each navigation page | ||||
:limit: how far shall we link | ||||
|
r6393 | |||
|
r18403 | The return is: | ||
|
r37716 | - a single element mappinglist | ||
|
r18403 | - containing a dictionary with a `before` and `after` key | ||
|
r37715 | - values are dictionaries with `label` and `node` keys | ||
|
r18403 | """ | ||
|
r18406 | if not self: | ||
# empty repo | ||||
|
r43346 | return templateutil.mappinglist( | ||
[ | ||||
{ | ||||
|
r43347 | b'before': templateutil.mappinglist([]), | ||
b'after': templateutil.mappinglist([]), | ||||
|
r43346 | }, | ||
] | ||||
) | ||||
|
r6393 | |||
|
r18425 | targets = [] | ||
|
r18403 | for f in _navseq(1, pagelen): | ||
if f > limit: | ||||
break | ||||
|
r18425 | targets.append(pos + f) | ||
targets.append(pos - f) | ||||
targets.sort() | ||||
|
r19094 | first = self._first() | ||
|
r43347 | navbefore = [{b'label': b'(%i)' % first, b'node': self.hex(first)}] | ||
|
r18425 | navafter = [] | ||
for rev in targets: | ||||
|
r18426 | if rev not in self._revlog: | ||
continue | ||||
|
r18425 | if pos < rev < limit: | ||
|
r43346 | navafter.append( | ||
|
r43347 | {b'label': b'+%d' % abs(rev - pos), b'node': self.hex(rev)} | ||
|
r43346 | ) | ||
|
r18425 | if 0 < rev < pos: | ||
|
r43346 | navbefore.append( | ||
|
r43347 | {b'label': b'-%d' % abs(rev - pos), b'node': self.hex(rev)} | ||
|
r43346 | ) | ||
|
r10254 | |||
|
r43347 | navafter.append({b'label': b'tip', b'node': b'tip'}) | ||
|
r18403 | |||
|
r37716 | # TODO: maybe this can be a scalar object supporting tomap() | ||
|
r43346 | return templateutil.mappinglist( | ||
[ | ||||
{ | ||||
|
r43347 | b'before': templateutil.mappinglist(navbefore), | ||
b'after': templateutil.mappinglist(navafter), | ||||
|
r43346 | }, | ||
] | ||||
) | ||||
|
r6393 | |||
|
r18408 | class filerevnav(revnav): | ||
|
r18409 | def __init__(self, repo, path): | ||
"""Navigation generation object | ||||
:repo: repo object we generate nav for | ||||
:path: path of the file we generate nav for | ||||
""" | ||||
# used for iteration | ||||
self._changelog = repo.unfiltered().changelog | ||||
# used for hex generation | ||||
self._revlog = repo.file(path) | ||||
def hex(self, rev): | ||||
return hex(self._changelog.node(self._revlog.linkrev(rev))) | ||||
|
r43346 | |||
|
r37717 | # TODO: maybe this can be a wrapper class for changectx/filectx list, which | ||
# yields {'ctx': ctx} | ||||
|
r37718 | def _ctxsgen(context, ctxs): | ||
|
r37717 | for s in ctxs: | ||
d = { | ||||
|
r43347 | b'node': s.hex(), | ||
b'rev': s.rev(), | ||||
b'user': s.user(), | ||||
b'date': s.date(), | ||||
b'description': s.description(), | ||||
b'branch': s.branch(), | ||||
|
r37717 | } | ||
|
r43347 | if util.safehasattr(s, b'path'): | ||
d[b'file'] = s.path() | ||||
|
r37717 | yield d | ||
|
r43346 | |||
|
r37718 | def _siblings(siblings=None, hiderev=None): | ||
if siblings is None: | ||||
siblings = [] | ||||
siblings = [s for s in siblings if s.node() != nullid] | ||||
if len(siblings) == 1 and siblings[0].rev() == hiderev: | ||||
siblings = [] | ||||
return templateutil.mappinggenerator(_ctxsgen, args=(siblings,)) | ||||
|
r6392 | |||
|
r43346 | |||
|
r34391 | def difffeatureopts(req, ui, section): | ||
|
r43346 | diffopts = diffutil.difffeatureopts( | ||
ui, untrusted=True, section=section, whitespace=True | ||||
) | ||||
|
r34391 | |||
|
r43347 | for k in ( | ||
b'ignorews', | ||||
b'ignorewsamount', | ||||
b'ignorewseol', | ||||
b'ignoreblanklines', | ||||
): | ||||
|
r36902 | v = req.qsparams.get(k) | ||
|
r34391 | if v is not None: | ||
|
r37102 | v = stringutil.parsebool(v) | ||
|
r34404 | setattr(diffopts, k, v if v is not None else True) | ||
|
r34391 | |||
return diffopts | ||||
|
r43346 | |||
|
r34391 | def annotate(req, fctx, ui): | ||
|
r43347 | diffopts = difffeatureopts(req, ui, b'annotate') | ||
|
r37083 | return fctx.annotate(follow=True, diffopts=diffopts) | ||
|
r30081 | |||
|
r43346 | |||
|
r7671 | def parents(ctx, hide=None): | ||
|
r24136 | if isinstance(ctx, context.basefilectx): | ||
introrev = ctx.introrev() | ||||
if ctx.changectx().rev() != introrev: | ||||
|
r24340 | return _siblings([ctx.repo()[introrev]], hide) | ||
|
r7671 | return _siblings(ctx.parents(), hide) | ||
|
r43346 | |||
|
r7671 | def children(ctx, hide=None): | ||
return _siblings(ctx.children(), hide) | ||||
|
r43346 | |||
|
r6434 | def renamelink(fctx): | ||
|
r6437 | r = fctx.renamed() | ||
|
r6392 | if r: | ||
|
r43347 | return templateutil.mappinglist([{b'file': r[0], b'node': hex(r[1])}]) | ||
|
r37921 | return templateutil.mappinglist([]) | ||
|
r6392 | |||
|
r43346 | |||
|
r6392 | def nodetagsdict(repo, node): | ||
|
r43347 | return templateutil.hybridlist(repo.nodetags(node), name=b'name') | ||
|
r6392 | |||
|
r43346 | |||
|
r13596 | def nodebookmarksdict(repo, node): | ||
|
r43347 | return templateutil.hybridlist(repo.nodebookmarks(node), name=b'name') | ||
|
r13596 | |||
|
r43346 | |||
|
r6392 | def nodebranchdict(repo, ctx): | ||
branches = [] | ||||
branch = ctx.branch() | ||||
# If this is an empty repo, ctx.node() == nullid, | ||||
|
r16719 | # ctx.branch() == 'default'. | ||
try: | ||||
branchnode = repo.branchtip(branch) | ||||
except error.RepoLookupError: | ||||
branchnode = None | ||||
if branchnode == ctx.node(): | ||||
|
r37924 | branches.append(branch) | ||
|
r43347 | return templateutil.hybridlist(branches, name=b'name') | ||
|
r6392 | |||
|
r43346 | |||
|
r6392 | def nodeinbranch(repo, ctx): | ||
branches = [] | ||||
branch = ctx.branch() | ||||
|
r16719 | try: | ||
branchnode = repo.branchtip(branch) | ||||
except error.RepoLookupError: | ||||
branchnode = None | ||||
|
r43347 | if branch != b'default' and branchnode != ctx.node(): | ||
|
r37925 | branches.append(branch) | ||
|
r43347 | return templateutil.hybridlist(branches, name=b'name') | ||
|
r6392 | |||
|
r43346 | |||
|
r6392 | def nodebranchnodefault(ctx): | ||
branches = [] | ||||
branch = ctx.branch() | ||||
|
r43347 | if branch != b'default': | ||
|
r37926 | branches.append(branch) | ||
|
r43347 | return templateutil.hybridlist(branches, name=b'name') | ||
|
r6392 | |||
|
r43346 | |||
|
r37930 | def _nodenamesgen(context, f, node, name): | ||
for t in f(node): | ||||
yield {name: t} | ||||
|
r43346 | |||
|
r37931 | def showtag(repo, t1, node=nullid): | ||
|
r43347 | args = (repo.nodetags, node, b'tag') | ||
|
r37930 | return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1) | ||
|
r6392 | |||
|
r43346 | |||
|
r37931 | def showbookmark(repo, t1, node=nullid): | ||
|
r43347 | args = (repo.nodebookmarks, node, b'bookmark') | ||
|
r37930 | return templateutil.mappinggenerator(_nodenamesgen, args=args, name=t1) | ||
|
r13596 | |||
|
r43346 | |||
r26129 | def branchentries(repo, stripecount, limit=0): | |||
tips = [] | ||||
heads = repo.heads() | ||||
parity = paritygen(stripecount) | ||||
sortkey = lambda item: (not item[1], item[0].rev()) | ||||
|
r37932 | def entries(context): | ||
r26129 | count = 0 | |||
if not tips: | ||||
for tag, hs, tip, closed in repo.branchmap().iterbranches(): | ||||
tips.append((repo[tip], closed)) | ||||
for ctx, closed in sorted(tips, key=sortkey, reverse=True): | ||||
if limit > 0 and count >= limit: | ||||
return | ||||
count += 1 | ||||
if closed: | ||||
|
r43347 | status = b'closed' | ||
r26129 | elif ctx.node() not in heads: | |||
|
r43347 | status = b'inactive' | ||
r26129 | else: | |||
|
r43347 | status = b'open' | ||
r26129 | yield { | |||
|
r43347 | b'parity': next(parity), | ||
b'branch': ctx.branch(), | ||||
b'status': status, | ||||
b'node': ctx.hex(), | ||||
b'date': ctx.date(), | ||||
r26129 | } | |||
|
r37932 | return templateutil.mappinggenerator(entries) | ||
r26129 | ||||
|
r43346 | |||
|
r6392 | def cleanpath(repo, path): | ||
|
r43347 | path = path.lstrip(b'/') | ||
|
r39507 | auditor = pathutil.pathauditor(repo.root, realfs=False) | ||
|
r43347 | return pathutil.canonpath(repo.root, b'', path, auditor=auditor) | ||
|
r6392 | |||
|
r43346 | |||
r25999 | def changectx(repo, req): | |||
|
r43347 | changeid = b"tip" | ||
if b'node' in req.qsparams: | ||||
changeid = req.qsparams[b'node'] | ||||
ipos = changeid.find(b':') | ||||
|
r17991 | if ipos != -1: | ||
|
r43346 | changeid = changeid[(ipos + 1) :] | ||
|
r17991 | |||
|
r37351 | return scmutil.revsymbol(repo, changeid) | ||
|
r17991 | |||
|
r43346 | |||
|
r17991 | def basechangectx(repo, req): | ||
|
r43347 | if b'node' in req.qsparams: | ||
changeid = req.qsparams[b'node'] | ||||
ipos = changeid.find(b':') | ||||
|
r17991 | if ipos != -1: | ||
changeid = changeid[:ipos] | ||||
|
r37351 | return scmutil.revsymbol(repo, changeid) | ||
|
r17991 | |||
return None | ||||
|
r43346 | |||
|
r6392 | def filectx(repo, req): | ||
|
r43347 | if b'file' not in req.qsparams: | ||
raise ErrorResponse(HTTP_NOT_FOUND, b'file not given') | ||||
path = cleanpath(repo, req.qsparams[b'file']) | ||||
if b'node' in req.qsparams: | ||||
changeid = req.qsparams[b'node'] | ||||
elif b'filenode' in req.qsparams: | ||||
changeid = req.qsparams[b'filenode'] | ||||
|
r6392 | else: | ||
|
r43347 | raise ErrorResponse(HTTP_NOT_FOUND, b'node or filenode not given') | ||
|
r6392 | try: | ||
|
r37351 | fctx = scmutil.revsymbol(repo, changeid)[path] | ||
|
r7637 | except error.RepoError: | ||
|
r6392 | fctx = repo.filectx(path, fileid=changeid) | ||
return fctx | ||||
|
r7310 | |||
|
r43346 | |||
|
r31665 | def linerange(req): | ||
|
r43347 | linerange = req.qsparams.getall(b'linerange') | ||
|
r36881 | if not linerange: | ||
|
r31665 | return None | ||
if len(linerange) > 1: | ||||
|
r43347 | raise ErrorResponse(HTTP_BAD_REQUEST, b'redundant linerange parameter') | ||
|
r31665 | try: | ||
|
r43347 | fromline, toline = map(int, linerange[0].split(b':', 1)) | ||
|
r31665 | except ValueError: | ||
|
r43347 | raise ErrorResponse(HTTP_BAD_REQUEST, b'invalid linerange parameter') | ||
|
r31665 | try: | ||
return util.processlinerange(fromline, toline) | ||||
except error.ParseError as exc: | ||||
|
r36272 | raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc)) | ||
|
r31665 | |||
|
r43346 | |||
|
r31665 | def formatlinerange(fromline, toline): | ||
|
r43347 | return b'%d:%d' % (fromline + 1, toline) | ||
|
r31665 | |||
|
r43346 | |||
|
r37933 | def _succsandmarkersgen(context, mapping): | ||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
|
r37521 | itemmappings = templatekw.showsuccsandmarkers(context, mapping) | ||
for item in itemmappings.tovalue(context, mapping): | ||||
|
r43347 | item[b'successors'] = _siblings( | ||
repo[successor] for successor in item[b'successors'] | ||||
|
r43346 | ) | ||
r35502 | yield item | |||
r35501 | ||||
|
r43346 | |||
|
r37933 | def succsandmarkers(context, mapping): | ||
return templateutil.mappinggenerator(_succsandmarkersgen, args=(mapping,)) | ||||
|
r43346 | |||
|
r36612 | # teach templater succsandmarkers is switched to (context, mapping) API | ||
|
r43347 | succsandmarkers._requires = {b'repo', b'ctx'} | ||
|
r36612 | |||
|
r43346 | |||
|
r37934 | def _whyunstablegen(context, mapping): | ||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
ctx = context.resource(mapping, b'ctx') | ||||
r36973 | ||||
entries = obsutil.whyunstable(repo, ctx) | ||||
for entry in entries: | ||||
|
r43347 | if entry.get(b'divergentnodes'): | ||
entry[b'divergentnodes'] = _siblings(entry[b'divergentnodes']) | ||||
r36973 | yield entry | |||
|
r43346 | |||
|
r37934 | def whyunstable(context, mapping): | ||
return templateutil.mappinggenerator(_whyunstablegen, args=(mapping,)) | ||||
|
r43346 | |||
|
r43347 | whyunstable._requires = {b'repo', b'ctx'} | ||
r36973 | ||||
|
r43346 | |||
r27294 | def commonentry(repo, ctx): | |||
|
r39830 | node = scmutil.binnode(ctx) | ||
r27294 | return { | |||
|
r36535 | # TODO: perhaps ctx.changectx() should be assigned if ctx is a | ||
# filectx, but I'm not pretty sure if that would always work because | ||||
# fctx.parents() != fctx.changectx.parents() for example. | ||||
|
r43347 | b'ctx': ctx, | ||
b'rev': ctx.rev(), | ||||
b'node': hex(node), | ||||
b'author': ctx.user(), | ||||
b'desc': ctx.description(), | ||||
b'date': ctx.date(), | ||||
b'extra': ctx.extra(), | ||||
b'phase': ctx.phasestr(), | ||||
b'obsolete': ctx.obsolete(), | ||||
b'succsandmarkers': succsandmarkers, | ||||
b'instabilities': templateutil.hybridlist( | ||||
ctx.instabilities(), name=b'instability' | ||||
|
r43346 | ), | ||
|
r43347 | b'whyunstable': whyunstable, | ||
b'branch': nodebranchnodefault(ctx), | ||||
b'inbranch': nodeinbranch(repo, ctx), | ||||
b'branches': nodebranchdict(repo, ctx), | ||||
b'tags': nodetagsdict(repo, node), | ||||
b'bookmarks': nodebookmarksdict(repo, node), | ||||
b'parent': lambda context, mapping: parents(ctx), | ||||
b'child': lambda context, mapping: children(ctx), | ||||
r27294 | } | |||
|
r43346 | |||
|
r36901 | def changelistentry(web, ctx): | ||
|
r23745 | '''Obtain a dictionary to be used for entries in a changelist. | ||
This function is called when producing items for the "entries" list passed | ||||
to the "shortlog" and "changelog" templates. | ||||
''' | ||||
repo = web.repo | ||||
rev = ctx.rev() | ||||
|
r39830 | n = scmutil.binnode(ctx) | ||
|
r43347 | showtags = showtag(repo, b'changelogtag', n) | ||
|
r37973 | files = listfilediffs(ctx.files(), n, web.maxfiles) | ||
|
r23745 | |||
r27294 | entry = commonentry(repo, ctx) | |||
|
r43346 | entry.update( | ||
{ | ||||
|
r43347 | b'allparents': lambda context, mapping: parents(ctx), | ||
b'parent': lambda context, mapping: parents(ctx, rev - 1), | ||||
b'child': lambda context, mapping: children(ctx, rev + 1), | ||||
b'changelogtag': showtags, | ||||
b'files': files, | ||||
|
r43346 | } | ||
) | ||||
r27294 | return entry | |||
|
r23745 | |||
|
r43346 | |||
|
r38054 | def changelistentries(web, revs, maxcount, parityfn): | ||
"""Emit up to N records for an iterable of revisions.""" | ||||
repo = web.repo | ||||
count = 0 | ||||
for rev in revs: | ||||
if count >= maxcount: | ||||
break | ||||
count += 1 | ||||
entry = changelistentry(web, repo[rev]) | ||||
|
r43347 | entry[b'parity'] = next(parityfn) | ||
|
r38054 | |||
yield entry | ||||
|
r43346 | |||
r25602 | def symrevorshortnode(req, ctx): | |||
|
r43347 | if b'node' in req.qsparams: | ||
return templatefilters.revescape(req.qsparams[b'node']) | ||||
r25602 | else: | |||
|
r39830 | return short(scmutil.binnode(ctx)) | ||
r25602 | ||||
|
r43346 | |||
|
r37970 | def _listfilesgen(context, ctx, stripecount): | ||
|
r37968 | parity = paritygen(stripecount) | ||
for blockno, f in enumerate(ctx.files()): | ||||
|
r43347 | template = b'filenodelink' if f in ctx else b'filenolink' | ||
|
r43346 | yield context.process( | ||
template, | ||||
{ | ||||
|
r43347 | b'node': ctx.hex(), | ||
b'file': f, | ||||
b'blockno': blockno + 1, | ||||
b'parity': next(parity), | ||||
|
r43346 | }, | ||
) | ||||
|
r37968 | |||
|
r36902 | def changesetentry(web, ctx): | ||
|
r24177 | '''Obtain a dictionary to be used to render the "changeset" template.''' | ||
|
r43347 | showtags = showtag(web.repo, b'changesettag', scmutil.binnode(ctx)) | ||
|
r43346 | showbookmarks = showbookmark( | ||
|
r43347 | web.repo, b'changesetbookmark', scmutil.binnode(ctx) | ||
|
r43346 | ) | ||
|
r24177 | showbranch = nodebranchnodefault(ctx) | ||
|
r36902 | basectx = basechangectx(web.repo, web.req) | ||
|
r24177 | if basectx is None: | ||
basectx = ctx.p1() | ||||
|
r43347 | style = web.config(b'web', b'style') | ||
if b'style' in web.req.qsparams: | ||||
style = web.req.qsparams[b'style'] | ||||
|
r24177 | |||
|
r36901 | diff = diffs(web, ctx, basectx, None, style) | ||
|
r24177 | |||
parity = paritygen(web.stripecount) | ||||
|
r38604 | diffstatsgen = diffstatgen(web.repo.ui, ctx, basectx) | ||
|
r38070 | diffstats = diffstat(ctx, diffstatsgen, parity) | ||
|
r24177 | |||
return dict( | ||||
diff=diff, | ||||
|
r36902 | symrev=symrevorshortnode(web.req, ctx), | ||
|
r24177 | basenode=basectx.hex(), | ||
changesettag=showtags, | ||||
changesetbookmark=showbookmarks, | ||||
changesetbranch=showbranch, | ||||
|
r43346 | files=templateutil.mappedgenerator( | ||
_listfilesgen, args=(ctx, web.stripecount) | ||||
), | ||||
|
r42524 | diffsummary=lambda context, mapping: diffsummary(diffstatsgen), | ||
|
r24177 | diffstat=diffstats, | ||
archives=web.archivelist(ctx.hex()), | ||||
|
r43346 | **pycompat.strkwargs(commonentry(web.repo, ctx)) | ||
) | ||||
|
r24177 | |||
|
r37972 | def _listfilediffsgen(context, files, node, max): | ||
|
r7311 | for f in files[:max]: | ||
|
r43347 | yield context.process(b'filedifflink', {b'node': hex(node), b'file': f}) | ||
|
r7311 | if len(files) > max: | ||
|
r43347 | yield context.process(b'fileellipses', {}) | ||
|
r7311 | |||
|
r43346 | |||
|
r37973 | def listfilediffs(files, node, max): | ||
|
r43346 | return templateutil.mappedgenerator( | ||
_listfilediffsgen, args=(files, node, max) | ||||
) | ||||
|
r37971 | |||
|
r38006 | def _prettyprintdifflines(context, lines, blockno, lineidprefix): | ||
|
r38004 | for lineno, l in enumerate(lines, 1): | ||
|
r43347 | difflineno = b"%d.%d" % (blockno, lineno) | ||
if l.startswith(b'+'): | ||||
ltype = b"difflineplus" | ||||
elif l.startswith(b'-'): | ||||
ltype = b"difflineminus" | ||||
elif l.startswith(b'@'): | ||||
ltype = b"difflineat" | ||||
|
r38004 | else: | ||
|
r43347 | ltype = b"diffline" | ||
|
r43346 | yield context.process( | ||
ltype, | ||||
{ | ||||
|
r43347 | b'line': l, | ||
b'lineno': lineno, | ||||
b'lineid': lineidprefix + b"l%s" % difflineno, | ||||
b'linenumber': b"% 8s" % difflineno, | ||||
|
r43346 | }, | ||
) | ||||
|
r38004 | |||
|
r43346 | def _diffsgen( | ||
context, | ||||
repo, | ||||
ctx, | ||||
basectx, | ||||
files, | ||||
style, | ||||
stripecount, | ||||
linerange, | ||||
lineidprefix, | ||||
): | ||||
|
r7310 | if files: | ||
|
r41825 | m = match.exact(files) | ||
|
r7310 | else: | ||
|
r41825 | m = match.always() | ||
|
r7310 | |||
diffopts = patch.diffopts(repo.ui, untrusted=True) | ||||
|
r38007 | parity = paritygen(stripecount) | ||
|
r7310 | |||
|
r41767 | diffhunks = patch.diffhunks(repo, basectx, ctx, m, opts=diffopts) | ||
|
r34856 | for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1): | ||
|
r43347 | if style != b'raw': | ||
|
r31276 | header = header[1:] | ||
|
r43347 | lines = [h + b'\n' for h in header] | ||
|
r31276 | for hunkrange, hunklines in hunks: | ||
|
r31666 | if linerange is not None and hunkrange is not None: | ||
s1, l1, s2, l2 = hunkrange | ||||
|
r31808 | if not mdiff.hunkinrange((s2, l2), linerange): | ||
|
r31666 | continue | ||
|
r31276 | lines.extend(hunklines) | ||
if lines: | ||||
|
r43346 | l = templateutil.mappedgenerator( | ||
_prettyprintdifflines, args=(lines, blockno, lineidprefix) | ||||
) | ||||
|
r38007 | yield { | ||
|
r43347 | b'parity': next(parity), | ||
b'blockno': blockno, | ||||
b'lines': l, | ||||
|
r38007 | } | ||
|
r43346 | |||
|
r43347 | def diffs(web, ctx, basectx, files, style, linerange=None, lineidprefix=b''): | ||
|
r43346 | args = ( | ||
web.repo, | ||||
ctx, | ||||
basectx, | ||||
files, | ||||
style, | ||||
web.stripecount, | ||||
linerange, | ||||
lineidprefix, | ||||
) | ||||
|
r43347 | return templateutil.mappinggenerator( | ||
_diffsgen, args=args, name=b'diffblock' | ||||
) | ||||
|
r7345 | |||
|
r43346 | |||
|
r38010 | def _compline(type, leftlineno, leftline, rightlineno, rightline): | ||
|
r43347 | lineid = leftlineno and (b"l%d" % leftlineno) or b'' | ||
lineid += rightlineno and (b"r%d" % rightlineno) or b'' | ||||
llno = b'%d' % leftlineno if leftlineno else b'' | ||||
rlno = b'%d' % rightlineno if rightlineno else b'' | ||||
|
r38010 | return { | ||
|
r43347 | b'type': type, | ||
b'lineid': lineid, | ||||
b'leftlineno': leftlineno, | ||||
b'leftlinenumber': b"% 6s" % llno, | ||||
b'leftline': leftline or b'', | ||||
b'rightlineno': rightlineno, | ||||
b'rightlinenumber': b"% 6s" % rlno, | ||||
b'rightline': rightline or b'', | ||||
|
r38010 | } | ||
|
r38008 | |||
|
r43346 | |||
|
r38010 | def _getcompblockgen(context, leftlines, rightlines, opcodes): | ||
|
r38009 | for type, llo, lhi, rlo, rhi in opcodes: | ||
|
r40192 | type = pycompat.sysbytes(type) | ||
|
r38009 | len1 = lhi - llo | ||
len2 = rhi - rlo | ||||
count = min(len1, len2) | ||||
|
r38806 | for i in pycompat.xrange(count): | ||
|
r43346 | yield _compline( | ||
type=type, | ||||
leftlineno=llo + i + 1, | ||||
leftline=leftlines[llo + i], | ||||
rightlineno=rlo + i + 1, | ||||
rightline=rightlines[rlo + i], | ||||
) | ||||
|
r38009 | if len1 > len2: | ||
|
r38806 | for i in pycompat.xrange(llo + count, lhi): | ||
|
r43346 | yield _compline( | ||
type=type, | ||||
leftlineno=i + 1, | ||||
leftline=leftlines[i], | ||||
rightlineno=None, | ||||
rightline=None, | ||||
) | ||||
|
r38009 | elif len2 > len1: | ||
|
r38806 | for i in pycompat.xrange(rlo + count, rhi): | ||
|
r43346 | yield _compline( | ||
type=type, | ||||
leftlineno=None, | ||||
leftline=None, | ||||
rightlineno=i + 1, | ||||
rightline=rightlines[i], | ||||
) | ||||
|
r17202 | |||
|
r38010 | def _getcompblock(leftlines, rightlines, opcodes): | ||
args = (leftlines, rightlines, opcodes) | ||||
|
r43346 | return templateutil.mappinggenerator( | ||
|
r43347 | _getcompblockgen, args=args, name=b'comparisonline' | ||
|
r43346 | ) | ||
|
r38010 | |||
|
r38012 | def _comparegen(context, contextnum, leftlines, rightlines): | ||
|
r38009 | '''Generator function that provides side-by-side comparison data.''' | ||
|
r17202 | s = difflib.SequenceMatcher(None, leftlines, rightlines) | ||
|
r38011 | if contextnum < 0: | ||
|
r38010 | l = _getcompblock(leftlines, rightlines, s.get_opcodes()) | ||
|
r43347 | yield {b'lines': l} | ||
|
r17202 | else: | ||
|
r38011 | for oc in s.get_grouped_opcodes(n=contextnum): | ||
|
r38010 | l = _getcompblock(leftlines, rightlines, oc) | ||
|
r43347 | yield {b'lines': l} | ||
|
r38012 | |||
|
r43346 | |||
|
r38013 | def compare(contextnum, leftlines, rightlines): | ||
|
r38012 | args = (contextnum, leftlines, rightlines) | ||
|
r43346 | return templateutil.mappinggenerator( | ||
|
r43347 | _comparegen, args=args, name=b'comparisonblock' | ||
|
r43346 | ) | ||
|
r17202 | |||
|
r38604 | def diffstatgen(ui, ctx, basectx): | ||
|
r14570 | '''Generator function that provides the diffstat data.''' | ||
|
r14490 | |||
|
r43347 | diffopts = patch.diffopts(ui, {b'noprefix': False}) | ||
|
r43346 | stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx, opts=diffopts))) | ||
|
r14490 | maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats) | ||
|
r14570 | while True: | ||
yield stats, maxname, maxtotal, addtotal, removetotal, binary | ||||
|
r43346 | |||
|
r14570 | def diffsummary(statgen): | ||
'''Return a short summary of the diff.''' | ||||
|
r29216 | stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen) | ||
|
r43347 | return _(b' %d files changed, %d insertions(+), %d deletions(-)\n') % ( | ||
|
r43346 | len(stats), | ||
addtotal, | ||||
removetotal, | ||||
) | ||||
|
r14570 | |||
|
r38069 | def _diffstattmplgen(context, ctx, statgen, parity): | ||
|
r29216 | stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen) | ||
|
r14561 | files = ctx.files() | ||
|
r14490 | |||
|
r14561 | def pct(i): | ||
if maxtotal == 0: | ||||
return 0 | ||||
return (float(i) / maxtotal) * 100 | ||||
|
r14490 | |||
|
r14562 | fileno = 0 | ||
|
r14561 | for filename, adds, removes, isbinary in stats: | ||
|
r43347 | template = b'diffstatlink' if filename in files else b'diffstatnolink' | ||
|
r14561 | total = adds + removes | ||
|
r14562 | fileno += 1 | ||
|
r43346 | yield context.process( | ||
template, | ||||
{ | ||||
|
r43347 | b'node': ctx.hex(), | ||
b'file': filename, | ||||
b'fileno': fileno, | ||||
b'total': total, | ||||
b'addpct': pct(adds), | ||||
b'removepct': pct(removes), | ||||
b'parity': next(parity), | ||||
|
r43346 | }, | ||
) | ||||
|
r14490 | |||
|
r38070 | def diffstat(ctx, statgen, parity): | ||
|
r38068 | '''Return a diffstat template for each file in the diff.''' | ||
|
r38069 | args = (ctx, statgen, parity) | ||
|
r38068 | return templateutil.mappedgenerator(_diffstattmplgen, args=args) | ||
|
r43346 | |||
|
r37714 | class sessionvars(templateutil.wrapped): | ||
|
r43347 | def __init__(self, vars, start=b'?'): | ||
|
r37712 | self._start = start | ||
self._vars = vars | ||||
|
r37713 | |||
|
r7345 | def __getitem__(self, key): | ||
|
r37712 | return self._vars[key] | ||
|
r37713 | |||
|
r7345 | def __setitem__(self, key, value): | ||
|
r37712 | self._vars[key] = value | ||
|
r37713 | |||
|
r7345 | def __copy__(self): | ||
|
r37712 | return sessionvars(copy.copy(self._vars), self._start) | ||
|
r37713 | |||
|
r38286 | def contains(self, context, mapping, item): | ||
item = templateutil.unwrapvalue(context, mapping, item) | ||||
return item in self._vars | ||||
|
r38261 | def getmember(self, context, mapping, key): | ||
|
r38262 | key = templateutil.unwrapvalue(context, mapping, key) | ||
|
r38261 | return self._vars.get(key) | ||
|
r38284 | def getmin(self, context, mapping): | ||
|
r43347 | raise error.ParseError(_(b'not comparable')) | ||
|
r38284 | |||
def getmax(self, context, mapping): | ||||
|
r43347 | raise error.ParseError(_(b'not comparable')) | ||
|
r38284 | |||
|
r38467 | def filter(self, context, mapping, select): | ||
# implement if necessary | ||||
|
r43347 | raise error.ParseError(_(b'not filterable')) | ||
|
r38467 | |||
|
r37714 | def itermaps(self, context): | ||
|
r37712 | separator = self._start | ||
|
r43376 | for key, value in sorted(pycompat.iteritems(self._vars)): | ||
|
r43346 | yield { | ||
|
r43347 | b'name': key, | ||
b'value': pycompat.bytestr(value), | ||||
b'separator': separator, | ||||
|
r34808 | } | ||
|
r43347 | separator = b'&' | ||
|
r12691 | |||
|
r37714 | def join(self, context, mapping, sep): | ||
# could be '{separator}{name}={value|urlescape}' | ||||
|
r43347 | raise error.ParseError(_(b'not displayable without template')) | ||
|
r37714 | |||
def show(self, context, mapping): | ||||
|
r43347 | return self.join(context, b'') | ||
|
r37714 | |||
|
r38308 | def tobool(self, context, mapping): | ||
return bool(self._vars) | ||||
|
r37714 | def tovalue(self, context, mapping): | ||
return self._vars | ||||
|
r43346 | |||
|
r27007 | class wsgiui(uimod.ui): | ||
|
r12691 | # default termwidth breaks under mod_wsgi | ||
def termwidth(self): | ||||
return 80 | ||||
|
r26162 | |||
|
r43346 | |||
|
r26162 | def getwebsubs(repo): | ||
websubtable = [] | ||||
|
r43347 | websubdefs = repo.ui.configitems(b'websub') | ||
|
r26162 | # we must maintain interhg backwards compatibility | ||
|
r43347 | websubdefs += repo.ui.configitems(b'interhg') | ||
|
r26162 | for key, pattern in websubdefs: | ||
# grab the delimiter from the character after the "s" | ||||
|
r36198 | unesc = pattern[1:2] | ||
|
r38494 | delim = stringutil.reescape(unesc) | ||
|
r26162 | |||
# identify portions of the pattern, taking care to avoid escaped | ||||
# delimiters. the replace format and flags are optional, but | ||||
# delimiters are required. | ||||
match = re.match( | ||||
|
r36201 | br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$' | ||
|
r43346 | % (delim, delim, delim), | ||
pattern, | ||||
) | ||||
|
r26162 | if not match: | ||
|
r43346 | repo.ui.warn( | ||
|
r43347 | _(b"websub: invalid pattern for %s: %s\n") % (key, pattern) | ||
|
r43346 | ) | ||
|
r26162 | continue | ||
# we need to unescape the delimiter for regexp and format | ||||
|
r36201 | delim_re = re.compile(br'(?<!\\)\\%s' % delim) | ||
|
r26162 | regexp = delim_re.sub(unesc, match.group(1)) | ||
format = delim_re.sub(unesc, match.group(2)) | ||||
# the pattern allows for 6 regexp flags, so set them if necessary | ||||
flagin = match.group(3) | ||||
flags = 0 | ||||
if flagin: | ||||
|
r43189 | for flag in pycompat.sysstr(flagin.upper()): | ||
|
r26162 | flags |= re.__dict__[flag] | ||
try: | ||||
regexp = re.compile(regexp, flags) | ||||
websubtable.append((regexp, format)) | ||||
except re.error: | ||||
|
r43346 | repo.ui.warn( | ||
|
r43347 | _(b"websub: invalid regexp for %s: %s\n") % (key, regexp) | ||
|
r43346 | ) | ||
|
r26162 | return websubtable | ||
r37928 | ||||
|
r43346 | |||
r37928 | def getgraphnode(repo, ctx): | |||
|
r43346 | return templatekw.getgraphnodecurrent( | ||
repo, ctx | ||||
) + templatekw.getgraphnodesymbol(ctx) | ||||