Below is the file 'handlers.py' from this revision. You can also download the file.

# Copyright (C) 2005 Grahame Bowland <grahame@angrygoats.net>
#
# This program is made available under the GNU GPL version 2.0 or
# greater. See the accompanying file COPYING for details.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE.

# Python standard modules
import os, cgi, sys, struct, rfc822, urllib
import tarfile, tempfile, datetime, cStringIO, heapq, binascii
# web.py
import web
# JSON utility module
import json
# FreeDesktop.org share mime info and icon theme specification
from fdo import sharedmimeinfo, icontheme
# Other bits of ViewMTN
import mtn, common, syntax, release, branchdiv
from ancestry import ancestry_graph
from render import *
hq = cgi.escape

# The user configuration file
import config

# MIME lookup instance
mimehelp = sharedmimeinfo.LookupHelper(getattr(config, "mime_map", None))
# Icon theme access
if config.icon_theme:
    try:
        mimeicon = icontheme.MimeIcon(icontheme.IconTheme(config.icon_theme), config.icon_size)
    except Exception, e:
        print>>sys.stderr, "\nFailed to load icon theme: perhaps you want to set config.icon_theme = None\n"
        raise e
else:
    mimeicon = None

class Index(object):
    def GET(self, ctxt):
        branches = list(ctxt.ops.branches ())
        ctxt.branchdivs.calculate_divisions (branches)
        def division_iter():
            bitter = iter(branches)
            divs = ctxt.branchdivs.divisions
            n_divs = len(divs)
            in_divs = {}
            look_for = 0
            def new_div (n):
                did = look_for
                in_divs[n] = did
                return "d", did, mtn.Branch(n), len(in_divs.keys ()) * 10
            def end_div (n):
                did = in_divs.pop (n)
                return "e", did, mtn.Branch(n), len(in_divs.keys ()) * 10
            def branch_line (b):
                return "b", 0, branch, 0
            for branch in bitter:
                for div in in_divs.keys(): # we alter it in the loop, copy..
                    if branch.name.find (div) != 0:
                        yield end_div (div)
                if look_for < n_divs:
                    if cmp(branch, divs[look_for]) > 0:
                        look_for += 1
                    if branch.name.find (divs[look_for]) == 0:
                        yield new_div (divs[look_for])
                        look_for += 1
                yield branch_line (branch)
            # any stragglers need to be closed
            for div in in_divs.keys():
                yield end_div (div)
        ctxt.render('index.html', page_title="Branches", branches=division_iter())

class About(object):
    def GET(self, ctxt):
        ctxt.render('about.html', page_title="About")

class Tags(object):
    def GET(self, ctxt, restrict_branch=None):
        # otherwise we couldn't use automate again..
        tags = list(ctxt.ops.tags())
        if restrict_branch != None:
            restrict_branch = mtn.Branch(restrict_branch)
            def tag_in(tag):
                for branch in tag.branches:
                    if branch.name == restrict_branch.name:
                        return tag
                return None
            tags = filter(tag_in, tags)
            template_file = "branchtags.html"
        else:
            template_file = "tags.html"
        tags.sort(lambda t1, t2: cmp(t1.name, t2.name))
        def revision_ago(rev):
            rv = ""
            for cert in ctxt.ops.certs(rev):
                if cert[4] == 'name' and cert[5] == 'date':
                    revdate = common.parse_timecert(cert[7])
                    rv = common.ago(revdate)
            return rv
        ctxt.render(template_file, page_title="Tags", tags=tags, revision_ago=revision_ago, branch=restrict_branch)

class Help(object):
    def GET(self, ctxt):
        ctxt.render('help.html', page_title="Help")

