view mercurial/hgweb.py @ 180:f25944662097

hgweb: Fix navigate to 0 bug
author mpm@selenic.com
date Fri, 27 May 2005 12:45:41 -0800
parents 8da1df932c16
children 089594a5bbde 089594a5bbde
line wrap: on
line source

#!/usr/bin/env python
#
# hgweb.py - 0.2 - 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
#    - web interface to a mercurial repository
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

# useful for debugging
import cgitb
cgitb.enable()

import os, cgi, time, re, difflib, sys, zlib
from mercurial.hg import *

def templatepath():
    for f in "templates/map", "../templates/map":
        p = os.path.join(os.path.dirname(__file__), f)
        if os.path.isfile(p): return p

def age(t):
    def plural(t, c):
        if c == 1: return t
        return t + "s"
    def fmt(t, c):
        return "%d %s" % (c, plural(t, c))

    now = time.time()
    delta = max(1, int(now - t))

    scales = [["second", 1],
              ["minute", 60],
              ["hour", 3600],
              ["day", 3600 * 24],
              ["week", 3600 * 24 * 7],
              ["month", 3600 * 24 * 30],
              ["year", 3600 * 24 * 365]]

    scales.reverse()

    for t, s in scales:
        n = delta / s
        if n >= 1: return fmt(t, n)

def nl2br(text):
    return text.replace('\n', '<br/>')

def obfuscate(text):
    return ''.join([ '&#%d' % ord(c) for c in text ])

def up(p):
    if p[0] != "/": p = "/" + p
    if p[-1] == "/": p = p[:-1]
    up = os.path.dirname(p)
    if up == "/":
        return "/"
    return up + "/"

def httphdr(type):
    print 'Content-type: %s\n' % type

def write(*things):
    for thing in things:
        if hasattr(thing, "__iter__"):
            for part in thing:
                write(part)
        else:
            sys.stdout.write(str(thing))

def template(tmpl, **map):
    while tmpl:
        m = re.search(r"#([a-zA-Z0-9]+)#", tmpl)
        if m:
            yield tmpl[:m.start(0)]
            v = map.get(m.group(1), "")
            yield callable(v) and v() or v
            tmpl = tmpl[m.end(0):]
        else:
            yield tmpl
            return

class templater:
    def __init__(self, mapfile):
        self.cache = {}
        self.map = {}
        self.base = os.path.dirname(mapfile)
        
        for l in file(mapfile):
            m = re.match(r'(\S+)\s*=\s*"(.*)"$', l)
            if m:
                self.cache[m.group(1)] = m.group(2)
            else:
                m = re.match(r'(\S+)\s*=\s*(\S+)', l)
                if m:
                    self.map[m.group(1)] = os.path.join(self.base, m.group(2))
                else:
                    raise "unknown map entry '%s'"  % l

    def __call__(self, t, **map):
        try:
            tmpl = self.cache[t]
        except KeyError:
            tmpl = self.cache[t] = file(self.map[t]).read()
        return template(tmpl, **map)
        
