# # # add_file "ancestry.py" # content [6000ee066cd8eb2522563d195869b039e40fd70b] # # add_file "branchdiv.py" # content [5fbaaeb9d3050ab0f510c34b4ccfe18e7fda89a1] # # add_file "handlers.py" # content [59c9ba315b35d9ffe2667872266876b511188802] # # add_file "links.py" # content [6ac854e0dfc2959803a89f2cb38c4fd7366eb070] # # add_file "render.py" # content [032026fe3b80f63a098218b7c77c90c4d7a2e518] # # patch "viewmtn.py" # from [c0e46420ea8ad46d0a3a4c1b80d981e4fd4970c0] # to [a3a3446ad7ea43e0ff0ceb6daa04dac0b7719bae] # ============================================================ --- ancestry.py 6000ee066cd8eb2522563d195869b039e40fd70b +++ ancestry.py 6000ee066cd8eb2522563d195869b039e40fd70b @@ -0,0 +1,172 @@ +# Copyright (C) 2005 Grahame Bowland +# +# 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. + +import rfc822, string, sha, os +import mtn +from links import link +from colorsys import hls_to_rgb +import config + +def ancestry_dot(ops, revision): + def dot_escape(s): + # TODO: kind of paranoid, should probably revise + permitted = string.digits + string.letters + ' -<>-:,address@hidden&.+_~?/' + return ''.join([t for t in s if t in permitted]) + revision = mtn.Revision(revision) + original_branches = [] + for cert in ops.certs(revision): + if cert[4] == 'name' and cert[5] == 'branch': + original_branches.append(cert[7]) + + # strategy: we want to show information about this revision's place + # in the overall graph, both forward and back, for revision_count + # revisions in both directions (if possible) + # + # we will show propogates as dashed arcs + # otherwise, a full arc + # + # we'll show the arcs leading away from the revisions at either end, + # to make it clear that this is one part of a larger picture + # + # it'd be neat if someone wrote a google-maps style browser; I have + # some ideas as to how to approach this problem. + + # revision graph is prone to change; someone could commit anywhere + # any time. so we'll have to generate this dotty file each time; + # let's write it into a temporary file (save some memory, no point + # keeping it about on disk) and sha1 hash the contents. + # we'll then see if ..png exists; if not, we'll + # generate it from the dot file + + # let's be general, it's fairly symmetrical in either direction anyway + # I think we want to show a consistent view over a depth vertically; at the + # very least we should always show the dangling arcs + arcs = set() + nodes = set() + visited = set() + + def visit_node(revision): + for node in ops.children(revision): + arcs.add((revision, node)) + nodes.add(node) + for node in ops.parents(revision): + arcs.add((node, revision)) + nodes.add(node) + visited.add(revision) + + def graph_build_iter(): + for node in (nodes - visited): + visit_node(node) + + # stolen from monotone-viz + def colour_from_string(str): + def f(off): + return ord(hashval[off]) / 256.0 + hashval = sha.new(str).digest() + hue = f(5) + li = f(1) * 0.15 + 0.55 + sat = f(2) * 0.5 + .5 + return ''.join(["%.2x" % int(x * 256) for x in hls_to_rgb(hue, li, sat)]) + + # for now, let's do three passes; seems to work fairly well + nodes.add(revision) + for i in xrange(3): + graph_build_iter() + + graph = '''\ +digraph ancestry { + ratio=compress + nodesep=0.1 + ranksep=0.2 + edge [dir=forward]; +''' + + # for each node, let's figure out it's colour, whether or not it's in our branch, + # and the label we'd give it; we need to look at all the nodes, as we need to know + # if off-screen nodes are propogates + + node_colour = {} + node_label = {} + node_in_branch = {} + + for node in nodes: + author, date = '', '' + branches = [] + for cert in ops.certs(node): + if cert[4] == 'name' and cert[5] == 'date': + date = cert[7] + elif cert[4] == 'name' and cert[5] == 'author': + author = cert[7] + elif cert[4] == 'name' and cert[5] == 'branch': + branches.append(cert[7]) + name, email = rfc822.parseaddr(author) + if name: + brief_name = name + else: + brief_name = author + node_label[node] = '%s on %s\\n%s' % (node.abbrev(), + dot_escape(date), + dot_escape(brief_name)) + node_colour[node] = colour_from_string(author) + for branch in original_branches: + if branch in branches: + node_in_branch[node] = True + break + + # draw visited nodes; other nodes are not actually shown + for node in visited: + line = ' "%s" ' % (node) + options = [] + nodeopts = config.graphopts['nodeopts'] + for option in nodeopts: + if option == 'fillcolor' and node_colour.has_key(node): + value = '#'+node_colour[node] + elif option == 'shape' and node == revision: + value = 'hexagon' + else: + value = nodeopts[option] + options.append('%s="%s"' % (option, value)) + options.append('label="%s"' % (node_label[node])) + options.append('href="%s"' % link(node).uri()) + line += '[' + ','.join(options) + ']' + graph += line + '\n' + + for node in (nodes - visited): + graph += ' "%s" [style="invis",label=""]\n' % (node) + + for (from_node, to_node) in arcs: + if node_in_branch.has_key(from_node) and node_in_branch.has_key(to_node): + style = "solid" + else: + style = "dashed" + graph += ' "%s"->"%s" [style="%s"]\n' % (from_node, to_node, style) + graph += '}' + return graph + +def ancestry_graph(ops, revision): + dot_data = ancestry_dot(ops, revision) + # okay, let's output the graph + graph_sha = sha.new(dot_data).hexdigest() + if not os.access(config.graphopts['directory'], os.R_OK): + os.mkdir(config.graphopts['directory']) + output_directory = os.path.join(config.graphopts['directory'], revision) + if not os.access(output_directory, os.R_OK): + os.mkdir(output_directory) + dot_file = os.path.join(output_directory, graph_sha+'.dot') + output_png = os.path.join(output_directory, 'graph.png') + output_imagemap = os.path.join(output_directory, 'imagemap.txt') + must_exist = (output_png, output_imagemap, dot_file) + if filter(lambda fname: not os.access(fname, os.R_OK), must_exist): + open(dot_file, 'w').write(dot_data) + command = "%s -Tcmapx -o %s -Tpng -o %s %s" % (config.graphopts['dot'], + output_imagemap, + output_png, + dot_file) + os.system(command) + return output_png, output_imagemap ============================================================ --- branchdiv.py 5fbaaeb9d3050ab0f510c34b4ccfe18e7fda89a1 +++ branchdiv.py 5fbaaeb9d3050ab0f510c34b4ccfe18e7fda89a1 @@ -0,0 +1,30 @@ +# Copyright (C) 2005 Grahame Bowland +# +# 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. + +from mk2 import MarkovChain + +class BranchDivisions(object): + def __init__ (self): + self.divisions = None + + def calculate_divisions (self, branches): + if self.divisions != None: + return + chain = MarkovChain (2, join_token='.', cutoff_func=MarkovChain.log_chunkable) + for branch in branches: + chain.update (branch.name.split ('.')) + chain.upchunk () + divisions = set () + for branch in branches: + for chunk in chain.upchunked: + idx = branch.name.find (chunk) + if idx != -1: + divisions.add (branch.name[idx:idx+len(chunk)]) + self.divisions = list(divisions) + self.divisions.sort () \ No newline at end of file ============================================================ --- handlers.py 59c9ba315b35d9ffe2667872266876b511188802 +++ handlers.py 59c9ba315b35d9ffe2667872266876b511188802 @@ -0,0 +1,780 @@ +# Copyright (C) 2005 Grahame Bowland +# +# 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 +from links import link, dynamic_join, static_join +from branchdiv import BranchDivisions +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 +# Renderer, sort out template inheritance, etc, etc. +renderer = Renderer() +# Figure out branch divisions +divisions = BranchDivisions () + +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) + +class Index(object): + def GET(self, ops): + branches = list(ops.branches ()) + divisions.calculate_divisions (branches) + def division_iter(): + bitter = iter(branches) + divs = divisions.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) + renderer.render('index.html', page_title="Branches", branches=division_iter()) + +class About(object): + def GET(self): + renderer.render('about.html', page_title="About") + +class Tags(object): + def GET(self, ops, restrict_branch=None): + # otherwise we couldn't use automate again.. + tags = list(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 ops.certs(rev): + if cert[4] == 'name' and cert[5] == 'date': + revdate = common.parse_timecert(cert[7]) + rv = common.ago(revdate) + return rv + renderer.render(template_file, page_title="Tags", tags=tags, revision_ago=revision_ago, branch=restrict_branch) + +class Help(object): + def GET(self): + renderer.render('help.html', page_title="Help") + +class Changes(object): + 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, 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, 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, ops, 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 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([link(d).html('diff') for d in diffs]) + else: + diffs = '' + rv.append((revision, diffs, ago, mtn.Author(author), '
\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, ops, 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) + changed, new_starting_point = self.branch_get_last_changes(ops, branch, from_change, to_change) + 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 + renderer.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(ops, 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, ops, 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(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 + renderer.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(ops, changed, pathinfo=pathinfo, constrain_diff_to=revision)) + +class HTMLBranchChanges(Changes): + def GET(self, ops, branch, from_change, to_change): + Changes.Branch_GET(self, ops, branch, from_change, to_change, "branchchanges.html") + +class RSSBranchChanges(Changes): + def GET(self, ops, branch, from_change, to_change): + Changes.Branch_GET(self, ops, 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, ops, from_change, to_change, revision, path): + revision = mtn.Revision(revision) + if not self.exists(ops, revision): + return web.notfound() + Changes.File_GET(self, ops, from_change, to_change, revision, path, "revisionfilechanges.html") + +class RevisionFileChangesRSS(Changes, RevisionPage): + def GET(self, ops, from_change, to_change, revision, path): + revision = mtn.Revision(revision) + if not self.exists(ops, revision): + return web.notfound() + Changes.File_GET(self, ops, from_change, to_change, revision, path, "revisionfilechangesrss.html") + +class RevisionInfo(RevisionPage): + def GET(self, ops, revision): + revision = mtn.Revision(revision) + if not self.exists(ops, revision): + return web.notfound() + certs = ops.certs(revision) + revisions = ops.get_revision(revision) + output_png, output_imagemap = ancestry_graph(ops, revision) + if os.access(output_imagemap, os.R_OK): + imagemap = open(output_imagemap).read().replace('\\n', ' by ') + imageuri = dynamic_join('revision/graph/' + revision) + else: + imagemap = imageuri = None + renderer.render('revisioninfo.html', + page_title="Revision %s" % revision.abbrev(), + revision=revision, + certs=certs_for_template(certs), + imagemap=imagemap, + imageuri=imageuri, + revisions=revisions_for_template(revision, revisions)) + +class RevisionDiff(RevisionPage): + def GET(self, ops, revision_from, revision_to, filename=None): + revision_from = mtn.Revision(revision_from) + revision_to = mtn.Revision(revision_to) + if not self.exists(ops, revision_from): + return web.notfound() + if not self.exists(ops, revision_to): + return web.notfound() + if filename != None: + files = [filename] + else: + files = [] + diff = ops.diff(revision_from, revision_to, files) + diff_obj = Diff(revision_from, revision_to, files) + renderer.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, ops, revision_from, revision_to, filename=None): + revision_from = mtn.Revision(revision_from) + revision_to = mtn.Revision(revision_to) + if not self.exists(ops, revision_from): + return web.notfound() + if not self.exists(ops, revision_to): + return web.notfound() + if filename != None: + files = [filename] + else: + files = [] + diff = 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, ops, revision, filename): + revision = mtn.Revision(revision) + if not self.exists(ops, revision): + return web.notfound() + language = filename.rsplit('.', 1)[-1] + fileid = RevisionPage.get_fileid(self, ops, revision, filename) + if not fileid: + return web.notfound() + contents = 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' + renderer.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, ops, revision, filename): + web.header('Content-Disposition', 'attachment; filename=%s' % filename) + revision = mtn.Revision(revision) + if not self.exists(ops, revision): + return web.notfound() + fileid = RevisionPage.get_fileid(self, ops, revision, filename) + if not fileid: + return web.notfound() + for idx, data in enumerate(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, ops, 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(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 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 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 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(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, ops, revision, path): + revision = mtn.Revision(revision) + if not self.exists(ops, revision): + return web.notfound() + branches = RevisionPage.branches_for_rev(self, ops, revision) + revisions = 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(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(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 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 dynamic_join('mimeicon/' + mime_type) + + renderer.render('revisionbrowse.html', + branches=branches, + branch_links=', '.join([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, ops, revision): + output_png, output_imagemap = ancestry_graph(ops, 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, ops, for_branch): + rv = { + 'type' : 'branch', + 'branch' : for_branch, + } + branch = mtn.Branch(for_branch) + changes, new_starting_point = Changes().branch_get_last_changes(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, ops, revision_id): + rv = { + 'type' : 'revision', + 'revision_id' : revision_id, + } + rev = mtn.Revision(revision_id) + certs = ops.certs(rev) + self.fill_from_certs(rv, certs) + return rv + + def GET(self, ops, 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)(ops, *args) + else: + return web.notfound() + print writer.write(rv) + +class BranchHead(object): + def GET(self, ops, 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 ops.heads(branch.name)] + if len(heads) == 0: + return web.notfound() + def proxyurl(revision): + return dynamic_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 = 'link' % (dynamic_join('branch/anyhead/' + \ + proxy_to + '/' + urllib.quote(branch.name, safe = ''))) + head_links = [] + for revision in heads: + author, date = '', '' + for cert in 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('%s %s at %s' % (proxyurl(revision), + revision.abbrev(), + link(author).html(), + hq(date))) + renderer.render('branchchoosehead.html', + page_title="Branch %s" % branch.name, + branch=branch, + proxy_to=proxy_to, + anyhead=anyhead, + head_links=head_links) + +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): + web.header('Content-Type', 'text/plain') + print "User-agent: *" + 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/']: + print "Disallow:", access_method + revision_page ============================================================ --- links.py 6ac854e0dfc2959803a89f2cb38c4fd7366eb070 +++ links.py 6ac854e0dfc2959803a89f2cb38c4fd7366eb070 @@ -0,0 +1,160 @@ +# Copyright (C) 2005 Grahame Bowland +# +# 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. + +# +# links.py: +# link to various objects within a monotone database +# + +import urllib, urlparse, cgi, binascii, rfc822 +import json +import config + +dynamic_join = lambda path: urlparse.urljoin(config.dynamic_uri_path, path) +static_join = lambda path: urlparse.urljoin(config.static_uri_path, path) + +hq = cgi.escape + +class Link(object): + def __init__(self, description=None, link_type=None, **kwargs): + self.absolute_uri = None + self.relative_uri = None + self.description = description + self.json_args = None + def nbhq(self, s): + return ' '.join([hq(t) for t in s.split(' ')]) + def uri(self): + return dynamic_join(self.relative_uri) + def html(self, override_description=None, force_nbsp=False): + if override_description: + if force_nbsp: + d = self.nbhq(override_description) + else: + d = hq(override_description) + else: + d = self.description + if self.relative_uri: + uri = dynamic_join(self.relative_uri) + elif self.absolute_uri: + uri = self.absolute_uri + else: + return self.description + rv = '%s' % (uri, d) + if self.json_args != None: + enc_args = binascii.hexlify(json.write(self.json_args)) + rv = '' % (hq(enc_args), + hq(str(self.__class__).split('.')[-1])) + rv + '' + return rv + +class AuthorLink(Link): + def __init__(self, author, **kwargs): + Link.__init__(*(self, ), **kwargs) + name, email = rfc822.parseaddr(author) + self.description = author + if email: + self.absolute_uri = "mailto:%s" % urllib.quote(email) + if name: + self.description = hq(name) + +class RevisionLink(Link): + def __init__(self, revision, **kwargs): + link_type = kwargs.get("link_type") + if link_type == "browse": + subpage = "browse" + else: + subpage = "info" + Link.__init__(*(self, ), **kwargs) + self.json_args = [str(revision)] + self.relative_uri = 'revision/%s/%s' % (subpage, revision) + self.description = revision.abbrev() + +class TagLink(Link): + def __init__(self, tag, **kwargs): + Link.__init__(*(self, ), **kwargs) + self.relative_uri = 'revision/info/%s' % (tag.revision) + self.description = tag.name + +class BranchLink(Link): + def __init__(self, branch, **kwargs): + Link.__init__(*(self, ), **kwargs) + self.json_args = [branch.name] + from_change, to_change = kwargs.get('from_change'), kwargs.get('to_change') + if from_change and to_change: + self.relative_uri = 'branch/changes/%s/from/%d/to/%d' % (urllib.quote(branch.name, safe = ''), from_change, to_change) + else: + self.relative_uri = 'branch/changes/' + urllib.quote(branch.name, safe = '') + self.description = hq(branch.name) + +class DiffLink(Link): + def __init__(self, diff, **kwargs): + Link.__init__(*(self, ), **kwargs) + if kwargs.get('link_type', None) == "raw": + mode = "rawdiff" + else: + mode = "diff" + self.relative_uri = 'revision/%s/' % (mode) + diff.from_rev + '/with/' + diff.to_rev + if isinstance(diff.fname, list): + # TODO: figure out a linking scheme for diffs of multiple files. + if len(diff.fname) > 0: + fname = diff.fname[0] + else: + fname = None + else: + fname = diff.fname + if fname is not None: + self.relative_uri += '/'+urllib.quote(fname) + self.description = "diff" + +class DirLink(Link): + def __init__(self, file, **kwargs): + Link.__init__(*(self, ), **kwargs) + # handle the root directory + if file.name == '/': + fn = '' + else: + fn = file.name + self.relative_uri = 'revision/browse/' + file.in_revision + '/' + urllib.quote(fn) + self.description = hq(file.name) + +class FileLink(Link): + def __init__(self, file, **kwargs): + Link.__init__(*(self, ), **kwargs) + if kwargs.has_key('for_download'): + access_method = 'downloadfile' + elif kwargs.has_key('for_changes'): + access_method = 'filechanges' + elif kwargs.has_key('for_changes_rss'): + access_method = 'filechanges/rss' + else: + access_method = 'file' + self.relative_uri = 'revision/' + access_method + '/' + file.in_revision + '/' + urllib.quote(file.name) + self.description = hq(file.name) + +type_to_link_class = { + 'author' : AuthorLink, + 'branch' : BranchLink, + 'diff' : DiffLink, + 'dir' : DirLink, + 'file' : FileLink, + 'revision' : RevisionLink, + 'tag' : TagLink, +} + +def link(obj, link_type=None, **kwargs): + link_class = type_to_link_class.get(obj.obj_type) + if not link_class: + raise LinkException("Unable to link to objects of type: '%s'" % (obj.obj_type)) + # ugh + if link_type: + kwargs['link_type'] = link_type + return link_class(obj, **kwargs) + + + + ============================================================ --- render.py 032026fe3b80f63a098218b7c77c90c4d7a2e518 +++ render.py 032026fe3b80f63a098218b7c77c90c4d7a2e518 @@ -0,0 +1,167 @@ +# Copyright (C) 2005 Grahame Bowland +# +# 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. + +import cgi, urllib, web +import mtn, release, config +from links import link, dynamic_join, static_join + +hq = cgi.escape + +def quicklog(changelog, max_size=None): + interesting_line = None + for line in changelog: + line = line.strip() + if line: + interesting_line = line + break + if not interesting_line: + return "" + if interesting_line.startswith('*'): + interesting_line = interesting_line[1:].strip() + if max_size and len(interesting_line) > max_size: + interesting_line = interesting_line[:max_size] + r_wspc = interesting_line.rfind(' ') + if r_wspc <> -1: + interesting_line = interesting_line[:r_wspc] + interesting_line += '..' + return interesting_line + +def timecert(certs): + revdate = None + for cert in certs: + if cert[4] == 'name' and cert[5] == 'date': + revdate = common.parse_timecert(cert[7]) + return revdate + +def normalise_changelog(changelog): + changelog = map(hq, changelog.split('\n')) + if changelog and changelog[-1] == '': + changelog = changelog[:-1] + return changelog + +class Diff(object): + def __init__(self, from_rev, to_rev, fname=None): + self.obj_type = 'diff' + self.fname = fname + self.from_rev = from_rev + self.to_rev = to_rev + +def prettify(s): + return ' '.join([hq(x[0].upper() + x[1:]) for x in s.replace("_", "").split(" ")]) + +def certs_for_template(cert_gen): + for cert in cert_gen: + if cert[0] == 'key' and len(cert) != 10: + raise Exception("Not a correctly formatted certificate: %s" % cert) + if cert[3] != 'ok': + raise Exception("Certificate failed check.") + + key = cert[1] + name = cert[5] + value = cert[7] + if name == "branch": + value = link(mtn.Branch(value)).html() + else: + value = '
'.join(map(hq, value.split('\n'))) + + yield { 'key' : key, + 'name' : prettify(name), + 'value' : value } + +def revisions_for_template(revision, rev_gen): + old_revisions = [] + stanzas = [] + grouping = None + for stanza in rev_gen: + stanza_type = stanza[0] + description, value = prettify(stanza_type), None + + if grouping == None: + grouping = description + if description != grouping: + if len(stanzas) > 0: + yield grouping, stanzas + grouping, stanzas = description, [] + + if stanza_type == "format_version" or \ + stanza_type == "new_manifest": + continue + elif stanza_type == "patch": + fname, from_id, to_id = stanza[1], stanza[3], stanza[5] + # if from_id is null, this is a new file + # since we're showing that information under "Add", so + # skip it here + if not from_id: + continue + diff_links = ','.join([link(Diff(old_revision, revision, fname)).html() for old_revision in old_revisions]) + value = "Patch file %s (%s)" % (link(mtn.File(fname, revision)).html(), diff_links) + elif stanza_type == "old_revision": + old_revision = mtn.Revision(stanza[1]) + if old_revision.is_empty: + value = "This revision is has no ancestor." + else: + old_revisions.append(old_revision) + value = "Old revision is: %s (%s)" % (link(old_revision).html(), link(Diff(old_revision, revision)).html()) + elif stanza_type == "add_file": + fname = stanza[1] + value = "Add file: %s" % (link(mtn.File(fname, revision)).html()) + elif stanza_type == "add_dir": + dname = stanza[1] + value = "Add directory: %s" % (hq(dname)) + elif stanza_type == "delete": + fname = stanza[1] + value = "Delete: %s" % (hq(fname)) + elif stanza_type == "set": + fname, attr, value = stanza[1], stanza[3], stanza[5] + value = "Set attribute '%s' to '%s' upon %s" % (hq(attr), hq(value), link(mtn.File(fname, revision)).html()) + elif stanza_type == "rename": + oldname, newname = stanza[1], stanza[3] + value = "Rename %s to %s" % (hq(oldname), link(mtn.File(newname, revision)).html()) + else: + value = "(this stanza type is not explicitly rendered; please report this.)\n%s" % hq(str(stanza)) + + if description != None: + stanzas.append(value) + + if len(stanzas) > 0: + yield grouping, stanzas + + +class Renderer(object): + def __init__(self): + # any templates that can be inherited from, should be added to the list here + self.templates = [ ('base.html', 'base'), + ('revision.html', 'revision'), + ('branch.html', 'branch'), + ('revisionfile.html', 'revisionfile'), + ('revisionfileview.html', 'revisionfileview') ] + self._templates_loaded = False + + # these variables will be available to any template + self.terms = { + 'dynamic_uri_path' : config.dynamic_uri_path, + 'dynamic_join' : dynamic_join, + 'urllib_quote' : urllib.quote, + 'static_uri_path' : config.static_uri_path, + 'static_join' : static_join, + 'link' : link, + 'version' : release.version, + } + + def load_templates(self): + if self._templates_loaded: return + for template, mod_name in self.templates: + web.render(template, None, True, mod_name) + self._templates_loaded = True + + def render(self, template, **kwargs): + self.load_templates() + terms = self.terms.copy() + terms.update(kwargs) + web.render(template, terms) ============================================================ --- viewmtn.py c0e46420ea8ad46d0a3a4c1b80d981e4fd4970c0 +++ viewmtn.py a3a3446ad7ea43e0ff0ceb6daa04dac0b7719bae @@ -9,369 +9,22 @@ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. -import os -import cgi -import mtn -import sha -import sys +import os, sys +from itertools import izip, chain, repeat +# web.py import web -import json -import struct -import string -import rfc822 +# Other bits of ViewMTN +import mtn, handlers +# The user configuration file import config -import common -import urllib -import urlparse -import syntax -import tarfile -import tempfile -import datetime -import cStringIO -from colorsys import hls_to_rgb -from fdo import sharedmimeinfo, icontheme -import release -hq = cgi.escape -import heapq -import binascii -from itertools import izip, chain, repeat -import web debug = web.debug # purloined from: http://docs.python.org/lib/itertools-recipes.html def grouper(n, iterable, padvalue=None): "grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')" return izip(*[chain(iterable, repeat(padvalue, n-1))]*n) - -# /about.psp -> /about -# /branch.psp -> /branch/{branch}/ -# /fileinbranch.psp -> /branch/{branch}/file/path (redir) -# /headofbranch.psp -> /branch/{branch}/head -# /tarofbranch.psp -> /branch/{branch}/tar - -# /revision.psp -> /revision/{id} -# /diff.psp -> /revision/{id}/diff/{id}[/{fname}] -# /file.psp -> /revision/{id}/file/{path} -# /manifest.psp -> /revision/{id}/browse/{subdir} -# /getfile.py -> /revision/{id}/file/{path}&download #??? -# /getdiff.py -> /revision/{id}/diff/{id}[/{fname}]&download #??? -# /gettar.py -> /revision/{id}/tar - -# /error.psp -> /error (perhaps not needed) -# /help.psp -> /help -# /index.psp -> / -# /tags.psp -> /tags - -# /getjson.py -> /json[...] (private) - -dynamic_join = lambda path: urlparse.urljoin(config.dynamic_uri_path, path) -static_join = lambda path: urlparse.urljoin(config.static_uri_path, path) - -def quicklog(changelog, max_size=None): - interesting_line = None - for line in changelog: - line = line.strip() - if line: - interesting_line = line - break - if not interesting_line: - return "" - if interesting_line.startswith('*'): - interesting_line = interesting_line[1:].strip() - if max_size and len(interesting_line) > max_size: - interesting_line = interesting_line[:max_size] - r_wspc = interesting_line.rfind(' ') - if r_wspc <> -1: - interesting_line = interesting_line[:r_wspc] - interesting_line += '..' - return interesting_line - -def timecert(certs): - revdate = None - for cert in certs: - if cert[4] == 'name' and cert[5] == 'date': - revdate = common.parse_timecert(cert[7]) - return revdate - -def nbhq(s): - return ' '.join([hq(t) for t in s.split(' ')]) - -def normalise_changelog(changelog): - changelog = map(hq, changelog.split('\n')) - if changelog and changelog[-1] == '': - changelog = changelog[:-1] - return changelog - -class Link(object): - def __init__(self, description=None, link_type=None, **kwargs): - self.absolute_uri = None - self.relative_uri = None - self.description = description - self.json_args = None - def uri(self): - return dynamic_join(self.relative_uri) - def html(self, override_description=None, force_nbsp=False): - if override_description: - if force_nbsp: - d = nbhq(override_description) - else: - d = hq(override_description) - else: - d = self.description - if self.relative_uri: - uri = dynamic_join(self.relative_uri) - elif self.absolute_uri: - uri = self.absolute_uri - else: - return self.description - rv = '%s' % (uri, d) - if self.json_args != None: - enc_args = binascii.hexlify(json.write(self.json_args)) - rv = '' % (hq(enc_args), - hq(str(self.__class__).split('.')[-1])) + rv + '' - return rv - -class AuthorLink(Link): - def __init__(self, author, **kwargs): - Link.__init__(*(self, ), **kwargs) - name, email = rfc822.parseaddr(author) - self.description = author - if email: - self.absolute_uri = "mailto:%s" % urllib.quote(email) - if name: - self.description = hq(name) - -class RevisionLink(Link): - def __init__(self, revision, **kwargs): - link_type = kwargs.get("link_type") - if link_type == "browse": - subpage = "browse" - else: - subpage = "info" - Link.__init__(*(self, ), **kwargs) - self.json_args = [str(revision)] - self.relative_uri = 'revision/%s/%s' % (subpage, revision) - self.description = revision.abbrev() - -class TagLink(Link): - def __init__(self, tag, **kwargs): - Link.__init__(*(self, ), **kwargs) - self.relative_uri = 'revision/info/%s' % (tag.revision) - self.description = tag.name - -class BranchLink(Link): - def __init__(self, branch, **kwargs): - Link.__init__(*(self, ), **kwargs) - self.json_args = [branch.name] - from_change, to_change = kwargs.get('from_change'), kwargs.get('to_change') - if from_change and to_change: - self.relative_uri = 'branch/changes/%s/from/%d/to/%d' % (urllib.quote(branch.name, safe = ''), from_change, to_change) - else: - self.relative_uri = 'branch/changes/' + urllib.quote(branch.name, safe = '') - self.description = hq(branch.name) - -class DiffLink(Link): - def __init__(self, diff, **kwargs): - Link.__init__(*(self, ), **kwargs) - if kwargs.get('link_type', None) == "raw": - mode = "rawdiff" - else: - mode = "diff" - self.relative_uri = 'revision/%s/' % (mode) + diff.from_rev + '/with/' + diff.to_rev - if isinstance(diff.fname, list): - # TODO: figure out a linking scheme for diffs of multiple files. - if len(diff.fname) > 0: - fname = diff.fname[0] - else: - fname = None - else: - fname = diff.fname - if fname is not None: - self.relative_uri += '/'+urllib.quote(fname) - self.description = "diff" - -class DirLink(Link): - def __init__(self, file, **kwargs): - Link.__init__(*(self, ), **kwargs) - # handle the root directory - if file.name == '/': - fn = '' - else: - fn = file.name - self.relative_uri = 'revision/browse/' + file.in_revision + '/' + urllib.quote(fn) - self.description = hq(file.name) - -class FileLink(Link): - def __init__(self, file, **kwargs): - Link.__init__(*(self, ), **kwargs) - if kwargs.has_key('for_download'): - access_method = 'downloadfile' - elif kwargs.has_key('for_changes'): - access_method = 'filechanges' - elif kwargs.has_key('for_changes_rss'): - access_method = 'filechanges/rss' - else: - access_method = 'file' - self.relative_uri = 'revision/' + access_method + '/' + file.in_revision + '/' + urllib.quote(file.name) - self.description = hq(file.name) - -class Diff(object): - def __init__(self, from_rev, to_rev, fname=None): - self.obj_type = 'diff' - self.fname = fname - self.from_rev = from_rev - self.to_rev = to_rev - -def prettify(s): - return ' '.join([hq(x[0].upper() + x[1:]) for x in s.replace("_", "").split(" ")]) - -def certs_for_template(cert_gen): - for cert in cert_gen: - if cert[0] == 'key' and len(cert) != 10: - raise Exception("Not a correctly formatted certificate: %s" % cert) - if cert[3] != 'ok': - raise Exception("Certificate failed check.") - - key = cert[1] - name = cert[5] - value = cert[7] - if name == "branch": - value = link(mtn.Branch(value)).html() - else: - value = '
'.join(map(hq, value.split('\n'))) - - yield { 'key' : key, - 'name' : prettify(name), - 'value' : value } - -def revisions_for_template(revision, rev_gen): - old_revisions = [] - stanzas = [] - grouping = None - for stanza in rev_gen: - stanza_type = stanza[0] - description, value = prettify(stanza_type), None - - if grouping == None: - grouping = description - if description != grouping: - if len(stanzas) > 0: - yield grouping, stanzas - grouping, stanzas = description, [] - - if stanza_type == "format_version" or \ - stanza_type == "new_manifest": - continue - elif stanza_type == "patch": - fname, from_id, to_id = stanza[1], stanza[3], stanza[5] - # if from_id is null, this is a new file - # since we're showing that information under "Add", so - # skip it here - if not from_id: - continue - diff_links = ','.join([link(Diff(old_revision, revision, fname)).html() for old_revision in old_revisions]) - value = "Patch file %s (%s)" % (link(mtn.File(fname, revision)).html(), diff_links) - elif stanza_type == "old_revision": - old_revision = mtn.Revision(stanza[1]) - if old_revision.is_empty: - value = "This revision is has no ancestor." - else: - old_revisions.append(old_revision) - value = "Old revision is: %s (%s)" % (link(old_revision).html(), link(Diff(old_revision, revision)).html()) - elif stanza_type == "add_file": - fname = stanza[1] - value = "Add file: %s" % (link(mtn.File(fname, revision)).html()) - elif stanza_type == "add_dir": - dname = stanza[1] - value = "Add directory: %s" % (hq(dname)) - elif stanza_type == "delete": - fname = stanza[1] - value = "Delete: %s" % (hq(fname)) - elif stanza_type == "set": - fname, attr, value = stanza[1], stanza[3], stanza[5] - value = "Set attribute '%s' to '%s' upon %s" % (hq(attr), hq(value), link(mtn.File(fname, revision)).html()) - elif stanza_type == "rename": - oldname, newname = stanza[1], stanza[3] - value = "Rename %s to %s" % (hq(oldname), link(mtn.File(newname, revision)).html()) - else: - value = "(this stanza type is not explicitly rendered; please report this.)\n%s" % hq(str(stanza)) - - if description != None: - stanzas.append(value) - - if len(stanzas) > 0: - yield grouping, stanzas - -type_to_link_class = { - 'author' : AuthorLink, - 'branch' : BranchLink, - 'diff' : DiffLink, - 'dir' : DirLink, - 'file' : FileLink, - 'revision' : RevisionLink, - 'tag' : TagLink, -} - -def link(obj, link_type=None, **kwargs): - link_class = type_to_link_class.get(obj.obj_type) - if not link_class: - raise LinkException("Unable to link to objects of type: '%s'" % (obj.obj_type)) - # ugh - if link_type: - kwargs['link_type'] = link_type - return link_class(obj, **kwargs) - -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) - -class Renderer(object): - def __init__(self): - # any templates that can be inherited from, should be added to the list here - self.templates = [ ('base.html', 'base'), - ('revision.html', 'revision'), - ('branch.html', 'branch'), - ('revisionfile.html', 'revisionfile'), - ('revisionfileview.html', 'revisionfileview') ] - self._templates_loaded = False - - # these variables will be available to any template - self.terms = { - 'dynamic_uri_path' : config.dynamic_uri_path, - 'dynamic_join' : dynamic_join, - 'urllib_quote' : urllib.quote, - 'static_uri_path' : config.static_uri_path, - 'static_join' : static_join, - 'link' : link, - 'version' : release.version, - } - - def load_templates(self): - if self._templates_loaded: return - for template, mod_name in self.templates: - web.render(template, None, True, mod_name) - self._templates_loaded = True - - def render(self, template, **kwargs): - self.load_templates() - terms = self.terms.copy() - terms.update(kwargs) - web.render(template, terms) - class OperationsFactory(object): def __init__ (self): # has the user specified a dbfiles hash? if so, use it @@ -400,923 +53,8 @@ class OperationsFactory(object): ops.per_request() return ops -# -# TODO: try and wrap these globals up in a single global object -# -renderer = Renderer() op_fact = OperationsFactory() -##ops = mtn.Operations([config.monotone, config.dbfile]) -mimehelp = sharedmimeinfo.LookupHelper(getattr(config, "mime_map", None)) -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 -from mk2 import MarkovChain - -class BranchDivisions(object): - def __init__ (self): - self.divisions = None - - def calculate_divisions (self, branches): - if self.divisions != None: - return - chain = MarkovChain (2, join_token='.', cutoff_func=MarkovChain.log_chunkable) - for branch in branches: - chain.update (branch.name.split ('.')) - chain.upchunk () - divisions = set () - for branch in branches: - for chunk in chain.upchunked: - idx = branch.name.find (chunk) - if idx != -1: - divisions.add (branch.name[idx:idx+len(chunk)]) - self.divisions = list(divisions) - self.divisions.sort () - -divisions = BranchDivisions () - -class Index(object): - def GET(self, ops): - branches = list(ops.branches ()) - divisions.calculate_divisions (branches) - def division_iter(): - bitter = iter(branches) - divs = divisions.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) - renderer.render('index.html', page_title="Branches", branches=division_iter()) - -class About(object): - def GET(self): - renderer.render('about.html', page_title="About") - -class Tags(object): - def GET(self, ops, restrict_branch=None): - # otherwise we couldn't use automate again.. - tags = list(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 ops.certs(rev): - if cert[4] == 'name' and cert[5] == 'date': - revdate = common.parse_timecert(cert[7]) - rv = common.ago(revdate) - return rv - renderer.render(template_file, page_title="Tags", tags=tags, revision_ago=revision_ago, branch=restrict_branch) - -class Help(object): - def GET(self): - renderer.render('help.html', page_title="Help") - -class Changes(object): - 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, 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, 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, ops, 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 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([link(d).html('diff') for d in diffs]) - else: - diffs = '' - rv.append((revision, diffs, ago, mtn.Author(author), '
\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, ops, 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) - changed, new_starting_point = self.branch_get_last_changes(ops, branch, from_change, to_change) - 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 - renderer.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(ops, 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, ops, 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(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 - renderer.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(ops, changed, pathinfo=pathinfo, constrain_diff_to=revision)) - -class HTMLBranchChanges(Changes): - def GET(self, ops, branch, from_change, to_change): - Changes.Branch_GET(self, ops, branch, from_change, to_change, "branchchanges.html") - -class RSSBranchChanges(Changes): - def GET(self, ops, branch, from_change, to_change): - Changes.Branch_GET(self, ops, 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, ops, from_change, to_change, revision, path): - revision = mtn.Revision(revision) - if not self.exists(ops, revision): - return web.notfound() - Changes.File_GET(self, ops, from_change, to_change, revision, path, "revisionfilechanges.html") - -class RevisionFileChangesRSS(Changes, RevisionPage): - def GET(self, ops, from_change, to_change, revision, path): - revision = mtn.Revision(revision) - if not self.exists(ops, revision): - return web.notfound() - Changes.File_GET(self, ops, from_change, to_change, revision, path, "revisionfilechangesrss.html") - -class RevisionInfo(RevisionPage): - def GET(self, ops, revision): - revision = mtn.Revision(revision) - if not self.exists(ops, revision): - return web.notfound() - certs = ops.certs(revision) - revisions = ops.get_revision(revision) - output_png, output_imagemap = ancestry_graph(ops, revision) - if os.access(output_imagemap, os.R_OK): - imagemap = open(output_imagemap).read().replace('\\n', ' by ') - imageuri = dynamic_join('revision/graph/' + revision) - else: - imagemap = imageuri = None - renderer.render('revisioninfo.html', - page_title="Revision %s" % revision.abbrev(), - revision=revision, - certs=certs_for_template(certs), - imagemap=imagemap, - imageuri=imageuri, - revisions=revisions_for_template(revision, revisions)) - -class RevisionDiff(RevisionPage): - def GET(self, ops, revision_from, revision_to, filename=None): - revision_from = mtn.Revision(revision_from) - revision_to = mtn.Revision(revision_to) - if not self.exists(ops, revision_from): - return web.notfound() - if not self.exists(ops, revision_to): - return web.notfound() - if filename != None: - files = [filename] - else: - files = [] - diff = ops.diff(revision_from, revision_to, files) - diff_obj = Diff(revision_from, revision_to, files) - renderer.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, ops, revision_from, revision_to, filename=None): - revision_from = mtn.Revision(revision_from) - revision_to = mtn.Revision(revision_to) - if not self.exists(ops, revision_from): - return web.notfound() - if not self.exists(ops, revision_to): - return web.notfound() - if filename != None: - files = [filename] - else: - files = [] - diff = 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, ops, revision, filename): - revision = mtn.Revision(revision) - if not self.exists(ops, revision): - return web.notfound() - language = filename.rsplit('.', 1)[-1] - fileid = RevisionPage.get_fileid(self, ops, revision, filename) - if not fileid: - return web.notfound() - contents = 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' - renderer.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, ops, revision, filename): - web.header('Content-Disposition', 'attachment; filename=%s' % filename) - revision = mtn.Revision(revision) - if not self.exists(ops, revision): - return web.notfound() - fileid = RevisionPage.get_fileid(self, ops, revision, filename) - if not fileid: - return web.notfound() - for idx, data in enumerate(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, ops, 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(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 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 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 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(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, ops, revision, path): - revision = mtn.Revision(revision) - if not self.exists(ops, revision): - return web.notfound() - branches = RevisionPage.branches_for_rev(self, ops, revision) - revisions = 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(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(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 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 dynamic_join('mimeicon/' + mime_type) - - renderer.render('revisionbrowse.html', - branches=branches, - branch_links=', '.join([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())) - -def ancestry_dot(ops, revision): - def dot_escape(s): - # kinda paranoid, should probably revise later - permitted=string.digits + string.letters + ' -<>-:,address@hidden&.+_~?/' - return ''.join([t for t in s if t in permitted]) - revision = mtn.Revision(revision) - original_branches = [] - for cert in ops.certs(revision): - if cert[4] == 'name' and cert[5] == 'branch': - original_branches.append(cert[7]) - - # strategy: we want to show information about this revision's place - # in the overall graph, both forward and back, for revision_count - # revisions in both directions (if possible) - # - # we will show propogates as dashed arcs - # otherwise, a full arc - # - # we'll show the arcs leading away from the revisions at either end, - # to make it clear that this is one part of a larger picture - # - # it'd be neat if someone wrote a google-maps style browser; I have - # some ideas as to how to approach this problem. - - # revision graph is prone to change; someone could commit anywhere - # any time. so we'll have to generate this dotty file each time; - # let's write it into a temporary file (save some memory, no point - # keeping it about on disk) and sha1 hash the contents. - # we'll then see if ..png exists; if not, we'll - # generate it from the dot file - - # let's be general, it's fairly symmetrical in either direction anyway - # I think we want to show a consistent view over a depth vertically; at the - # very least we should always show the dangling arcs - arcs = set() - nodes = set() - visited = set() - - def visit_node(revision): - for node in ops.children(revision): - arcs.add((revision, node)) - nodes.add(node) - for node in ops.parents(revision): - arcs.add((node, revision)) - nodes.add(node) - visited.add(revision) - - def graph_build_iter(): - for node in (nodes - visited): - visit_node(node) - - # stolen from monotone-viz - def colour_from_string(str): - def f(off): - return ord(hashval[off]) / 256.0 - hashval = sha.new(str).digest() - hue = f(5) - li = f(1) * 0.15 + 0.55 - sat = f(2) * 0.5 + .5 - return ''.join(["%.2x" % int(x * 256) for x in hls_to_rgb(hue, li, sat)]) - - # for now, let's do three passes; seems to work fairly well - nodes.add(revision) - for i in xrange(3): - graph_build_iter() - - graph = '''\ -digraph ancestry { - ratio=compress - nodesep=0.1 - ranksep=0.2 - edge [dir=forward]; -''' - - # for each node, let's figure out it's colour, whether or not it's in our branch, - # and the label we'd give it; we need to look at all the nodes, as we need to know - # if off-screen nodes are propogates - - node_colour = {} - node_label = {} - node_in_branch = {} - - for node in nodes: - author, date = '', '' - branches = [] - for cert in ops.certs(node): - if cert[4] == 'name' and cert[5] == 'date': - date = cert[7] - elif cert[4] == 'name' and cert[5] == 'author': - author = cert[7] - elif cert[4] == 'name' and cert[5] == 'branch': - branches.append(cert[7]) - name, email = rfc822.parseaddr(author) - if name: - brief_name = name - else: - brief_name = author - node_label[node] = '%s on %s\\n%s' % (node.abbrev(), - dot_escape(date), - dot_escape(brief_name)) - node_colour[node] = colour_from_string(author) - for branch in original_branches: - if branch in branches: - node_in_branch[node] = True - break - - # draw visited nodes; other nodes are not actually shown - for node in visited: - line = ' "%s" ' % (node) - options = [] - nodeopts = config.graphopts['nodeopts'] - for option in nodeopts: - if option == 'fillcolor' and node_colour.has_key(node): - value = '#'+node_colour[node] - elif option == 'shape' and node == revision: - value = 'hexagon' - else: - value = nodeopts[option] - options.append('%s="%s"' % (option, value)) - options.append('label="%s"' % (node_label[node])) - options.append('href="%s"' % link(node).uri()) - line += '[' + ','.join(options) + ']' - graph += line + '\n' - - for node in (nodes - visited): - graph += ' "%s" [style="invis",label=""]\n' % (node) - - for (from_node, to_node) in arcs: - if node_in_branch.has_key(from_node) and node_in_branch.has_key(to_node): - style = "solid" - else: - style = "dashed" - graph += ' "%s"->"%s" [style="%s"]\n' % (from_node, to_node, style) - graph += '}' - return graph - -def ancestry_graph(ops, revision): - dot_data = ancestry_dot(ops, revision) - # okay, let's output the graph - graph_sha = sha.new(dot_data).hexdigest() - if not os.access(config.graphopts['directory'], os.R_OK): - os.mkdir(config.graphopts['directory']) - output_directory = os.path.join(config.graphopts['directory'], revision) - if not os.access(output_directory, os.R_OK): - os.mkdir(output_directory) - dot_file = os.path.join(output_directory, graph_sha+'.dot') - output_png = os.path.join(output_directory, 'graph.png') - output_imagemap = os.path.join(output_directory, 'imagemap.txt') - must_exist = (output_png, output_imagemap, dot_file) - if filter(lambda fname: not os.access(fname, os.R_OK), must_exist): - open(dot_file, 'w').write(dot_data) - command = "%s -Tcmapx -o %s -Tpng -o %s %s" % (config.graphopts['dot'], - output_imagemap, - output_png, - dot_file) - os.system(command) - return output_png, output_imagemap - -class RevisionGraph(object): - def GET(self, ops, revision): - output_png, output_imagemap = ancestry_graph(ops, 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, ops, for_branch): - rv = { - 'type' : 'branch', - 'branch' : for_branch, - } - branch = mtn.Branch(for_branch) - changes, new_starting_point = Changes().branch_get_last_changes(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, ops, revision_id): - rv = { - 'type' : 'revision', - 'revision_id' : revision_id, - } - rev = mtn.Revision(revision_id) - certs = ops.certs(rev) - self.fill_from_certs(rv, certs) - return rv - - def GET(self, ops, 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)(ops, *args) - else: - return web.notfound() - print writer.write(rv) - -class BranchHead(object): - def GET(self, ops, 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 ops.heads(branch.name)] - if len(heads) == 0: - return web.notfound() - def proxyurl(revision): - return dynamic_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 = 'link' % (dynamic_join('branch/anyhead/' + proxy_to + '/' + urllib.quote(branch.name, safe = ''))) - head_links = [] - for revision in heads: - author, date = '', '' - for cert in 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('%s %s at %s' % (proxyurl(revision), - revision.abbrev(), - link(author).html(), - hq(date))) - renderer.render('branchchoosehead.html', - page_title="Branch %s" % branch.name, - branch=branch, - proxy_to=proxy_to, - anyhead=anyhead, - head_links=head_links) - -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): - web.header('Content-Type', 'text/plain') - print "User-agent: *" - 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/']: - print "Disallow:", access_method + revision_page - common_urls = ( # these don't care about multiple databases specified via the URL r'about', 'About', @@ -1374,27 +112,32 @@ if __name__ == '__main__': def assemble_urls(): fvars = {} urls = () + for url, fn in grouper (2, common_urls): url = r'^/' + url - urls += (url, fn) - fvars[fn] = globals()[fn] + if hasattr(handlers, fn): + fvars[fn] = getattr(handlers, fn) + urls += (url, fn) + else: + print >>sys.stderr, "*** URL defined for non-existant handler %s: %s" % (fn, url) - def get_db_closure (fn): - the_cls = globals()[fn]() + def get_db_closure (handler): class PerDBClosure(object): def GET (self, *args, **kwargs): db, other_args = args[0], args[1:] ops = op_fact.get_ops (db) if ops is None: return web.notfound() - return the_cls.GET (ops, *other_args, **kwargs) - rv = PerDBClosure - return rv + return handler.GET (ops, *other_args, **kwargs) + return PerDBClosure for url, fn in grouper (2, perdb_urls): - url = r'^/([A-Za-z]+/)?' + url - urls += (url, fn) - fvars[fn] = get_db_closure (fn) + if hasattr(handlers, fn): + url = r'^/([A-Za-z]+/)?' + url + urls += (url, fn) + fvars[fn] = get_db_closure (getattr(handlers, fn)()) + else: + print >>sys.stderr, "*** URL defined for non-existant handler %s: %s" % (fn, url) return urls, fvars