Mercurial > public > mercurial-scm > hg-stable
diff mercurial/hgweb/__init__.py @ 2311:b832b6eb65ab
Moving hgweb.py into it's own module in preparation for breaking it up.
author | Eric Hopper <hopper@omnifarious.org> |
---|---|
date | Thu, 18 May 2006 11:48:33 -0700 |
parents | mercurial/hgweb.py@714f4d25a7a9 |
children | f789602ba840 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/hgweb/__init__.py Thu May 18 11:48:33 2006 -0700 @@ -0,0 +1,1142 @@ +# hgweb.py - web interface to a mercurial 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, cgi, sys +import mimetypes +from mercurial.demandload import demandload +demandload(globals(), "time re socket zlib errno ConfigParser tempfile") +demandload(globals(), "StringIO BaseHTTPServer SocketServer urllib") +demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater") +from mercurial.node import * +from mercurial.i18n import gettext as _ + +def splitURI(uri): + """ Return path and query splited from uri + + Just like CGI environment, the path is unquoted, the query is + not. + """ + if '?' in uri: + path, query = uri.split('?', 1) + else: + path, query = uri, '' + return urllib.unquote(path), query + +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 get_mtime(repo_path): + hg_path = os.path.join(repo_path, ".hg") + cl_path = os.path.join(hg_path, "00changelog.i") + if os.path.exists(os.path.join(cl_path)): + return os.stat(cl_path).st_mtime + else: + return os.stat(hg_path).st_mtime + +def staticfile(directory, fname): + """return a file inside directory with guessed content-type header + + fname always uses '/' as directory separator and isn't allowed to + contain unusual path components. + Content-type is guessed using the mimetypes module. + Return an empty string if fname is illegal or file not found. + + """ + parts = fname.split('/') + path = directory + for part in parts: + if (part in ('', os.curdir, os.pardir) or + os.sep in part or os.altsep is not None and os.altsep in part): + return "" + path = os.path.join(path, part) + try: + os.stat(path) + ct = mimetypes.guess_type(path)[0] or "text/plain" + return "Content-type: %s\n\n%s" % (ct, file(path).read()) + except (TypeError, OSError): + # illegal fname or unreadable file + return "" + +class hgrequest(object): + def __init__(self, inp=None, out=None, env=None): + self.inp = inp or sys.stdin + self.out = out or sys.stdout + self.env = env or os.environ + self.form = cgi.parse(self.inp, self.env, keep_blank_values=1) + + def write(self, *things): + for thing in things: + if hasattr(thing, "__iter__"): + for part in thing: + self.write(part) + else: + try: + self.out.write(str(thing)) + except socket.error, inst: + if inst[0] != errno.ECONNRESET: + raise + + def header(self, headers=[('Content-type','text/html')]): + for header in headers: + self.out.write("%s: %s\r\n" % header) + self.out.write("\r\n") + + def httphdr(self, type, file="", size=0): + + headers = [('Content-type', type)] + if file: + headers.append(('Content-disposition', 'attachment; filename=%s' % file)) + if size > 0: + headers.append(('Content-length', str(size))) + self.header(headers) + +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): + for i in self.archives: + if self.repo.ui.configbool("web", "allow" + i, False): + 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) + node = self.repo.changelog.node(rev) + 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) + pos = end - 1 + + 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', 'x-bzip2'), + 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'), + '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 "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': + req.httphdr("application/mercurial-0.1") + h = self.repo.heads() + req.write(" ".join(map(hex, h)) + "\n") + + elif cmd == 'branches': + req.httphdr("application/mercurial-0.1") + nodes = [] + if req.form.has_key('nodes'): + nodes = map(bin, req.form['nodes'][0].split(" ")) + for b in self.repo.branches(nodes): + req.write(" ".join(map(hex, b)) + "\n") + + elif cmd == 'between': + req.httphdr("application/mercurial-0.1") + nodes = [] + if req.form.has_key('pairs'): + pairs = [map(bin, p.split("-")) + for p in req.form['pairs'][0].split(" ")] + for b in self.repo.between(pairs): + req.write(" ".join(map(hex, b)) + "\n") + + 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] + if (type in self.archives and + 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")) + +def create_server(ui, repo): + use_threads = True + + def openlog(opt, default): + if opt and opt != '-': + return open(opt, 'w') + return default + + address = ui.config("web", "address", "") + port = int(ui.config("web", "port", 8000)) + use_ipv6 = ui.configbool("web", "ipv6") + webdir_conf = ui.config("web", "webdir_conf") + accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout) + errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr) + + if use_threads: + try: + from threading import activeCount + except ImportError: + use_threads = False + + if use_threads: + _mixin = SocketServer.ThreadingMixIn + else: + if hasattr(os, "fork"): + _mixin = SocketServer.ForkingMixIn + else: + class _mixin: pass + + class MercurialHTTPServer(_mixin, BaseHTTPServer.HTTPServer): + pass + + class IPv6HTTPServer(MercurialHTTPServer): + address_family = getattr(socket, 'AF_INET6', None) + + def __init__(self, *args, **kwargs): + if self.address_family is None: + raise hg.RepoError(_('IPv6 not available on this system')) + BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) + + class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler): + + def log_error(self, format, *args): + errorlog.write("%s - - [%s] %s\n" % (self.address_string(), + self.log_date_time_string(), + format % args)) + + def log_message(self, format, *args): + accesslog.write("%s - - [%s] %s\n" % (self.address_string(), + self.log_date_time_string(), + format % args)) + + def do_POST(self): + try: + self.do_hgweb() + except socket.error, inst: + if inst[0] != errno.EPIPE: + raise + + def do_GET(self): + self.do_POST() + + def do_hgweb(self): + path_info, query = splitURI(self.path) + + env = {} + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['REQUEST_METHOD'] = self.command + env['SERVER_NAME'] = self.server.server_name + env['SERVER_PORT'] = str(self.server.server_port) + env['REQUEST_URI'] = "/" + env['PATH_INFO'] = path_info + if query: + env['QUERY_STRING'] = query + host = self.address_string() + if host != self.client_address[0]: + env['REMOTE_HOST'] = host + env['REMOTE_ADDR'] = self.client_address[0] + + if self.headers.typeheader is None: + env['CONTENT_TYPE'] = self.headers.type + else: + env['CONTENT_TYPE'] = self.headers.typeheader + length = self.headers.getheader('content-length') + if length: + env['CONTENT_LENGTH'] = length + accept = [] + for line in self.headers.getallmatchingheaders('accept'): + if line[:1] in "\t\n\r ": + accept.append(line.strip()) + else: + accept = accept + line[7:].split(',') + env['HTTP_ACCEPT'] = ','.join(accept) + + req = hgrequest(self.rfile, self.wfile, env) + self.send_response(200, "Script output follows") + + if webdir_conf: + hgwebobj = hgwebdir(webdir_conf) + elif repo is not None: + hgwebobj = hgweb(repo.__class__(repo.ui, repo.origroot)) + else: + raise hg.RepoError(_('no repo found')) + hgwebobj.run(req) + + + if use_ipv6: + return IPv6HTTPServer((address, port), hgwebhandler) + else: + return MercurialHTTPServer((address, port), hgwebhandler) + +# This is a stopgap +class hgwebdir(object): + def __init__(self, config): + def cleannames(items): + return [(name.strip(os.sep), path) for name, path in items] + + self.motd = "" + self.repos_sorted = ('name', False) + if isinstance(config, (list, tuple)): + self.repos = cleannames(config) + self.repos_sorted = ('', False) + elif isinstance(config, dict): + self.repos = cleannames(config.items()) + self.repos.sort() + else: + cp = ConfigParser.SafeConfigParser() + cp.read(config) + self.repos = [] + if cp.has_section('web') and cp.has_option('web', 'motd'): + self.motd = cp.get('web', 'motd') + if cp.has_section('paths'): + self.repos.extend(cleannames(cp.items('paths'))) + if cp.has_section('collections'): + for prefix, root in cp.items('collections'): + for path in util.walkrepos(root): + repo = os.path.normpath(path) + name = repo + if name.startswith(prefix): + name = name[len(prefix):] + self.repos.append((name.lstrip(os.sep), repo)) + self.repos.sort() + + def run(self, req=hgrequest()): + def header(**map): + yield tmpl("header", **map) + + def footer(**map): + yield tmpl("footer", motd=self.motd, **map) + + m = os.path.join(templater.templatepath(), "map") + tmpl = templater.templater(m, templater.common_filters, + defaults={"header": header, + "footer": footer}) + + def archivelist(ui, nodeid, url): + for i in ['zip', 'gz', 'bz2']: + if ui.configbool("web", "allow" + i, False): + yield {"type" : i, "node": nodeid, "url": url} + + def entries(sortcolumn="", descending=False, **map): + rows = [] + parity = 0 + for name, path in self.repos: + u = ui.ui() + try: + u.readconfig(os.path.join(path, '.hg', 'hgrc')) + except IOError: + pass + get = u.config + + url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name]) + .replace("//", "/")) + + # update time with local timezone + try: + d = (get_mtime(path), util.makedate()[1]) + except OSError: + continue + + contact = (get("ui", "username") or # preferred + get("web", "contact") or # deprecated + get("web", "author", "")) # also + description = get("web", "description", "") + name = get("web", "name", name) + row = dict(contact=contact or "unknown", + contact_sort=contact.upper() or "unknown", + name=name, + name_sort=name, + url=url, + description=description or "unknown", + description_sort=description.upper() or "unknown", + lastchange=d, + lastchange_sort=d[1]-d[0], + archives=archivelist(u, "tip", url)) + if (not sortcolumn + or (sortcolumn, descending) == self.repos_sorted): + # fast path for unsorted output + row['parity'] = parity + parity = 1 - parity + yield row + else: + rows.append((row["%s_sort" % sortcolumn], row)) + if rows: + rows.sort() + if descending: + rows.reverse() + for key, row in rows: + row['parity'] = parity + parity = 1 - parity + yield row + + virtual = req.env.get("PATH_INFO", "").strip('/') + if virtual: + real = dict(self.repos).get(virtual) + if real: + try: + hgweb(real).run(req) + except IOError, inst: + req.write(tmpl("error", error=inst.strerror)) + except hg.RepoError, inst: + req.write(tmpl("error", error=str(inst))) + else: + req.write(tmpl("notfound", repo=virtual)) + else: + if req.form.has_key('static'): + static = os.path.join(templater.templatepath(), "static") + fname = req.form['static'][0] + req.write(staticfile(static, fname) + or tmpl("error", error="%r not found" % fname)) + else: + sortable = ["name", "description", "contact", "lastchange"] + sortcolumn, descending = self.repos_sorted + if req.form.has_key('sort'): + sortcolumn = req.form['sort'][0] + descending = sortcolumn.startswith('-') + if descending: + sortcolumn = sortcolumn[1:] + if sortcolumn not in sortable: + sortcolumn = "" + + sort = [("sort_%s" % column, + "%s%s" % ((not descending and column == sortcolumn) + and "-" or "", column)) + for column in sortable] + req.write(tmpl("index", entries=entries, + sortcolumn=sortcolumn, descending=descending, + **dict(sort)))