class hgweb:
    maxchanges = 20
    maxfiles = 10

    def __init__(self, path, name, templatemap = ""):
        templatemap = templatemap or templatepath()

        self.reponame = name
        self.repo = repository(ui(), path)
        self.t = templater(templatemap)

    def date(self, cs):
        return time.asctime(time.gmtime(float(cs[2].split(' ')[0])))

    def listfiles(self, files, mf):
        for f in files[:self.maxfiles]:
            yield self.t("filenodelink", node = hex(mf[f]), file = f)
        if len(files) > self.maxfiles:
            yield self.t("fileellipses")

    def listfilediffs(self, files, changeset):
        for f in files[:self.maxfiles]:
            yield self.t("filedifflink", node = hex(changeset), file = f)
        if len(files) > self.maxfiles:
            yield self.t("fileellipses")

    def parent(self, t1, node=nullid, rev=-1, **args):
        if node != hex(nullid):
            yield self.t(t1, node = node, rev = rev, **args)

    def diff(self, node1, node2, files):
        def filterfiles(list, files):
            l = [ x for x in list if x in files ]
            
            for f in files:
                if f[-1] != os.sep: f += os.sep
                l += [ x for x in list if x.startswith(f) ]
            return l

        parity = [0]
        def diffblock(diff, f, fn):
            yield self.t("diffblock",
                         lines = prettyprintlines(diff),
                         parity = parity[0],
                         file = f,
                         filenode = hex(fn))
            parity[0] = 1 - parity[0]
            
        def prettyprintlines(diff):
            for l in diff.splitlines(1):
                line = cgi.escape(l)
                if line.startswith('+'):
                    yield self.t("difflineplus", line = line)
                elif line.startswith('-'):
                    yield self.t("difflineminus", line = line)
                elif line.startswith('@'):
                    yield self.t("difflineat", line = line)
                else:
                    yield self.t("diffline", line = line)

        r = self.repo
        cl = r.changelog
        mf = r.manifest
        change1 = cl.read(node1)
        change2 = cl.read(node2)
        mmap1 = mf.read(change1[0])
        mmap2 = mf.read(change2[0])
        date1 = self.date(change1)
        date2 = self.date(change2)

        c, a, d = r.diffrevs(node1, node2)
        c, a, d = map(lambda x: filterfiles(x, files), (c, a, d))

        for f in c:
            to = r.file(f).read(mmap1[f])
            tn = r.file(f).read(mmap2[f])
            yield diffblock(mdiff.unidiff(to, date1, tn, date2, f), f, tn)
        for f in a:
            to = ""
            tn = r.file(f).read(mmap2[f])
            yield diffblock(mdiff.unidiff(to, date1, tn, date2, f), f, tn)
        for f in d:
            to = r.file(f).read(mmap1[f])
            tn = ""
            yield diffblock(mdiff.unidiff(to, date1, tn, date2, f), f, tn)

    def header(self):
        yield self.t("header", repo = self.reponame)

    def footer(self):
        yield self.t("footer", repo = self.reponame)

    def changelog(self, pos):
        def changenav():
            def seq(factor = 1):
                yield 1 * factor
                yield 3 * factor
                #yield 5 * factor
                for f in seq(factor * 10):
                    yield f

            l = []
            for f in seq():
                if f < self.maxchanges / 2: continue
                if f > count: break
                r = "%d" % f
                if pos + f < count - (f/2): l.append(("+" + r, pos + f))
                if pos - f >= 0 + (f/2): l.insert(0, ("-" + r, pos - f))

            yield self.t("naventry", rev = 0, label="(0)")
                    
            for label, rev in l:
                yield self.t("naventry", label = label, rev = rev)

            yield self.t("naventry", rev = count - 1, label="tip")

        def changelist():
            parity = (start - end) & 1
            cl = self.repo.changelog
            l = [] # build a list in forward order for efficiency
            for i in range(start, end + 1):
                n = cl.node(i)
                changes = cl.read(n)
                hn = hex(n)
                p1, p2 = cl.parents(n)
                t = float(changes[2].split(' ')[0])

                l.insert(0, self.t(
                    'changelogentry',
                    parity = parity,
                    author = obfuscate(changes[1]),
                    shortdesc = cgi.escape(changes[4].splitlines()[0]),
                    age = age(t),
                    parent1 = self.parent("changelogparent",
                                          hex(p1), cl.rev(p1)),
                    parent2 = self.parent("changelogparent",
                                          hex(p2), cl.rev(p2)),
                    p1 = hex(p1), p2 = hex(p2),
                    p1rev = cl.rev(p1), p2rev = cl.rev(p2),
                    manifest = hex(changes[0]),
                    desc = nl2br(cgi.escape(changes[4])),
                    date = time.asctime(time.gmtime(t)),
                    files = self.listfilediffs(changes[3], n),
                    rev = i,
                    node = hn))
                parity = 1 - parity

            yield l

        cl = self.repo.changelog
        mf = cl.read(cl.tip())[0]
        count = cl.count()
        end = min(pos, count - 1)
        start = max(0, pos - self.maxchanges)
        end = min(count - 1, start + self.maxchanges)

        yield self.t('changelog',
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     changenav = changenav,
                     manifest = hex(mf),
                     rev = pos, changesets = count, entries = changelist)

    def changeset(self, nodeid):
        n = bin(nodeid)
        cl = self.repo.changelog
        changes = cl.read(n)
        p1, p2 = cl.parents(n)
        p1rev, p2rev = cl.rev(p1), cl.rev(p2)
        t = float(changes[2].split(' ')[0])
        
        files = []
        mf = self.repo.manifest.read(changes[0])
        for f in changes[3]:
            files.append(self.t("filenodelink",
                                filenode = hex(mf[f]), file = f))

        def diff():
            yield self.diff(p1, n, changes[3])

        yield self.t('changeset',
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     diff = diff,
                     rev = cl.rev(n),
                     node = nodeid,
                     shortdesc = cgi.escape(changes[4].splitlines()[0]),
                     parent1 = self.parent("changesetparent",
                                           hex(p1), cl.rev(p1)),
                     parent2 = self.parent("changesetparent",
                                           hex(p2), cl.rev(p2)),
                     p1 = hex(p1), p2 = hex(p2),
                     p1rev = cl.rev(p1), p2rev = cl.rev(p2),
                     manifest = hex(changes[0]),
                     author = obfuscate(changes[1]),
                     desc = nl2br(cgi.escape(changes[4])),
                     date = time.asctime(time.gmtime(t)),
                     files = files)

    def filelog(self, f, filenode):
        cl = self.repo.changelog
        fl = self.repo.file(f)
        count = fl.count()

        def entries():
            l = []
            parity = (count - 1) & 1
            
            for i in range(count):

                n = fl.node(i)
                lr = fl.linkrev(n)
                cn = cl.node(lr)
                cs = cl.read(cl.node(lr))
                p1, p2 = fl.parents(n)
                t = float(cs[2].split(' ')[0])

                l.insert(0, self.t("filelogentry",
                                   parity = parity,
                                   filenode = hex(n),
                                   filerev = i,
                                   file = f,
                                   node = hex(cn),
                                   author = obfuscate(cs[1]),
                                   age = age(t),
                                   date = time.asctime(time.gmtime(t)),
                                   shortdesc = cgi.escape(cs[4].splitlines()[0]),
                                   p1 = hex(p1), p2 = hex(p2),
                                   p1rev = fl.rev(p1), p2rev = fl.rev(p2)))
                parity = 1 - parity

            yield l

        yield self.t("filelog",
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     file = f,
                     filenode = filenode,
                     entries = entries)

    def filerevision(self, f, node):
        fl = self.repo.file(f)
        n = bin(node)
        text = cgi.escape(fl.read(n))
        changerev = fl.linkrev(n)
        cl = self.repo.changelog
        cn = cl.node(changerev)
        cs = cl.read(cn)
        p1, p2 = fl.parents(n)
        t = float(cs[2].split(' ')[0])
        mfn = cs[0]

        def lines():
            for l, t in enumerate(text.splitlines(1)):
                yield self.t("fileline",
                             line = t,
                             linenumber = "% 6d" % (l + 1),
                             parity = l & 1)
        
        yield self.t("filerevision", file = f,
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     filenode = node,
                     path = up(f),
                     text = lines(),
                     rev = changerev,
                     node = hex(cn),
                     manifest = hex(mfn),
                     author = obfuscate(cs[1]),
                     age = age(t),
                     date = time.asctime(time.gmtime(t)),
                     shortdesc = cgi.escape(cs[4].splitlines()[0]),
                     parent1 = self.parent("filerevparent",
                                           hex(p1), fl.rev(p1), file=f),
                     parent2 = self.parent("filerevparent",
                                           hex(p2), fl.rev(p2), file=f),
                     p1 = hex(p1), p2 = hex(p2),
                     p1rev = fl.rev(p1), p2rev = fl.rev(p2))


    def fileannotate(self, f, node):
        bcache = {}
        ncache = {}
        fl = self.repo.file(f)
        n = bin(node)
        changerev = fl.linkrev(n)

        cl = self.repo.changelog
        cn = cl.node(changerev)
        cs = cl.read(cn)
        p1, p2 = fl.parents(n)
        t = float(cs[2].split(' ')[0])
        mfn = cs[0]

        def annotate():
            parity = 1
            last = None
            for r, l in fl.annotate(n):
                try:
                    cnode = ncache[r]
                except KeyError:
                    cnode = ncache[r] = self.repo.changelog.node(r)
                    
                try:
                    name = bcache[r]
                except KeyError:
                    cl = self.repo.changelog.read(cnode)
                    name = cl[1]
                    f = name.find('@')
                    if f >= 0:
                        name = name[:f]
                    bcache[r] = name

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

                yield self.t("annotateline",
                             parity = parity,
                             node = hex(cnode),
                             rev = r,
                             author = name,
                             file = f,
                             line = cgi.escape(l))

        yield self.t("fileannotate",
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     file = f,
                     filenode = node,
                     annotate = annotate,
                     path = up(f),
                     rev = changerev,
                     node = hex(cn),
                     manifest = hex(mfn),
                     author = obfuscate(cs[1]),
                     age = age(t),
                     date = time.asctime(time.gmtime(t)),
                     shortdesc = cgi.escape(cs[4].splitlines()[0]),
                     parent1 = self.parent("fileannotateparent",
                                           hex(p1), fl.rev(p1), file=f),
                     parent2 = self.parent("fileannotateparent",
                                           hex(p2), fl.rev(p2), file=f),
                     p1 = hex(p1), p2 = hex(p2),
                     p1rev = fl.rev(p1), p2rev = fl.rev(p2))

    def manifest(self, mnode, path):
        mf = self.repo.manifest.read(bin(mnode))
        rev = self.repo.manifest.rev(bin(mnode))
        node = self.repo.changelog.node(rev)

        files = {}
 
        p = path[1:]
        l = len(p)

        for f,n in mf.items():
            if f[:l] != p:
                continue
            remain = f[l:]
            if "/" in remain:
                short = remain[:remain.find("/") + 1] # bleah
                files[short] = (f, None)
            else:
                short = os.path.basename(remain)
                files[short] = (f, n)

        def filelist():
            parity = 0
            fl = files.keys()
            fl.sort()
            for f in fl:
                full, fnode = files[f]
                if fnode:
                    yield self.t("manifestfileentry",
                                 file = full,
                                 manifest = mnode,
                                 filenode = hex(fnode),
                                 parity = parity,
                                 basename = f)
                else:
                    yield self.t("manifestdirentry",
                                 parity = parity,
                                 path = os.path.join(path, f),
                                 manifest = mnode, basename = f[:-1])
                parity = 1 - parity

        yield self.t("manifest",
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     manifest = mnode,
                     rev = rev,
                     node = hex(node),
                     path = path,
                     up = up(path),
                     entries = filelist)

    def tags(self):
        cl = self.repo.changelog
        mf = cl.read(cl.tip())[0]

        self.repo.lookup(0) # prime the cache
        i = self.repo.tags.items()
        i.sort()

        def entries():
            parity = 0
            for k,n in i:
                yield self.t("tagentry",
                             parity = parity,
                             tag = k,
                             node = hex(n))
                parity = 1 - parity

        yield self.t("tags",
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     manifest = hex(mf),
                     entries = entries)

    def filediff(self, file, changeset):
        n = bin(changeset)
        cl = self.repo.changelog
        p1 = cl.parents(n)[0]
        cs = cl.read(n)
        mf = self.repo.manifest.read(cs[0])
        
        def diff():
            yield self.diff(p1, n, file)

        yield self.t("filediff",
                     header = self.header(),
                     footer = self.footer(),
                     repo = self.reponame,
                     file = file,
                     filenode = hex(mf[file]),
                     node = changeset,
                     rev = self.repo.changelog.rev(n),
                     p1 = hex(p1),
                     p1rev = self.repo.changelog.rev(p1),
                     diff = diff)
                     
    # add tags to things
    # tags -> list of changesets corresponding to tags
    # find tag, changeset, file

    def run(self):
        args = cgi.parse()

        if not args.has_key('cmd') or args['cmd'][0] == 'changelog':
            hi = self.repo.changelog.count()
            if args.has_key('rev'):
                hi = args['rev'][0]
                try:
                    hi = self.repo.changelog.rev(self.repo.lookup(hi))
                except KeyError:
                    hi = self.repo.changelog.count()

            write(self.changelog(hi))
            
        elif args['cmd'][0] == 'changeset':
            write(self.changeset(args['node'][0]))

        elif args['cmd'][0] == 'manifest':
            write(self.manifest(args['manifest'][0], args['path'][0]))

        elif args['cmd'][0] == 'tags':
            write(self.tags())

        elif args['cmd'][0] == 'filediff':
            write(self.filediff(args['file'][0], args['node'][0]))

        elif args['cmd'][0] == 'file':
            write(self.filerevision(args['file'][0], args['filenode'][0]))

        elif args['cmd'][0] == 'annotate':
            write(self.fileannotate(args['file'][0], args['filenode'][0]))

        elif args['cmd'][0] == 'filelog':
            write(self.filelog(args['file'][0], args['filenode'][0]))

        elif args['cmd'][0] == 'branches':
            httphdr("text/plain")
            nodes = []
            if args.has_key('nodes'):
                nodes = map(bin, args['nodes'][0].split(" "))
            for b in self.repo.branches(nodes):
                sys.stdout.write(" ".join(map(hex, b)) + "\n")

        elif args['cmd'][0] == 'between':
            httphdr("text/plain")
            nodes = []
            if args.has_key('pairs'):
                pairs = [ map(bin, p.split("-"))
                          for p in args['pairs'][0].split(" ") ]
            for b in self.repo.between(pairs):
                sys.stdout.write(" ".join(map(hex, b)) + "\n")

        elif args['cmd'][0] == 'changegroup':
            httphdr("application/hg-changegroup")
            nodes = []
            if args.has_key('roots'):
                nodes = map(bin, args['roots'][0].split(" "))

            z = zlib.compressobj()
            for chunk in self.repo.changegroup(nodes):
                sys.stdout.write(z.compress(chunk))

            sys.stdout.write(z.flush())

        else:
            write(self.t("error"))

