##// END OF EJS Templates
thirdparty: vendor futures 3.2.0...
thirdparty: vendor futures 3.2.0 Python 3 has a concurrent.futures package in the standard library for representing futures. The "futures" package on PyPI is a backport of this package to work with Python 2. The wire protocol code today has its own future concept for handling of "batch" requests. The frame-based protocol will also want to use futures. I've heavily used the "futures" package on Python 2 in other projects and it is pretty nice. It even has a built-in thread and process pool for running functions in parallel. I've used this heavily for concurrent I/O and other GIL-less activities. The existing futures API in the wire protocol code is not as nice as concurrent.futures. Since concurrent.futures is in the Python standard library and will presumably be the long-term future for futures in our code base, let's vendor the backport so we can use proper futures today. # no-check-commit because of style violations Differential Revision: https://phab.mercurial-scm.org/D3261

File last commit:

r37533:8a5ee6aa default
r37641:eb687c28 default
Show More
webutil.py
717 lines | 22.6 KiB | text/x-python | PythonLexer
# 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>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
import copy
import difflib
import os
import re
from ..i18n import _
from ..node import hex, nullid, short
from .common import (
ErrorResponse,
HTTP_BAD_REQUEST,
HTTP_NOT_FOUND,
paritygen,
)
from .. import (
context,
error,
match,
mdiff,
obsutil,
patch,
pathutil,
pycompat,
scmutil,
templatefilters,
templatekw,
templateutil,
ui as uimod,
util,
)
from ..utils import (
stringutil,
)
archivespecs = util.sortdict((
('zip', ('application/zip', 'zip', '.zip', None)),
('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
))
def archivelist(ui, nodeid, url=None):
allowed = ui.configlist('web', 'allow_archive', untrusted=True)
archives = []
for typ, spec in archivespecs.iteritems():
if typ in allowed or ui.configbool('web', 'allow' + typ,
untrusted=True):
archives.append({
'type': typ,
'extension': spec[2],
'node': nodeid,
'url': url,
})
return templateutil.mappinglist(archives)
def up(p):
if p[0:1] != "/":
p = "/" + p
if p[-1:] == "/":
p = p[:-1]
up = os.path.dirname(p)
if up == "/":
return "/"
return up + "/"
def _navseq(step, firststep=None):
if firststep:
yield firststep
if firststep >= 20 and firststep <= 40:
firststep = 50
yield firststep
assert step > 0
assert firststep > 0
while step <= firststep:
step *= 10
while True:
yield 1 * step
yield 3 * step
step *= 10
class revnav(object):
def __init__(self, repo):
"""Navigation generation object
:repo: repo object we generate nav for
"""
# used for hex generation
self._revlog = repo.changelog
def __nonzero__(self):
"""return True if any revision to navigate over"""
return self._first() is not None
__bool__ = __nonzero__
def _first(self):
"""return the minimum non-filtered changeset or None"""
try:
return next(iter(self._revlog))
except StopIteration:
return None
def hex(self, rev):
return hex(self._revlog.node(rev))
def gen(self, pos, pagelen, limit):
"""computes label and revision id for navigation link
:pos: is the revision relative to which we generate navigation.
:pagelen: the size of each navigation page
:limit: how far shall we link
The return is:
- a single element tuple
- containing a dictionary with a `before` and `after` key
- values are generator functions taking arbitrary number of kwargs
- yield items are dictionaries with `label` and `node` keys
"""
if not self:
# empty repo
return ({'before': (), 'after': ()},)
targets = []
for f in _navseq(1, pagelen):
if f > limit:
break
targets.append(pos + f)
targets.append(pos - f)
targets.sort()
first = self._first()
navbefore = [("(%i)" % first, self.hex(first))]
navafter = []
for rev in targets:
if rev not in self._revlog:
continue
if pos < rev < limit:
navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
if 0 < rev < pos:
navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
navafter.append(("tip", "tip"))
data = lambda i: {"label": i[0], "node": i[1]}
return ({'before': lambda **map: (data(i) for i in navbefore),
'after': lambda **map: (data(i) for i in navafter)},)
class filerevnav(revnav):
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)))
class _siblings(object):
def __init__(self, siblings=None, hiderev=None):
if siblings is None:
siblings = []
self.siblings = [s for s in siblings if s.node() != nullid]
if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
self.siblings = []
def __iter__(self):
for s in self.siblings:
d = {
'node': s.hex(),
'rev': s.rev(),
'user': s.user(),
'date': s.date(),
'description': s.description(),
'branch': s.branch(),
}
if util.safehasattr(s, 'path'):
d['file'] = s.path()
yield d
def __len__(self):
return len(self.siblings)
def difffeatureopts(req, ui, section):
diffopts = patch.difffeatureopts(ui, untrusted=True,
section=section, whitespace=True)
for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
v = req.qsparams.get(k)
if v is not None:
v = stringutil.parsebool(v)
setattr(diffopts, k, v if v is not None else True)
return diffopts
def annotate(req, fctx, ui):
diffopts = difffeatureopts(req, ui, 'annotate')
return fctx.annotate(follow=True, diffopts=diffopts)
def parents(ctx, hide=None):
if isinstance(ctx, context.basefilectx):
introrev = ctx.introrev()
if ctx.changectx().rev() != introrev:
return _siblings([ctx.repo()[introrev]], hide)
return _siblings(ctx.parents(), hide)
def children(ctx, hide=None):
return _siblings(ctx.children(), hide)
def renamelink(fctx):
r = fctx.renamed()
if r:
return [{'file': r[0], 'node': hex(r[1])}]
return []
def nodetagsdict(repo, node):
return [{"name": i} for i in repo.nodetags(node)]
def nodebookmarksdict(repo, node):
return [{"name": i} for i in repo.nodebookmarks(node)]
def nodebranchdict(repo, ctx):
branches = []
branch = ctx.branch()
# If this is an empty repo, ctx.node() == nullid,
# ctx.branch() == 'default'.
try:
branchnode = repo.branchtip(branch)
except error.RepoLookupError:
branchnode = None
if branchnode == ctx.node():
branches.append({"name": branch})
return branches
def nodeinbranch(repo, ctx):
branches = []
branch = ctx.branch()
try:
branchnode = repo.branchtip(branch)
except error.RepoLookupError:
branchnode = None
if branch != 'default' and branchnode != ctx.node():
branches.append({"name": branch})
return branches
def nodebranchnodefault(ctx):
branches = []
branch = ctx.branch()
if branch != 'default':
branches.append({"name": branch})
return branches
def showtag(repo, tmpl, t1, node=nullid, **args):
args = pycompat.byteskwargs(args)
for t in repo.nodetags(node):
lm = args.copy()
lm['tag'] = t
yield tmpl.generate(t1, lm)
def showbookmark(repo, tmpl, t1, node=nullid, **args):
args = pycompat.byteskwargs(args)
for t in repo.nodebookmarks(node):
lm = args.copy()
lm['bookmark'] = t
yield tmpl.generate(t1, lm)
def branchentries(repo, stripecount, limit=0):
tips = []
heads = repo.heads()
parity = paritygen(stripecount)
sortkey = lambda item: (not item[1], item[0].rev())
def entries(**map):
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:
status = 'closed'
elif ctx.node() not in heads:
status = 'inactive'
else:
status = 'open'
yield {
'parity': next(parity),
'branch': ctx.branch(),
'status': status,
'node': ctx.hex(),
'date': ctx.date()
}
return entries
def cleanpath(repo, path):
path = path.lstrip('/')
return pathutil.canonpath(repo.root, '', path)
def changectx(repo, req):
changeid = "tip"
if 'node' in req.qsparams:
changeid = req.qsparams['node']
ipos = changeid.find(':')
if ipos != -1:
changeid = changeid[(ipos + 1):]
return scmutil.revsymbol(repo, changeid)
def basechangectx(repo, req):
if 'node' in req.qsparams:
changeid = req.qsparams['node']
ipos = changeid.find(':')
if ipos != -1:
changeid = changeid[:ipos]
return scmutil.revsymbol(repo, changeid)
return None
def filectx(repo, req):
if 'file' not in req.qsparams:
raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
path = cleanpath(repo, req.qsparams['file'])
if 'node' in req.qsparams:
changeid = req.qsparams['node']
elif 'filenode' in req.qsparams:
changeid = req.qsparams['filenode']
else:
raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
try:
fctx = scmutil.revsymbol(repo, changeid)[path]
except error.RepoError:
fctx = repo.filectx(path, fileid=changeid)
return fctx
def linerange(req):
linerange = req.qsparams.getall('linerange')
if not linerange:
return None
if len(linerange) > 1:
raise ErrorResponse(HTTP_BAD_REQUEST,
'redundant linerange parameter')
try:
fromline, toline = map(int, linerange[0].split(':', 1))
except ValueError:
raise ErrorResponse(HTTP_BAD_REQUEST,
'invalid linerange parameter')
try:
return util.processlinerange(fromline, toline)
except error.ParseError as exc:
raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
def formatlinerange(fromline, toline):
return '%d:%d' % (fromline + 1, toline)
def succsandmarkers(context, mapping):
repo = context.resource(mapping, 'repo')
itemmappings = templatekw.showsuccsandmarkers(context, mapping)
for item in itemmappings.tovalue(context, mapping):
item['successors'] = _siblings(repo[successor]
for successor in item['successors'])
yield item
# teach templater succsandmarkers is switched to (context, mapping) API
succsandmarkers._requires = {'repo', 'ctx'}
def whyunstable(context, mapping):
repo = context.resource(mapping, 'repo')
ctx = context.resource(mapping, 'ctx')
entries = obsutil.whyunstable(repo, ctx)
for entry in entries:
if entry.get('divergentnodes'):
entry['divergentnodes'] = _siblings(entry['divergentnodes'])
yield entry
whyunstable._requires = {'repo', 'ctx'}
def commonentry(repo, ctx):
node = ctx.node()
return {
# 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.
'ctx': ctx,
'rev': ctx.rev(),
'node': hex(node),
'author': ctx.user(),
'desc': ctx.description(),
'date': ctx.date(),
'extra': ctx.extra(),
'phase': ctx.phasestr(),
'obsolete': ctx.obsolete(),
'succsandmarkers': succsandmarkers,
'instabilities': [{"instability": i} for i in ctx.instabilities()],
'whyunstable': whyunstable,
'branch': nodebranchnodefault(ctx),
'inbranch': nodeinbranch(repo, ctx),
'branches': nodebranchdict(repo, ctx),
'tags': nodetagsdict(repo, node),
'bookmarks': nodebookmarksdict(repo, node),
'parent': lambda **x: parents(ctx),
'child': lambda **x: children(ctx),
}
def changelistentry(web, ctx):
'''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()
n = ctx.node()
showtags = showtag(repo, web.tmpl, 'changelogtag', n)
files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
entry = commonentry(repo, ctx)
entry.update(
allparents=lambda **x: parents(ctx),
parent=lambda **x: parents(ctx, rev - 1),
child=lambda **x: children(ctx, rev + 1),
changelogtag=showtags,
files=files,
)
return entry
def symrevorshortnode(req, ctx):
if 'node' in req.qsparams:
return templatefilters.revescape(req.qsparams['node'])
else:
return short(ctx.node())
def changesetentry(web, ctx):
'''Obtain a dictionary to be used to render the "changeset" template.'''
showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
ctx.node())
showbranch = nodebranchnodefault(ctx)
files = []
parity = paritygen(web.stripecount)
for blockno, f in enumerate(ctx.files()):
template = 'filenodelink' if f in ctx else 'filenolink'
files.append(web.tmpl.generate(template, {
'node': ctx.hex(),
'file': f,
'blockno': blockno + 1,
'parity': next(parity),
}))
basectx = basechangectx(web.repo, web.req)
if basectx is None:
basectx = ctx.p1()
style = web.config('web', 'style')
if 'style' in web.req.qsparams:
style = web.req.qsparams['style']
diff = diffs(web, ctx, basectx, None, style)
parity = paritygen(web.stripecount)
diffstatsgen = diffstatgen(ctx, basectx)
diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
return dict(
diff=diff,
symrev=symrevorshortnode(web.req, ctx),
basenode=basectx.hex(),
changesettag=showtags,
changesetbookmark=showbookmarks,
changesetbranch=showbranch,
files=files,
diffsummary=lambda **x: diffsummary(diffstatsgen),
diffstat=diffstats,
archives=web.archivelist(ctx.hex()),
**pycompat.strkwargs(commonentry(web.repo, ctx)))
def listfilediffs(tmpl, files, node, max):
for f in files[:max]:
yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
if len(files) > max:
yield tmpl.generate('fileellipses', {})
def diffs(web, ctx, basectx, files, style, linerange=None,
lineidprefix=''):
def prettyprintlines(lines, blockno):
for lineno, l in enumerate(lines, 1):
difflineno = "%d.%d" % (blockno, lineno)
if l.startswith('+'):
ltype = "difflineplus"
elif l.startswith('-'):
ltype = "difflineminus"
elif l.startswith('@'):
ltype = "difflineat"
else:
ltype = "diffline"
yield web.tmpl.generate(ltype, {
'line': l,
'lineno': lineno,
'lineid': lineidprefix + "l%s" % difflineno,
'linenumber': "% 8s" % difflineno,
})
repo = web.repo
if files:
m = match.exact(repo.root, repo.getcwd(), files)
else:
m = match.always(repo.root, repo.getcwd())
diffopts = patch.diffopts(repo.ui, untrusted=True)
node1 = basectx.node()
node2 = ctx.node()
parity = paritygen(web.stripecount)
diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
if style != 'raw':
header = header[1:]
lines = [h + '\n' for h in header]
for hunkrange, hunklines in hunks:
if linerange is not None and hunkrange is not None:
s1, l1, s2, l2 = hunkrange
if not mdiff.hunkinrange((s2, l2), linerange):
continue
lines.extend(hunklines)
if lines:
yield web.tmpl.generate('diffblock', {
'parity': next(parity),
'blockno': blockno,
'lines': prettyprintlines(lines, blockno),
})
def compare(tmpl, context, leftlines, rightlines):
'''Generator function that provides side-by-side comparison data.'''
def compline(type, leftlineno, leftline, rightlineno, rightline):
lineid = leftlineno and ("l%d" % leftlineno) or ''
lineid += rightlineno and ("r%d" % rightlineno) or ''
llno = '%d' % leftlineno if leftlineno else ''
rlno = '%d' % rightlineno if rightlineno else ''
return tmpl.generate('comparisonline', {
'type': type,
'lineid': lineid,
'leftlineno': leftlineno,
'leftlinenumber': "% 6s" % llno,
'leftline': leftline or '',
'rightlineno': rightlineno,
'rightlinenumber': "% 6s" % rlno,
'rightline': rightline or '',
})
def getblock(opcodes):
for type, llo, lhi, rlo, rhi in opcodes:
len1 = lhi - llo
len2 = rhi - rlo
count = min(len1, len2)
for i in xrange(count):
yield compline(type=type,
leftlineno=llo + i + 1,
leftline=leftlines[llo + i],
rightlineno=rlo + i + 1,
rightline=rightlines[rlo + i])
if len1 > len2:
for i in xrange(llo + count, lhi):
yield compline(type=type,
leftlineno=i + 1,
leftline=leftlines[i],
rightlineno=None,
rightline=None)
elif len2 > len1:
for i in xrange(rlo + count, rhi):
yield compline(type=type,
leftlineno=None,
leftline=None,
rightlineno=i + 1,
rightline=rightlines[i])
s = difflib.SequenceMatcher(None, leftlines, rightlines)
if context < 0:
yield tmpl.generate('comparisonblock',
{'lines': getblock(s.get_opcodes())})
else:
for oc in s.get_grouped_opcodes(n=context):
yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
def diffstatgen(ctx, basectx):
'''Generator function that provides the diffstat data.'''
stats = patch.diffstatdata(
util.iterlines(ctx.diff(basectx, noprefix=False)))
maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
while True:
yield stats, maxname, maxtotal, addtotal, removetotal, binary
def diffsummary(statgen):
'''Return a short summary of the diff.'''
stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
len(stats), addtotal, removetotal)
def diffstat(tmpl, ctx, statgen, parity):
'''Return a diffstat template for each file in the diff.'''
stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
files = ctx.files()
def pct(i):
if maxtotal == 0:
return 0
return (float(i) / maxtotal) * 100
fileno = 0
for filename, adds, removes, isbinary in stats:
template = 'diffstatlink' if filename in files else 'diffstatnolink'
total = adds + removes
fileno += 1
yield tmpl.generate(template, {
'node': ctx.hex(),
'file': filename,
'fileno': fileno,
'total': total,
'addpct': pct(adds),
'removepct': pct(removes),
'parity': next(parity),
})
class sessionvars(object):
def __init__(self, vars, start='?'):
self.start = start
self.vars = vars
def __getitem__(self, key):
return self.vars[key]
def __setitem__(self, key, value):
self.vars[key] = value
def __copy__(self):
return sessionvars(copy.copy(self.vars), self.start)
def __iter__(self):
separator = self.start
for key, value in sorted(self.vars.iteritems()):
yield {'name': key,
'value': pycompat.bytestr(value),
'separator': separator,
}
separator = '&'
class wsgiui(uimod.ui):
# default termwidth breaks under mod_wsgi
def termwidth(self):
return 80
def getwebsubs(repo):
websubtable = []
websubdefs = repo.ui.configitems('websub')
# we must maintain interhg backwards compatibility
websubdefs += repo.ui.configitems('interhg')
for key, pattern in websubdefs:
# grab the delimiter from the character after the "s"
unesc = pattern[1:2]
delim = re.escape(unesc)
# 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(
br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
% (delim, delim, delim), pattern)
if not match:
repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
% (key, pattern))
continue
# we need to unescape the delimiter for regexp and format
delim_re = re.compile(br'(?<!\\)\\%s' % delim)
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:
for flag in flagin.upper():
flags |= re.__dict__[flag]
try:
regexp = re.compile(regexp, flags)
websubtable.append((regexp, format))
except re.error:
repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
% (key, regexp))
return websubtable