class Changes(object):
    class ComparisonRev:
        def __init__(self, ops, revision):
            self.revision = revision
            self.certs = list(ops.certs(self.revision))
            self.date = None
            for cert in self.certs:
                if cert[4] == 'name' and cert[5] == 'date':
                    self.date = common.parse_timecert(cert[7])
        def __repr__(self):
            return "ComparisonRev <%s>" % (repr(self.revision))
        def __cmp__(self, other):
            # irritating edge-case, heapq compares us to empty string if
            # there's only one thing in the list
            if not other:
                return 1
            return cmp(other.date, self.date)


    def __get_last_changes(self, ops, start_from, parent_func, selection_func, n):
        """returns at least n revisions that are parents of the revisions in start_from,
        ordered by time (descending). selection_func is called for each revision, and
        that revision is only included in the result if the function returns True."""
        # revised algorithm in colaboration with Matthias Radestock
        #
        # use a heapq to keep a list of interesting revisions
        # pop from the heapq; get the most recent interesting revision
        # insert the parents of this revision into the heap
        # .. repeat until len(heap) >= to_count
        if not start_from:
            raise Exception("get_last_changes() unable to find somewhere to start.")

        last_result = None
        in_result = set()
        result = []
        revq = []
        for rev in start_from:
            heapq.heappush(revq, Changes.ComparisonRev(ops, rev))
        while len(result) < n:
        #           print >>sys.stderr, "start_revq state:", map(lambda x: (x.revision, x.date), revq)
            # update based on the last result we output
            if last_result != None:
                parents = filter(None, parent_func(last_result.revision))
                for parent_rev in parents:
                    if parent_rev == None:
                        continue
                    heapq.heappush(revq, Changes.ComparisonRev(ops, parent_rev))

            # try and find something we haven't already output in the heap
            last_result = None
            while revq:
                candidate = heapq.heappop(revq)
                if not (candidate.revision in in_result) and selection_func(candidate.revision):
                    last_result = candidate
                    break
            if last_result == None:
                break
            # follow the newest edge
            in_result.add(last_result.revision)
            result.append(last_result)

        rv = map (lambda x: (x.revision, x.certs), result), revq
        return rv

    def on_our_branch(self, ops, branch, revision):
        rv = False
        for cert in ops.certs(revision):
            if cert[4] == 'name' and cert[5] == 'branch':
                if cert[7] == branch.name:
                    rv = True
        return rv

    def for_template(self, ctxt, revs, pathinfo=None, constrain_diff_to=None):
        rv = []
        for rev, certs in revs:
            rev_branch = ""
            revision, diffs, ago, author, changelog, shortlog, when = mtn.Revision(rev), [], "", mtn.Author(""), "", "", ""
            for cert in certs:
                if cert[4] != 'name':
                    continue
                if cert[5] == "branch":
                    rev_branch = cert[7]
                elif cert[5] == 'date':
                    revdate = common.parse_timecert(cert[7])
                    ago = common.ago(revdate)
                    when = revdate.strftime('%a, %d %b %Y %H:%M:%S GMT')
                elif cert[5] == 'author':
                    author = mtn.Author(cert[7])
                elif cert[5] == 'changelog':
                    changelog = normalise_changelog(cert[7]) # NB: this HTML escapes!
                    shortlog = quicklog(changelog) # so this is also HTML escaped.
            if pathinfo != None:
                filename = pathinfo.get(rev)
            else:
                filename = None
            if constrain_diff_to:
                diff_to_revision = mtn.Revision(constrain_diff_to)
            else:
                diff_to_revision = revision
            for stanza in ctxt.ops.get_revision(rev):
                if stanza and stanza[0] == "old_revision":
                    diffs.append(Diff(mtn.Revision(stanza[1]), diff_to_revision, filename))
            if diffs:
                if constrain_diff_to: diffs = [ diffs[0] ]
                diffs = '| ' + ', '.join([ctxt.link(d).html('diff') for d in diffs])
            else:
                diffs = ''
            rv.append((revision, diffs, ago, mtn.Author(author), '<br />\n'.join(changelog), shortlog, when, mtn.File(filename, rev)))
        return rv

    def determine_bounds(self, from_change, to_change):
        per_page = 10
        max_per_page = 100
        if from_change: from_change = int(from_change)
        else: from_change = 0
        if to_change: to_change = int(to_change)
        else: to_change = per_page
        if to_change - from_change > max_per_page:
            to_change = from_change + max_per_page
        next_from = to_change
        next_to = to_change + per_page
        previous_from = from_change - per_page
        previous_to = previous_from + per_page
        return (from_change, to_change, next_from, next_to, previous_from, previous_to)

    def branch_get_last_changes(self, ops, branch, from_change, to_change):
        heads = [t for t in ops.heads(branch.name)]
        if not heads:
            return web.notfound()
        changed, new_starting_point = self.__get_last_changes(ops,
                                                              heads,
                                                              lambda r: ops.parents(r),
                                                              lambda r: self.on_our_branch(ops, branch, r),
                                                              to_change)
        return changed, new_starting_point

    def Branch_GET(self, ctxt, branch, from_change, to_change, template_name):
        branch = mtn.Branch(branch)
        from_change, to_change, next_from, next_to, previous_from, previous_to = self.determine_bounds(from_change, to_change)
        last_changes = self.branch_get_last_changes(ctxt.ops, branch, from_change, to_change)
        if last_changes is None:
            return web.notfound()
        changed, new_starting_point = last_changes
        changed = changed[from_change:to_change]
        if len(changed) != to_change - from_change:
            next_from, next_to = None, None
        if from_change <= 0:
            previous_from, previous_to = None, None
        ctxt.render(template_name,
                    page_title="Branch %s" % branch.name,
                    branch=branch,
                    from_change=from_change,
                    to_change=to_change,
                    previous_from=previous_from,
                    previous_to=previous_to,
                    next_from=next_from,
                    next_to=next_to,
                    display_revs=self.for_template(ctxt, changed))

    def file_get_last_changes(self, ops, from_change, to_change, revision, path):
        def content_changed_fn(start_revision, start_path, in_revision, pathinfo):
            uniq = set()
            parents = list(ops.parents(in_revision))
            for parent in parents:
                stanza = list(ops.get_corresponding_path(start_revision, start_path, parent))
                # file does not exist in this revision; skip!
                if not stanza:
                    continue
                # follow the white rabbit
                current_path = stanza[0][1]
                stanza = list(ops.get_content_changed(parent, current_path))
                to_add = stanza[0][1]
                uniq.add(to_add)
                pathinfo[to_add] = current_path
            return list(uniq)
        pathinfo = {}
        # not just the starting revision! we might not have changed 'path' in the starting rev..
        start_at = content_changed_fn(revision, path, revision, pathinfo)
        changed, new_starting_point = self.__get_last_changes(ops,
                                                              start_at,
                                                              lambda r: content_changed_fn(revision, path, r, pathinfo),
                                                              lambda r: True,
                                                              to_change)
        return changed, new_starting_point, pathinfo

    def File_GET(self, ctxt, from_change, to_change, revision, path, template_name):
        from_change, to_change, next_from, next_to, previous_from, previous_to = self.determine_bounds(from_change, to_change)
        changed, new_starting_point, pathinfo = self.file_get_last_changes(ctxt.ops, from_change, to_change, revision, path)
        changed = changed[from_change:to_change]
        if len(changed) != to_change - from_change:
            next_from, next_to = None, None
        if from_change <= 0:
            previous_from, previous_to = None, None
        ctxt.render(template_name,
                    page_title="Changes to '%s' (from %s)" % (cgi.escape(path), revision.abbrev()),
                    filename=mtn.File(path, revision),
                    revision=revision,
                    from_change=from_change,
                    to_change=to_change,
                    previous_from=previous_from,
                    previous_to=previous_to,
                    next_from=next_from,
                    next_to=next_to,
                    display_revs=self.for_template(ctxt, changed, pathinfo=pathinfo, constrain_diff_to=revision))