def server(path, name, templates, address, port):

    import BaseHTTPServer
    import sys, os

    class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
        def do_POST(self):
            self.do_hgweb()

        def do_GET(self):
            self.do_hgweb()

        def do_hgweb(self):
            query = ""
            p = self.path.find("?")
            if p:
                query = self.path[p + 1:]
                query = query.replace('+', ' ')
        
            env = {}
            env['GATEWAY_INTERFACE'] = 'CGI/1.1'
            env['REQUEST_METHOD'] = self.command
            if query:
                env['QUERY_STRING'] = query
            host = self.address_string()
            if host != self.client_address[0]:
                env['REMOTE_HOST'] = host
                env['REMOTE_ADDR'] = self.client_address[0]

            if self.headers.typeheader is None:
                env['CONTENT_TYPE'] = self.headers.type
            else:
                env['CONTENT_TYPE'] = self.headers.typeheader
            length = self.headers.getheader('content-length')
            if length:
                env['CONTENT_LENGTH'] = length
            accept = []
            for line in self.headers.getallmatchingheaders('accept'):
                if line[:1] in "\t\n\r ":
                    accept.append(line.strip())
                else:
                    accept = accept + line[7:].split(',')
            env['HTTP_ACCEPT'] = ','.join(accept)

            os.environ.update(env)

            save = sys.argv, sys.stdin, sys.stdout, sys.stderr
            try:
                sys.stdin = self.rfile
                sys.stdout = self.wfile
                sys.argv = ["hgweb.py"]
                if '=' not in query:
                    sys.argv.append(query)
                self.send_response(200, "Script output follows")
                hg.run()
            finally:
                sys.argv, sys.stdin, sys.stdout, sys.stderr = save

    hg = hgweb(path, name, templates)
    httpd = BaseHTTPServer.HTTPServer((address, port), hgwebhandler)
    httpd.serve_forever()