mercurial/hgweb/hgweb_mod.py
author Brendan Cully <brendan@kublai.com>
Wed, 04 Oct 2006 17:04:40 -0700
changeset 3260 1f1af9b273e8
parent 3231 35d61e653174
child 3261 329ac0c0c0e8
permissions -rw-r--r--
hgweb: accept NewWebInterface URLs

# hgweb/hgweb_mod.py - Web interface for a repository.
#
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
# Copyright 2005, 2006 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 mimetools cStringIO sys tempfile")
demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,streamclone,patch")
demandload(globals(), "mercurial:templater")
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'
        self.stripecount = 1
        self.templatepath = self.repo.ui.config("web", "templates",
                                                templater.templatepath())

    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.stripecount = int(self.repo.ui.config("web", "stripes", 1))
            self.maxshortchanges = int(self.repo.ui.config("web", "maxshortchanges", 60))
            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.configlist("web", "allow_archive")
        for i, spec in self.archive_specs.iteritems():
            if i in allowed or self.repo.ui.configbool("web", "allow" + i):
                yield {"type" : i, "extension" : spec[2], "node" : nodeid}

    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=[], hiderev=None, **args):
        siblings = [s for s in siblings if s.node() != nullid]
        if len(siblings) == 1 and siblings[0].rev() == hiderev:
            return
        for s in siblings:
            yield dict(node=hex(s.node()), rev=s.rev(), **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.status(node1, node2)[:5]
        if files:
            modified, added, removed = map(lambda x: filterfiles(files, x),
                                           (modified, added, removed))

        diffopts = patch.diffopts(self.repo.ui)
        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,
                                          opts=diffopts), f, tn)
        for f in added:
            to = None
            tn = r.file(f).read(mmap2[f])
            yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
                                          opts=diffopts), f, tn)
        for f in removed:
            to = r.file(f).read(mmap1[f])
            tn = None
            yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
                                          opts=diffopts), f, tn)

    def changelog(self, ctx, shortlog=False):
        pos = ctx.rev()
        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
            maxchanges = shortlog and self.maxshortchanges or self.maxchanges
            for f in seq(1, maxchanges):
                if f < 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):
                ctx = self.repo.changectx(i)
                n = ctx.node()

                l.insert(0, {"parity": parity,
                             "author": ctx.user(),
                             "parent": self.siblings(ctx.parents(), i - 1),
                             "child": self.siblings(ctx.children(), i + 1),
                             "changelogtag": self.showtag("changelogtag",n),
                             "desc": ctx.description(),
                             "date": ctx.date(),
                             "files": self.listfilediffs(ctx.files(), n),
                             "rev": i,
                             "node": hex(n)})
                parity = 1 - parity

            for e in l:
                yield e

        maxchanges = shortlog and self.maxshortchanges or self.maxchanges
        cl = self.repo.changelog
        count = cl.count()
        start = max(0, pos - maxchanges + 1)
        end = min(count, start + maxchanges)
        pos = end - 1

        yield self.t(shortlog and 'shortlog' or 'changelog',
                     changenav=changenav,
                     node=hex(cl.tip()),
                     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):
                        ctx = self.repo.changectx(j)
                        l.append(ctx)
                    l.reverse()
                    for e in l:
                        yield e

            for ctx in revgen():
                miss = 0
                for q in qw:
                    if not (q in ctx.user().lower() or
                            q in ctx.description().lower() or
                            q in " ".join(ctx.files()[:20]).lower()):
                        miss = 1
                        break
                if miss:
                    continue

                count += 1
                n = ctx.node()

                yield self.t('searchentry',
                             parity=self.stripes(count),
                             author=ctx.user(),
                             parent=self.siblings(ctx.parents()),
                             child=self.siblings(ctx.children()),
                             changelogtag=self.showtag("changelogtag",n),
                             desc=ctx.description(),
                             date=ctx.date(),
                             files=self.listfilediffs(ctx.files(), n),
                             rev=ctx.rev(),
                             node=hex(n))

                if count >= self.maxchanges:
                    break

        cl = self.repo.changelog

        yield self.t('search',
                     query=query,
                     node=hex(cl.tip()),
                     entries=changelist)

    def changeset(self, ctx):
        n = ctx.node()
        parents = ctx.parents()
        p1 = parents[0].node()

        files = []
        parity = 0
        for f in ctx.files():
            files.append(self.t("filenodelink",
                                node=hex(n), file=f,
                                parity=parity))
            parity = 1 - parity

        def diff(**map):
            yield self.diff(p1, n, None)

        yield self.t('changeset',
                     diff=diff,
                     rev=ctx.rev(),
                     node=hex(n),
                     parent=self.siblings(parents),
                     child=self.siblings(ctx.children()),
                     changesettag=self.showtag("changesettag",n),
                     author=ctx.user(),
                     desc=ctx.description(),
                     date=ctx.date(),
                     files=files,
                     archives=self.archivelist(hex(n)))

    def filelog(self, fctx):
        f = fctx.path()
        fl = fctx.filelog()
        count = fl.count()

        def entries(**map):
            l = []
            parity = (count - 1) & 1

            for i in range(count):
                ctx = fctx.filectx(i)
                n = fl.node(i)

                l.insert(0, {"parity": parity,
                             "filerev": i,
                             "file": f,
                             "node": hex(ctx.node()),
                             "author": ctx.user(),
                             "date": ctx.date(),
                             "rename": self.renamelink(fl, n),
                             "parent": self.siblings(fctx.parents(), file=f),
                             "child": self.siblings(fctx.children(), file=f),
                             "desc": ctx.description()})
                parity = 1 - parity

            for e in l:
                yield e

        yield self.t("filelog", file=f, node=hex(fctx.node()), entries=entries)

    def filerevision(self, fctx):
        f = fctx.path()
        text = fctx.data()
        fl = fctx.filelog()
        n = fctx.filenode()

        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": self.stripes(l)}

        yield self.t("filerevision",
                     file=f,
                     path=_up(f),
                     text=lines(),
                     raw=rawtext,
                     mimetype=mt,
                     rev=fctx.rev(),
                     node=hex(fctx.node()),
                     author=fctx.user(),
                     date=fctx.date(),
                     parent=self.siblings(fctx.parents(), file=f),
                     child=self.siblings(fctx.children(), file=f),
                     rename=self.renamelink(fl, n),
                     permissions=fctx.manifest().execf(f))

    def fileannotate(self, fctx):
        f = fctx.path()
        n = fctx.filenode()
        fl = fctx.filelog()

        def annotate(**map):
            parity = 0
            last = None
            for f, l in fctx.annotate(follow=True):
                fnode = f.filenode()
                name = self.repo.ui.shortuser(f.user())

                if last != fnode:
                    parity = 1 - parity
                    last = fnode

                yield {"parity": parity,
                       "node": hex(f.node()),
                       "rev": f.rev(),
                       "author": name,
                       "file": f.path(),
                       "line": l}

        yield self.t("fileannotate",
                     file=f,
                     annotate=annotate,
                     path=_up(f),
                     rev=fctx.rev(),
                     node=hex(fctx.node()),
                     author=fctx.user(),
                     date=fctx.date(),
                     rename=self.renamelink(fl, n),
                     parent=self.siblings(fctx.parents(), file=f),
                     child=self.siblings(fctx.children(), file=f),
                     permissions=fctx.manifest().execf(f))

    def manifest(self, ctx, path):
        mf = ctx.manifest()
        node = ctx.node()

        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.index("/") + 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,
                       "filenode": hex(fnode),
                       "parity": self.stripes(parity),
                       "basename": f,
                       "permissions": mf.execf(full)}
                parity += 1

        def dirlist(**map):
            parity = 0
            fl = files.keys()
            fl.sort()
            for f in fl:
                full, fnode = files[f]
                if fnode:
                    continue

                yield {"parity": self.stripes(parity),
                       "path": os.path.join(path, f),
                       "basename": f[:-1]}
                parity += 1

        yield self.t("manifest",
                     rev=ctx.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

        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": self.stripes(parity),
                       "tag": k,
                       "date": cl.read(n)[2],
                       "node": hex(n)}
                parity += 1

        yield self.t("tags",
                     node=hex(self.repo.changelog.tip()),
                     entries=lambda **x: entries(False, **x),
                     entriesnotip=lambda **x: entries(True, **x))

    def summary(self):
        cl = self.repo.changelog

        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)
                t = c[2]

                yield self.t("tagentry",
                             parity = self.stripes(parity),
                             tag = k,
                             node = hex(n),
                             date = t)
                parity += 1

        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],
                    desc = changes[4],
                    date = t,
                    rev = i,
                    node = hn))
                parity = 1 - parity

            yield l

        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
                 tags = tagentries,
                 shortlog = changelist,
                 node = hex(self.repo.changelog.tip()),
                 archives=self.archivelist("tip"))

    def filediff(self, fctx):
        n = fctx.node()
        path = fctx.path()
        parents = fctx.changectx().parents()
        p1 = parents[0].node()

        def diff(**map):
            yield self.diff(p1, n, [path])

        yield self.t("filediff",
                     file=path,
                     node=hex(n),
                     rev=fctx.rev(),
                     parent=self.siblings(parents),
                     child=self.siblings(fctx.children()),
                     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 cleanpath(self, path):
        p = util.normpath(path)
        if p[:2] == "..":
            raise Exception("suspicious path")
        return p

    def run(self):
        if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
            raise RuntimeError("This function is only intended to be called while running as a CGI script.")
        import mercurial.hgweb.wsgicgi as wsgicgi
        from request import wsgiapplication
        def make_web_app():
            return self
        wsgicgi.launch(wsgiapplication(make_web_app))

    def run_wsgi(self, req):
        def header(**map):
            header_file = cStringIO.StringIO(''.join(self.t("header", **map)))
            msg = mimetools.Message(header_file, 0)
            req.header(msg.items())
            yield header_file.read()

        def rawfileheader(**map):
            req.header([('Content-type', map['mimetype']),
                        ('Content-disposition', 'filename=%s' % map['file']),
                        ('Content-length', str(len(map['raw'])))])
            yield ''

        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)],
                'sl': [('cmd', ['shortlog']), ('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]

        def rewrite_request(req):
            '''translate new web interface to traditional format'''

            if req.form.has_key('cmd'):
                # old style
                return
            if req.env.has_key('SCRIPT_URL'):
                # run through web server
                base = req.env['SCRIPT_URL']
                # strip repo and leading '/' or '?'
                query = req.env['REQUEST_URI'][len(base)+1:]
            else:
                query = req.env['PATH_INFO'][1:]

            args = query.split('/', 2)
            if not args or not args[0]:
                return

            cmd = args.pop(0)
            if cmd == 'raw':
                req.form['style'] = ['raw']
                cmd = 'rev'
            elif cmd == 'rawdiff':
                req.form['style'] = ['raw']
                cmd = 'diff'
            req.form['cmd'] = [cmd]

            if args and args[0]:
                node = args.pop(0)
                req.form['node'] = [node]
            if args:
                req.form['file'] = args

            if cmd == 'static':
                req.form['file'] = req.form['node']
            elif cmd == 'archive':
                fn = req.form['node'][0]
                for type_, spec in self.archive_specs.iteritems():
                    ext = spec[2]
                    if fn.endswith(ext):
                        req.form['node'] = [fn[:-len(ext)]]
                        req.form['type'] = [type_]

        self.refresh()

        expand_form(req.form)
        rewrite_request(req)

        m = os.path.join(self.templatepath, "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(self.templatepath, 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,
                                               "rawfileheader": rawfileheader,
                                               })

        if not req.form.has_key('cmd'):
            req.form['cmd'] = [self.t.cache['default'],]

        cmd = req.form['cmd'][0]

        method = getattr(self, 'do_' + cmd, None)
        if method:
            method(req)
        else:
            req.write(self.t("error"))

    def changectx(self, req):
        if req.form.has_key('node'):
            changeid = req.form['node'][0]
        else:
            changeid = req.form['manifest'][0]
        try:
            ctx = self.repo.changectx(changeid)
        except hg.RepoError:
            man = self.repo.manifest
            mn = man.lookup(changeid)
            ctx = self.repo.changectx(man.linkrev(mn))

        return ctx

    def filectx(self, req):
        path = self.cleanpath(req.form['file'][0])
        if req.form.has_key('node'):
            changeid = req.form['node'][0]
        else:
            changeid = req.form['filenode'][0]
        try:
            ctx = self.repo.changectx(changeid)
            fctx = ctx.filectx(path)
        except hg.RepoError:
            fctx = self.repo.filectx(path, fileid=changeid)

        return fctx

    def stripes(self, parity):
        "make horizontal stripes for easier reading"
        if self.stripecount:
            return (1 + parity / self.stripecount) & 1
        else:
            return 0

    def do_log(self, req):
        if req.form.has_key('file') and req.form['file'][0]:
            self.do_filelog(req)
        else:
            self.do_changelog(req)

    def do_rev(self, req):
        self.do_changeset(req)

    def do_file(self, req):
        path = req.form.get('file', [''])[0]
        if path:
            try:
                req.write(self.filerevision(self.filectx(req)))
                return
            except hg.RepoError:
                pass
            path = self.cleanpath(path)

        req.write(self.manifest(self.changectx(req), '/' + path))

    def do_diff(self, req):
        self.do_filediff(req)

    def do_changelog(self, req, shortlog = False):
        if req.form.has_key('node'):
            ctx = self.changectx(req)
        else:
            if req.form.has_key('rev'):
                hi = req.form['rev'][0]
            else:
                hi = self.repo.changelog.count() - 1
            try:
                ctx = self.repo.changectx(hi)
            except hg.RepoError:
                req.write(self.search(hi)) # XXX redirect to 404 page?
                return

        req.write(self.changelog(ctx, shortlog = shortlog))

    def do_shortlog(self, req):
        self.do_changelog(req, shortlog = True)

    def do_changeset(self, req):
        ctx = self.repo.changectx(req.form['node'][0])
        req.write(self.changeset(ctx))

    def do_manifest(self, req):
        req.write(self.manifest(self.changectx(req),
                                self.cleanpath(req.form['path'][0])))

    def do_tags(self, req):
        req.write(self.tags())

    def do_summary(self, req):
        req.write(self.summary())

    def do_filediff(self, req):
        req.write(self.filediff(self.filectx(req)))

    def do_annotate(self, req):
        req.write(self.fileannotate(self.filectx(req)))

    def do_filelog(self, req):
        req.write(self.filelog(self.filectx(req)))

    def do_heads(self, req):
        resp = " ".join(map(hex, self.repo.heads())) + "\n"
        req.httphdr("application/mercurial-0.1", length=len(resp))
        req.write(resp)

    def do_branches(self, req):
        nodes = []
        if req.form.has_key('nodes'):
            nodes = map(bin, req.form['nodes'][0].split(" "))
        resp = cStringIO.StringIO()
        for b in self.repo.branches(nodes):
            resp.write(" ".join(map(hex, b)) + "\n")
        resp = resp.getvalue()
        req.httphdr("application/mercurial-0.1", length=len(resp))
        req.write(resp)

    def do_between(self, req):
        if req.form.has_key('pairs'):
            pairs = [map(bin, p.split("-"))
                     for p in req.form['pairs'][0].split(" ")]
        resp = cStringIO.StringIO()
        for b in self.repo.between(pairs):
            resp.write(" ".join(map(hex, b)) + "\n")
        resp = resp.getvalue()
        req.httphdr("application/mercurial-0.1", length=len(resp))
        req.write(resp)

    def do_changegroup(self, req):
        req.httphdr("application/mercurial-0.1")
        nodes = []
        if not self.allowpull:
            return

        if req.form.has_key('roots'):
            nodes = map(bin, req.form['roots'][0].split(" "))

        z = zlib.compressobj()
        f = self.repo.changegroup(nodes, 'serve')
        while 1:
            chunk = f.read(4096)
            if not chunk:
                break
            req.write(z.compress(chunk))

        req.write(z.flush())

    def do_archive(self, req):
        changeset = self.repo.lookup(req.form['node'][0])
        type_ = req.form['type'][0]
        allowed = self.repo.ui.configlist("web", "allow_archive")
        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"))

    def do_static(self, req):
        fname = req.form['file'][0]
        static = self.repo.ui.config("web", "static",
                                     os.path.join(self.templatepath,
                                                  "static"))
        req.write(staticfile(static, fname, req)
                  or self.t("error", error="%r not found" % fname))

    def do_capabilities(self, req):
        caps = ['unbundle']
        if self.repo.ui.configbool('server', 'uncompressed'):
            caps.append('stream=%d' % self.repo.revlogversion)
        resp = ' '.join(caps)
        req.httphdr("application/mercurial-0.1", length=len(resp))
        req.write(resp)

    def check_perm(self, req, op, default):
        '''check permission for operation based on user auth.
        return true if op allowed, else false.
        default is policy to use if no config given.'''

        user = req.env.get('REMOTE_USER')

        deny = self.repo.ui.configlist('web', 'deny_' + op)
        if deny and (not user or deny == ['*'] or user in deny):
            return False

        allow = self.repo.ui.configlist('web', 'allow_' + op)
        return (allow and (allow == ['*'] or user in allow)) or default

    def do_unbundle(self, req):
        def bail(response, headers={}):
            length = int(req.env['CONTENT_LENGTH'])
            for s in util.filechunkiter(req, limit=length):
                # drain incoming bundle, else client will not see
                # response when run outside cgi script
                pass
            req.httphdr("application/mercurial-0.1", headers=headers)
            req.write('0\n')
            req.write(response)

        # require ssl by default, auth info cannot be sniffed and
        # replayed
        ssl_req = self.repo.ui.configbool('web', 'push_ssl', True)
        if ssl_req:
            if not req.env.get('HTTPS'):
                bail(_('ssl required\n'))
                return
            proto = 'https'
        else:
            proto = 'http'

        # do not allow push unless explicitly allowed
        if not self.check_perm(req, 'push', False):
            bail(_('push not authorized\n'),
                 headers={'status': '401 Unauthorized'})
            return

        req.httphdr("application/mercurial-0.1")

        their_heads = req.form['heads'][0].split(' ')

        def check_heads():
            heads = map(hex, self.repo.heads())
            return their_heads == [hex('force')] or their_heads == heads

        # fail early if possible
        if not check_heads():
            bail(_('unsynced changes\n'))
            return

        # do not lock repo until all changegroup data is
        # streamed. save to temporary file.

        fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
        fp = os.fdopen(fd, 'wb+')
        try:
            length = int(req.env['CONTENT_LENGTH'])
            for s in util.filechunkiter(req, limit=length):
                fp.write(s)

            lock = self.repo.lock()
            try:
                if not check_heads():
                    req.write('0\n')
                    req.write(_('unsynced changes\n'))
                    return

                fp.seek(0)

                # send addchangegroup output to client

                old_stdout = sys.stdout
                sys.stdout = cStringIO.StringIO()

                try:
                    url = 'remote:%s:%s' % (proto,
                                            req.env.get('REMOTE_HOST', ''))
                    ret = self.repo.addchangegroup(fp, 'serve', url)
                finally:
                    val = sys.stdout.getvalue()
                    sys.stdout = old_stdout
                req.write('%d\n' % ret)
                req.write(val)
            finally:
                lock.release()
        finally:
            fp.close()
            os.unlink(tempname)

    def do_stream_out(self, req):
        req.httphdr("application/mercurial-0.1")
        streamclone.stream_out(self.repo, req)