merge with crew.
--- a/mercurial/hgweb.py Thu May 18 13:52:55 2006 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1142 +0,0 @@
-# hgweb.py - web interface to a mercurial repository
-#
-# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
-# Copyright 2005 Matt Mackall <mpm@selenic.com>
-#
-# This software may be used and distributed according to the terms
-# of the GNU General Public License, incorporated herein by reference.
-
-import os, cgi, sys
-import mimetypes
-from demandload import demandload
-demandload(globals(), "mdiff time re socket zlib errno ui hg ConfigParser")
-demandload(globals(), "tempfile StringIO BaseHTTPServer util SocketServer")
-demandload(globals(), "archival mimetypes templater urllib")
-from node import *
-from i18n import gettext as _
-
-def splitURI(uri):
- """ Return path and query splited from uri
-
- Just like CGI environment, the path is unquoted, the query is
- not.
- """
- if '?' in uri:
- path, query = uri.split('?', 1)
- else:
- path, query = uri, ''
- return urllib.unquote(path), query
-
-def up(p):
- if p[0] != "/":
- p = "/" + p
- if p[-1] == "/":
- p = p[:-1]
- up = os.path.dirname(p)
- if up == "/":
- return "/"
- return up + "/"
-
-def get_mtime(repo_path):
- hg_path = os.path.join(repo_path, ".hg")
- cl_path = os.path.join(hg_path, "00changelog.i")
- if os.path.exists(os.path.join(cl_path)):
- return os.stat(cl_path).st_mtime
- else:
- return os.stat(hg_path).st_mtime
-
-def staticfile(directory, fname):
- """return a file inside directory with guessed content-type header
-
- fname always uses '/' as directory separator and isn't allowed to
- contain unusual path components.
- Content-type is guessed using the mimetypes module.
- Return an empty string if fname is illegal or file not found.
-
- """
- parts = fname.split('/')
- path = directory
- for part in parts:
- if (part in ('', os.curdir, os.pardir) or
- os.sep in part or os.altsep is not None and os.altsep in part):
- return ""
- path = os.path.join(path, part)
- try:
- os.stat(path)
- ct = mimetypes.guess_type(path)[0] or "text/plain"
- return "Content-type: %s\n\n%s" % (ct, file(path).read())
- except (TypeError, OSError):
- # illegal fname or unreadable file
- return ""
-
-class hgrequest(object):
- def __init__(self, inp=None, out=None, env=None):
- self.inp = inp or sys.stdin
- self.out = out or sys.stdout
- self.env = env or os.environ
- self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
-
- def write(self, *things):
- for thing in things:
- if hasattr(thing, "__iter__"):
- for part in thing:
- self.write(part)
- else:
- try:
- self.out.write(str(thing))
- except socket.error, inst:
- if inst[0] != errno.ECONNRESET:
- raise
-
- def header(self, headers=[('Content-type','text/html')]):
- for header in headers:
- self.out.write("%s: %s\r\n" % header)
- self.out.write("\r\n")
-
- def httphdr(self, type, file="", size=0):
-
- headers = [('Content-type', type)]
- if file:
- headers.append(('Content-disposition', 'attachment; filename=%s' % file))
- if size > 0:
- headers.append(('Content-length', str(size)))
- self.header(headers)
-
-class hgweb(object):
- def __init__(self, repo, name=None):
- if type(repo) == type(""):
- self.repo = hg.repository(ui.ui(), repo)
- else:
- self.repo = repo
-
- self.mtime = -1
- self.reponame = name
- self.archives = 'zip', 'gz', 'bz2'
-
- def refresh(self):
- mtime = get_mtime(self.repo.root)
- if mtime != self.mtime:
- self.mtime = mtime
- self.repo = hg.repository(self.repo.ui, self.repo.root)
- self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
- self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
- self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
-
- def archivelist(self, nodeid):
- for i in self.archives:
- if self.repo.ui.configbool("web", "allow" + i, False):
- yield {"type" : i, "node" : nodeid, "url": ""}
-
- def listfiles(self, files, mf):
- for f in files[:self.maxfiles]:
- yield self.t("filenodelink", node=hex(mf[f]), file=f)
- if len(files) > self.maxfiles:
- yield self.t("fileellipses")
-
- def listfilediffs(self, files, changeset):
- for f in files[:self.maxfiles]:
- yield self.t("filedifflink", node=hex(changeset), file=f)
- if len(files) > self.maxfiles:
- yield self.t("fileellipses")
-
- def siblings(self, siblings=[], rev=None, hiderev=None, **args):
- if not rev:
- rev = lambda x: ""
- siblings = [s for s in siblings if s != nullid]
- if len(siblings) == 1 and rev(siblings[0]) == hiderev:
- return
- for s in siblings:
- yield dict(node=hex(s), rev=rev(s), **args)
-
- def renamelink(self, fl, node):
- r = fl.renamed(node)
- if r:
- return [dict(file=r[0], node=hex(r[1]))]
- return []
-
- def showtag(self, t1, node=nullid, **args):
- for t in self.repo.nodetags(node):
- yield self.t(t1, tag=t, **args)
-
- def diff(self, node1, node2, files):
- def filterfiles(filters, files):
- l = [x for x in files if x in filters]
-
- for t in filters:
- if t and t[-1] != os.sep:
- t += os.sep
- l += [x for x in files if x.startswith(t)]
- return l
-
- parity = [0]
- def diffblock(diff, f, fn):
- yield self.t("diffblock",
- lines=prettyprintlines(diff),
- parity=parity[0],
- file=f,
- filenode=hex(fn or nullid))
- parity[0] = 1 - parity[0]
-
- def prettyprintlines(diff):
- for l in diff.splitlines(1):
- if l.startswith('+'):
- yield self.t("difflineplus", line=l)
- elif l.startswith('-'):
- yield self.t("difflineminus", line=l)
- elif l.startswith('@'):
- yield self.t("difflineat", line=l)
- else:
- yield self.t("diffline", line=l)
-
- r = self.repo
- cl = r.changelog
- mf = r.manifest
- change1 = cl.read(node1)
- change2 = cl.read(node2)
- mmap1 = mf.read(change1[0])
- mmap2 = mf.read(change2[0])
- date1 = util.datestr(change1[2])
- date2 = util.datestr(change2[2])
-
- modified, added, removed, deleted, unknown = r.changes(node1, node2)
- if files:
- modified, added, removed = map(lambda x: filterfiles(files, x),
- (modified, added, removed))
-
- diffopts = self.repo.ui.diffopts()
- showfunc = diffopts['showfunc']
- ignorews = diffopts['ignorews']
- for f in modified:
- to = r.file(f).read(mmap1[f])
- tn = r.file(f).read(mmap2[f])
- yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
- showfunc=showfunc, ignorews=ignorews), f, tn)
- for f in added:
- to = None
- tn = r.file(f).read(mmap2[f])
- yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
- showfunc=showfunc, ignorews=ignorews), f, tn)
- for f in removed:
- to = r.file(f).read(mmap1[f])
- tn = None
- yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
- showfunc=showfunc, ignorews=ignorews), f, tn)
-
- def changelog(self, pos):
- def changenav(**map):
- def seq(factor, maxchanges=None):
- if maxchanges:
- yield maxchanges
- if maxchanges >= 20 and maxchanges <= 40:
- yield 50
- else:
- yield 1 * factor
- yield 3 * factor
- for f in seq(factor * 10):
- yield f
-
- l = []
- last = 0
- for f in seq(1, self.maxchanges):
- if f < self.maxchanges or f <= last:
- continue
- if f > count:
- break
- last = f
- r = "%d" % f
- if pos + f < count:
- l.append(("+" + r, pos + f))
- if pos - f >= 0:
- l.insert(0, ("-" + r, pos - f))
-
- yield {"rev": 0, "label": "(0)"}
-
- for label, rev in l:
- yield {"label": label, "rev": rev}
-
- yield {"label": "tip", "rev": "tip"}
-
- def changelist(**map):
- parity = (start - end) & 1
- cl = self.repo.changelog
- l = [] # build a list in forward order for efficiency
- for i in range(start, end):
- n = cl.node(i)
- changes = cl.read(n)
- hn = hex(n)
-
- l.insert(0, {"parity": parity,
- "author": changes[1],
- "parent": self.siblings(cl.parents(n), cl.rev,
- cl.rev(n) - 1),
- "child": self.siblings(cl.children(n), cl.rev,
- cl.rev(n) + 1),
- "changelogtag": self.showtag("changelogtag",n),
- "manifest": hex(changes[0]),
- "desc": changes[4],
- "date": changes[2],
- "files": self.listfilediffs(changes[3], n),
- "rev": i,
- "node": hn})
- parity = 1 - parity
-
- for e in l:
- yield e
-
- cl = self.repo.changelog
- mf = cl.read(cl.tip())[0]
- count = cl.count()
- start = max(0, pos - self.maxchanges + 1)
- end = min(count, start + self.maxchanges)
- pos = end - 1
-
- yield self.t('changelog',
- changenav=changenav,
- manifest=hex(mf),
- rev=pos, changesets=count, entries=changelist,
- archives=self.archivelist("tip"))
-
- def search(self, query):
-
- def changelist(**map):
- cl = self.repo.changelog
- count = 0
- qw = query.lower().split()
-
- def revgen():
- for i in range(cl.count() - 1, 0, -100):
- l = []
- for j in range(max(0, i - 100), i):
- n = cl.node(j)
- changes = cl.read(n)
- l.append((n, j, changes))
- l.reverse()
- for e in l:
- yield e
-
- for n, i, changes in revgen():
- miss = 0
- for q in qw:
- if not (q in changes[1].lower() or
- q in changes[4].lower() or
- q in " ".join(changes[3][:20]).lower()):
- miss = 1
- break
- if miss:
- continue
-
- count += 1
- hn = hex(n)
-
- yield self.t('searchentry',
- parity=count & 1,
- author=changes[1],
- parent=self.siblings(cl.parents(n), cl.rev),
- child=self.siblings(cl.children(n), cl.rev),
- changelogtag=self.showtag("changelogtag",n),
- manifest=hex(changes[0]),
- desc=changes[4],
- date=changes[2],
- files=self.listfilediffs(changes[3], n),
- rev=i,
- node=hn)
-
- if count >= self.maxchanges:
- break
-
- cl = self.repo.changelog
- mf = cl.read(cl.tip())[0]
-
- yield self.t('search',
- query=query,
- manifest=hex(mf),
- entries=changelist)
-
- def changeset(self, nodeid):
- cl = self.repo.changelog
- n = self.repo.lookup(nodeid)
- nodeid = hex(n)
- changes = cl.read(n)
- p1 = cl.parents(n)[0]
-
- files = []
- mf = self.repo.manifest.read(changes[0])
- for f in changes[3]:
- files.append(self.t("filenodelink",
- filenode=hex(mf.get(f, nullid)), file=f))
-
- def diff(**map):
- yield self.diff(p1, n, None)
-
- yield self.t('changeset',
- diff=diff,
- rev=cl.rev(n),
- node=nodeid,
- parent=self.siblings(cl.parents(n), cl.rev),
- child=self.siblings(cl.children(n), cl.rev),
- changesettag=self.showtag("changesettag",n),
- manifest=hex(changes[0]),
- author=changes[1],
- desc=changes[4],
- date=changes[2],
- files=files,
- archives=self.archivelist(nodeid))
-
- def filelog(self, f, filenode):
- cl = self.repo.changelog
- fl = self.repo.file(f)
- filenode = hex(fl.lookup(filenode))
- count = fl.count()
-
- def entries(**map):
- l = []
- parity = (count - 1) & 1
-
- for i in range(count):
- n = fl.node(i)
- lr = fl.linkrev(n)
- cn = cl.node(lr)
- cs = cl.read(cl.node(lr))
-
- l.insert(0, {"parity": parity,
- "filenode": hex(n),
- "filerev": i,
- "file": f,
- "node": hex(cn),
- "author": cs[1],
- "date": cs[2],
- "rename": self.renamelink(fl, n),
- "parent": self.siblings(fl.parents(n),
- fl.rev, file=f),
- "child": self.siblings(fl.children(n),
- fl.rev, file=f),
- "desc": cs[4]})
- parity = 1 - parity
-
- for e in l:
- yield e
-
- yield self.t("filelog", file=f, filenode=filenode, entries=entries)
-
- def filerevision(self, f, node):
- fl = self.repo.file(f)
- n = fl.lookup(node)
- node = hex(n)
- text = fl.read(n)
- changerev = fl.linkrev(n)
- cl = self.repo.changelog
- cn = cl.node(changerev)
- cs = cl.read(cn)
- mfn = cs[0]
-
- mt = mimetypes.guess_type(f)[0]
- rawtext = text
- if util.binary(text):
- mt = mt or 'application/octet-stream'
- text = "(binary:%s)" % mt
- mt = mt or 'text/plain'
-
- def lines():
- for l, t in enumerate(text.splitlines(1)):
- yield {"line": t,
- "linenumber": "% 6d" % (l + 1),
- "parity": l & 1}
-
- yield self.t("filerevision",
- file=f,
- filenode=node,
- path=up(f),
- text=lines(),
- raw=rawtext,
- mimetype=mt,
- rev=changerev,
- node=hex(cn),
- manifest=hex(mfn),
- author=cs[1],
- date=cs[2],
- parent=self.siblings(fl.parents(n), fl.rev, file=f),
- child=self.siblings(fl.children(n), fl.rev, file=f),
- rename=self.renamelink(fl, n),
- permissions=self.repo.manifest.readflags(mfn)[f])
-
- def fileannotate(self, f, node):
- bcache = {}
- ncache = {}
- fl = self.repo.file(f)
- n = fl.lookup(node)
- node = hex(n)
- changerev = fl.linkrev(n)
-
- cl = self.repo.changelog
- cn = cl.node(changerev)
- cs = cl.read(cn)
- mfn = cs[0]
-
- def annotate(**map):
- parity = 1
- last = None
- for r, l in fl.annotate(n):
- try:
- cnode = ncache[r]
- except KeyError:
- cnode = ncache[r] = self.repo.changelog.node(r)
-
- try:
- name = bcache[r]
- except KeyError:
- cl = self.repo.changelog.read(cnode)
- bcache[r] = name = self.repo.ui.shortuser(cl[1])
-
- if last != cnode:
- parity = 1 - parity
- last = cnode
-
- yield {"parity": parity,
- "node": hex(cnode),
- "rev": r,
- "author": name,
- "file": f,
- "line": l}
-
- yield self.t("fileannotate",
- file=f,
- filenode=node,
- annotate=annotate,
- path=up(f),
- rev=changerev,
- node=hex(cn),
- manifest=hex(mfn),
- author=cs[1],
- date=cs[2],
- rename=self.renamelink(fl, n),
- parent=self.siblings(fl.parents(n), fl.rev, file=f),
- child=self.siblings(fl.children(n), fl.rev, file=f),
- permissions=self.repo.manifest.readflags(mfn)[f])
-
- def manifest(self, mnode, path):
- man = self.repo.manifest
- mn = man.lookup(mnode)
- mnode = hex(mn)
- mf = man.read(mn)
- rev = man.rev(mn)
- node = self.repo.changelog.node(rev)
- mff = man.readflags(mn)
-
- files = {}
-
- p = path[1:]
- if p and p[-1] != "/":
- p += "/"
- l = len(p)
-
- for f,n in mf.items():
- if f[:l] != p:
- continue
- remain = f[l:]
- if "/" in remain:
- short = remain[:remain.find("/") + 1] # bleah
- files[short] = (f, None)
- else:
- short = os.path.basename(remain)
- files[short] = (f, n)
-
- def filelist(**map):
- parity = 0
- fl = files.keys()
- fl.sort()
- for f in fl:
- full, fnode = files[f]
- if not fnode:
- continue
-
- yield {"file": full,
- "manifest": mnode,
- "filenode": hex(fnode),
- "parity": parity,
- "basename": f,
- "permissions": mff[full]}
- parity = 1 - parity
-
- def dirlist(**map):
- parity = 0
- fl = files.keys()
- fl.sort()
- for f in fl:
- full, fnode = files[f]
- if fnode:
- continue
-
- yield {"parity": parity,
- "path": os.path.join(path, f),
- "manifest": mnode,
- "basename": f[:-1]}
- parity = 1 - parity
-
- yield self.t("manifest",
- manifest=mnode,
- rev=rev,
- node=hex(node),
- path=path,
- up=up(path),
- fentries=filelist,
- dentries=dirlist,
- archives=self.archivelist(hex(node)))
-
- def tags(self):
- cl = self.repo.changelog
- mf = cl.read(cl.tip())[0]
-
- i = self.repo.tagslist()
- i.reverse()
-
- def entries(notip=False, **map):
- parity = 0
- for k,n in i:
- if notip and k == "tip": continue
- yield {"parity": parity,
- "tag": k,
- "tagmanifest": hex(cl.read(n)[0]),
- "date": cl.read(n)[2],
- "node": hex(n)}
- parity = 1 - parity
-
- yield self.t("tags",
- manifest=hex(mf),
- entries=lambda **x: entries(False, **x),
- entriesnotip=lambda **x: entries(True, **x))
-
- def summary(self):
- cl = self.repo.changelog
- mf = cl.read(cl.tip())[0]
-
- i = self.repo.tagslist()
- i.reverse()
-
- def tagentries(**map):
- parity = 0
- count = 0
- for k,n in i:
- if k == "tip": # skip tip
- continue;
-
- count += 1
- if count > 10: # limit to 10 tags
- break;
-
- c = cl.read(n)
- m = c[0]
- t = c[2]
-
- yield self.t("tagentry",
- parity = parity,
- tag = k,
- node = hex(n),
- date = t,
- tagmanifest = hex(m))
- parity = 1 - parity
-
- def changelist(**map):
- parity = 0
- cl = self.repo.changelog
- l = [] # build a list in forward order for efficiency
- for i in range(start, end):
- n = cl.node(i)
- changes = cl.read(n)
- hn = hex(n)
- t = changes[2]
-
- l.insert(0, self.t(
- 'shortlogentry',
- parity = parity,
- author = changes[1],
- manifest = hex(changes[0]),
- desc = changes[4],
- date = t,
- rev = i,
- node = hn))
- parity = 1 - parity
-
- yield l
-
- cl = self.repo.changelog
- mf = cl.read(cl.tip())[0]
- count = cl.count()
- start = max(0, count - self.maxchanges)
- end = min(count, start + self.maxchanges)
- pos = end - 1
-
- yield self.t("summary",
- desc = self.repo.ui.config("web", "description", "unknown"),
- owner = (self.repo.ui.config("ui", "username") or # preferred
- self.repo.ui.config("web", "contact") or # deprecated
- self.repo.ui.config("web", "author", "unknown")), # also
- lastchange = (0, 0), # FIXME
- manifest = hex(mf),
- tags = tagentries,
- shortlog = changelist)
-
- def filediff(self, file, changeset):
- cl = self.repo.changelog
- n = self.repo.lookup(changeset)
- changeset = hex(n)
- p1 = cl.parents(n)[0]
- cs = cl.read(n)
- mf = self.repo.manifest.read(cs[0])
-
- def diff(**map):
- yield self.diff(p1, n, [file])
-
- yield self.t("filediff",
- file=file,
- filenode=hex(mf.get(file, nullid)),
- node=changeset,
- rev=self.repo.changelog.rev(n),
- parent=self.siblings(cl.parents(n), cl.rev),
- child=self.siblings(cl.children(n), cl.rev),
- diff=diff)
-
- archive_specs = {
- 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
- 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
- 'zip': ('application/zip', 'zip', '.zip', None),
- }
-
- def archive(self, req, cnode, type):
- reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
- name = "%s-%s" % (reponame, short(cnode))
- mimetype, artype, extension, encoding = self.archive_specs[type]
- headers = [('Content-type', mimetype),
- ('Content-disposition', 'attachment; filename=%s%s' %
- (name, extension))]
- if encoding:
- headers.append(('Content-encoding', encoding))
- req.header(headers)
- archival.archive(self.repo, req.out, cnode, artype, prefix=name)
-
- # add tags to things
- # tags -> list of changesets corresponding to tags
- # find tag, changeset, file
-
- def run(self, req=hgrequest()):
- def clean(path):
- p = util.normpath(path)
- if p[:2] == "..":
- raise "suspicious path"
- return p
-
- def header(**map):
- yield self.t("header", **map)
-
- def footer(**map):
- yield self.t("footer",
- motd=self.repo.ui.config("web", "motd", ""),
- **map)
-
- def expand_form(form):
- shortcuts = {
- 'cl': [('cmd', ['changelog']), ('rev', None)],
- 'cs': [('cmd', ['changeset']), ('node', None)],
- 'f': [('cmd', ['file']), ('filenode', None)],
- 'fl': [('cmd', ['filelog']), ('filenode', None)],
- 'fd': [('cmd', ['filediff']), ('node', None)],
- 'fa': [('cmd', ['annotate']), ('filenode', None)],
- 'mf': [('cmd', ['manifest']), ('manifest', None)],
- 'ca': [('cmd', ['archive']), ('node', None)],
- 'tags': [('cmd', ['tags'])],
- 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
- 'static': [('cmd', ['static']), ('file', None)]
- }
-
- for k in shortcuts.iterkeys():
- if form.has_key(k):
- for name, value in shortcuts[k]:
- if value is None:
- value = form[k]
- form[name] = value
- del form[k]
-
- self.refresh()
-
- expand_form(req.form)
-
- t = self.repo.ui.config("web", "templates", templater.templatepath())
- static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
- m = os.path.join(t, "map")
- style = self.repo.ui.config("web", "style", "")
- if req.form.has_key('style'):
- style = req.form['style'][0]
- if style:
- b = os.path.basename("map-" + style)
- p = os.path.join(t, b)
- if os.path.isfile(p):
- m = p
-
- port = req.env["SERVER_PORT"]
- port = port != "80" and (":" + port) or ""
- uri = req.env["REQUEST_URI"]
- if "?" in uri:
- uri = uri.split("?")[0]
- url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
- if not self.reponame:
- self.reponame = (self.repo.ui.config("web", "name")
- or uri.strip('/') or self.repo.root)
-
- self.t = templater.templater(m, templater.common_filters,
- defaults={"url": url,
- "repo": self.reponame,
- "header": header,
- "footer": footer,
- })
-
- if not req.form.has_key('cmd'):
- req.form['cmd'] = [self.t.cache['default'],]
-
- cmd = req.form['cmd'][0]
- if cmd == 'changelog':
- hi = self.repo.changelog.count() - 1
- if req.form.has_key('rev'):
- hi = req.form['rev'][0]
- try:
- hi = self.repo.changelog.rev(self.repo.lookup(hi))
- except hg.RepoError:
- req.write(self.search(hi)) # XXX redirect to 404 page?
- return
-
- req.write(self.changelog(hi))
-
- elif cmd == 'changeset':
- req.write(self.changeset(req.form['node'][0]))
-
- elif cmd == 'manifest':
- req.write(self.manifest(req.form['manifest'][0],
- clean(req.form['path'][0])))
-
- elif cmd == 'tags':
- req.write(self.tags())
-
- elif cmd == 'summary':
- req.write(self.summary())
-
- elif cmd == 'filediff':
- req.write(self.filediff(clean(req.form['file'][0]),
- req.form['node'][0]))
-
- elif cmd == 'file':
- req.write(self.filerevision(clean(req.form['file'][0]),
- req.form['filenode'][0]))
-
- elif cmd == 'annotate':
- req.write(self.fileannotate(clean(req.form['file'][0]),
- req.form['filenode'][0]))
-
- elif cmd == 'filelog':
- req.write(self.filelog(clean(req.form['file'][0]),
- req.form['filenode'][0]))
-
- elif cmd == 'heads':
- req.httphdr("application/mercurial-0.1")
- h = self.repo.heads()
- req.write(" ".join(map(hex, h)) + "\n")
-
- elif cmd == 'branches':
- req.httphdr("application/mercurial-0.1")
- nodes = []
- if req.form.has_key('nodes'):
- nodes = map(bin, req.form['nodes'][0].split(" "))
- for b in self.repo.branches(nodes):
- req.write(" ".join(map(hex, b)) + "\n")
-
- elif cmd == 'between':
- req.httphdr("application/mercurial-0.1")
- nodes = []
- if req.form.has_key('pairs'):
- pairs = [map(bin, p.split("-"))
- for p in req.form['pairs'][0].split(" ")]
- for b in self.repo.between(pairs):
- req.write(" ".join(map(hex, b)) + "\n")
-
- elif cmd == 'changegroup':
- req.httphdr("application/mercurial-0.1")
- nodes = []
- if not self.allowpull:
- return
-
- if req.form.has_key('roots'):
- nodes = map(bin, req.form['roots'][0].split(" "))
-
- z = zlib.compressobj()
- f = self.repo.changegroup(nodes, 'serve')
- while 1:
- chunk = f.read(4096)
- if not chunk:
- break
- req.write(z.compress(chunk))
-
- req.write(z.flush())
-
- elif cmd == 'archive':
- changeset = self.repo.lookup(req.form['node'][0])
- type = req.form['type'][0]
- if (type in self.archives and
- self.repo.ui.configbool("web", "allow" + type, False)):
- self.archive(req, changeset, type)
- return
-
- req.write(self.t("error"))
-
- elif cmd == 'static':
- fname = req.form['file'][0]
- req.write(staticfile(static, fname)
- or self.t("error", error="%r not found" % fname))
-
- else:
- req.write(self.t("error"))
-
-def create_server(ui, repo):
- use_threads = True
-
- def openlog(opt, default):
- if opt and opt != '-':
- return open(opt, 'w')
- return default
-
- address = ui.config("web", "address", "")
- port = int(ui.config("web", "port", 8000))
- use_ipv6 = ui.configbool("web", "ipv6")
- webdir_conf = ui.config("web", "webdir_conf")
- accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
- errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
-
- if use_threads:
- try:
- from threading import activeCount
- except ImportError:
- use_threads = False
-
- if use_threads:
- _mixin = SocketServer.ThreadingMixIn
- else:
- if hasattr(os, "fork"):
- _mixin = SocketServer.ForkingMixIn
- else:
- class _mixin: pass
-
- class MercurialHTTPServer(_mixin, BaseHTTPServer.HTTPServer):
- pass
-
- class IPv6HTTPServer(MercurialHTTPServer):
- address_family = getattr(socket, 'AF_INET6', None)
-
- def __init__(self, *args, **kwargs):
- if self.address_family is None:
- raise hg.RepoError(_('IPv6 not available on this system'))
- BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
-
- class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
-
- def log_error(self, format, *args):
- errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
- self.log_date_time_string(),
- format % args))
-
- def log_message(self, format, *args):
- accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
- self.log_date_time_string(),
- format % args))
-
- def do_POST(self):
- try:
- self.do_hgweb()
- except socket.error, inst:
- if inst[0] != errno.EPIPE:
- raise
-
- def do_GET(self):
- self.do_POST()
-
- def do_hgweb(self):
- path_info, query = splitURI(self.path)
-
- env = {}
- env['GATEWAY_INTERFACE'] = 'CGI/1.1'
- env['REQUEST_METHOD'] = self.command
- env['SERVER_NAME'] = self.server.server_name
- env['SERVER_PORT'] = str(self.server.server_port)
- env['REQUEST_URI'] = "/"
- env['PATH_INFO'] = path_info
- if query:
- env['QUERY_STRING'] = query
- host = self.address_string()
- if host != self.client_address[0]:
- env['REMOTE_HOST'] = host
- env['REMOTE_ADDR'] = self.client_address[0]
-
- if self.headers.typeheader is None:
- env['CONTENT_TYPE'] = self.headers.type
- else:
- env['CONTENT_TYPE'] = self.headers.typeheader
- length = self.headers.getheader('content-length')
- if length:
- env['CONTENT_LENGTH'] = length
- accept = []
- for line in self.headers.getallmatchingheaders('accept'):
- if line[:1] in "\t\n\r ":
- accept.append(line.strip())
- else:
- accept = accept + line[7:].split(',')
- env['HTTP_ACCEPT'] = ','.join(accept)
-
- req = hgrequest(self.rfile, self.wfile, env)
- self.send_response(200, "Script output follows")
-
- if webdir_conf:
- hgwebobj = hgwebdir(webdir_conf)
- elif repo is not None:
- hgwebobj = hgweb(repo.__class__(repo.ui, repo.origroot))
- else:
- raise hg.RepoError(_('no repo found'))
- hgwebobj.run(req)
-
-
- if use_ipv6:
- return IPv6HTTPServer((address, port), hgwebhandler)
- else:
- return MercurialHTTPServer((address, port), hgwebhandler)
-
-# This is a stopgap
-class hgwebdir(object):
- def __init__(self, config):
- def cleannames(items):
- return [(name.strip(os.sep), path) for name, path in items]
-
- self.motd = ""
- self.repos_sorted = ('name', False)
- if isinstance(config, (list, tuple)):
- self.repos = cleannames(config)
- self.repos_sorted = ('', False)
- elif isinstance(config, dict):
- self.repos = cleannames(config.items())
- self.repos.sort()
- else:
- cp = ConfigParser.SafeConfigParser()
- cp.read(config)
- self.repos = []
- if cp.has_section('web') and cp.has_option('web', 'motd'):
- self.motd = cp.get('web', 'motd')
- if cp.has_section('paths'):
- self.repos.extend(cleannames(cp.items('paths')))
- if cp.has_section('collections'):
- for prefix, root in cp.items('collections'):
- for path in util.walkrepos(root):
- repo = os.path.normpath(path)
- name = repo
- if name.startswith(prefix):
- name = name[len(prefix):]
- self.repos.append((name.lstrip(os.sep), repo))
- self.repos.sort()
-
- def run(self, req=hgrequest()):
- def header(**map):
- yield tmpl("header", **map)
-
- def footer(**map):
- yield tmpl("footer", motd=self.motd, **map)
-
- m = os.path.join(templater.templatepath(), "map")
- tmpl = templater.templater(m, templater.common_filters,
- defaults={"header": header,
- "footer": footer})
-
- def archivelist(ui, nodeid, url):
- for i in ['zip', 'gz', 'bz2']:
- if ui.configbool("web", "allow" + i, False):
- yield {"type" : i, "node": nodeid, "url": url}
-
- def entries(sortcolumn="", descending=False, **map):
- rows = []
- parity = 0
- for name, path in self.repos:
- u = ui.ui()
- try:
- u.readconfig(os.path.join(path, '.hg', 'hgrc'))
- except IOError:
- pass
- get = u.config
-
- url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
- .replace("//", "/"))
-
- # update time with local timezone
- try:
- d = (get_mtime(path), util.makedate()[1])
- except OSError:
- continue
-
- contact = (get("ui", "username") or # preferred
- get("web", "contact") or # deprecated
- get("web", "author", "")) # also
- description = get("web", "description", "")
- name = get("web", "name", name)
- row = dict(contact=contact or "unknown",
- contact_sort=contact.upper() or "unknown",
- name=name,
- name_sort=name,
- url=url,
- description=description or "unknown",
- description_sort=description.upper() or "unknown",
- lastchange=d,
- lastchange_sort=d[1]-d[0],
- archives=archivelist(u, "tip", url))
- if (not sortcolumn
- or (sortcolumn, descending) == self.repos_sorted):
- # fast path for unsorted output
- row['parity'] = parity
- parity = 1 - parity
- yield row
- else:
- rows.append((row["%s_sort" % sortcolumn], row))
- if rows:
- rows.sort()
- if descending:
- rows.reverse()
- for key, row in rows:
- row['parity'] = parity
- parity = 1 - parity
- yield row
-
- virtual = req.env.get("PATH_INFO", "").strip('/')
- if virtual:
- real = dict(self.repos).get(virtual)
- if real:
- try:
- hgweb(real).run(req)
- except IOError, inst:
- req.write(tmpl("error", error=inst.strerror))
- except hg.RepoError, inst:
- req.write(tmpl("error", error=str(inst)))
- else:
- req.write(tmpl("notfound", repo=virtual))
- else:
- if req.form.has_key('static'):
- static = os.path.join(templater.templatepath(), "static")
- fname = req.form['static'][0]
- req.write(staticfile(static, fname)
- or tmpl("error", error="%r not found" % fname))
- else:
- sortable = ["name", "description", "contact", "lastchange"]
- sortcolumn, descending = self.repos_sorted
- if req.form.has_key('sort'):
- sortcolumn = req.form['sort'][0]
- descending = sortcolumn.startswith('-')
- if descending:
- sortcolumn = sortcolumn[1:]
- if sortcolumn not in sortable:
- sortcolumn = ""
-
- sort = [("sort_%s" % column,
- "%s%s" % ((not descending and column == sortcolumn)
- and "-" or "", column))
- for column in sortable]
- req.write(tmpl("index", entries=entries,
- sortcolumn=sortcolumn, descending=descending,
- **dict(sort)))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/hgweb/__init__.py Thu May 18 16:49:45 2006 -0700
@@ -0,0 +1,1142 @@
+# hgweb.py - web interface to a mercurial repository
+#
+# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
+# Copyright 2005 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+import os, cgi, sys
+import mimetypes
+from mercurial.demandload import demandload
+demandload(globals(), "time re socket zlib errno ConfigParser tempfile")
+demandload(globals(), "StringIO BaseHTTPServer SocketServer urllib")
+demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
+from mercurial.node import *
+from mercurial.i18n import gettext as _
+
+def splitURI(uri):
+ """ Return path and query splited from uri
+
+ Just like CGI environment, the path is unquoted, the query is
+ not.
+ """
+ if '?' in uri:
+ path, query = uri.split('?', 1)
+ else:
+ path, query = uri, ''
+ return urllib.unquote(path), query
+
+def up(p):
+ if p[0] != "/":
+ p = "/" + p
+ if p[-1] == "/":
+ p = p[:-1]
+ up = os.path.dirname(p)
+ if up == "/":
+ return "/"
+ return up + "/"
+
+def get_mtime(repo_path):
+ hg_path = os.path.join(repo_path, ".hg")
+ cl_path = os.path.join(hg_path, "00changelog.i")
+ if os.path.exists(os.path.join(cl_path)):
+ return os.stat(cl_path).st_mtime
+ else:
+ return os.stat(hg_path).st_mtime
+
+def staticfile(directory, fname):
+ """return a file inside directory with guessed content-type header
+
+ fname always uses '/' as directory separator and isn't allowed to
+ contain unusual path components.
+ Content-type is guessed using the mimetypes module.
+ Return an empty string if fname is illegal or file not found.
+
+ """
+ parts = fname.split('/')
+ path = directory
+ for part in parts:
+ if (part in ('', os.curdir, os.pardir) or
+ os.sep in part or os.altsep is not None and os.altsep in part):
+ return ""
+ path = os.path.join(path, part)
+ try:
+ os.stat(path)
+ ct = mimetypes.guess_type(path)[0] or "text/plain"
+ return "Content-type: %s\n\n%s" % (ct, file(path).read())
+ except (TypeError, OSError):
+ # illegal fname or unreadable file
+ return ""
+
+class hgrequest(object):
+ def __init__(self, inp=None, out=None, env=None):
+ self.inp = inp or sys.stdin
+ self.out = out or sys.stdout
+ self.env = env or os.environ
+ self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
+
+ def write(self, *things):
+ for thing in things:
+ if hasattr(thing, "__iter__"):
+ for part in thing:
+ self.write(part)
+ else:
+ try:
+ self.out.write(str(thing))
+ except socket.error, inst:
+ if inst[0] != errno.ECONNRESET:
+ raise
+
+ def header(self, headers=[('Content-type','text/html')]):
+ for header in headers:
+ self.out.write("%s: %s\r\n" % header)
+ self.out.write("\r\n")
+
+ def httphdr(self, type, file="", size=0):
+
+ headers = [('Content-type', type)]
+ if file:
+ headers.append(('Content-disposition', 'attachment; filename=%s' % file))
+ if size > 0:
+ headers.append(('Content-length', str(size)))
+ self.header(headers)
+
+class hgweb(object):
+ def __init__(self, repo, name=None):
+ if type(repo) == type(""):
+ self.repo = hg.repository(ui.ui(), repo)
+ else:
+ self.repo = repo
+
+ self.mtime = -1
+ self.reponame = name
+ self.archives = 'zip', 'gz', 'bz2'
+
+ def refresh(self):
+ mtime = get_mtime(self.repo.root)
+ if mtime != self.mtime:
+ self.mtime = mtime
+ self.repo = hg.repository(self.repo.ui, self.repo.root)
+ self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
+ self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
+ self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
+
+ def archivelist(self, nodeid):
+ for i in self.archives:
+ if self.repo.ui.configbool("web", "allow" + i, False):
+ yield {"type" : i, "node" : nodeid, "url": ""}
+
+ def listfiles(self, files, mf):
+ for f in files[:self.maxfiles]:
+ yield self.t("filenodelink", node=hex(mf[f]), file=f)
+ if len(files) > self.maxfiles:
+ yield self.t("fileellipses")
+
+ def listfilediffs(self, files, changeset):
+ for f in files[:self.maxfiles]:
+ yield self.t("filedifflink", node=hex(changeset), file=f)
+ if len(files) > self.maxfiles:
+ yield self.t("fileellipses")
+
+ def siblings(self, siblings=[], rev=None, hiderev=None, **args):
+ if not rev:
+ rev = lambda x: ""
+ siblings = [s for s in siblings if s != nullid]
+ if len(siblings) == 1 and rev(siblings[0]) == hiderev:
+ return
+ for s in siblings:
+ yield dict(node=hex(s), rev=rev(s), **args)
+
+ def renamelink(self, fl, node):
+ r = fl.renamed(node)
+ if r:
+ return [dict(file=r[0], node=hex(r[1]))]
+ return []
+
+ def showtag(self, t1, node=nullid, **args):
+ for t in self.repo.nodetags(node):
+ yield self.t(t1, tag=t, **args)
+
+ def diff(self, node1, node2, files):
+ def filterfiles(filters, files):
+ l = [x for x in files if x in filters]
+
+ for t in filters:
+ if t and t[-1] != os.sep:
+ t += os.sep
+ l += [x for x in files if x.startswith(t)]
+ return l
+
+ parity = [0]
+ def diffblock(diff, f, fn):
+ yield self.t("diffblock",
+ lines=prettyprintlines(diff),
+ parity=parity[0],
+ file=f,
+ filenode=hex(fn or nullid))
+ parity[0] = 1 - parity[0]
+
+ def prettyprintlines(diff):
+ for l in diff.splitlines(1):
+ if l.startswith('+'):
+ yield self.t("difflineplus", line=l)
+ elif l.startswith('-'):
+ yield self.t("difflineminus", line=l)
+ elif l.startswith('@'):
+ yield self.t("difflineat", line=l)
+ else:
+ yield self.t("diffline", line=l)
+
+ r = self.repo
+ cl = r.changelog
+ mf = r.manifest
+ change1 = cl.read(node1)
+ change2 = cl.read(node2)
+ mmap1 = mf.read(change1[0])
+ mmap2 = mf.read(change2[0])
+ date1 = util.datestr(change1[2])
+ date2 = util.datestr(change2[2])
+
+ modified, added, removed, deleted, unknown = r.changes(node1, node2)
+ if files:
+ modified, added, removed = map(lambda x: filterfiles(files, x),
+ (modified, added, removed))
+
+ diffopts = self.repo.ui.diffopts()
+ showfunc = diffopts['showfunc']
+ ignorews = diffopts['ignorews']
+ for f in modified:
+ to = r.file(f).read(mmap1[f])
+ tn = r.file(f).read(mmap2[f])
+ yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
+ showfunc=showfunc, ignorews=ignorews), f, tn)
+ for f in added:
+ to = None
+ tn = r.file(f).read(mmap2[f])
+ yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
+ showfunc=showfunc, ignorews=ignorews), f, tn)
+ for f in removed:
+ to = r.file(f).read(mmap1[f])
+ tn = None
+ yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
+ showfunc=showfunc, ignorews=ignorews), f, tn)
+
+ def changelog(self, pos):
+ def changenav(**map):
+ def seq(factor, maxchanges=None):
+ if maxchanges:
+ yield maxchanges
+ if maxchanges >= 20 and maxchanges <= 40:
+ yield 50
+ else:
+ yield 1 * factor
+ yield 3 * factor
+ for f in seq(factor * 10):
+ yield f
+
+ l = []
+ last = 0
+ for f in seq(1, self.maxchanges):
+ if f < self.maxchanges or f <= last:
+ continue
+ if f > count:
+ break
+ last = f
+ r = "%d" % f
+ if pos + f < count:
+ l.append(("+" + r, pos + f))
+ if pos - f >= 0:
+ l.insert(0, ("-" + r, pos - f))
+
+ yield {"rev": 0, "label": "(0)"}
+
+ for label, rev in l:
+ yield {"label": label, "rev": rev}
+
+ yield {"label": "tip", "rev": "tip"}
+
+ def changelist(**map):
+ parity = (start - end) & 1
+ cl = self.repo.changelog
+ l = [] # build a list in forward order for efficiency
+ for i in range(start, end):
+ n = cl.node(i)
+ changes = cl.read(n)
+ hn = hex(n)
+
+ l.insert(0, {"parity": parity,
+ "author": changes[1],
+ "parent": self.siblings(cl.parents(n), cl.rev,
+ cl.rev(n) - 1),
+ "child": self.siblings(cl.children(n), cl.rev,
+ cl.rev(n) + 1),
+ "changelogtag": self.showtag("changelogtag",n),
+ "manifest": hex(changes[0]),
+ "desc": changes[4],
+ "date": changes[2],
+ "files": self.listfilediffs(changes[3], n),
+ "rev": i,
+ "node": hn})
+ parity = 1 - parity
+
+ for e in l:
+ yield e
+
+ cl = self.repo.changelog
+ mf = cl.read(cl.tip())[0]
+ count = cl.count()
+ start = max(0, pos - self.maxchanges + 1)
+ end = min(count, start + self.maxchanges)
+ pos = end - 1
+
+ yield self.t('changelog',
+ changenav=changenav,
+ manifest=hex(mf),
+ rev=pos, changesets=count, entries=changelist,
+ archives=self.archivelist("tip"))
+
+ def search(self, query):
+
+ def changelist(**map):
+ cl = self.repo.changelog
+ count = 0
+ qw = query.lower().split()
+
+ def revgen():
+ for i in range(cl.count() - 1, 0, -100):
+ l = []
+ for j in range(max(0, i - 100), i):
+ n = cl.node(j)
+ changes = cl.read(n)
+ l.append((n, j, changes))
+ l.reverse()
+ for e in l:
+ yield e
+
+ for n, i, changes in revgen():
+ miss = 0
+ for q in qw:
+ if not (q in changes[1].lower() or
+ q in changes[4].lower() or
+ q in " ".join(changes[3][:20]).lower()):
+ miss = 1
+ break
+ if miss:
+ continue
+
+ count += 1
+ hn = hex(n)
+
+ yield self.t('searchentry',
+ parity=count & 1,
+ author=changes[1],
+ parent=self.siblings(cl.parents(n), cl.rev),
+ child=self.siblings(cl.children(n), cl.rev),
+ changelogtag=self.showtag("changelogtag",n),
+ manifest=hex(changes[0]),
+ desc=changes[4],
+ date=changes[2],
+ files=self.listfilediffs(changes[3], n),
+ rev=i,
+ node=hn)
+
+ if count >= self.maxchanges:
+ break
+
+ cl = self.repo.changelog
+ mf = cl.read(cl.tip())[0]
+
+ yield self.t('search',
+ query=query,
+ manifest=hex(mf),
+ entries=changelist)
+
+ def changeset(self, nodeid):
+ cl = self.repo.changelog
+ n = self.repo.lookup(nodeid)
+ nodeid = hex(n)
+ changes = cl.read(n)
+ p1 = cl.parents(n)[0]
+
+ files = []
+ mf = self.repo.manifest.read(changes[0])
+ for f in changes[3]:
+ files.append(self.t("filenodelink",
+ filenode=hex(mf.get(f, nullid)), file=f))
+
+ def diff(**map):
+ yield self.diff(p1, n, None)
+
+ yield self.t('changeset',
+ diff=diff,
+ rev=cl.rev(n),
+ node=nodeid,
+ parent=self.siblings(cl.parents(n), cl.rev),
+ child=self.siblings(cl.children(n), cl.rev),
+ changesettag=self.showtag("changesettag",n),
+ manifest=hex(changes[0]),
+ author=changes[1],
+ desc=changes[4],
+ date=changes[2],
+ files=files,
+ archives=self.archivelist(nodeid))
+
+ def filelog(self, f, filenode):
+ cl = self.repo.changelog
+ fl = self.repo.file(f)
+ filenode = hex(fl.lookup(filenode))
+ count = fl.count()
+
+ def entries(**map):
+ l = []
+ parity = (count - 1) & 1
+
+ for i in range(count):
+ n = fl.node(i)
+ lr = fl.linkrev(n)
+ cn = cl.node(lr)
+ cs = cl.read(cl.node(lr))
+
+ l.insert(0, {"parity": parity,
+ "filenode": hex(n),
+ "filerev": i,
+ "file": f,
+ "node": hex(cn),
+ "author": cs[1],
+ "date": cs[2],
+ "rename": self.renamelink(fl, n),
+ "parent": self.siblings(fl.parents(n),
+ fl.rev, file=f),
+ "child": self.siblings(fl.children(n),
+ fl.rev, file=f),
+ "desc": cs[4]})
+ parity = 1 - parity
+
+ for e in l:
+ yield e
+
+ yield self.t("filelog", file=f, filenode=filenode, entries=entries)
+
+ def filerevision(self, f, node):
+ fl = self.repo.file(f)
+ n = fl.lookup(node)
+ node = hex(n)
+ text = fl.read(n)
+ changerev = fl.linkrev(n)
+ cl = self.repo.changelog
+ cn = cl.node(changerev)
+ cs = cl.read(cn)
+ mfn = cs[0]
+
+ mt = mimetypes.guess_type(f)[0]
+ rawtext = text
+ if util.binary(text):
+ mt = mt or 'application/octet-stream'
+ text = "(binary:%s)" % mt
+ mt = mt or 'text/plain'
+
+ def lines():
+ for l, t in enumerate(text.splitlines(1)):
+ yield {"line": t,
+ "linenumber": "% 6d" % (l + 1),
+ "parity": l & 1}
+
+ yield self.t("filerevision",
+ file=f,
+ filenode=node,
+ path=up(f),
+ text=lines(),
+ raw=rawtext,
+ mimetype=mt,
+ rev=changerev,
+ node=hex(cn),
+ manifest=hex(mfn),
+ author=cs[1],
+ date=cs[2],
+ parent=self.siblings(fl.parents(n), fl.rev, file=f),
+ child=self.siblings(fl.children(n), fl.rev, file=f),
+ rename=self.renamelink(fl, n),
+ permissions=self.repo.manifest.readflags(mfn)[f])
+
+ def fileannotate(self, f, node):
+ bcache = {}
+ ncache = {}
+ fl = self.repo.file(f)
+ n = fl.lookup(node)
+ node = hex(n)
+ changerev = fl.linkrev(n)
+
+ cl = self.repo.changelog
+ cn = cl.node(changerev)
+ cs = cl.read(cn)
+ mfn = cs[0]
+
+ def annotate(**map):
+ parity = 1
+ last = None
+ for r, l in fl.annotate(n):
+ try:
+ cnode = ncache[r]
+ except KeyError:
+ cnode = ncache[r] = self.repo.changelog.node(r)
+
+ try:
+ name = bcache[r]
+ except KeyError:
+ cl = self.repo.changelog.read(cnode)
+ bcache[r] = name = self.repo.ui.shortuser(cl[1])
+
+ if last != cnode:
+ parity = 1 - parity
+ last = cnode
+
+ yield {"parity": parity,
+ "node": hex(cnode),
+ "rev": r,
+ "author": name,
+ "file": f,
+ "line": l}
+
+ yield self.t("fileannotate",
+ file=f,
+ filenode=node,
+ annotate=annotate,
+ path=up(f),
+ rev=changerev,
+ node=hex(cn),
+ manifest=hex(mfn),
+ author=cs[1],
+ date=cs[2],
+ rename=self.renamelink(fl, n),
+ parent=self.siblings(fl.parents(n), fl.rev, file=f),
+ child=self.siblings(fl.children(n), fl.rev, file=f),
+ permissions=self.repo.manifest.readflags(mfn)[f])
+
+ def manifest(self, mnode, path):
+ man = self.repo.manifest
+ mn = man.lookup(mnode)
+ mnode = hex(mn)
+ mf = man.read(mn)
+ rev = man.rev(mn)
+ node = self.repo.changelog.node(rev)
+ mff = man.readflags(mn)
+
+ files = {}
+
+ p = path[1:]
+ if p and p[-1] != "/":
+ p += "/"
+ l = len(p)
+
+ for f,n in mf.items():
+ if f[:l] != p:
+ continue
+ remain = f[l:]
+ if "/" in remain:
+ short = remain[:remain.find("/") + 1] # bleah
+ files[short] = (f, None)
+ else:
+ short = os.path.basename(remain)
+ files[short] = (f, n)
+
+ def filelist(**map):
+ parity = 0
+ fl = files.keys()
+ fl.sort()
+ for f in fl:
+ full, fnode = files[f]
+ if not fnode:
+ continue
+
+ yield {"file": full,
+ "manifest": mnode,
+ "filenode": hex(fnode),
+ "parity": parity,
+ "basename": f,
+ "permissions": mff[full]}
+ parity = 1 - parity
+
+ def dirlist(**map):
+ parity = 0
+ fl = files.keys()
+ fl.sort()
+ for f in fl:
+ full, fnode = files[f]
+ if fnode:
+ continue
+
+ yield {"parity": parity,
+ "path": os.path.join(path, f),
+ "manifest": mnode,
+ "basename": f[:-1]}
+ parity = 1 - parity
+
+ yield self.t("manifest",
+ manifest=mnode,
+ rev=rev,
+ node=hex(node),
+ path=path,
+ up=up(path),
+ fentries=filelist,
+ dentries=dirlist,
+ archives=self.archivelist(hex(node)))
+
+ def tags(self):
+ cl = self.repo.changelog
+ mf = cl.read(cl.tip())[0]
+
+ i = self.repo.tagslist()
+ i.reverse()
+
+ def entries(notip=False, **map):
+ parity = 0
+ for k,n in i:
+ if notip and k == "tip": continue
+ yield {"parity": parity,
+ "tag": k,
+ "tagmanifest": hex(cl.read(n)[0]),
+ "date": cl.read(n)[2],
+ "node": hex(n)}
+ parity = 1 - parity
+
+ yield self.t("tags",
+ manifest=hex(mf),
+ entries=lambda **x: entries(False, **x),
+ entriesnotip=lambda **x: entries(True, **x))
+
+ def summary(self):
+ cl = self.repo.changelog
+ mf = cl.read(cl.tip())[0]
+
+ i = self.repo.tagslist()
+ i.reverse()
+
+ def tagentries(**map):
+ parity = 0
+ count = 0
+ for k,n in i:
+ if k == "tip": # skip tip
+ continue;
+
+ count += 1
+ if count > 10: # limit to 10 tags
+ break;
+
+ c = cl.read(n)
+ m = c[0]
+ t = c[2]
+
+ yield self.t("tagentry",
+ parity = parity,
+ tag = k,
+ node = hex(n),
+ date = t,
+ tagmanifest = hex(m))
+ parity = 1 - parity
+
+ def changelist(**map):
+ parity = 0
+ cl = self.repo.changelog
+ l = [] # build a list in forward order for efficiency
+ for i in range(start, end):
+ n = cl.node(i)
+ changes = cl.read(n)
+ hn = hex(n)
+ t = changes[2]
+
+ l.insert(0, self.t(
+ 'shortlogentry',
+ parity = parity,
+ author = changes[1],
+ manifest = hex(changes[0]),
+ desc = changes[4],
+ date = t,
+ rev = i,
+ node = hn))
+ parity = 1 - parity
+
+ yield l
+
+ cl = self.repo.changelog
+ mf = cl.read(cl.tip())[0]
+ count = cl.count()
+ start = max(0, count - self.maxchanges)
+ end = min(count, start + self.maxchanges)
+ pos = end - 1
+
+ yield self.t("summary",
+ desc = self.repo.ui.config("web", "description", "unknown"),
+ owner = (self.repo.ui.config("ui", "username") or # preferred
+ self.repo.ui.config("web", "contact") or # deprecated
+ self.repo.ui.config("web", "author", "unknown")), # also
+ lastchange = (0, 0), # FIXME
+ manifest = hex(mf),
+ tags = tagentries,
+ shortlog = changelist)
+
+ def filediff(self, file, changeset):
+ cl = self.repo.changelog
+ n = self.repo.lookup(changeset)
+ changeset = hex(n)
+ p1 = cl.parents(n)[0]
+ cs = cl.read(n)
+ mf = self.repo.manifest.read(cs[0])
+
+ def diff(**map):
+ yield self.diff(p1, n, [file])
+
+ yield self.t("filediff",
+ file=file,
+ filenode=hex(mf.get(file, nullid)),
+ node=changeset,
+ rev=self.repo.changelog.rev(n),
+ parent=self.siblings(cl.parents(n), cl.rev),
+ child=self.siblings(cl.children(n), cl.rev),
+ diff=diff)
+
+ archive_specs = {
+ 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
+ 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
+ 'zip': ('application/zip', 'zip', '.zip', None),
+ }
+
+ def archive(self, req, cnode, type):
+ reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
+ name = "%s-%s" % (reponame, short(cnode))
+ mimetype, artype, extension, encoding = self.archive_specs[type]
+ headers = [('Content-type', mimetype),
+ ('Content-disposition', 'attachment; filename=%s%s' %
+ (name, extension))]
+ if encoding:
+ headers.append(('Content-encoding', encoding))
+ req.header(headers)
+ archival.archive(self.repo, req.out, cnode, artype, prefix=name)
+
+ # add tags to things
+ # tags -> list of changesets corresponding to tags
+ # find tag, changeset, file
+
+ def run(self, req=hgrequest()):
+ def clean(path):
+ p = util.normpath(path)
+ if p[:2] == "..":
+ raise "suspicious path"
+ return p
+
+ def header(**map):
+ yield self.t("header", **map)
+
+ def footer(**map):
+ yield self.t("footer",
+ motd=self.repo.ui.config("web", "motd", ""),
+ **map)
+
+ def expand_form(form):
+ shortcuts = {
+ 'cl': [('cmd', ['changelog']), ('rev', None)],
+ 'cs': [('cmd', ['changeset']), ('node', None)],
+ 'f': [('cmd', ['file']), ('filenode', None)],
+ 'fl': [('cmd', ['filelog']), ('filenode', None)],
+ 'fd': [('cmd', ['filediff']), ('node', None)],
+ 'fa': [('cmd', ['annotate']), ('filenode', None)],
+ 'mf': [('cmd', ['manifest']), ('manifest', None)],
+ 'ca': [('cmd', ['archive']), ('node', None)],
+ 'tags': [('cmd', ['tags'])],
+ 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
+ 'static': [('cmd', ['static']), ('file', None)]
+ }
+
+ for k in shortcuts.iterkeys():
+ if form.has_key(k):
+ for name, value in shortcuts[k]:
+ if value is None:
+ value = form[k]
+ form[name] = value
+ del form[k]
+
+ self.refresh()
+
+ expand_form(req.form)
+
+ t = self.repo.ui.config("web", "templates", templater.templatepath())
+ static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
+ m = os.path.join(t, "map")
+ style = self.repo.ui.config("web", "style", "")
+ if req.form.has_key('style'):
+ style = req.form['style'][0]
+ if style:
+ b = os.path.basename("map-" + style)
+ p = os.path.join(t, b)
+ if os.path.isfile(p):
+ m = p
+
+ port = req.env["SERVER_PORT"]
+ port = port != "80" and (":" + port) or ""
+ uri = req.env["REQUEST_URI"]
+ if "?" in uri:
+ uri = uri.split("?")[0]
+ url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
+ if not self.reponame:
+ self.reponame = (self.repo.ui.config("web", "name")
+ or uri.strip('/') or self.repo.root)
+
+ self.t = templater.templater(m, templater.common_filters,
+ defaults={"url": url,
+ "repo": self.reponame,
+ "header": header,
+ "footer": footer,
+ })
+
+ if not req.form.has_key('cmd'):
+ req.form['cmd'] = [self.t.cache['default'],]
+
+ cmd = req.form['cmd'][0]
+ if cmd == 'changelog':
+ hi = self.repo.changelog.count() - 1
+ if req.form.has_key('rev'):
+ hi = req.form['rev'][0]
+ try:
+ hi = self.repo.changelog.rev(self.repo.lookup(hi))
+ except hg.RepoError:
+ req.write(self.search(hi)) # XXX redirect to 404 page?
+ return
+
+ req.write(self.changelog(hi))
+
+ elif cmd == 'changeset':
+ req.write(self.changeset(req.form['node'][0]))
+
+ elif cmd == 'manifest':
+ req.write(self.manifest(req.form['manifest'][0],
+ clean(req.form['path'][0])))
+
+ elif cmd == 'tags':
+ req.write(self.tags())
+
+ elif cmd == 'summary':
+ req.write(self.summary())
+
+ elif cmd == 'filediff':
+ req.write(self.filediff(clean(req.form['file'][0]),
+ req.form['node'][0]))
+
+ elif cmd == 'file':
+ req.write(self.filerevision(clean(req.form['file'][0]),
+ req.form['filenode'][0]))
+
+ elif cmd == 'annotate':
+ req.write(self.fileannotate(clean(req.form['file'][0]),
+ req.form['filenode'][0]))
+
+ elif cmd == 'filelog':
+ req.write(self.filelog(clean(req.form['file'][0]),
+ req.form['filenode'][0]))
+
+ elif cmd == 'heads':
+ req.httphdr("application/mercurial-0.1")
+ h = self.repo.heads()
+ req.write(" ".join(map(hex, h)) + "\n")
+
+ elif cmd == 'branches':
+ req.httphdr("application/mercurial-0.1")
+ nodes = []
+ if req.form.has_key('nodes'):
+ nodes = map(bin, req.form['nodes'][0].split(" "))
+ for b in self.repo.branches(nodes):
+ req.write(" ".join(map(hex, b)) + "\n")
+
+ elif cmd == 'between':
+ req.httphdr("application/mercurial-0.1")
+ nodes = []
+ if req.form.has_key('pairs'):
+ pairs = [map(bin, p.split("-"))
+ for p in req.form['pairs'][0].split(" ")]
+ for b in self.repo.between(pairs):
+ req.write(" ".join(map(hex, b)) + "\n")
+
+ elif cmd == 'changegroup':
+ req.httphdr("application/mercurial-0.1")
+ nodes = []
+ if not self.allowpull:
+ return
+
+ if req.form.has_key('roots'):
+ nodes = map(bin, req.form['roots'][0].split(" "))
+
+ z = zlib.compressobj()
+ f = self.repo.changegroup(nodes, 'serve')
+ while 1:
+ chunk = f.read(4096)
+ if not chunk:
+ break
+ req.write(z.compress(chunk))
+
+ req.write(z.flush())
+
+ elif cmd == 'archive':
+ changeset = self.repo.lookup(req.form['node'][0])
+ type = req.form['type'][0]
+ if (type in self.archives and
+ self.repo.ui.configbool("web", "allow" + type, False)):
+ self.archive(req, changeset, type)
+ return
+
+ req.write(self.t("error"))
+
+ elif cmd == 'static':
+ fname = req.form['file'][0]
+ req.write(staticfile(static, fname)
+ or self.t("error", error="%r not found" % fname))
+
+ else:
+ req.write(self.t("error"))
+
+def create_server(ui, repo):
+ use_threads = True
+
+ def openlog(opt, default):
+ if opt and opt != '-':
+ return open(opt, 'w')
+ return default
+
+ address = ui.config("web", "address", "")
+ port = int(ui.config("web", "port", 8000))
+ use_ipv6 = ui.configbool("web", "ipv6")
+ webdir_conf = ui.config("web", "webdir_conf")
+ accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
+ errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
+
+ if use_threads:
+ try:
+ from threading import activeCount
+ except ImportError:
+ use_threads = False
+
+ if use_threads:
+ _mixin = SocketServer.ThreadingMixIn
+ else:
+ if hasattr(os, "fork"):
+ _mixin = SocketServer.ForkingMixIn
+ else:
+ class _mixin: pass
+
+ class MercurialHTTPServer(_mixin, BaseHTTPServer.HTTPServer):
+ pass
+
+ class IPv6HTTPServer(MercurialHTTPServer):
+ address_family = getattr(socket, 'AF_INET6', None)
+
+ def __init__(self, *args, **kwargs):
+ if self.address_family is None:
+ raise hg.RepoError(_('IPv6 not available on this system'))
+ BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
+
+ class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+ def log_error(self, format, *args):
+ errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
+ self.log_date_time_string(),
+ format % args))
+
+ def log_message(self, format, *args):
+ accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
+ self.log_date_time_string(),
+ format % args))
+
+ def do_POST(self):
+ try:
+ self.do_hgweb()
+ except socket.error, inst:
+ if inst[0] != errno.EPIPE:
+ raise
+
+ def do_GET(self):
+ self.do_POST()
+
+ def do_hgweb(self):
+ path_info, query = splitURI(self.path)
+
+ env = {}
+ env['GATEWAY_INTERFACE'] = 'CGI/1.1'
+ env['REQUEST_METHOD'] = self.command
+ env['SERVER_NAME'] = self.server.server_name
+ env['SERVER_PORT'] = str(self.server.server_port)
+ env['REQUEST_URI'] = "/"
+ env['PATH_INFO'] = path_info
+ if query:
+ env['QUERY_STRING'] = query
+ host = self.address_string()
+ if host != self.client_address[0]:
+ env['REMOTE_HOST'] = host
+ env['REMOTE_ADDR'] = self.client_address[0]
+
+ if self.headers.typeheader is None:
+ env['CONTENT_TYPE'] = self.headers.type
+ else:
+ env['CONTENT_TYPE'] = self.headers.typeheader
+ length = self.headers.getheader('content-length')
+ if length:
+ env['CONTENT_LENGTH'] = length
+ accept = []
+ for line in self.headers.getallmatchingheaders('accept'):
+ if line[:1] in "\t\n\r ":
+ accept.append(line.strip())
+ else:
+ accept = accept + line[7:].split(',')
+ env['HTTP_ACCEPT'] = ','.join(accept)
+
+ req = hgrequest(self.rfile, self.wfile, env)
+ self.send_response(200, "Script output follows")
+
+ if webdir_conf:
+ hgwebobj = hgwebdir(webdir_conf)
+ elif repo is not None:
+ hgwebobj = hgweb(repo.__class__(repo.ui, repo.origroot))
+ else:
+ raise hg.RepoError(_('no repo found'))
+ hgwebobj.run(req)
+
+
+ if use_ipv6:
+ return IPv6HTTPServer((address, port), hgwebhandler)
+ else:
+ return MercurialHTTPServer((address, port), hgwebhandler)
+
+# This is a stopgap
+class hgwebdir(object):
+ def __init__(self, config):
+ def cleannames(items):
+ return [(name.strip(os.sep), path) for name, path in items]
+
+ self.motd = ""
+ self.repos_sorted = ('name', False)
+ if isinstance(config, (list, tuple)):
+ self.repos = cleannames(config)
+ self.repos_sorted = ('', False)
+ elif isinstance(config, dict):
+ self.repos = cleannames(config.items())
+ self.repos.sort()
+ else:
+ cp = ConfigParser.SafeConfigParser()
+ cp.read(config)
+ self.repos = []
+ if cp.has_section('web') and cp.has_option('web', 'motd'):
+ self.motd = cp.get('web', 'motd')
+ if cp.has_section('paths'):
+ self.repos.extend(cleannames(cp.items('paths')))
+ if cp.has_section('collections'):
+ for prefix, root in cp.items('collections'):
+ for path in util.walkrepos(root):
+ repo = os.path.normpath(path)
+ name = repo
+ if name.startswith(prefix):
+ name = name[len(prefix):]
+ self.repos.append((name.lstrip(os.sep), repo))
+ self.repos.sort()
+
+ def run(self, req=hgrequest()):
+ def header(**map):
+ yield tmpl("header", **map)
+
+ def footer(**map):
+ yield tmpl("footer", motd=self.motd, **map)
+
+ m = os.path.join(templater.templatepath(), "map")
+ tmpl = templater.templater(m, templater.common_filters,
+ defaults={"header": header,
+ "footer": footer})
+
+ def archivelist(ui, nodeid, url):
+ for i in ['zip', 'gz', 'bz2']:
+ if ui.configbool("web", "allow" + i, False):
+ yield {"type" : i, "node": nodeid, "url": url}
+
+ def entries(sortcolumn="", descending=False, **map):
+ rows = []
+ parity = 0
+ for name, path in self.repos:
+ u = ui.ui()
+ try:
+ u.readconfig(os.path.join(path, '.hg', 'hgrc'))
+ except IOError:
+ pass
+ get = u.config
+
+ url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
+ .replace("//", "/"))
+
+ # update time with local timezone
+ try:
+ d = (get_mtime(path), util.makedate()[1])
+ except OSError:
+ continue
+
+ contact = (get("ui", "username") or # preferred
+ get("web", "contact") or # deprecated
+ get("web", "author", "")) # also
+ description = get("web", "description", "")
+ name = get("web", "name", name)
+ row = dict(contact=contact or "unknown",
+ contact_sort=contact.upper() or "unknown",
+ name=name,
+ name_sort=name,
+ url=url,
+ description=description or "unknown",
+ description_sort=description.upper() or "unknown",
+ lastchange=d,
+ lastchange_sort=d[1]-d[0],
+ archives=archivelist(u, "tip", url))
+ if (not sortcolumn
+ or (sortcolumn, descending) == self.repos_sorted):
+ # fast path for unsorted output
+ row['parity'] = parity
+ parity = 1 - parity
+ yield row
+ else:
+ rows.append((row["%s_sort" % sortcolumn], row))
+ if rows:
+ rows.sort()
+ if descending:
+ rows.reverse()
+ for key, row in rows:
+ row['parity'] = parity
+ parity = 1 - parity
+ yield row
+
+ virtual = req.env.get("PATH_INFO", "").strip('/')
+ if virtual:
+ real = dict(self.repos).get(virtual)
+ if real:
+ try:
+ hgweb(real).run(req)
+ except IOError, inst:
+ req.write(tmpl("error", error=inst.strerror))
+ except hg.RepoError, inst:
+ req.write(tmpl("error", error=str(inst)))
+ else:
+ req.write(tmpl("notfound", repo=virtual))
+ else:
+ if req.form.has_key('static'):
+ static = os.path.join(templater.templatepath(), "static")
+ fname = req.form['static'][0]
+ req.write(staticfile(static, fname)
+ or tmpl("error", error="%r not found" % fname))
+ else:
+ sortable = ["name", "description", "contact", "lastchange"]
+ sortcolumn, descending = self.repos_sorted
+ if req.form.has_key('sort'):
+ sortcolumn = req.form['sort'][0]
+ descending = sortcolumn.startswith('-')
+ if descending:
+ sortcolumn = sortcolumn[1:]
+ if sortcolumn not in sortable:
+ sortcolumn = ""
+
+ sort = [("sort_%s" % column,
+ "%s%s" % ((not descending and column == sortcolumn)
+ and "-" or "", column))
+ for column in sortable]
+ req.write(tmpl("index", entries=entries,
+ sortcolumn=sortcolumn, descending=descending,
+ **dict(sort)))
--- a/setup.py Thu May 18 13:52:55 2006 -0700
+++ b/setup.py Thu May 18 16:49:45 2006 -0700
@@ -57,6 +57,8 @@
self.includes += mercurial.packagescan.getmodules(self.build_lib,
'mercurial')
self.includes += mercurial.packagescan.getmodules(self.build_lib,
+ 'mercurial/hgweb')
+ self.includes += mercurial.packagescan.getmodules(self.build_lib,
'hgext')
build_exe.finalize_options(self)
except ImportError:
@@ -85,7 +87,7 @@
url='http://selenic.com/mercurial',
description='Scalable distributed SCM',
license='GNU GPL',
- packages=['mercurial', 'hgext'],
+ packages=['mercurial', 'mercurial.hgweb', 'hgext'],
ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
Extension('mercurial.bdiff', ['mercurial/bdiff.c'])],
data_files=[('mercurial/templates',