class HTMLBranchChanges(Changes):
    def GET(self, ctxt, branch, from_change, to_change):
        Changes.Branch_GET(self, ctxt, branch, from_change, to_change, "branchchanges.html")

class RSSBranchChanges(Changes):
    def GET(self, ctxt, branch, from_change, to_change):
        Changes.Branch_GET(self, ctxt, branch, from_change, to_change, "branchchangesrss.html")

class RevisionPage(object):
    def get_fileid(self, ops, revision, filename):
        rv = None
        for stanza in ops.get_manifest_of(revision):
            if stanza[0] != 'file':
                continue
            if stanza[1] == filename:
                rv = stanza[3]
        return rv

    def exists(self, ops, revision):
        try:
            certs = [t for t in ops.certs(revision)]
            return True
        except mtn.MonotoneException:
            return False

    def branches_for_rev(self, ops, revisions_val):
        rv = []
        for stanza in ops.certs(revisions_val):
            if stanza[4] == 'name' and stanza[5] == 'branch':
                rv.append(stanza[7])
        return rv

class RevisionFileChanges(Changes, RevisionPage):
    def GET(self, ctxt, from_change, to_change, revision, path):
        revision = mtn.Revision(revision)
        if not self.exists(ctxt.ops, revision):
            return web.notfound()
        Changes.File_GET(self, ctxt, from_change, to_change, revision, path, "revisionfilechanges.html")

