mercurial/hgweb/__init__.py
author Eric Hopper <hopper@omnifarious.org>
Wed, 31 May 2006 08:03:29 -0700
changeset 2355 eb08fb4d41e1
parent 2328 f789602ba840
child 2356 2db831b33e8f
permissions -rw-r--r--
Splitting up hgweb so it's easier to change.

# 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(), "mercurial:mdiff,ui,hg,util,archival,templater")
demandload(globals(), "mercurial.hgweb.request:hgrequest")
demandload(globals(), "mercurial.hgweb.server:create_server")
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 + "/"

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 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)
        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)
        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"))

# 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)))