##// END OF EJS Templates
http server: support persistent connections....
http server: support persistent connections. only "hg serve" affected yet. http server running cgi script will not use persistent connections. support for fastcgi will help that. clients that support keepalive can use one tcp connection for all commands during clone and pull. this makes latency of binary search during pull much lower over wan. if server does not know content-length, it will force connection to close at end. right fix is to use chunked transfer-encoding but this is easier and does not hurt performance. only command that is affected is "changegroup" which is always last command during a pull.

File last commit:

r2434:a2df85ad default
r2434:a2df85ad default
Show More
hgweb_mod.py
828 lines | 28.4 KiB | text/x-python | PythonLexer
# hgweb/hgweb_mod.py - Web interface for a repository.
#
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
# Copyright 2005 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
import os
import os.path
import mimetypes
from mercurial.demandload import demandload
demandload(globals(), "re zlib ConfigParser cStringIO")
demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
demandload(globals(), "mercurial.hgweb.request:hgrequest")
demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
from mercurial.node import *
from mercurial.i18n import gettext as _
def _up(p):
if p[0] != "/":
p = "/" + p
if p[-1] == "/":
p = p[:-1]
up = os.path.dirname(p)
if up == "/":
return "/"
return up + "/"
class hgweb(object):
def __init__(self, repo, name=None):
if type(repo) == type(""):
self.repo = hg.repository(ui.ui(), repo)
else:
self.repo = repo
self.mtime = -1
self.reponame = name
self.archives = 'zip', 'gz', 'bz2'
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)
self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
def archivelist(self, nodeid):
allowed = (self.repo.ui.config("web", "allow_archive", "")
.replace(",", " ").split())
for i in self.archives:
if i in allowed or self.repo.ui.configbool("web", "allow" + i):
yield {"type" : i, "node" : nodeid, "url": ""}
def listfiles(self, files, mf):
for f in files[:self.maxfiles]:
yield self.t("filenodelink", node=hex(mf[f]), file=f)
if len(files) > self.maxfiles:
yield self.t("fileellipses")
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")
def siblings(self, siblings=[], rev=None, hiderev=None, **args):
if not rev:
rev = lambda x: ""
siblings = [s for s in siblings if s != nullid]
if len(siblings) == 1 and rev(siblings[0]) == hiderev:
return
for s in siblings:
yield dict(node=hex(s), rev=rev(s), **args)
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):
yield self.t(t1, tag=t, **args)
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
cl = r.changelog
mf = r.manifest
change1 = cl.read(node1)
change2 = cl.read(node2)
mmap1 = mf.read(change1[0])
mmap2 = mf.read(change2[0])
date1 = util.datestr(change1[2])
date2 = util.datestr(change2[2])
modified, added, removed, deleted, unknown = r.changes(node1, node2)
if files:
modified, added, removed = map(lambda x: filterfiles(files, x),
(modified, added, removed))
diffopts = self.repo.ui.diffopts()
showfunc = diffopts['showfunc']
ignorews = diffopts['ignorews']
for f in modified:
to = r.file(f).read(mmap1[f])
tn = r.file(f).read(mmap2[f])
yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
showfunc=showfunc, ignorews=ignorews), f, tn)
for f in added:
to = None
tn = r.file(f).read(mmap2[f])
yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
showfunc=showfunc, ignorews=ignorews), f, tn)
for f in removed:
to = r.file(f).read(mmap1[f])
tn = None
yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
showfunc=showfunc, ignorews=ignorews), f, tn)
def changelog(self, pos):
def changenav(**map):
def seq(factor, maxchanges=None):
if maxchanges:
yield maxchanges
if maxchanges >= 20 and maxchanges <= 40:
yield 50
else:
yield 1 * factor
yield 3 * factor
for f in seq(factor * 10):
yield f
l = []
last = 0
for f in seq(1, self.maxchanges):
if f < self.maxchanges or f <= last:
continue
if f > count:
break
last = f
r = "%d" % f
if pos + f < count:
l.append(("+" + r, pos + f))
if pos - f >= 0:
l.insert(0, ("-" + r, pos - f))
yield {"rev": 0, "label": "(0)"}
for label, rev in l:
yield {"label": label, "rev": rev}
yield {"label": "tip", "rev": "tip"}
def changelist(**map):
parity = (start - end) & 1
cl = self.repo.changelog
l = [] # build a list in forward order for efficiency
for i in range(start, end):
n = cl.node(i)
changes = cl.read(n)
hn = hex(n)
l.insert(0, {"parity": parity,
"author": changes[1],
"parent": self.siblings(cl.parents(n), cl.rev,
cl.rev(n) - 1),
"child": self.siblings(cl.children(n), cl.rev,
cl.rev(n) + 1),
"changelogtag": self.showtag("changelogtag",n),
"manifest": hex(changes[0]),
"desc": changes[4],
"date": changes[2],
"files": self.listfilediffs(changes[3], n),
"rev": i,
"node": hn})
parity = 1 - parity
for e in l:
yield e
cl = self.repo.changelog
mf = cl.read(cl.tip())[0]
count = cl.count()
start = max(0, pos - self.maxchanges + 1)
end = min(count, start + self.maxchanges)
pos = end - 1
yield self.t('changelog',
changenav=changenav,
manifest=hex(mf),
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():
for i in range(cl.count() - 1, 0, -100):
l = []
for j in range(max(0, i - 100), i):
n = cl.node(j)
changes = cl.read(n)
l.append((n, j, changes))
l.reverse()
for e in l:
yield e
for n, i, changes in revgen():
miss = 0
for q in qw:
if not (q in changes[1].lower() or
q in changes[4].lower() or
q in " ".join(changes[3][:20]).lower()):
miss = 1
break
if miss:
continue
count += 1
hn = hex(n)
yield self.t('searchentry',
parity=count & 1,
author=changes[1],
parent=self.siblings(cl.parents(n), cl.rev),
child=self.siblings(cl.children(n), cl.rev),
changelogtag=self.showtag("changelogtag",n),
manifest=hex(changes[0]),
desc=changes[4],
date=changes[2],
files=self.listfilediffs(changes[3], n),
rev=i,
node=hn)
if count >= self.maxchanges:
break
cl = self.repo.changelog
mf = cl.read(cl.tip())[0]
yield self.t('search',
query=query,
manifest=hex(mf),
entries=changelist)
def changeset(self, nodeid):
cl = self.repo.changelog
n = self.repo.lookup(nodeid)
nodeid = hex(n)
changes = cl.read(n)
p1 = cl.parents(n)[0]
files = []
mf = self.repo.manifest.read(changes[0])
for f in changes[3]:
files.append(self.t("filenodelink",
filenode=hex(mf.get(f, nullid)), file=f))
def diff(**map):
yield self.diff(p1, n, None)
yield self.t('changeset',
diff=diff,
rev=cl.rev(n),
node=nodeid,
parent=self.siblings(cl.parents(n), cl.rev),
child=self.siblings(cl.children(n), cl.rev),
changesettag=self.showtag("changesettag",n),
manifest=hex(changes[0]),
author=changes[1],
desc=changes[4],
date=changes[2],
files=files,
archives=self.archivelist(nodeid))
def filelog(self, f, filenode):
cl = self.repo.changelog
fl = self.repo.file(f)
filenode = hex(fl.lookup(filenode))
count = fl.count()
def entries(**map):
l = []
parity = (count - 1) & 1
for i in range(count):
n = fl.node(i)
lr = fl.linkrev(n)
cn = cl.node(lr)
cs = cl.read(cl.node(lr))
l.insert(0, {"parity": parity,
"filenode": hex(n),
"filerev": i,
"file": f,
"node": hex(cn),
"author": cs[1],
"date": cs[2],
"rename": self.renamelink(fl, n),
"parent": self.siblings(fl.parents(n),
fl.rev, file=f),
"child": self.siblings(fl.children(n),
fl.rev, file=f),
"desc": cs[4]})
parity = 1 - parity
for e in l:
yield e
yield self.t("filelog", file=f, filenode=filenode, entries=entries)
def filerevision(self, f, node):
fl = self.repo.file(f)
n = fl.lookup(node)
node = hex(n)
text = fl.read(n)
changerev = fl.linkrev(n)
cl = self.repo.changelog
cn = cl.node(changerev)
cs = cl.read(cn)
mfn = cs[0]
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),
"parity": l & 1}
yield self.t("filerevision",
file=f,
filenode=node,
path=_up(f),
text=lines(),
raw=rawtext,
mimetype=mt,
rev=changerev,
node=hex(cn),
manifest=hex(mfn),
author=cs[1],
date=cs[2],
parent=self.siblings(fl.parents(n), fl.rev, file=f),
child=self.siblings(fl.children(n), fl.rev, file=f),
rename=self.renamelink(fl, n),
permissions=self.repo.manifest.readflags(mfn)[f])
def fileannotate(self, f, node):
bcache = {}
ncache = {}
fl = self.repo.file(f)
n = fl.lookup(node)
node = hex(n)
changerev = fl.linkrev(n)
cl = self.repo.changelog
cn = cl.node(changerev)
cs = cl.read(cn)
mfn = cs[0]
def annotate(**map):
parity = 1
last = None
for r, l in fl.annotate(n):
try:
cnode = ncache[r]
except KeyError:
cnode = ncache[r] = self.repo.changelog.node(r)
try:
name = bcache[r]
except KeyError:
cl = self.repo.changelog.read(cnode)
bcache[r] = name = self.repo.ui.shortuser(cl[1])
if last != cnode:
parity = 1 - parity
last = cnode
yield {"parity": parity,
"node": hex(cnode),
"rev": r,
"author": name,
"file": f,
"line": l}
yield self.t("fileannotate",
file=f,
filenode=node,
annotate=annotate,
path=_up(f),
rev=changerev,
node=hex(cn),
manifest=hex(mfn),
author=cs[1],
date=cs[2],
rename=self.renamelink(fl, n),
parent=self.siblings(fl.parents(n), fl.rev, file=f),
child=self.siblings(fl.children(n), fl.rev, file=f),
permissions=self.repo.manifest.readflags(mfn)[f])
def manifest(self, mnode, path):
man = self.repo.manifest
mn = man.lookup(mnode)
mnode = hex(mn)
mf = man.read(mn)
rev = man.rev(mn)
changerev = man.linkrev(mn)
node = self.repo.changelog.node(changerev)
mff = man.readflags(mn)
files = {}
p = path[1:]
if p and p[-1] != "/":
p += "/"
l = len(p)
for f,n in mf.items():
if f[:l] != p:
continue
remain = f[l:]
if "/" in remain:
short = remain[:remain.find("/") + 1] # bleah
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,
"manifest": mnode,
"filenode": hex(fnode),
"parity": parity,
"basename": f,
"permissions": mff[full]}
parity = 1 - parity
def dirlist(**map):
parity = 0
fl = files.keys()
fl.sort()
for f in fl:
full, fnode = files[f]
if fnode:
continue
yield {"parity": parity,
"path": os.path.join(path, f),
"manifest": mnode,
"basename": f[:-1]}
parity = 1 - parity
yield self.t("manifest",
manifest=mnode,
rev=rev,
node=hex(node),
path=path,
up=_up(path),
fentries=filelist,
dentries=dirlist,
archives=self.archivelist(hex(node)))
def tags(self):
cl = self.repo.changelog
mf = cl.read(cl.tip())[0]
i = self.repo.tagslist()
i.reverse()
def entries(notip=False, **map):
parity = 0
for k,n in i:
if notip and k == "tip": continue
yield {"parity": parity,
"tag": k,
"tagmanifest": hex(cl.read(n)[0]),
"date": cl.read(n)[2],
"node": hex(n)}
parity = 1 - parity
yield self.t("tags",
manifest=hex(mf),
entries=lambda **x: entries(False, **x),
entriesnotip=lambda **x: entries(True, **x))
def summary(self):
cl = self.repo.changelog
mf = cl.read(cl.tip())[0]
i = self.repo.tagslist()
i.reverse()
def tagentries(**map):
parity = 0
count = 0
for k,n in i:
if k == "tip": # skip tip
continue;
count += 1
if count > 10: # limit to 10 tags
break;
c = cl.read(n)
m = c[0]
t = c[2]
yield self.t("tagentry",
parity = parity,
tag = k,
node = hex(n),
date = t,
tagmanifest = hex(m))
parity = 1 - parity
def changelist(**map):
parity = 0
cl = self.repo.changelog
l = [] # build a list in forward order for efficiency
for i in range(start, end):
n = cl.node(i)
changes = cl.read(n)
hn = hex(n)
t = changes[2]
l.insert(0, self.t(
'shortlogentry',
parity = parity,
author = changes[1],
manifest = hex(changes[0]),
desc = changes[4],
date = t,
rev = i,
node = hn))
parity = 1 - parity
yield l
cl = self.repo.changelog
mf = cl.read(cl.tip())[0]
count = cl.count()
start = max(0, count - self.maxchanges)
end = min(count, start + self.maxchanges)
yield self.t("summary",
desc = self.repo.ui.config("web", "description", "unknown"),
owner = (self.repo.ui.config("ui", "username") or # preferred
self.repo.ui.config("web", "contact") or # deprecated
self.repo.ui.config("web", "author", "unknown")), # also
lastchange = (0, 0), # FIXME
manifest = hex(mf),
tags = tagentries,
shortlog = changelist)
def filediff(self, file, changeset):
cl = self.repo.changelog
n = self.repo.lookup(changeset)
changeset = hex(n)
p1 = cl.parents(n)[0]
cs = cl.read(n)
mf = self.repo.manifest.read(cs[0])
def diff(**map):
yield self.diff(p1, n, [file])
yield self.t("filediff",
file=file,
filenode=hex(mf.get(file, nullid)),
node=changeset,
rev=self.repo.changelog.rev(n),
parent=self.siblings(cl.parents(n), cl.rev),
child=self.siblings(cl.children(n), cl.rev),
diff=diff)
archive_specs = {
'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
'zip': ('application/zip', 'zip', '.zip', None),
}
def archive(self, req, cnode, type_):
reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
name = "%s-%s" % (reponame, short(cnode))
mimetype, artype, extension, encoding = self.archive_specs[type_]
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
def run(self, req=hgrequest()):
def clean(path):
p = util.normpath(path)
if p[:2] == "..":
raise Exception("suspicious path")
return p
def header(**map):
yield self.t("header", **map)
def footer(**map):
yield self.t("footer",
motd=self.repo.ui.config("web", "motd", ""),
**map)
def expand_form(form):
shortcuts = {
'cl': [('cmd', ['changelog']), ('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)]
}
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]
self.refresh()
expand_form(req.form)
t = self.repo.ui.config("web", "templates", templater.templatepath())
static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
m = os.path.join(t, "map")
style = self.repo.ui.config("web", "style", "")
if req.form.has_key('style'):
style = req.form['style'][0]
if style:
b = os.path.basename("map-" + style)
p = os.path.join(t, b)
if os.path.isfile(p):
m = p
port = req.env["SERVER_PORT"]
port = port != "80" and (":" + port) or ""
uri = req.env["REQUEST_URI"]
if "?" in uri:
uri = uri.split("?")[0]
url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
if not self.reponame:
self.reponame = (self.repo.ui.config("web", "name")
or uri.strip('/') or self.repo.root)
self.t = templater.templater(m, templater.common_filters,
defaults={"url": url,
"repo": self.reponame,
"header": header,
"footer": footer,
})
if not req.form.has_key('cmd'):
req.form['cmd'] = [self.t.cache['default'],]
cmd = req.form['cmd'][0]
if cmd == 'changelog':
hi = self.repo.changelog.count() - 1
if req.form.has_key('rev'):
hi = req.form['rev'][0]
try:
hi = self.repo.changelog.rev(self.repo.lookup(hi))
except hg.RepoError:
req.write(self.search(hi)) # XXX redirect to 404 page?
return
req.write(self.changelog(hi))
elif cmd == 'changeset':
req.write(self.changeset(req.form['node'][0]))
elif cmd == 'manifest':
req.write(self.manifest(req.form['manifest'][0],
clean(req.form['path'][0])))
elif cmd == 'tags':
req.write(self.tags())
elif cmd == 'summary':
req.write(self.summary())
elif cmd == 'filediff':
req.write(self.filediff(clean(req.form['file'][0]),
req.form['node'][0]))
elif cmd == 'file':
req.write(self.filerevision(clean(req.form['file'][0]),
req.form['filenode'][0]))
elif cmd == 'annotate':
req.write(self.fileannotate(clean(req.form['file'][0]),
req.form['filenode'][0]))
elif cmd == 'filelog':
req.write(self.filelog(clean(req.form['file'][0]),
req.form['filenode'][0]))
elif cmd == 'heads':
resp = " ".join(map(hex, self.repo.heads())) + "\n"
req.httphdr("application/mercurial-0.1", length=len(resp))
req.write(resp)
elif cmd == 'branches':
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)
elif cmd == 'between':
nodes = []
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)
elif cmd == 'changegroup':
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())
elif cmd == 'archive':
changeset = self.repo.lookup(req.form['node'][0])
type_ = req.form['type'][0]
allowed = self.repo.ui.config("web", "allow_archive", "").split()
if (type_ in self.archives and (type_ in allowed or
self.repo.ui.configbool("web", "allow" + type_, False))):
self.archive(req, changeset, type_)
return
req.write(self.t("error"))
elif cmd == 'static':
fname = req.form['file'][0]
req.write(staticfile(static, fname)
or self.t("error", error="%r not found" % fname))
else:
req.write(self.t("error"))
req.done()