hgweb_mod.py
908 lines
| 31.7 KiB
| text/x-python
|
PythonLexer
|
r2391 | # hgweb/hgweb_mod.py - Web interface for a repository. | ||
|
r2356 | # | ||
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||||
|
r4635 | # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||
|
r2356 | # | ||
# This software may be used and distributed according to the terms | ||||
# of the GNU General Public License, incorporated herein by reference. | ||||
|
r5595 | import os, mimetypes, re, mimetools, cStringIO | ||
|
r2356 | from mercurial.node import * | ||
|
r5833 | from mercurial import mdiff, ui, hg, util, archival, patch, hook | ||
|
r3877 | from mercurial import revlog, templater | ||
|
r5779 | from common import ErrorResponse, get_mtime, style_map, paritygen, get_contact | ||
|
r5566 | from request import wsgirequest | ||
|
r5598 | import webcommands, protocol | ||
|
r2356 | |||
|
r5597 | shortcuts = { | ||
'cl': [('cmd', ['changelog']), ('rev', None)], | ||||
'sl': [('cmd', ['shortlog']), ('rev', None)], | ||||
'cs': [('cmd', ['changeset']), ('node', None)], | ||||
'f': [('cmd', ['file']), ('filenode', None)], | ||||
'fl': [('cmd', ['filelog']), ('filenode', None)], | ||||
'fd': [('cmd', ['filediff']), ('node', None)], | ||||
'fa': [('cmd', ['annotate']), ('filenode', None)], | ||||
'mf': [('cmd', ['manifest']), ('manifest', None)], | ||||
'ca': [('cmd', ['archive']), ('node', None)], | ||||
'tags': [('cmd', ['tags'])], | ||||
'tip': [('cmd', ['changeset']), ('node', ['tip'])], | ||||
'static': [('cmd', ['static']), ('file', None)] | ||||
} | ||||
|
r2356 | def _up(p): | ||
if p[0] != "/": | ||||
p = "/" + p | ||||
if p[-1] == "/": | ||||
p = p[:-1] | ||||
up = os.path.dirname(p) | ||||
if up == "/": | ||||
return "/" | ||||
return up + "/" | ||||
|
r3422 | def revnavgen(pos, pagelen, limit, nodefunc): | ||
|
r3407 | def seq(factor, limit=None): | ||
if limit: | ||||
yield limit | ||||
if limit >= 20 and limit <= 40: | ||||
yield 50 | ||||
else: | ||||
yield 1 * factor | ||||
yield 3 * factor | ||||
for f in seq(factor * 10): | ||||
yield f | ||||
def nav(**map): | ||||
l = [] | ||||
last = 0 | ||||
for f in seq(1, pagelen): | ||||
if f < pagelen or f <= last: | ||||
continue | ||||
if f > limit: | ||||
break | ||||
last = f | ||||
if pos + f < limit: | ||||
|
r3422 | l.append(("+%d" % f, hex(nodefunc(pos + f).node()))) | ||
|
r3407 | if pos - f >= 0: | ||
|
r3422 | l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node()))) | ||
|
r3407 | |||
|
r3424 | try: | ||
yield {"label": "(0)", "node": hex(nodefunc('0').node())} | ||||
|
r3407 | |||
|
r3424 | for label, node in l: | ||
yield {"label": label, "node": node} | ||||
|
r3407 | |||
|
r3427 | yield {"label": "tip", "node": "tip"} | ||
|
r3424 | except hg.RepoError: | ||
pass | ||||
|
r3407 | |||
return nav | ||||
|
r2356 | class hgweb(object): | ||
def __init__(self, repo, name=None): | ||||
|
r4874 | if isinstance(repo, str): | ||
|
r5289 | parentui = ui.ui(report_untrusted=False, interactive=False) | ||
self.repo = hg.repository(parentui, repo) | ||||
|
r2356 | else: | ||
self.repo = repo | ||||
|
r5833 | hook.redirect(True) | ||
|
r2356 | self.mtime = -1 | ||
self.reponame = name | ||||
self.archives = 'zip', 'gz', 'bz2' | ||||
|
r2666 | self.stripecount = 1 | ||
|
r3555 | # a repo owner may set web.templates in .hg/hgrc to get any file | ||
# readable by the user running the CGI script | ||||
self.templatepath = self.config("web", "templates", | ||||
templater.templatepath(), | ||||
untrusted=False) | ||||
# The CGI scripts are often run by a user different from the repo owner. | ||||
# Trust the settings from the .hg/hgrc files by default. | ||||
def config(self, section, name, default=None, untrusted=True): | ||||
return self.repo.ui.config(section, name, default, | ||||
untrusted=untrusted) | ||||
def configbool(self, section, name, default=False, untrusted=True): | ||||
return self.repo.ui.configbool(section, name, default, | ||||
untrusted=untrusted) | ||||
def configlist(self, section, name, default=None, untrusted=True): | ||||
return self.repo.ui.configlist(section, name, default, | ||||
untrusted=untrusted) | ||||
|
r2356 | |||
def refresh(self): | ||||
mtime = get_mtime(self.repo.root) | ||||
if mtime != self.mtime: | ||||
self.mtime = mtime | ||||
self.repo = hg.repository(self.repo.ui, self.repo.root) | ||||
|
r3555 | self.maxchanges = int(self.config("web", "maxchanges", 10)) | ||
self.stripecount = int(self.config("web", "stripes", 1)) | ||||
self.maxshortchanges = int(self.config("web", "maxshortchanges", 60)) | ||||
self.maxfiles = int(self.config("web", "maxfiles", 10)) | ||||
self.allowpull = self.configbool("web", "allowpull", True) | ||||
|
r4690 | self.encoding = self.config("web", "encoding", util._encoding) | ||
|
r2356 | |||
|
r5591 | def run(self): | ||
if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): | ||||
raise RuntimeError("This function is only intended to be called while running as a CGI script.") | ||||
import mercurial.hgweb.wsgicgi as wsgicgi | ||||
wsgicgi.launch(self) | ||||
def __call__(self, env, respond): | ||||
req = wsgirequest(env, respond) | ||||
self.run_wsgi(req) | ||||
return req | ||||
def run_wsgi(self, req): | ||||
|
r5596 | self.refresh() | ||
|
r5591 | |||
|
r5596 | # expand form shortcuts | ||
|
r5591 | |||
|
r5596 | for k in shortcuts.iterkeys(): | ||
if k in req.form: | ||||
for name, value in shortcuts[k]: | ||||
if value is None: | ||||
value = req.form[k] | ||||
req.form[name] = value | ||||
del req.form[k] | ||||
|
r5591 | |||
|
r5596 | # work with CGI variables to create coherent structure | ||
# use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME | ||||
|
r5591 | |||
|
r5596 | req.url = req.env['SCRIPT_NAME'] | ||
if not req.url.endswith('/'): | ||||
req.url += '/' | ||||
|
r5915 | if 'REPO_NAME' in req.env: | ||
|
r5596 | req.url += req.env['REPO_NAME'] + '/' | ||
|
r5591 | |||
|
r5596 | if req.env.get('PATH_INFO'): | ||
parts = req.env.get('PATH_INFO').strip('/').split('/') | ||||
repo_parts = req.env.get('REPO_NAME', '').split('/') | ||||
if parts[:len(repo_parts)] == repo_parts: | ||||
parts = parts[len(repo_parts):] | ||||
query = '/'.join(parts) | ||||
else: | ||||
query = req.env['QUERY_STRING'].split('&', 1)[0] | ||||
query = query.split(';', 1)[0] | ||||
|
r5591 | |||
|
r5596 | # translate user-visible url structure to internal structure | ||
args = query.split('/', 2) | ||||
if 'cmd' not in req.form and args and args[0]: | ||||
|
r5591 | |||
cmd = args.pop(0) | ||||
style = cmd.rfind('-') | ||||
if style != -1: | ||||
req.form['style'] = [cmd[:style]] | ||||
cmd = cmd[style+1:] | ||||
|
r5596 | |||
|
r5591 | # avoid accepting e.g. style parameter as command | ||
|
r5598 | if hasattr(webcommands, cmd) or hasattr(protocol, cmd): | ||
|
r5591 | req.form['cmd'] = [cmd] | ||
if args and args[0]: | ||||
node = args.pop(0) | ||||
req.form['node'] = [node] | ||||
if args: | ||||
req.form['file'] = args | ||||
if cmd == 'static': | ||||
req.form['file'] = req.form['node'] | ||||
elif cmd == 'archive': | ||||
fn = req.form['node'][0] | ||||
for type_, spec in self.archive_specs.iteritems(): | ||||
ext = spec[2] | ||||
if fn.endswith(ext): | ||||
req.form['node'] = [fn[:-len(ext)]] | ||||
req.form['type'] = [type_] | ||||
|
r5599 | # actually process the request | ||
try: | ||||
|
r5600 | cmd = req.form.get('cmd', [''])[0] | ||
if hasattr(protocol, cmd): | ||||
method = getattr(protocol, cmd) | ||||
|
r5599 | method(self, req) | ||
|
r5600 | else: | ||
|
r5890 | |||
|
r5600 | tmpl = self.templater(req) | ||
if cmd == '': | ||||
req.form['cmd'] = [tmpl.cache['default']] | ||||
cmd = req.form['cmd'][0] | ||||
|
r5890 | |||
|
r5923 | if cmd == 'file' and 'raw' in req.form.get('style', []): | ||
|
r5890 | webcommands.rawfile(self, req, tmpl) | ||
else: | ||||
getattr(webcommands, cmd)(self, req, tmpl) | ||||
|
r5600 | del tmpl | ||
except revlog.LookupError, err: | ||||
req.respond(404, tmpl( | ||||
'error', error='revision not found: %s' % err.name)) | ||||
except (hg.RepoError, revlog.RevlogError), inst: | ||||
req.respond('500 Internal Server Error', | ||||
tmpl('error', error=str(inst))) | ||||
except ErrorResponse, inst: | ||||
req.respond(inst.code, tmpl('error', error=inst.message)) | ||||
except AttributeError: | ||||
req.respond(400, tmpl('error', error='No such method: ' + cmd)) | ||||
|
r5599 | |||
def templater(self, req): | ||||
|
r5596 | # determine scheme, port and server name | ||
# this is needed to create absolute urls | ||||
|
r5591 | |||
proto = req.env.get('wsgi.url_scheme') | ||||
if proto == 'https': | ||||
proto = 'https' | ||||
default_port = "443" | ||||
else: | ||||
proto = 'http' | ||||
default_port = "80" | ||||
port = req.env["SERVER_PORT"] | ||||
port = port != default_port and (":" + port) or "" | ||||
urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port) | ||||
staticurl = self.config("web", "staticurl") or req.url + 'static/' | ||||
if not staticurl.endswith('/'): | ||||
staticurl += '/' | ||||
|
r5596 | # some functions for the templater | ||
def header(**map): | ||||
header_file = cStringIO.StringIO( | ||||
|
r5600 | ''.join(tmpl("header", encoding=self.encoding, **map))) | ||
|
r5596 | msg = mimetools.Message(header_file, 0) | ||
req.header(msg.items()) | ||||
yield header_file.read() | ||||
def footer(**map): | ||||
|
r5600 | yield tmpl("footer", **map) | ||
|
r5596 | |||
def motd(**map): | ||||
yield self.config("web", "motd", "") | ||||
def sessionvars(**map): | ||||
fields = [] | ||||
|
r5915 | if 'style' in req.form: | ||
|
r5596 | style = req.form['style'][0] | ||
if style != self.config('web', 'style', ''): | ||||
fields.append(('style', style)) | ||||
separator = req.url[-1] == '?' and ';' or '?' | ||||
for name, value in fields: | ||||
yield dict(name=name, value=value, separator=separator) | ||||
separator = ';' | ||||
|
r5599 | # figure out which style to use | ||
|
r5596 | style = self.config("web", "style", "") | ||
|
r5915 | if 'style' in req.form: | ||
|
r5596 | style = req.form['style'][0] | ||
mapfile = style_map(self.templatepath, style) | ||||
|
r5591 | if not self.reponame: | ||
self.reponame = (self.config("web", "name") | ||||
or req.env.get('REPO_NAME') | ||||
or req.url.strip('/') or self.repo.root) | ||||
|
r5599 | # create the templater | ||
|
r5600 | tmpl = templater.templater(mapfile, templater.common_filters, | ||
defaults={"url": req.url, | ||||
"staticurl": staticurl, | ||||
"urlbase": urlbase, | ||||
"repo": self.reponame, | ||||
"header": header, | ||||
"footer": footer, | ||||
"motd": motd, | ||||
"sessionvars": sessionvars | ||||
}) | ||||
return tmpl | ||||
|
r5591 | |||
|
r2356 | def archivelist(self, nodeid): | ||
|
r3555 | allowed = self.configlist("web", "allow_archive") | ||
|
r3260 | for i, spec in self.archive_specs.iteritems(): | ||
|
r3555 | if i in allowed or self.configbool("web", "allow" + i): | ||
|
r3260 | yield {"type" : i, "extension" : spec[2], "node" : nodeid} | ||
|
r2356 | |||
|
r5600 | def listfilediffs(self, tmpl, files, changeset): | ||
|
r2356 | for f in files[:self.maxfiles]: | ||
|
r5600 | yield tmpl("filedifflink", node=hex(changeset), file=f) | ||
|
r2356 | if len(files) > self.maxfiles: | ||
|
r5600 | yield tmpl("fileellipses") | ||
|
r2356 | |||
|
r3208 | def siblings(self, siblings=[], hiderev=None, **args): | ||
siblings = [s for s in siblings if s.node() != nullid] | ||||
if len(siblings) == 1 and siblings[0].rev() == hiderev: | ||||
|
r2356 | return | ||
for s in siblings: | ||||
|
r3392 | d = {'node': hex(s.node()), 'rev': s.rev()} | ||
|
r3394 | if hasattr(s, 'path'): | ||
d['file'] = s.path() | ||||
|
r3392 | d.update(args) | ||
yield d | ||||
|
r2356 | |||
def renamelink(self, fl, node): | ||||
r = fl.renamed(node) | ||||
if r: | ||||
return [dict(file=r[0], node=hex(r[1]))] | ||||
return [] | ||||
|
r4539 | def nodetagsdict(self, node): | ||
return [{"name": i} for i in self.repo.nodetags(node)] | ||||
|
r4538 | |||
|
r4539 | def nodebranchdict(self, ctx): | ||
branches = [] | ||||
branch = ctx.branch() | ||||
|
r5331 | # If this is an empty repo, ctx.node() == nullid, | ||
# ctx.branch() == 'default', but branchtags() is | ||||
# an empty dict. Using dict.get avoids a traceback. | ||||
if self.repo.branchtags().get(branch) == ctx.node(): | ||||
|
r4539 | branches.append({"name": branch}) | ||
return branches | ||||
|
r4538 | |||
|
r5600 | def showtag(self, tmpl, t1, node=nullid, **args): | ||
|
r2356 | for t in self.repo.nodetags(node): | ||
|
r5600 | yield tmpl(t1, tag=t, **args) | ||
|
r2356 | |||
|
r5600 | def diff(self, tmpl, node1, node2, files): | ||
|
r2356 | def filterfiles(filters, files): | ||
l = [x for x in files if x in filters] | ||||
for t in filters: | ||||
if t and t[-1] != os.sep: | ||||
t += os.sep | ||||
l += [x for x in files if x.startswith(t)] | ||||
return l | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r2356 | def diffblock(diff, f, fn): | ||
|
r5600 | yield tmpl("diffblock", | ||
lines=prettyprintlines(diff), | ||||
parity=parity.next(), | ||||
file=f, | ||||
filenode=hex(fn or nullid)) | ||||
|
r2356 | |||
def prettyprintlines(diff): | ||||
for l in diff.splitlines(1): | ||||
if l.startswith('+'): | ||||
|
r5600 | yield tmpl("difflineplus", line=l) | ||
|
r2356 | elif l.startswith('-'): | ||
|
r5600 | yield tmpl("difflineminus", line=l) | ||
|
r2356 | elif l.startswith('@'): | ||
|
r5600 | yield tmpl("difflineat", line=l) | ||
|
r2356 | else: | ||
|
r5600 | yield tmpl("diffline", line=l) | ||
|
r2356 | |||
r = self.repo | ||||
|
r3973 | c1 = r.changectx(node1) | ||
c2 = r.changectx(node2) | ||||
date1 = util.datestr(c1.date()) | ||||
date2 = util.datestr(c2.date()) | ||||
|
r2356 | |||
|
r2876 | modified, added, removed, deleted, unknown = r.status(node1, node2)[:5] | ||
|
r2356 | if files: | ||
modified, added, removed = map(lambda x: filterfiles(files, x), | ||||
(modified, added, removed)) | ||||
|
r3555 | diffopts = patch.diffopts(self.repo.ui, untrusted=True) | ||
|
r2356 | for f in modified: | ||
|
r3973 | to = c1.filectx(f).data() | ||
tn = c2.filectx(f).data() | ||||
|
r5486 | yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f, | ||
|
r2874 | opts=diffopts), f, tn) | ||
|
r2356 | for f in added: | ||
to = None | ||||
|
r3973 | tn = c2.filectx(f).data() | ||
|
r5486 | yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f, | ||
|
r2874 | opts=diffopts), f, tn) | ||
|
r2356 | for f in removed: | ||
|
r3973 | to = c1.filectx(f).data() | ||
|
r2356 | tn = None | ||
|
r5486 | yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f, | ||
|
r2874 | opts=diffopts), f, tn) | ||
|
r2356 | |||
|
r5600 | def changelog(self, tmpl, ctx, shortlog=False): | ||
|
r5269 | def changelist(limit=0,**map): | ||
|
r2356 | cl = self.repo.changelog | ||
l = [] # build a list in forward order for efficiency | ||||
|
r3473 | for i in xrange(start, end): | ||
|
r3208 | ctx = self.repo.changectx(i) | ||
n = ctx.node() | ||||
|
r2356 | |||
|
r4462 | l.insert(0, {"parity": parity.next(), | ||
|
r3208 | "author": ctx.user(), | ||
"parent": self.siblings(ctx.parents(), i - 1), | ||||
"child": self.siblings(ctx.children(), i + 1), | ||||
|
r2356 | "changelogtag": self.showtag("changelogtag",n), | ||
|
r3208 | "desc": ctx.description(), | ||
"date": ctx.date(), | ||||
|
r5600 | "files": self.listfilediffs(tmpl, ctx.files(), n), | ||
|
r2356 | "rev": i, | ||
|
r4538 | "node": hex(n), | ||
|
r4539 | "tags": self.nodetagsdict(n), | ||
"branches": self.nodebranchdict(ctx)}) | ||||
|
r2356 | |||
|
r5269 | if limit > 0: | ||
l = l[:limit] | ||||
|
r2356 | for e in l: | ||
yield e | ||||
|
r2684 | maxchanges = shortlog and self.maxshortchanges or self.maxchanges | ||
|
r2356 | cl = self.repo.changelog | ||
count = cl.count() | ||||
|
r3407 | pos = ctx.rev() | ||
|
r2684 | start = max(0, pos - maxchanges + 1) | ||
end = min(count, start + maxchanges) | ||||
|
r2356 | pos = end - 1 | ||
|
r4462 | parity = paritygen(self.stripecount, offset=start-end) | ||
|
r2356 | |||
|
r3422 | changenav = revnavgen(pos, maxchanges, count, self.repo.changectx) | ||
|
r3407 | |||
|
r5889 | return tmpl(shortlog and 'shortlog' or 'changelog', | ||
changenav=changenav, | ||||
node=hex(cl.tip()), | ||||
rev=pos, changesets=count, | ||||
entries=lambda **x: changelist(limit=0,**x), | ||||
latestentry=lambda **x: changelist(limit=1,**x), | ||||
archives=self.archivelist("tip")) | ||||
|
r2356 | |||
|
r5600 | def search(self, tmpl, query): | ||
|
r2356 | |||
def changelist(**map): | ||||
cl = self.repo.changelog | ||||
count = 0 | ||||
qw = query.lower().split() | ||||
def revgen(): | ||||
|
r3473 | for i in xrange(cl.count() - 1, 0, -100): | ||
|
r2356 | l = [] | ||
|
r3473 | for j in xrange(max(0, i - 100), i): | ||
|
r3208 | ctx = self.repo.changectx(j) | ||
l.append(ctx) | ||||
|
r2356 | l.reverse() | ||
for e in l: | ||||
yield e | ||||
|
r3208 | for ctx in revgen(): | ||
|
r2356 | miss = 0 | ||
for q in qw: | ||||
|
r3208 | if not (q in ctx.user().lower() or | ||
q in ctx.description().lower() or | ||||
|
r4323 | q in " ".join(ctx.files()).lower()): | ||
|
r2356 | miss = 1 | ||
break | ||||
if miss: | ||||
continue | ||||
count += 1 | ||||
|
r3208 | n = ctx.node() | ||
|
r2356 | |||
|
r5600 | yield tmpl('searchentry', | ||
parity=parity.next(), | ||||
author=ctx.user(), | ||||
parent=self.siblings(ctx.parents()), | ||||
child=self.siblings(ctx.children()), | ||||
changelogtag=self.showtag("changelogtag",n), | ||||
desc=ctx.description(), | ||||
date=ctx.date(), | ||||
files=self.listfilediffs(tmpl, ctx.files(), n), | ||||
rev=ctx.rev(), | ||||
node=hex(n), | ||||
tags=self.nodetagsdict(n), | ||||
branches=self.nodebranchdict(ctx)) | ||||
|
r2356 | |||
if count >= self.maxchanges: | ||||
break | ||||
cl = self.repo.changelog | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r2356 | |||
|
r5889 | return tmpl('search', | ||
query=query, | ||||
node=hex(cl.tip()), | ||||
entries=changelist, | ||||
archives=self.archivelist("tip")) | ||||
|
r2356 | |||
|
r5600 | def changeset(self, tmpl, ctx): | ||
|
r3208 | n = ctx.node() | ||
parents = ctx.parents() | ||||
p1 = parents[0].node() | ||||
|
r2356 | |||
files = [] | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r3208 | for f in ctx.files(): | ||
|
r5600 | files.append(tmpl("filenodelink", | ||
node=hex(n), file=f, | ||||
parity=parity.next())) | ||||
|
r2356 | |||
def diff(**map): | ||||
|
r5600 | yield self.diff(tmpl, p1, n, None) | ||
|
r2356 | |||
|
r5889 | return tmpl('changeset', | ||
diff=diff, | ||||
rev=ctx.rev(), | ||||
node=hex(n), | ||||
parent=self.siblings(parents), | ||||
child=self.siblings(ctx.children()), | ||||
changesettag=self.showtag("changesettag",n), | ||||
author=ctx.user(), | ||||
desc=ctx.description(), | ||||
date=ctx.date(), | ||||
files=files, | ||||
archives=self.archivelist(hex(n)), | ||||
tags=self.nodetagsdict(n), | ||||
branches=self.nodebranchdict(ctx)) | ||||
|
r2356 | |||
|
r5600 | def filelog(self, tmpl, fctx): | ||
|
r3206 | f = fctx.path() | ||
fl = fctx.filelog() | ||||
|
r2356 | count = fl.count() | ||
|
r3407 | pagelen = self.maxshortchanges | ||
|
r3409 | pos = fctx.filerev() | ||
|
r3673 | start = max(0, pos - pagelen + 1) | ||
|
r3409 | end = min(count, start + pagelen) | ||
pos = end - 1 | ||||
|
r4462 | parity = paritygen(self.stripecount, offset=start-end) | ||
|
r2356 | |||
|
r5269 | def entries(limit=0, **map): | ||
|
r2356 | l = [] | ||
|
r3473 | for i in xrange(start, end): | ||
|
r3208 | ctx = fctx.filectx(i) | ||
|
r2356 | n = fl.node(i) | ||
|
r4462 | l.insert(0, {"parity": parity.next(), | ||
|
r2356 | "filerev": i, | ||
"file": f, | ||||
|
r3206 | "node": hex(ctx.node()), | ||
"author": ctx.user(), | ||||
"date": ctx.date(), | ||||
|
r2356 | "rename": self.renamelink(fl, n), | ||
|
r3392 | "parent": self.siblings(fctx.parents()), | ||
"child": self.siblings(fctx.children()), | ||||
|
r3206 | "desc": ctx.description()}) | ||
|
r2356 | |||
|
r5269 | if limit > 0: | ||
l = l[:limit] | ||||
|
r2356 | for e in l: | ||
yield e | ||||
|
r3422 | nodefunc = lambda x: fctx.filectx(fileid=x) | ||
nav = revnavgen(pos, pagelen, count, nodefunc) | ||||
|
r5889 | return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav, | ||
entries=lambda **x: entries(limit=0, **x), | ||||
latestentry=lambda **x: entries(limit=1, **x)) | ||||
|
r2356 | |||
|
r5600 | def filerevision(self, tmpl, fctx): | ||
|
r3206 | f = fctx.path() | ||
text = fctx.data() | ||||
fl = fctx.filelog() | ||||
n = fctx.filenode() | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r2356 | |||
mt = mimetypes.guess_type(f)[0] | ||||
rawtext = text | ||||
if util.binary(text): | ||||
mt = mt or 'application/octet-stream' | ||||
text = "(binary:%s)" % mt | ||||
mt = mt or 'text/plain' | ||||
def lines(): | ||||
for l, t in enumerate(text.splitlines(1)): | ||||
yield {"line": t, | ||||
"linenumber": "% 6d" % (l + 1), | ||||
|
r4462 | "parity": parity.next()} | ||
|
r2356 | |||
|
r5889 | return tmpl("filerevision", | ||
file=f, | ||||
path=_up(f), | ||||
text=lines(), | ||||
raw=rawtext, | ||||
mimetype=mt, | ||||
rev=fctx.rev(), | ||||
node=hex(fctx.node()), | ||||
author=fctx.user(), | ||||
date=fctx.date(), | ||||
desc=fctx.description(), | ||||
parent=self.siblings(fctx.parents()), | ||||
child=self.siblings(fctx.children()), | ||||
rename=self.renamelink(fl, n), | ||||
permissions=fctx.manifest().flags(f)) | ||||
|
r2356 | |||
|
r5600 | def fileannotate(self, tmpl, fctx): | ||
|
r3206 | f = fctx.path() | ||
|
r3173 | n = fctx.filenode() | ||
fl = fctx.filelog() | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r2356 | |||
def annotate(**map): | ||||
last = None | ||||
|
r3175 | for f, l in fctx.annotate(follow=True): | ||
fnode = f.filenode() | ||||
|
r3173 | name = self.repo.ui.shortuser(f.user()) | ||
|
r2356 | |||
|
r3175 | if last != fnode: | ||
last = fnode | ||||
|
r2356 | |||
|
r4462 | yield {"parity": parity.next(), | ||
|
r3178 | "node": hex(f.node()), | ||
|
r3403 | "rev": f.rev(), | ||
|
r2356 | "author": name, | ||
|
r3173 | "file": f.path(), | ||
|
r2356 | "line": l} | ||
|
r5889 | return tmpl("fileannotate", | ||
file=f, | ||||
annotate=annotate, | ||||
path=_up(f), | ||||
rev=fctx.rev(), | ||||
node=hex(fctx.node()), | ||||
author=fctx.user(), | ||||
date=fctx.date(), | ||||
desc=fctx.description(), | ||||
rename=self.renamelink(fl, n), | ||||
parent=self.siblings(fctx.parents()), | ||||
child=self.siblings(fctx.children()), | ||||
permissions=fctx.manifest().flags(f)) | ||||
|
r2356 | |||
|
r5600 | def manifest(self, tmpl, ctx, path): | ||
|
r3205 | mf = ctx.manifest() | ||
node = ctx.node() | ||||
|
r2356 | |||
files = {} | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r2356 | |||
|
r3595 | if path and path[-1] != "/": | ||
path += "/" | ||||
l = len(path) | ||||
abspath = "/" + path | ||||
|
r2356 | |||
|
r3673 | for f, n in mf.items(): | ||
|
r3595 | if f[:l] != path: | ||
|
r2356 | continue | ||
remain = f[l:] | ||||
if "/" in remain: | ||||
|
r2579 | short = remain[:remain.index("/") + 1] # bleah | ||
|
r2356 | files[short] = (f, None) | ||
else: | ||||
short = os.path.basename(remain) | ||||
files[short] = (f, n) | ||||
|
r5561 | if not files: | ||
raise ErrorResponse(404, 'Path not found: ' + path) | ||||
|
r2356 | def filelist(**map): | ||
fl = files.keys() | ||||
fl.sort() | ||||
for f in fl: | ||||
full, fnode = files[f] | ||||
if not fnode: | ||||
continue | ||||
|
r5273 | fctx = ctx.filectx(full) | ||
|
r2356 | yield {"file": full, | ||
|
r4462 | "parity": parity.next(), | ||
|
r2356 | "basename": f, | ||
|
r5273 | "date": fctx.changectx().date(), | ||
"size": fctx.size(), | ||||
|
r4745 | "permissions": mf.flags(full)} | ||
|
r2356 | |||
def dirlist(**map): | ||||
fl = files.keys() | ||||
fl.sort() | ||||
for f in fl: | ||||
full, fnode = files[f] | ||||
if fnode: | ||||
continue | ||||
|
r4462 | yield {"parity": parity.next(), | ||
|
r5064 | "path": "%s%s" % (abspath, f), | ||
|
r2356 | "basename": f[:-1]} | ||
|
r5889 | return tmpl("manifest", | ||
rev=ctx.rev(), | ||||
node=hex(node), | ||||
path=abspath, | ||||
up=_up(abspath), | ||||
upparity=parity.next(), | ||||
fentries=filelist, | ||||
dentries=dirlist, | ||||
archives=self.archivelist(hex(node)), | ||||
tags=self.nodetagsdict(node), | ||||
branches=self.nodebranchdict(ctx)) | ||||
|
r2356 | |||
|
r5600 | def tags(self, tmpl): | ||
|
r2356 | i = self.repo.tagslist() | ||
i.reverse() | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r2356 | |||
|
r5269 | def entries(notip=False,limit=0, **map): | ||
count = 0 | ||||
|
r3673 | for k, n in i: | ||
if notip and k == "tip": | ||||
continue | ||||
|
r5269 | if limit > 0 and count >= limit: | ||
continue | ||||
count = count + 1 | ||||
|
r4462 | yield {"parity": parity.next(), | ||
|
r2356 | "tag": k, | ||
|
r3973 | "date": self.repo.changectx(n).date(), | ||
|
r2356 | "node": hex(n)} | ||
|
r5889 | return tmpl("tags", | ||
node=hex(self.repo.changelog.tip()), | ||||
entries=lambda **x: entries(False,0, **x), | ||||
entriesnotip=lambda **x: entries(True,0, **x), | ||||
latestentry=lambda **x: entries(True,1, **x)) | ||||
|
r2356 | |||
|
r5600 | def summary(self, tmpl): | ||
|
r2356 | i = self.repo.tagslist() | ||
i.reverse() | ||||
def tagentries(**map): | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r2356 | count = 0 | ||
|
r3673 | for k, n in i: | ||
|
r2356 | if k == "tip": # skip tip | ||
continue; | ||||
count += 1 | ||||
if count > 10: # limit to 10 tags | ||||
break; | ||||
|
r5600 | yield tmpl("tagentry", | ||
parity=parity.next(), | ||||
tag=k, | ||||
node=hex(n), | ||||
date=self.repo.changectx(n).date()) | ||||
|
r2356 | |||
|
r4300 | |||
def branches(**map): | ||||
|
r4462 | parity = paritygen(self.stripecount) | ||
|
r3499 | |||
|
r4300 | b = self.repo.branchtags() | ||
l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()] | ||||
l.sort() | ||||
|
r3499 | |||
|
r4300 | for r,n,t in l: | ||
ctx = self.repo.changectx(n) | ||||
|
r3499 | |||
|
r4462 | yield {'parity': parity.next(), | ||
|
r4300 | 'branch': t, | ||
'node': hex(n), | ||||
|
r3499 | 'date': ctx.date()} | ||
|
r2356 | def changelist(**map): | ||
|
r4462 | parity = paritygen(self.stripecount, offset=start-end) | ||
|
r2356 | l = [] # build a list in forward order for efficiency | ||
|
r3473 | for i in xrange(start, end): | ||
|
r3973 | ctx = self.repo.changectx(i) | ||
|
r4538 | n = ctx.node() | ||
hn = hex(n) | ||||
|
r2356 | |||
|
r5600 | l.insert(0, tmpl( | ||
'shortlogentry', | ||||
|
r4462 | parity=parity.next(), | ||
|
r3973 | author=ctx.user(), | ||
desc=ctx.description(), | ||||
date=ctx.date(), | ||||
rev=i, | ||||
|
r4538 | node=hn, | ||
|
r4539 | tags=self.nodetagsdict(n), | ||
branches=self.nodebranchdict(ctx))) | ||||
|
r2356 | |||
yield l | ||||
|
r3973 | cl = self.repo.changelog | ||
|
r2356 | count = cl.count() | ||
start = max(0, count - self.maxchanges) | ||||
end = min(count, start + self.maxchanges) | ||||
|
r5889 | return tmpl("summary", | ||
desc=self.config("web", "description", "unknown"), | ||||
owner=get_contact(self.config) or "unknown", | ||||
lastchange=cl.read(cl.tip())[2], | ||||
tags=tagentries, | ||||
branches=branches, | ||||
shortlog=changelist, | ||||
node=hex(cl.tip()), | ||||
archives=self.archivelist("tip")) | ||||
|
r2356 | |||
|
r5600 | def filediff(self, tmpl, fctx): | ||
|
r3220 | n = fctx.node() | ||
path = fctx.path() | ||||
|
r3406 | parents = fctx.parents() | ||
p1 = parents and parents[0].node() or nullid | ||||
|
r2356 | |||
def diff(**map): | ||||
|
r5600 | yield self.diff(tmpl, p1, n, [path]) | ||
|
r2356 | |||
|
r5889 | return tmpl("filediff", | ||
file=path, | ||||
node=hex(n), | ||||
rev=fctx.rev(), | ||||
parent=self.siblings(parents), | ||||
child=self.siblings(fctx.children()), | ||||
diff=diff) | ||||
|
r2356 | |||
archive_specs = { | ||||
|
r2361 | 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None), | ||
'gz': ('application/x-tar', 'tgz', '.tar.gz', None), | ||||
|
r2356 | 'zip': ('application/zip', 'zip', '.zip', None), | ||
} | ||||
|
r5600 | def archive(self, tmpl, req, key, type_): | ||
|
r2356 | reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame)) | ||
|
r4669 | cnode = self.repo.lookup(key) | ||
arch_version = key | ||||
if cnode == key or key == 'tip': | ||||
|
r4164 | arch_version = short(cnode) | ||
name = "%s-%s" % (reponame, arch_version) | ||||
|
r2394 | mimetype, artype, extension, encoding = self.archive_specs[type_] | ||
|
r2356 | headers = [('Content-type', mimetype), | ||
('Content-disposition', 'attachment; filename=%s%s' % | ||||
(name, extension))] | ||||
if encoding: | ||||
headers.append(('Content-encoding', encoding)) | ||||
req.header(headers) | ||||
|
r5886 | archival.archive(self.repo, req, cnode, artype, prefix=name) | ||
|
r2356 | |||
# add tags to things | ||||
# tags -> list of changesets corresponding to tags | ||||
# find tag, changeset, file | ||||
|
r2436 | def cleanpath(self, path): | ||
|
r3595 | path = path.lstrip('/') | ||
|
r3382 | return util.canonpath(self.repo.root, '', path) | ||
|
r2436 | |||
|
r3226 | def changectx(self, req): | ||
|
r5915 | if 'node' in req.form: | ||
|
r3226 | changeid = req.form['node'][0] | ||
|
r5915 | elif 'manifest' in req.form: | ||
|
r3333 | changeid = req.form['manifest'][0] | ||
|
r3226 | else: | ||
|
r3333 | changeid = self.repo.changelog.count() - 1 | ||
|
r3226 | try: | ||
ctx = self.repo.changectx(changeid) | ||||
except hg.RepoError: | ||||
man = self.repo.manifest | ||||
mn = man.lookup(changeid) | ||||
ctx = self.repo.changectx(man.linkrev(mn)) | ||||
return ctx | ||||
def filectx(self, req): | ||||
path = self.cleanpath(req.form['file'][0]) | ||||
|
r5915 | if 'node' in req.form: | ||
|
r3226 | changeid = req.form['node'][0] | ||
else: | ||||
changeid = req.form['filenode'][0] | ||||
try: | ||||
ctx = self.repo.changectx(changeid) | ||||
fctx = ctx.filectx(path) | ||||
except hg.RepoError: | ||||
fctx = self.repo.filectx(path, fileid=changeid) | ||||
return fctx | ||||
|
r2466 | def check_perm(self, req, op, default): | ||
'''check permission for operation based on user auth. | ||||
return true if op allowed, else false. | ||||
default is policy to use if no config given.''' | ||||
user = req.env.get('REMOTE_USER') | ||||
|
r3555 | deny = self.configlist('web', 'deny_' + op) | ||
|
r2466 | if deny and (not user or deny == ['*'] or user in deny): | ||
return False | ||||
|
r3555 | allow = self.configlist('web', 'allow_' + op) | ||
|
r2466 | return (allow and (allow == ['*'] or user in allow)) or default | ||