hgweb_mod.py
379 lines
| 14.2 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. | ||||
|
r6393 | import os, mimetypes | ||
from mercurial.node import hex, nullid | ||||
|
r6217 | from mercurial.repo import RepoError | ||
|
r6393 | from mercurial import mdiff, ui, hg, util, patch, hook | ||
|
r6152 | from mercurial import revlog, templater, templatefilters, changegroup | ||
|
r6393 | from common import get_mtime, style_map, paritygen, countgen, ErrorResponse | ||
|
r5993 | from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR | ||
|
r5566 | from request import wsgirequest | ||
|
r6392 | import webcommands, protocol, webutil | ||
|
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 | |||
class hgweb(object): | ||||
|
r6141 | def __init__(self, repo, name=None): | ||
|
r4874 | if isinstance(repo, str): | ||
|
r6141 | parentui = ui.ui(report_untrusted=False, interactive=False) | ||
|
r5289 | 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 | ||
|
r6152 | self._capabilities = None | ||
|
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) | ||
|
r6152 | self._capabilities = None | ||
def capabilities(self): | ||||
if self._capabilities is not None: | ||||
return self._capabilities | ||||
caps = ['lookup', 'changegroupsubset'] | ||||
if self.configbool('server', 'uncompressed'): | ||||
caps.append('stream=%d' % self.repo.changelog.version) | ||||
if changegroup.bundlepriority: | ||||
caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority)) | ||||
self._capabilities = caps | ||||
return caps | ||||
|
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 | |||
|
r6459 | if 'PATH_INFO' in req.env: | ||
parts = req.env['PATH_INFO'].strip('/').split('/') | ||||
|
r5596 | 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_] | ||||
|
r6149 | # process this if it's a protocol request | ||
cmd = req.form.get('cmd', [''])[0] | ||||
if cmd in protocol.__all__: | ||||
method = getattr(protocol, cmd) | ||||
method(self, req) | ||||
return | ||||
# process the web interface request | ||||
|
r5599 | |||
try: | ||||
|
r6149 | tmpl = self.templater(req) | ||
|
r6391 | ctype = tmpl('mimetype', encoding=self.encoding) | ||
ctype = templater.stringify(ctype) | ||||
|
r6149 | |||
if cmd == '': | ||||
req.form['cmd'] = [tmpl.cache['default']] | ||||
cmd = req.form['cmd'][0] | ||||
|
r5890 | |||
|
r6149 | if cmd not in webcommands.__all__: | ||
|
r6368 | msg = 'no such method: %s' % cmd | ||
|
r6149 | raise ErrorResponse(HTTP_BAD_REQUEST, msg) | ||
elif cmd == 'file' and 'raw' in req.form.get('style', []): | ||||
self.ctype = ctype | ||||
content = webcommands.rawfile(self, req, tmpl) | ||||
else: | ||||
content = getattr(webcommands, cmd)(self, req, tmpl) | ||||
req.respond(HTTP_OK, ctype) | ||||
|
r5890 | |||
|
r6149 | req.write(content) | ||
del tmpl | ||||
|
r5600 | |||
except revlog.LookupError, err: | ||||
|
r5993 | req.respond(HTTP_NOT_FOUND, ctype) | ||
|
r6374 | msg = str(err) | ||
if 'manifest' not in msg: | ||||
|
r6368 | msg = 'revision not found: %s' % err.name | ||
req.write(tmpl('error', error=msg)) | ||||
|
r6217 | except (RepoError, revlog.RevlogError), inst: | ||
|
r5993 | req.respond(HTTP_SERVER_ERROR, ctype) | ||
req.write(tmpl('error', error=str(inst))) | ||||
|
r5600 | except ErrorResponse, inst: | ||
|
r5993 | req.respond(inst.code, ctype) | ||
req.write(tmpl('error', error=inst.message)) | ||||
|
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): | ||||
|
r6391 | yield tmpl('header', encoding=self.encoding, **map) | ||
|
r5596 | |||
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 | ||
|
r5976 | tmpl = templater.templater(mapfile, templatefilters.filters, | ||
|
r5600 | 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 | |||
|
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 | |||
|
r6122 | blockcount = countgen() | ||
|
r2356 | def prettyprintlines(diff): | ||
|
r6122 | blockno = blockcount.next() | ||
for lineno, l in enumerate(diff.splitlines(1)): | ||||
if blockno == 0: | ||||
lineno = lineno + 1 | ||||
else: | ||||
lineno = "%d.%d" % (blockno, lineno + 1) | ||||
|
r2356 | if l.startswith('+'): | ||
|
r6123 | ltype = "difflineplus" | ||
|
r2356 | elif l.startswith('-'): | ||
|
r6123 | ltype = "difflineminus" | ||
|
r2356 | elif l.startswith('@'): | ||
|
r6123 | ltype = "difflineat" | ||
|
r2356 | else: | ||
|
r6123 | ltype = "diffline" | ||
yield tmpl(ltype, | ||||
line=l, | ||||
lineid="l%s" % lineno, | ||||
linenumber="% 8s" % lineno) | ||||
|
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 | |||
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), | ||
} | ||||
|
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 | ||