class RevisionFileChangesRSS(Changes, RevisionPage):
    def GET(self, ctxt, from_change, to_change, revision, path):
        revision = mtn.Revision(revision)
        if not self.exists(ctxt.ops, revision):
            return web.notfound()
        Changes.File_GET(self, ctxt, from_change, to_change, revision, path, "revisionfilechangesrss.html")

class RevisionInfo(RevisionPage):
    def GET(self, ctxt, revision):
        revision = mtn.Revision(revision)
        if not self.exists(ctxt.ops, revision):
            return web.notfound()
        certs = ctxt.ops.certs(revision)
        revisions = ctxt.ops.get_revision(revision)
        output_png, output_imagemap = ancestry_graph(ctxt, revision)
        if os.access(output_imagemap, os.R_OK):
            imagemap = open(output_imagemap).read().replace('\\n', ' by ')
            imageuri = ctxt.perdb_join('revision/graph/' + revision)
        else:
            imagemap = imageuri = None
        ctxt.render('revisioninfo.html',
                    page_title="Revision %s" % revision.abbrev(),
                    revision=revision,
                    certs=certs_for_template(ctxt, certs),
                    imagemap=imagemap,
                    imageuri=imageuri,
                    revisions=revisions_for_template(ctxt, revision, revisions))

class RevisionDiff(RevisionPage):
    def GET(self, ctxt, revision_from, revision_to, filename=None):
        revision_from = mtn.Revision(revision_from)
        revision_to = mtn.Revision(revision_to)
        if not self.exists(ctxt.ops, revision_from):
            return web.notfound()
        if not self.exists(ctxt.ops, revision_to):
            return web.notfound()
        if filename != None:
            files = [filename]
        else:
            files = []
        diff = ctxt.ops.diff(revision_from, revision_to, files)
        diff_obj = Diff(revision_from, revision_to, files)
        ctxt.render('revisiondiff.html',
                    page_title="Diff from %s to %s" % (revision_from.abbrev(), revision_to.abbrev()),
                    revision=revision_from,
                    revision_from=revision_from,
                    revision_to=revision_to,
                    diff=syntax.highlight(diff, 'diff'),
                    diff_obj=diff_obj,
                    files=files)

class RevisionRawDiff(RevisionPage):
    def GET(self, ctxt, revision_from, revision_to, filename=None):
        revision_from = mtn.Revision(revision_from)
        revision_to = mtn.Revision(revision_to)
        if not self.exists(ctxt.ops, revision_from):
            return web.notfound()
        if not self.exists(ctxt.ops, revision_to):
            return web.notfound()
        if filename != None:
            files = [filename]
        else:
            files = []
        diff = ctxt.ops.diff(revision_from, revision_to, files)
        web.header('Content-Type', 'text/x-diff')
        for line in diff:
            sys.stdout.write (line)
        sys.stdout.flush()

class RevisionFile(RevisionPage):
    def GET(self, ctxt, revision, filename):
        revision = mtn.Revision(revision)
        if not self.exists(ctxt.ops, revision):
            return web.notfound()
        language = filename.rsplit('.', 1)[-1]
        fileid = RevisionPage.get_fileid(self, ctxt.ops, revision, filename)
        if not fileid:
            return web.notfound()
        contents = ctxt.ops.get_file(fileid)
        mimetype = mimehelp.lookup(filename, '')
        mime_to_template = {
            'image/jpeg'           : 'revisionfileimg.html',
            'image/png'            : 'revisionfileimg.html',
            'image/gif'            : 'revisionfileimg.html',
            'image/svg+xml'        : 'revisionfileobj.html',
            'application/pdf'      : 'revisionfileobj.html',
            'application/x-python' : 'revisionfiletxt.html',
            'application/x-perl'   : 'revisionfiletxt.html',
        }
        template = mime_to_template.get(mimetype, None)
        if not template:
            if mimetype.startswith('text/'):
                template = 'revisionfiletxt.html'
            else:
                template = 'revisionfilebin.html'
        ctxt.render(template,
                    filename=mtn.File(filename, revision),
                    page_title="File %s in revision %s" % (filename, revision.abbrev()),
                    revision=revision,
                    mimetype=mimetype,
                    contents=syntax.highlight(contents, language))

