sapling/mercurial/hgweb.py
mpm@selenic.com e4397937b5 Prettify the web interface
Add header, footer templates
Add null parent handling
Combine files and directories
Add parity flag for alternating line colors
Add line numbers to filerevision
2005-05-23 20:57:48 -08:00

576 lines
19 KiB
Python

#!/usr/bin/env python
#
# hgweb.py - 0.2 - 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
# - web interface to a mercurial repository
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
# useful for debugging
import cgitb
cgitb.enable()
import os, cgi, time, re, difflib, sys, zlib
from mercurial.hg import *
def age(t):
def plural(t, c):
if c == 1: return t
return t + "s"
def fmt(t, c):
return "%d %s" % (c, plural(t, c))
now = time.time()
delta = max(1, int(now - t))
scales = [["second", 1],
["minute", 60],
["hour", 3600],
["day", 3600 * 24],
["week", 3600 * 24 * 7],
["month", 3600 * 24 * 30],
["year", 3600 * 24 * 365]]
scales.reverse()
for t, s in scales:
n = delta / s
if n >= 1: return fmt(t, n)
def nl2br(text):
return text.replace('\n', '<br/>')
def obfuscate(text):
return ''.join([ '&#%d' % ord(c) for c in text ])
def up(p):
if p[0] != "/": p = "/" + p
if p[-1] == "/": p = p[:-1]
up = os.path.dirname(p)
if up == "/":
return "/"
return up + "/"
def httphdr(type):
print 'Content-type: %s\n' % type
def write(*things):
for thing in things:
if hasattr(thing, "__iter__"):
for part in thing:
write(part)
else:
sys.stdout.write(str(thing))
def template(tmpl, **map):
while tmpl:
m = re.search(r"#([a-zA-Z0-9]+)#", tmpl)
if m:
yield tmpl[:m.start(0)]
v = map.get(m.group(1), "")
yield callable(v) and v() or v
tmpl = tmpl[m.end(0):]
else:
yield tmpl
return
class templater:
def __init__(self, mapfile):
self.cache = {}
self.map = {}
self.base = os.path.dirname(mapfile)
for l in file(mapfile):
m = re.match(r'(\S+)\s*=\s*"(.*)"$', l)
if m:
self.cache[m.group(1)] = m.group(2)
else:
m = re.match(r'(\S+)\s*=\s*(\S+)', l)
if m:
self.map[m.group(1)] = os.path.join(self.base, m.group(2))
else:
raise "unknown map entry '%s'" % l
def __call__(self, t, **map):
try:
tmpl = self.cache[t]
except KeyError:
tmpl = self.cache[t] = file(self.map[t]).read()
return template(tmpl, **map)
class hgweb:
maxchanges = 20
maxfiles = 10
def __init__(self, path, name, templatemap):
self.reponame = name
self.repo = repository(ui(), path)
self.t = templater(templatemap)
def date(self, cs):
return time.asctime(time.gmtime(float(cs[2].split(' ')[0])))
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 parent(self, t1, node, rev):
if node != hex(nullid):
yield self.t(t1, node = node, rev = rev)
def diff(self, node1, node2, files):
def filterfiles(list, files):
l = [ x for x in list if x in files ]
for f in files:
if f[-1] != os.sep: f += os.sep
l += [ x for x in list if x.startswith(f) ]
return l
def prettyprint(diff):
for l in diff.splitlines(1):
line = cgi.escape(l)
if line.startswith('+'):
yield self.t("difflineplus", line = line)
elif line.startswith('-'):
yield self.t("difflineminus", line = line)
elif line.startswith('@'):
yield self.t("difflineat", line = line)
else:
yield self.t("diffline", line = line)
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 = self.date(change1)
date2 = self.date(change2)
c, a, d = r.diffrevs(node1, node2)
c, a, d = map(lambda x: filterfiles(x, files), (c, a, d))
for f in c:
to = r.file(f).read(mmap1[f])
tn = r.file(f).read(mmap2[f])
yield prettyprint(mdiff.unidiff(to, date1, tn, date2, f))
for f in a:
to = ""
tn = r.file(f).read(mmap2[f])
yield prettyprint(mdiff.unidiff(to, date1, tn, date2, f))
for f in d:
to = r.file(f).read(mmap1[f])
tn = ""
yield prettyprint(mdiff.unidiff(to, date1, tn, date2, f))
def header(self):
yield self.t("header", repo = self.reponame)
def footer(self):
yield self.t("footer", repo = self.reponame)
def changelog(self, pos=None):
def changenav():
def seq(factor = 1):
yield 1 * factor
yield 2 * factor
yield 5 * factor
for f in seq(factor * 10):
yield f
linear = range(0, count - 2, self.maxchanges)[0:8]
for i in linear:
yield self.t("naventry", rev = max(i, 1))
for s in seq():
if s > count - 2: break
if s > linear[-1]:
yield self.t("naventry", rev = s)
yield self.t("naventry", rev = count - 1)
def changelist():
parity = (start - end) & 1
cl = self.repo.changelog
l = [] # build a list in forward order for efficiency
for i in range(start, end + 1):
n = cl.node(i)
changes = cl.read(n)
hn = hex(n)
p1, p2 = cl.parents(n)
t = float(changes[2].split(' ')[0])
l.insert(0, self.t(
'changelogentry',
parity = parity,
author = obfuscate(changes[1]),
shortdesc = cgi.escape(changes[4].splitlines()[0]),
age = age(t),
parent1 = self.parent("changelogparent",
hex(p1), cl.rev(p1)),
parent2 = self.parent("changelogparent",
hex(p2), cl.rev(p2)),
p1 = hex(p1), p2 = hex(p2),
p1rev = cl.rev(p1), p2rev = cl.rev(p2),
manifest = hex(changes[0]),
desc = nl2br(cgi.escape(changes[4])),
date = time.asctime(time.gmtime(t)),
files = self.listfilediffs(changes[3], n),
rev = i,
node = hn))
parity = 1 - parity
yield l
count = self.repo.changelog.count()
pos = pos or count - 1
end = min(pos, count - 1)
start = max(0, pos - self.maxchanges)
end = min(count - 1, start + self.maxchanges)
yield self.t('changelog',
header = self.header(),
footer = self.footer(),
repo = self.reponame,
changenav = changenav,
rev = pos, changesets = count, entries = changelist)
def changeset(self, nodeid):
n = bin(nodeid)
cl = self.repo.changelog
changes = cl.read(n)
p1, p2 = cl.parents(n)
p1rev, p2rev = cl.rev(p1), cl.rev(p2)
t = float(changes[2].split(' ')[0])
files = []
mf = self.repo.manifest.read(changes[0])
for f in changes[3]:
files.append(self.t("filenodelink",
filenode = hex(mf[f]), file = f))
def diff():
yield self.diff(p1, n, changes[3])
yield self.t('changeset',
header = self.header(),
footer = self.footer(),
repo = self.reponame,
diff = diff,
rev = cl.rev(n),
node = nodeid,
shortdesc = cgi.escape(changes[4].splitlines()[0]),
parent1 = self.parent("changesetparent",
hex(p1), cl.rev(p1)),
parent2 = self.parent("changesetparent",
hex(p2), cl.rev(p2)),
p1 = hex(p1), p2 = hex(p2),
p1rev = cl.rev(p1), p2rev = cl.rev(p2),
manifest = hex(changes[0]),
author = obfuscate(changes[1]),
desc = nl2br(cgi.escape(changes[4])),
date = time.asctime(time.gmtime(t)),
files = files)
def filelog(self, f, filenode):
cl = self.repo.changelog
fl = self.repo.file(f)
count = fl.count()
def entries():
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))
p1, p2 = fl.parents(n)
t = float(cs[2].split(' ')[0])
l.insert(0, self.t("filelogentry",
parity = parity,
filenode = hex(n),
filerev = i,
file = f,
node = hex(cn),
author = obfuscate(cs[1]),
age = age(t),
date = time.asctime(time.gmtime(t)),
shortdesc = cgi.escape(cs[4].splitlines()[0]),
p1 = hex(p1), p2 = hex(p2),
p1rev = fl.rev(p1), p2rev = fl.rev(p2)))
parity = 1 - parity
yield l
yield self.t("filelog",
header = self.header(),
footer = self.footer(),
repo = self.reponame,
file = f,
filenode = filenode,
entries = entries)
def filerevision(self, f, node):
fl = self.repo.file(f)
n = bin(node)
text = cgi.escape(fl.read(n))
changerev = fl.linkrev(n)
cl = self.repo.changelog
cn = cl.node(changerev)
cs = cl.read(cn)
p1, p2 = fl.parents(n)
t = float(cs[2].split(' ')[0])
mfn = cs[0]
def lines():
for l, t in enumerate(text.splitlines(1)):
yield self.t("fileline",
line = t,
linenumber = "% 6d" % (l + 1),
parity = l & 1)
yield self.t("filerevision", file = f,
header = self.header(),
footer = self.footer(),
repo = self.reponame,
filenode = node,
path = up(f),
text = lines(),
rev = changerev,
node = hex(cn),
manifest = hex(mfn),
author = obfuscate(cs[1]),
age = age(t),
date = time.asctime(time.gmtime(t)),
shortdesc = cgi.escape(cs[4].splitlines()[0]),
parent1 = self.parent("filerevparent",
hex(p1), fl.rev(p1)),
parent2 = self.parent("filerevparent",
hex(p2), fl.rev(p2)),
p1 = hex(p1), p2 = hex(p2),
p1rev = fl.rev(p1), p2rev = fl.rev(p2))
def fileannotate(self, f, node):
bcache = {}
ncache = {}
fl = self.repo.file(f)
n = bin(node)
changerev = fl.linkrev(n)
cl = self.repo.changelog
cn = cl.node(changerev)
cs = cl.read(cn)
p1, p2 = fl.parents(n)
t = float(cs[2].split(' ')[0])
mfn = cs[0]
def annotate():
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)
name = cl[1]
f = name.find('@')
if f >= 0:
name = name[:f]
bcache[r] = name
if last != cnode:
parity = 1 - parity
last = cnode
yield self.t("annotateline",
parity = parity,
node = hex(cnode),
rev = r,
author = name,
file = f,
line = cgi.escape(l))
yield self.t("fileannotate",
header = self.header(),
footer = self.footer(),
repo = self.reponame,
file = f,
filenode = node,
annotate = annotate,
path = up(f),
rev = changerev,
node = hex(cn),
manifest = hex(mfn),
author = obfuscate(cs[1]),
age = age(t),
date = time.asctime(time.gmtime(t)),
shortdesc = cgi.escape(cs[4].splitlines()[0]),
parent1 = self.parent("filerevparent",
hex(p1), fl.rev(p1)),
parent2 = self.parent("filerevparent",
hex(p2), fl.rev(p2)),
p1 = hex(p1), p2 = hex(p2),
p1rev = fl.rev(p1), p2rev = fl.rev(p2))
def manifest(self, mnode, path):
mf = self.repo.manifest.read(bin(mnode))
rev = self.repo.manifest.rev(bin(mnode))
node = self.repo.changelog.node(rev)
files = {}
p = path[1:]
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():
parity = 0
fl = files.keys()
fl.sort()
for f in fl:
full, fnode = files[f]
if fnode:
yield self.t("manifestfileentry",
file = full,
manifest = mnode,
filenode = hex(fnode),
parity = parity,
basename = f)
else:
yield self.t("manifestdirentry",
parity = parity,
path = os.path.join(path, f),
manifest = mnode, basename = f[:-1])
parity = 1 - parity
yield self.t("manifest",
header = self.header(),
footer = self.footer(),
repo = self.reponame,
manifest = mnode,
rev = rev,
node = hex(node),
path = path,
up = up(path),
entries = filelist)
def filediff(self, file, changeset):
n = bin(changeset)
cl = self.repo.changelog
p1 = cl.parents(n)[0]
cs = cl.read(n)
mf = self.repo.manifest.read(cs[0])
def diff():
yield self.diff(p1, n, file)
yield self.t("filediff",
header = self.header(),
footer = self.footer(),
repo = self.reponame,
file = file,
filenode = hex(mf[file]),
node = changeset,
rev = self.repo.changelog.rev(n),
p1 = hex(p1),
p1rev = self.repo.changelog.rev(p1),
diff = diff)
# add tags to things
# tags -> list of changesets corresponding to tags
# find tag, changeset, file
def run(self):
args = cgi.parse()
if not args.has_key('cmd') or args['cmd'][0] == 'changelog':
hi = self.repo.changelog.count()
if args.has_key('pos'):
hi = int(args['pos'][0])
write(self.changelog(hi))
elif args['cmd'][0] == 'changeset':
write(self.changeset(args['node'][0]))
elif args['cmd'][0] == 'manifest':
write(self.manifest(args['manifest'][0], args['path'][0]))
elif args['cmd'][0] == 'filediff':
write(self.filediff(args['file'][0], args['node'][0]))
elif args['cmd'][0] == 'file':
write(self.filerevision(args['file'][0], args['filenode'][0]))
elif args['cmd'][0] == 'annotate':
write(self.fileannotate(args['file'][0], args['filenode'][0]))
elif args['cmd'][0] == 'filelog':
write(self.filelog(args['file'][0], args['filenode'][0]))
elif args['cmd'][0] == 'branches':
httphdr("text/plain")
nodes = []
if args.has_key('nodes'):
nodes = map(bin, args['nodes'][0].split(" "))
for b in self.repo.branches(nodes):
sys.stdout.write(" ".join(map(hex, b)) + "\n")
elif args['cmd'][0] == 'between':
httphdr("text/plain")
nodes = []
if args.has_key('pairs'):
pairs = [ map(bin, p.split("-"))
for p in args['pairs'][0].split(" ") ]
for b in self.repo.between(pairs):
sys.stdout.write(" ".join(map(hex, b)) + "\n")
elif args['cmd'][0] == 'changegroup':
httphdr("application/hg-changegroup")
nodes = []
if args.has_key('roots'):
nodes = map(bin, args['roots'][0].split(" "))
z = zlib.compressobj()
for chunk in self.repo.changegroup(nodes):
sys.stdout.write(z.compress(chunk))
sys.stdout.write(z.flush())
else:
write(self.t("error"))
if __name__ == "__main__":
hgweb().run()