hgweb_mod.py
1152 lines
| 39.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> | ||||
|
r2859 | # Copyright 2005, 2006 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. | ||||
|
r3974 | import os, mimetypes, re, zlib, mimetools, cStringIO, sys | ||
|
r3877 | import tempfile, urllib, bz2 | ||
|
r2356 | from mercurial.node import * | ||
from mercurial.i18n import gettext as _ | ||||
|
r3877 | from mercurial import mdiff, ui, hg, util, archival, streamclone, patch | ||
from mercurial import revlog, templater | ||||
from common import get_mtime, staticfile, style_map | ||||
|
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): | ||||
if type(repo) == type(""): | ||||
|
r3557 | self.repo = hg.repository(ui.ui(report_untrusted=False), repo) | ||
|
r2356 | else: | ||
self.repo = repo | ||||
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) | ||||
|
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 | |||
def listfilediffs(self, files, changeset): | ||||
for f in files[:self.maxfiles]: | ||||
yield self.t("filedifflink", node=hex(changeset), file=f) | ||||
if len(files) > self.maxfiles: | ||||
yield self.t("fileellipses") | ||||
|
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 [] | ||||
def showtag(self, t1, node=nullid, **args): | ||||
for t in self.repo.nodetags(node): | ||||
|
r3673 | yield self.t(t1, tag=t, **args) | ||
|
r2356 | |||
def diff(self, node1, node2, files): | ||||
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 | ||||
parity = [0] | ||||
def diffblock(diff, f, fn): | ||||
yield self.t("diffblock", | ||||
lines=prettyprintlines(diff), | ||||
parity=parity[0], | ||||
file=f, | ||||
filenode=hex(fn or nullid)) | ||||
parity[0] = 1 - parity[0] | ||||
def prettyprintlines(diff): | ||||
for l in diff.splitlines(1): | ||||
if l.startswith('+'): | ||||
yield self.t("difflineplus", line=l) | ||||
elif l.startswith('-'): | ||||
yield self.t("difflineminus", line=l) | ||||
elif l.startswith('@'): | ||||
yield self.t("difflineat", line=l) | ||||
else: | ||||
yield self.t("diffline", line=l) | ||||
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() | ||||
|
r2356 | yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, | ||
|
r2874 | opts=diffopts), f, tn) | ||
|
r2356 | for f in added: | ||
to = None | ||||
|
r3973 | tn = c2.filectx(f).data() | ||
|
r2356 | yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, | ||
|
r2874 | opts=diffopts), f, tn) | ||
|
r2356 | for f in removed: | ||
|
r3973 | to = c1.filectx(f).data() | ||
|
r2356 | tn = None | ||
yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, | ||||
|
r2874 | opts=diffopts), f, tn) | ||
|
r2356 | |||
|
r3220 | def changelog(self, ctx, shortlog=False): | ||
|
r2356 | def changelist(**map): | ||
parity = (start - end) & 1 | ||||
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 | |||
l.insert(0, {"parity": parity, | ||||
|
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(), | ||||
"files": self.listfilediffs(ctx.files(), n), | ||||
|
r2356 | "rev": i, | ||
|
r3208 | "node": hex(n)}) | ||
|
r2356 | parity = 1 - parity | ||
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 | ||
|
r3422 | changenav = revnavgen(pos, maxchanges, count, self.repo.changectx) | ||
|
r3407 | |||
|
r2684 | yield self.t(shortlog and 'shortlog' or 'changelog', | ||
|
r2356 | changenav=changenav, | ||
|
r3205 | node=hex(cl.tip()), | ||
|
r2356 | rev=pos, changesets=count, entries=changelist, | ||
archives=self.archivelist("tip")) | ||||
def search(self, query): | ||||
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 | ||||
q in " ".join(ctx.files()[:20]).lower()): | ||||
|
r2356 | miss = 1 | ||
break | ||||
if miss: | ||||
continue | ||||
count += 1 | ||||
|
r3208 | n = ctx.node() | ||
|
r2356 | |||
yield self.t('searchentry', | ||||
|
r2666 | parity=self.stripes(count), | ||
|
r3208 | author=ctx.user(), | ||
parent=self.siblings(ctx.parents()), | ||||
child=self.siblings(ctx.children()), | ||||
|
r2356 | changelogtag=self.showtag("changelogtag",n), | ||
|
r3208 | desc=ctx.description(), | ||
date=ctx.date(), | ||||
files=self.listfilediffs(ctx.files(), n), | ||||
rev=ctx.rev(), | ||||
node=hex(n)) | ||||
|
r2356 | |||
if count >= self.maxchanges: | ||||
break | ||||
cl = self.repo.changelog | ||||
yield self.t('search', | ||||
query=query, | ||||
|
r3205 | node=hex(cl.tip()), | ||
|
r2356 | entries=changelist) | ||
|
r3220 | def changeset(self, ctx): | ||
|
r3208 | n = ctx.node() | ||
parents = ctx.parents() | ||||
p1 = parents[0].node() | ||||
|
r2356 | |||
files = [] | ||||
|
r3179 | parity = 0 | ||
|
r3208 | for f in ctx.files(): | ||
|
r2356 | files.append(self.t("filenodelink", | ||
|
r3206 | node=hex(n), file=f, | ||
|
r3179 | parity=parity)) | ||
parity = 1 - parity | ||||
|
r2356 | |||
def diff(**map): | ||||
yield self.diff(p1, n, None) | ||||
yield self.t('changeset', | ||||
diff=diff, | ||||
|
r3208 | rev=ctx.rev(), | ||
node=hex(n), | ||||
parent=self.siblings(parents), | ||||
child=self.siblings(ctx.children()), | ||||
|
r2356 | changesettag=self.showtag("changesettag",n), | ||
|
r3208 | author=ctx.user(), | ||
desc=ctx.description(), | ||||
date=ctx.date(), | ||||
|
r2356 | files=files, | ||
|
r3220 | archives=self.archivelist(hex(n))) | ||
|
r2356 | |||
|
r3206 | def filelog(self, fctx): | ||
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 | ||||
|
r2356 | |||
def entries(**map): | ||||
l = [] | ||||
parity = (count - 1) & 1 | ||||
|
r3473 | for i in xrange(start, end): | ||
|
r3208 | ctx = fctx.filectx(i) | ||
|
r2356 | n = fl.node(i) | ||
l.insert(0, {"parity": parity, | ||||
"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 | parity = 1 - parity | ||
for e in l: | ||||
yield e | ||||
|
r3422 | nodefunc = lambda x: fctx.filectx(fileid=x) | ||
nav = revnavgen(pos, pagelen, count, nodefunc) | ||||
|
r3407 | yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav, | ||
entries=entries) | ||||
|
r2356 | |||
|
r3206 | def filerevision(self, fctx): | ||
f = fctx.path() | ||||
text = fctx.data() | ||||
fl = fctx.filelog() | ||||
n = fctx.filenode() | ||||
|
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), | ||||
|
r2666 | "parity": self.stripes(l)} | ||
|
r2356 | |||
yield self.t("filerevision", | ||||
file=f, | ||||
path=_up(f), | ||||
text=lines(), | ||||
raw=rawtext, | ||||
mimetype=mt, | ||||
|
r3206 | rev=fctx.rev(), | ||
node=hex(fctx.node()), | ||||
author=fctx.user(), | ||||
date=fctx.date(), | ||||
|
r3395 | desc=fctx.description(), | ||
|
r3392 | parent=self.siblings(fctx.parents()), | ||
child=self.siblings(fctx.children()), | ||||
|
r2356 | rename=self.renamelink(fl, n), | ||
|
r3206 | permissions=fctx.manifest().execf(f)) | ||
|
r2356 | |||
|
r3206 | def fileannotate(self, fctx): | ||
f = fctx.path() | ||||
|
r3173 | n = fctx.filenode() | ||
fl = fctx.filelog() | ||||
|
r2356 | |||
def annotate(**map): | ||||
|
r2666 | parity = 0 | ||
|
r2356 | 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: | ||
|
r2356 | parity = 1 - parity | ||
|
r3175 | last = fnode | ||
|
r2356 | |||
yield {"parity": parity, | ||||
|
r3178 | "node": hex(f.node()), | ||
|
r3403 | "rev": f.rev(), | ||
|
r2356 | "author": name, | ||
|
r3173 | "file": f.path(), | ||
|
r2356 | "line": l} | ||
yield self.t("fileannotate", | ||||
file=f, | ||||
annotate=annotate, | ||||
path=_up(f), | ||||
|
r3173 | rev=fctx.rev(), | ||
|
r3177 | node=hex(fctx.node()), | ||
|
r3173 | author=fctx.user(), | ||
date=fctx.date(), | ||||
|
r3391 | desc=fctx.description(), | ||
|
r2356 | rename=self.renamelink(fl, n), | ||
|
r3392 | parent=self.siblings(fctx.parents()), | ||
child=self.siblings(fctx.children()), | ||||
|
r3173 | permissions=fctx.manifest().execf(f)) | ||
|
r2356 | |||
|
r3205 | def manifest(self, ctx, path): | ||
mf = ctx.manifest() | ||||
node = ctx.node() | ||||
|
r2356 | |||
files = {} | ||||
|
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) | ||||
def filelist(**map): | ||||
parity = 0 | ||||
fl = files.keys() | ||||
fl.sort() | ||||
for f in fl: | ||||
full, fnode = files[f] | ||||
if not fnode: | ||||
continue | ||||
yield {"file": full, | ||||
|
r2666 | "parity": self.stripes(parity), | ||
|
r2356 | "basename": f, | ||
|
r3305 | "size": ctx.filectx(full).size(), | ||
|
r2857 | "permissions": mf.execf(full)} | ||
|
r2666 | parity += 1 | ||
|
r2356 | |||
def dirlist(**map): | ||||
parity = 0 | ||||
fl = files.keys() | ||||
fl.sort() | ||||
for f in fl: | ||||
full, fnode = files[f] | ||||
if fnode: | ||||
continue | ||||
|
r2666 | yield {"parity": self.stripes(parity), | ||
|
r3595 | "path": os.path.join(abspath, f), | ||
|
r2356 | "basename": f[:-1]} | ||
|
r2666 | parity += 1 | ||
|
r2356 | |||
yield self.t("manifest", | ||||
|
r3205 | rev=ctx.rev(), | ||
|
r2356 | node=hex(node), | ||
|
r3595 | path=abspath, | ||
up=_up(abspath), | ||||
|
r2356 | fentries=filelist, | ||
dentries=dirlist, | ||||
archives=self.archivelist(hex(node))) | ||||
def tags(self): | ||||
i = self.repo.tagslist() | ||||
i.reverse() | ||||
def entries(notip=False, **map): | ||||
parity = 0 | ||||
|
r3673 | for k, n in i: | ||
if notip and k == "tip": | ||||
continue | ||||
|
r2666 | yield {"parity": self.stripes(parity), | ||
|
r2356 | "tag": k, | ||
|
r3973 | "date": self.repo.changectx(n).date(), | ||
|
r2356 | "node": hex(n)} | ||
|
r2666 | parity += 1 | ||
|
r2356 | |||
yield self.t("tags", | ||||
|
r3205 | node=hex(self.repo.changelog.tip()), | ||
|
r2356 | entries=lambda **x: entries(False, **x), | ||
entriesnotip=lambda **x: entries(True, **x)) | ||||
def summary(self): | ||||
i = self.repo.tagslist() | ||||
i.reverse() | ||||
def tagentries(**map): | ||||
parity = 0 | ||||
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; | ||||
yield self.t("tagentry", | ||||
|
r3973 | parity=self.stripes(parity), | ||
tag=k, | ||||
node=hex(n), | ||||
date=self.repo.changectx(n).date()) | ||||
|
r2666 | parity += 1 | ||
|
r2356 | |||
|
r3499 | def heads(**map): | ||
parity = 0 | ||||
count = 0 | ||||
for node in self.repo.heads(): | ||||
count += 1 | ||||
if count > 10: | ||||
break; | ||||
ctx = self.repo.changectx(node) | ||||
yield {'parity': self.stripes(parity), | ||||
'branch': ctx.branch(), | ||||
'node': hex(node), | ||||
'date': ctx.date()} | ||||
parity += 1 | ||||
|
r2356 | def changelist(**map): | ||
parity = 0 | ||||
l = [] # build a list in forward order for efficiency | ||||
|
r3473 | for i in xrange(start, end): | ||
|
r3973 | ctx = self.repo.changectx(i) | ||
hn = hex(ctx.node()) | ||||
|
r2356 | |||
l.insert(0, self.t( | ||||
'shortlogentry', | ||||
|
r3973 | parity=parity, | ||
author=ctx.user(), | ||||
desc=ctx.description(), | ||||
date=ctx.date(), | ||||
rev=i, | ||||
node=hn)) | ||||
|
r2356 | parity = 1 - parity | ||
yield l | ||||
|
r3973 | cl = self.repo.changelog | ||
|
r2356 | count = cl.count() | ||
start = max(0, count - self.maxchanges) | ||||
end = min(count, start + self.maxchanges) | ||||
yield self.t("summary", | ||||
|
r3973 | desc=self.config("web", "description", "unknown"), | ||
owner=(self.config("ui", "username") or # preferred | ||||
self.config("web", "contact") or # deprecated | ||||
self.config("web", "author", "unknown")), # also | ||||
lastchange=cl.read(cl.tip())[2], | ||||
tags=tagentries, | ||||
heads=heads, | ||||
shortlog=changelist, | ||||
node=hex(cl.tip()), | ||||
|
r2683 | archives=self.archivelist("tip")) | ||
|
r2356 | |||
|
r3220 | def filediff(self, fctx): | ||
n = fctx.node() | ||||
path = fctx.path() | ||||
|
r3406 | parents = fctx.parents() | ||
p1 = parents and parents[0].node() or nullid | ||||
|
r2356 | |||
def diff(**map): | ||||
|
r3220 | yield self.diff(p1, n, [path]) | ||
|
r2356 | |||
yield self.t("filediff", | ||||
|
r3220 | file=path, | ||
|
r3208 | node=hex(n), | ||
|
r3220 | rev=fctx.rev(), | ||
|
r3208 | parent=self.siblings(parents), | ||
|
r3220 | child=self.siblings(fctx.children()), | ||
|
r2356 | diff=diff) | ||
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), | ||
} | ||||
|
r2394 | def archive(self, req, cnode, type_): | ||
|
r2356 | reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame)) | ||
name = "%s-%s" % (reponame, short(cnode)) | ||||
|
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) | ||||
archival.archive(self.repo, req.out, cnode, artype, prefix=name) | ||||
# 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 | |||
|
r2535 | def run(self): | ||
|
r2538 | if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): | ||
|
r2535 | raise RuntimeError("This function is only intended to be called while running as a CGI script.") | ||
import mercurial.hgweb.wsgicgi as wsgicgi | ||||
from request import wsgiapplication | ||||
def make_web_app(): | ||||
|
r2538 | return self | ||
|
r2535 | wsgicgi.launch(wsgiapplication(make_web_app)) | ||
def run_wsgi(self, req): | ||||
|
r2356 | def header(**map): | ||
|
r3781 | header_file = cStringIO.StringIO( | ||
|
r3882 | ''.join(self.t("header", encoding=util._encoding, **map))) | ||
|
r2514 | msg = mimetools.Message(header_file, 0) | ||
req.header(msg.items()) | ||||
yield header_file.read() | ||||
|
r2356 | |||
|
r2534 | def rawfileheader(**map): | ||
req.header([('Content-type', map['mimetype']), | ||||
('Content-disposition', 'filename=%s' % map['file']), | ||||
('Content-length', str(len(map['raw'])))]) | ||||
yield '' | ||||
|
r2356 | def footer(**map): | ||
|
r3488 | yield self.t("footer", **map) | ||
def motd(**map): | ||||
|
r3555 | yield self.config("web", "motd", "") | ||
|
r2356 | |||
def expand_form(form): | ||||
shortcuts = { | ||||
'cl': [('cmd', ['changelog']), ('rev', None)], | ||||
|
r2684 | 'sl': [('cmd', ['shortlog']), ('rev', None)], | ||
|
r2356 | '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)] | ||||
} | ||||
for k in shortcuts.iterkeys(): | ||||
if form.has_key(k): | ||||
for name, value in shortcuts[k]: | ||||
if value is None: | ||||
value = form[k] | ||||
form[name] = value | ||||
del form[k] | ||||
|
r3260 | def rewrite_request(req): | ||
'''translate new web interface to traditional format''' | ||||
|
r3263 | def spliturl(req): | ||
def firstitem(query): | ||||
return query.split('&', 1)[0].split(';', 1)[0] | ||||
|
r3327 | def normurl(url): | ||
inner = '/'.join([x for x in url.split('/') if x]) | ||||
tl = len(url) > 1 and url.endswith('/') and '/' or '' | ||||
|
r3263 | |||
|
r3327 | return '%s%s%s' % (url.startswith('/') and '/' or '', | ||
inner, tl) | ||||
|
r3606 | root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0])) | ||
|
r3327 | pi = normurl(req.env.get('PATH_INFO', '')) | ||
|
r3263 | if pi: | ||
|
r3327 | # strip leading / | ||
pi = pi[1:] | ||||
if pi: | ||||
root = root[:-len(pi)] | ||||
if req.env.has_key('REPO_NAME'): | ||||
rn = req.env['REPO_NAME'] + '/' | ||||
root += rn | ||||
query = pi[len(rn):] | ||||
|
r3263 | else: | ||
|
r3327 | query = pi | ||
|
r3263 | else: | ||
|
r3327 | root += '?' | ||
|
r3263 | query = firstitem(req.env['QUERY_STRING']) | ||
|
r3327 | return (root, query) | ||
|
r3263 | |||
req.url, query = spliturl(req) | ||||
|
r3260 | if req.form.has_key('cmd'): | ||
# old style | ||||
return | ||||
args = query.split('/', 2) | ||||
if not args or not args[0]: | ||||
return | ||||
cmd = args.pop(0) | ||||
|
r3261 | style = cmd.rfind('-') | ||
if style != -1: | ||||
req.form['style'] = [cmd[:style]] | ||||
cmd = cmd[style+1:] | ||||
|
r3263 | # avoid accepting e.g. style parameter as command | ||
if hasattr(self, 'do_' + cmd): | ||||
req.form['cmd'] = [cmd] | ||||
|
r3260 | |||
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_] | ||||
|
r3362 | def sessionvars(**map): | ||
fields = [] | ||||
if req.form.has_key('style'): | ||||
style = req.form['style'][0] | ||||
|
r3555 | if style != self.config('web', 'style', ''): | ||
|
r3362 | fields.append(('style', style)) | ||
|
r3363 | separator = req.url[-1] == '?' and ';' or '?' | ||
|
r3362 | for name, value in fields: | ||
|
r3363 | yield dict(name=name, value=value, separator=separator) | ||
separator = ';' | ||||
|
r3270 | |||
|
r2356 | self.refresh() | ||
expand_form(req.form) | ||||
|
r3260 | rewrite_request(req) | ||
|
r2356 | |||
|
r3555 | style = self.config("web", "style", "") | ||
|
r2356 | if req.form.has_key('style'): | ||
style = req.form['style'][0] | ||||
|
r3276 | mapfile = style_map(self.templatepath, style) | ||
|
r2356 | |||
|
r3423 | port = req.env["SERVER_PORT"] | ||
port = port != "80" and (":" + port) or "" | ||||
urlbase = 'http://%s%s' % (req.env['SERVER_NAME'], port) | ||||
|
r4084 | staticurl = self.config("web", "staticurl") or req.url + 'static/' | ||
if not staticurl.endswith('/'): | ||||
staticurl += '/' | ||||
|
r3263 | |||
|
r2356 | if not self.reponame: | ||
|
r3555 | self.reponame = (self.config("web", "name") | ||
|
r3263 | or req.env.get('REPO_NAME') | ||
or req.url.strip('/') or self.repo.root) | ||||
|
r2356 | |||
|
r3276 | self.t = templater.templater(mapfile, templater.common_filters, | ||
|
r3263 | defaults={"url": req.url, | ||
|
r4084 | "staticurl": staticurl, | ||
|
r3423 | "urlbase": urlbase, | ||
|
r2356 | "repo": self.reponame, | ||
"header": header, | ||||
"footer": footer, | ||||
|
r3488 | "motd": motd, | ||
|
r2534 | "rawfileheader": rawfileheader, | ||
|
r3363 | "sessionvars": sessionvars | ||
|
r2356 | }) | ||
if not req.form.has_key('cmd'): | ||||
|
r3673 | req.form['cmd'] = [self.t.cache['default']] | ||
|
r2356 | |||
cmd = req.form['cmd'][0] | ||||
|
r2436 | method = getattr(self, 'do_' + cmd, None) | ||
if method: | ||||
|
r3359 | try: | ||
method(req) | ||||
except (hg.RepoError, revlog.RevlogError), inst: | ||||
req.write(self.t("error", error=str(inst))) | ||||
|
r2356 | else: | ||
|
r3358 | req.write(self.t("error", error='No such method: ' + cmd)) | ||
|
r2436 | |||
|
r3226 | def changectx(self, req): | ||
if req.form.has_key('node'): | ||||
changeid = req.form['node'][0] | ||||
|
r3333 | elif req.form.has_key('manifest'): | ||
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]) | ||||
if req.form.has_key('node'): | ||||
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 | ||||
|
r2666 | def stripes(self, parity): | ||
"make horizontal stripes for easier reading" | ||||
if self.stripecount: | ||||
return (1 + parity / self.stripecount) & 1 | ||||
else: | ||||
return 0 | ||||
|
r3260 | def do_log(self, req): | ||
if req.form.has_key('file') and req.form['file'][0]: | ||||
self.do_filelog(req) | ||||
else: | ||||
self.do_changelog(req) | ||||
def do_rev(self, req): | ||||
self.do_changeset(req) | ||||
def do_file(self, req): | ||||
|
r3595 | path = self.cleanpath(req.form.get('file', [''])[0]) | ||
|
r3260 | if path: | ||
try: | ||||
req.write(self.filerevision(self.filectx(req))) | ||||
return | ||||
|
r3936 | except revlog.LookupError: | ||
|
r3260 | pass | ||
|
r3595 | req.write(self.manifest(self.changectx(req), path)) | ||
|
r3260 | |||
def do_diff(self, req): | ||||
self.do_filediff(req) | ||||
|
r3226 | def do_changelog(self, req, shortlog = False): | ||
if req.form.has_key('node'): | ||||
ctx = self.changectx(req) | ||||
|
r3220 | else: | ||
|
r3226 | if req.form.has_key('rev'): | ||
hi = req.form['rev'][0] | ||||
else: | ||||
hi = self.repo.changelog.count() - 1 | ||||
try: | ||||
ctx = self.repo.changectx(hi) | ||||
except hg.RepoError: | ||||
req.write(self.search(hi)) # XXX redirect to 404 page? | ||||
return | ||||
|
r2436 | |||
|
r3226 | req.write(self.changelog(ctx, shortlog = shortlog)) | ||
|
r2436 | |||
|
r2684 | def do_shortlog(self, req): | ||
|
r3226 | self.do_changelog(req, shortlog = True) | ||
|
r2684 | |||
|
r2436 | def do_changeset(self, req): | ||
|
r3333 | req.write(self.changeset(self.changectx(req))) | ||
|
r2436 | |||
def do_manifest(self, req): | ||||
|
r3226 | req.write(self.manifest(self.changectx(req), | ||
|
r2436 | self.cleanpath(req.form['path'][0]))) | ||
def do_tags(self, req): | ||||
req.write(self.tags()) | ||||
def do_summary(self, req): | ||||
req.write(self.summary()) | ||||
def do_filediff(self, req): | ||||
|
r3226 | req.write(self.filediff(self.filectx(req))) | ||
|
r2436 | |||
def do_annotate(self, req): | ||||
|
r3226 | req.write(self.fileannotate(self.filectx(req))) | ||
|
r2436 | |||
def do_filelog(self, req): | ||||
|
r3226 | req.write(self.filelog(self.filectx(req))) | ||
|
r2436 | |||
|
r3444 | def do_lookup(self, req): | ||
|
r3445 | try: | ||
r = hex(self.repo.lookup(req.form['key'][0])) | ||||
success = 1 | ||||
except Exception,inst: | ||||
r = str(inst) | ||||
success = 0 | ||||
resp = "%s %s\n" % (success, r) | ||||
|
r3444 | req.httphdr("application/mercurial-0.1", length=len(resp)) | ||
req.write(resp) | ||||
|
r2436 | def do_heads(self, req): | ||
resp = " ".join(map(hex, self.repo.heads())) + "\n" | ||||
req.httphdr("application/mercurial-0.1", length=len(resp)) | ||||
req.write(resp) | ||||
def do_branches(self, req): | ||||
nodes = [] | ||||
if req.form.has_key('nodes'): | ||||
nodes = map(bin, req.form['nodes'][0].split(" ")) | ||||
resp = cStringIO.StringIO() | ||||
for b in self.repo.branches(nodes): | ||||
resp.write(" ".join(map(hex, b)) + "\n") | ||||
resp = resp.getvalue() | ||||
req.httphdr("application/mercurial-0.1", length=len(resp)) | ||||
req.write(resp) | ||||
def do_between(self, req): | ||||
if req.form.has_key('pairs'): | ||||
pairs = [map(bin, p.split("-")) | ||||
for p in req.form['pairs'][0].split(" ")] | ||||
resp = cStringIO.StringIO() | ||||
for b in self.repo.between(pairs): | ||||
resp.write(" ".join(map(hex, b)) + "\n") | ||||
resp = resp.getvalue() | ||||
req.httphdr("application/mercurial-0.1", length=len(resp)) | ||||
req.write(resp) | ||||
def do_changegroup(self, req): | ||||
req.httphdr("application/mercurial-0.1") | ||||
nodes = [] | ||||
if not self.allowpull: | ||||
return | ||||
if req.form.has_key('roots'): | ||||
nodes = map(bin, req.form['roots'][0].split(" ")) | ||||
z = zlib.compressobj() | ||||
f = self.repo.changegroup(nodes, 'serve') | ||||
while 1: | ||||
chunk = f.read(4096) | ||||
if not chunk: | ||||
break | ||||
req.write(z.compress(chunk)) | ||||
req.write(z.flush()) | ||||
|
r3444 | def do_changegroupsubset(self, req): | ||
req.httphdr("application/mercurial-0.1") | ||||
bases = [] | ||||
heads = [] | ||||
if not self.allowpull: | ||||
return | ||||
if req.form.has_key('bases'): | ||||
bases = [bin(x) for x in req.form['bases'][0].split(' ')] | ||||
if req.form.has_key('heads'): | ||||
heads = [bin(x) for x in req.form['heads'][0].split(' ')] | ||||
z = zlib.compressobj() | ||||
f = self.repo.changegroupsubset(bases, heads, 'serve') | ||||
while 1: | ||||
chunk = f.read(4096) | ||||
if not chunk: | ||||
break | ||||
req.write(z.compress(chunk)) | ||||
req.write(z.flush()) | ||||
|
r2436 | def do_archive(self, req): | ||
changeset = self.repo.lookup(req.form['node'][0]) | ||||
type_ = req.form['type'][0] | ||||
|
r3555 | allowed = self.configlist("web", "allow_archive") | ||
|
r2436 | if (type_ in self.archives and (type_ in allowed or | ||
|
r3555 | self.configbool("web", "allow" + type_, False))): | ||
|
r2436 | self.archive(req, changeset, type_) | ||
return | ||||
req.write(self.t("error")) | ||||
def do_static(self, req): | ||||
fname = req.form['file'][0] | ||||
|
r3555 | # a repo owner may set web.static in .hg/hgrc to get any file | ||
# readable by the user running the CGI script | ||||
static = self.config("web", "static", | ||||
os.path.join(self.templatepath, "static"), | ||||
untrusted=False) | ||||
|
r2514 | req.write(staticfile(static, fname, req) | ||
|
r2436 | or self.t("error", error="%r not found" % fname)) | ||
|
r2442 | |||
def do_capabilities(self, req): | ||||
|
r3612 | caps = ['lookup', 'changegroupsubset'] | ||
|
r3555 | if self.configbool('server', 'uncompressed'): | ||
|
r2621 | caps.append('stream=%d' % self.repo.revlogversion) | ||
|
r3612 | # XXX: make configurable and/or share code with do_unbundle: | ||
unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN'] | ||||
if unbundleversions: | ||||
caps.append('unbundle=%s' % ','.join(unbundleversions)) | ||||
|
r2621 | resp = ' '.join(caps) | ||
|
r2442 | req.httphdr("application/mercurial-0.1", length=len(resp)) | ||
req.write(resp) | ||||
|
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 | ||
|
r2464 | def do_unbundle(self, req): | ||
|
r2466 | def bail(response, headers={}): | ||
length = int(req.env['CONTENT_LENGTH']) | ||||
for s in util.filechunkiter(req, limit=length): | ||||
# drain incoming bundle, else client will not see | ||||
# response when run outside cgi script | ||||
pass | ||||
req.httphdr("application/mercurial-0.1", headers=headers) | ||||
req.write('0\n') | ||||
req.write(response) | ||||
# require ssl by default, auth info cannot be sniffed and | ||||
# replayed | ||||
|
r3555 | ssl_req = self.configbool('web', 'push_ssl', True) | ||
|
r2673 | if ssl_req: | ||
if not req.env.get('HTTPS'): | ||||
bail(_('ssl required\n')) | ||||
return | ||||
proto = 'https' | ||||
else: | ||||
proto = 'http' | ||||
|
r2466 | |||
# do not allow push unless explicitly allowed | ||||
if not self.check_perm(req, 'push', False): | ||||
bail(_('push not authorized\n'), | ||||
headers={'status': '401 Unauthorized'}) | ||||
return | ||||
req.httphdr("application/mercurial-0.1") | ||||
|
r2464 | their_heads = req.form['heads'][0].split(' ') | ||
def check_heads(): | ||||
heads = map(hex, self.repo.heads()) | ||||
return their_heads == [hex('force')] or their_heads == heads | ||||
# fail early if possible | ||||
if not check_heads(): | ||||
|
r2466 | bail(_('unsynced changes\n')) | ||
|
r2464 | return | ||
# do not lock repo until all changegroup data is | ||||
# streamed. save to temporary file. | ||||
fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') | ||||
fp = os.fdopen(fd, 'wb+') | ||||
try: | ||||
length = int(req.env['CONTENT_LENGTH']) | ||||
for s in util.filechunkiter(req, limit=length): | ||||
fp.write(s) | ||||
lock = self.repo.lock() | ||||
try: | ||||
if not check_heads(): | ||||
req.write('0\n') | ||||
req.write(_('unsynced changes\n')) | ||||
return | ||||
fp.seek(0) | ||||
|
r3610 | header = fp.read(6) | ||
if not header.startswith("HG"): | ||||
# old client with uncompressed bundle | ||||
def generator(f): | ||||
yield header | ||||
for chunk in f: | ||||
yield chunk | ||||
elif not header.startswith("HG10"): | ||||
req.write("0\n") | ||||
req.write(_("unknown bundle version\n")) | ||||
return | ||||
elif header == "HG10GZ": | ||||
def generator(f): | ||||
zd = zlib.decompressobj() | ||||
for chunk in f: | ||||
yield zd.decompress(chunk) | ||||
elif header == "HG10BZ": | ||||
def generator(f): | ||||
zd = bz2.BZ2Decompressor() | ||||
zd.decompress("BZ") | ||||
for chunk in f: | ||||
yield zd.decompress(chunk) | ||||
elif header == "HG10UN": | ||||
def generator(f): | ||||
for chunk in f: | ||||
yield chunk | ||||
else: | ||||
req.write("0\n") | ||||
req.write(_("unknown bundle compression type\n")) | ||||
return | ||||
gen = generator(util.filechunkiter(fp, 4096)) | ||||
|
r2464 | |||
# send addchangegroup output to client | ||||
old_stdout = sys.stdout | ||||
sys.stdout = cStringIO.StringIO() | ||||
try: | ||||
|
r2673 | url = 'remote:%s:%s' % (proto, | ||
req.env.get('REMOTE_HOST', '')) | ||||
|
r3610 | ret = self.repo.addchangegroup(util.chunkbuffer(gen), | ||
'serve', url) | ||||
|
r2464 | finally: | ||
|
r2558 | val = sys.stdout.getvalue() | ||
|
r2464 | sys.stdout = old_stdout | ||
|
r2558 | req.write('%d\n' % ret) | ||
req.write(val) | ||||
|
r2464 | finally: | ||
lock.release() | ||||
finally: | ||||
fp.close() | ||||
os.unlink(tempname) | ||||
|
r2612 | |||
def do_stream_out(self, req): | ||||
req.httphdr("application/mercurial-0.1") | ||||
streamclone.stream_out(self.repo, req) | ||||