class RevisionDownloadFile(RevisionPage):
    def GET(self, ctxt, revision, filename):
        web.header('Content-Disposition', 'attachment; filename=%s' % filename)
        revision = mtn.Revision(revision)
        if not self.exists(ctxt.ops, revision):
            return web.notfound()
        fileid = RevisionPage.get_fileid(self, ctxt.ops, revision, filename)
        if not fileid:
            return web.notfound()
        for idx, data in enumerate(ctxt.ops.get_file(fileid)):
            if idx == 0:
                mimetype = mimehelp.lookup(filename, data)
                web.header('Content-Type', mimetype)
            sys.stdout.write(data)
        sys.stdout.flush()

class RevisionTar(RevisionPage):
    def GET(self, ctxt, revision):
        # we'll output in the USTAR tar format; documentation taken from:
        # http://en.wikipedia.org/wiki/Tar_%28file_format%29
        revision = mtn.Revision(revision)
        if not self.exists(ctxt.ops, revision):
            return web.notfound()
        filename = "%s.tar" % revision
        web.header('Content-Disposition', 'attachment; filename=%s' % filename)
        web.header('Content-Type', 'application/x-tar')
        manifest = [stanza for stanza in ctxt.ops.get_manifest_of(revision)]
        # for now; we might want to come up with something more interesting;
        # maybe the branch name (but there might be multiple branches?)
        basedirname = revision
        tarobj = tarfile.open(name=filename, mode="w", fileobj=sys.stdout)
        dir_mode, file_mode = "0700", "0600"
        certs = {}
        for stanza in manifest:
            stanza_type = stanza[0]
            if stanza_type != 'file':
                continue
            filename, fileid = stanza[1], stanza[3]
            filecontents = cStringIO.StringIO()
            filesize = 0
            for data in ctxt.ops.get_file(fileid):
                filesize += len(data)
                filecontents.write(data)
            ti = tarfile.TarInfo()
            ti.name = os.path.join(revision, filename)
            ti.mode, ti.type = 00600, tarfile.REGTYPE
            ti.uid = ti.gid = 0
            # determine the most recent of the content marks
            content_marks = [t[1] for t in ctxt.ops.get_content_changed(revision, filename)]
            if len(content_marks) > 0:
                # just pick one to make this faster
                content_mark = content_marks[0]
                since_epoch = timecert(ctxt.ops.certs(content_mark)) - datetime.datetime.fromtimestamp(0)
                ti.mtime = since_epoch.days * 24 * 60 * 60 + since_epoch.seconds
            else:
                ti.mtime = 0
            ti.size = filesize
            filecontents.seek(0)
            tarobj.addfile(ti, filecontents)

class RevisionBrowse(RevisionPage):
    def GET(self, ctxt, revision, path):
        revision = mtn.Revision(revision)
        if not self.exists(ctxt.ops, revision):
            return web.notfound()
        branches = RevisionPage.branches_for_rev(self, ctxt.ops, revision)
        revisions = ctxt.ops.get_revision(revision)

        def components(path):
            # NB: mtn internally uses '/' for paths, so we shouldn't use os.path.join()
            # we should do things manually; otherwise we'll break on other platforms
            # when we accidentally use \ or : or whatever.
            #
            # also, let's handle the case of spurious extra / characters
            # whatever we return should make sense as '/'.join(rv)
            rv = []
            while path:
                path = path.lstrip('/')
                pc = path.split('/', 1)
                if len(pc) == 2:
                    rv.append(pc[0])
                    path = pc[1]
                else:
                    rv.append(pc[0])
                    path = ''
            return rv

        path = path or ""
        path_components = components(path)
        normalised_path = '/'.join(path_components)
        # TODO: detect whether or not this exists and skip the following if it doesn't.
        page_title = "Browsing revision %s: dir %s/" % (revision.abbrev(), normalised_path or '')

        if len(branches) > 0:
            if len(branches) == 1:
                branch_plural = 'branch'
            else:
                branch_plural = 'branches'
            page_title += " of %s %s" % (branch_plural, ', '.join(branches))

        def cut_manifest_to_subdir():
            manifest = list(ctxt.ops.get_manifest_of(revision))
            in_the_dir = False
            for stanza in manifest:
                stanza_type = stanza[0]
                if stanza_type != "file" and stanza_type != "dir":
                    continue
                this_path = stanza[1]

                if not in_the_dir:
                    if stanza_type == "dir" and this_path == normalised_path:
                        in_the_dir = True
                    continue

                this_path_components = components(this_path)
                # debug(["inthedir", stanza_type, this_path, len(this_path_components), len(path_components)])
                if stanza_type == "dir":
                    # are we still in our directory?
                    if len(this_path_components) > len(path_components) and \
                            this_path_components[:len(path_components)] == path_components:
                        # is this an immediate subdirectory of our directory?
                        if len(this_path_components) == len(path_components) + 1:
                            yield (stanza_type, this_path)
                    else:
                        in_the_dir = False
                        # and we've come out of the dir ne'er to re-enter, so..
                        break
                elif stanza_type == "file" and len(this_path_components) == len(path_components) + 1:
                    yield (stanza_type, this_path)

        def info_for_manifest(entry_iter):
            # should probably limit memory usage (worst case is this gets huge)
            # but for now, this is really a needed optimisation, as most of the
            # time a single cert will be seen *many* times
            certs = {}
            certinfo = {}

            def get_cert(revision):
                if not certs.has_key(revision):
                    # subtle bug slipped in here; ops.cert() is a generator
                    # so we can't just store it in a cache!
                    certs[revision] = list(ctxt.ops.certs(revision))
                return certs[revision]

            def _get_certinfo(revision):
                author, ago, shortlog = None, None, None
                for cert in get_cert(revision):
                    if cert[4] != 'name':
                        continue
                    name, value = cert[5], cert[7]
                    if name == "author":
                        author = mtn.Author(value)
                    elif name == "date":
                        revdate = common.parse_timecert(value)
                        ago = common.ago(revdate)
                    elif name == "changelog":
                        shortlog = quicklog(normalise_changelog(value), 40)
                to_return = (author, ago, shortlog)
                return [t or "" for t in to_return]

            def get_certinfo(revision):
                if not certinfo.has_key(revision):
                    certinfo[revision] = _get_certinfo(revision)
                return certinfo[revision]

            for stanza_type, this_path in entry_iter:
                # determine the most recent of the content marks
                content_marks = [t[1] for t in ctxt.ops.get_content_changed(revision, this_path)]
                for mark in content_marks:
                    get_cert(mark)
                if len(content_marks):
                    content_marks.sort(lambda b, a: cmp(timecert(certs[a]), timecert(certs[b])))
                    content_mark = mtn.Revision(content_marks[0])
                    author, ago, shortlog = get_certinfo(content_mark)
                else:
                    author, ago, shortlog, content_mark = mtn.Author(""), "", "", None
                if stanza_type == "file":
                    file_obj = mtn.File(this_path, revision)
                    mime_type = mimehelp.lookup(this_path, "")
                else:
                    file_obj = mtn.Dir(this_path, revision)
                    mime_type = 'inode/directory'
                yield (stanza_type, file_obj, author, ago, content_mark, shortlog, mime_type)

        def path_links(components):
            # we always want a link to '/'
            yield mtn.Dir('/', revision)
            running_path = ""
            for component in components:
                running_path += component + "/"
                yield mtn.Dir(running_path, revision)

        def row_class():
            while True:
                yield "odd"
                yield "even"

        def mime_icon(mime_type):
            return ctxt.nodb_join('mimeicon/' + mime_type)

        ctxt.render('revisionbrowse.html',
                    branches=branches,
                    branch_links=', '.join([ctxt.link(mtn.Branch(b)).html() for b in branches]),
                    path=path,
                    page_title=page_title,
                    revision=revision,
                    path_links=path_links(path_components),
                    row_class=row_class(),
                    mime_icon=mime_icon,
                    entries=info_for_manifest(cut_manifest_to_subdir()))

class RevisionGraph(object):
    def GET(self, ctxt, revision):
        output_png, output_imagemap = ancestry_graph(ctxt, revision)
        if os.access(output_png, os.R_OK):
            web.header('Content-Type', 'image/png')
            sys.stdout.write(open(output_png).read())
        else:
            return web.notfound()

class Json(object):
    def fill_from_certs(self, rv, certs):
        for cert in certs:
            if cert[4] != 'name':
                continue
            if cert[5] == 'author':
                rv['author'] = cert[7]
            elif cert[5] == 'date':
                revdate = common.parse_timecert(cert[7])
                rv['ago'] = common.ago(revdate)

    def BranchLink(self, ctxt, for_branch):
        rv = {
            'type' : 'branch',
            'branch' : for_branch,
        }
        branch = mtn.Branch(for_branch)
        changes, new_starting_point = Changes().branch_get_last_changes(ctxt.ops, branch, 0, 1)
        if len(changes) < 1:
            return web.notfound()
        if not changes:
            rv['error_string'] = 'no revisions in branch'
        else:
            rev, certs = changes[0]
            self.fill_from_certs(rv, certs)
        return rv

    def RevisionLink(self, ctxt, revision_id):
        rv = {
            'type' : 'revision',
            'revision_id' : revision_id,
        }
        rev = mtn.Revision(revision_id)
        certs = ctxt.ops.certs(rev)
        self.fill_from_certs(rv, certs)
        return rv

    def GET(self, ctxt, method, encoded_args):
        writer = json.JsonWriter()
        if not encoded_args.startswith('js_'):
            return web.notfound()
        args = json.read(binascii.unhexlify((encoded_args[3:])))
        if hasattr(self, method):
            rv = getattr(self, method)(ctxt, *args)
        else:
            return web.notfound()
        print writer.write(rv)

class BranchHead(object):
    def GET(self, ctxt, head_method, proxy_to, branch, extra_path):
        branch = mtn.Branch(branch)
        valid = ('browse', 'file', 'downloadfile', 'info', 'tar', 'graph')
        if not proxy_to in valid:
            return web.notfound()
        heads = [head for head in ctxt.ops.heads(branch.name)]
        if len(heads) == 0:
            return web.notfound()
        def proxyurl(revision):
            return ctxt.perdb_join('revision/' + proxy_to + '/' + revision + urllib.quote(extra_path))
        if len(heads) == 1 or head_method == 'anyhead':
            web.redirect(proxyurl(heads[0]))
        else:
            # present an option to the user to choose the head
            anyhead = '<a href="%s">link</a>' % (ctxt.perdb_join('branch/anyhead/' + \
                      proxy_to + '/' + urllib.quote(branch.name, safe = '')))
            head_links = []
            for revision in heads:
                author, date = '', ''
                for cert in ctxt.ops.certs(revision):
                    if cert[4] == 'name' and cert[5] == 'date':
                        date = cert[7]
                    elif cert[4] == 'name' and cert[5] == 'author':
                        author = mtn.Author(cert[7])
                head_links.append('<a href="%s">%s</a> %s at %s' % (proxyurl(revision),
                                                                    revision.abbrev(),
                                                                    ctxt.link(author).html(),
                                                                    hq(date)))
            ctxt.render('branchchoosehead.html',
                        page_title="Branch %s" % branch.name,
                        branch=branch,
                        proxy_to=proxy_to,
                        anyhead=anyhead,
                        head_links=head_links)

class Databases(object):
    def GET(self, ctxt):
        databases = []
        for k in ctxt.db_summary:
            databases.append ({'name':k, 'description':ctxt.db_summary[k]})
        ctxt.render('databases.html',
                    page_title="Change database",
                    databases=databases)


class MimeIcon(object):
    def GET(self, type, sub_type):
        if not mimeicon:
            return web.notfound()
        mime_type = type+'/'+sub_type
        icon_file = mimeicon.lookup(mime_type)
        if icon_file:
            web.header('Content-Type', 'image/png')
            sys.stdout.write(open(icon_file).read())
        else:
            return web.notfound()

class RobotsTxt(object):
    def GET(self, ctxt):
        web.header('Content-Type', 'text/plain')
        print "User-agent: *"
        for dbname in [None] + ctxt.db_summary.keys():
            for revision_page in ['tar', 'downloadfile', 'graph', 'file', 'browse', 'diff', 'info', 'graph']:
                # the goal is just to let a robot trawl through the most recent changes, and deny access
                # to expensive pointless things. We don't want a robot indexing every file in every revision,
                # as this is an enormous amount of information.
                for access_method in ['/revision/', '/branch/head/', '/branch/anyhead/', '/branch/changes/from/', '/json/', '/mimeicon/']:
                    disallow = access_method + revision_page
                    if not dbname is None:
                        disallow = '/' + dbname + disallow
                    print "Disallow:", disallow