# # # patch "templates/revisioninfo.html" # from [48136f1e28536c5dabbfa7754566e049aed42635] # to [3e952d4ed26f5e0a2172bef6049b7eb16008bb3d] # # patch "viewmtn.py" # from [bf1d37e20485ec6133d1d5ae72dffaeb9bbb6adf] # to [3c6bfb2f2ac0ffdb7d8df467af5f4ce179a4927a] # ============================================================ --- templates/revisioninfo.html 48136f1e28536c5dabbfa7754566e049aed42635 +++ templates/revisioninfo.html 3e952d4ed26f5e0a2172bef6049b7eb16008bb3d @@ -1,7 +1,8 @@ #extends revision #def body +

Certificates

@@ -20,7 +21,9 @@ #end for
+
+

Revision Details

@@ -41,5 +44,14 @@ #end for
+
+#filter Filter +$imagemap +#filter WebSafe + +
+Ancestry of $revision +
+ #end def ============================================================ --- viewmtn.py bf1d37e20485ec6133d1d5ae72dffaeb9bbb6adf +++ viewmtn.py 3c6bfb2f2ac0ffdb7d8df467af5f4ce179a4927a @@ -1,8 +1,9 @@ import mtn #!/usr/bin/env python2.4 import os import cgi import mtn +import sha import sys import web import struct @@ -467,10 +468,18 @@ class RevisionInfo(RevisionPage): revision = mtn.Revision(revision) certs = ops.certs(revision) revisions = ops.get_revision(revision) + output_png, output_imagemap = ancestry_graph(revision) + if os.access(output_imagemap, os.R_OK): + imagemap = open(output_imagemap).read() + 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): @@ -697,60 +706,122 @@ class RevisionBrowse(RevisionPage): row_class=row_class(), entries=info_for_manifest(cut_manifest_to_subdir())) -class RevisionGraph: - def GET(self, revision, revision_count=10): +def ancestry_dot(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) + # 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. - 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 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 - print "graph code for revision is: ", revision + # 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() - # 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 + 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) - # 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() - def directed_graph_nodes(from_revision, direction, mode_node_fn): - nodes = [t for t in mode_node_fn()] - return nodes + def graph_build_iter(): + for node in (nodes - visited): + visit_node(node) - graph = ''' + # 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(map(lambda x: "%.2x" % int(x * 256), hls_to_rgb(hue, li, sat))) + + nodes.add(revision) + for i in xrange(3): + graph_build_iter() + + graph = '''\ digraph ancestry { ratio=compress nodesep=0.1 ranksep=0.2 - edge [dir=back]; + edge [dir=forward]; ''' - graph += ''' -} -''' - parent_nodes = directed_graph_nodes(ops.parents) - child_nodes = directed_graph_nodes(ops.children) + # 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: + options.append('%s="%s"' % (option, nodeopts[option])) + options.append('label="%s"' % node.abbrev()) + options.append('href="%s"' % link(node).uri()) + line += '[' + ','.join(options) + ']' + graph += line + '\n' - + for (from_node, to_node) in arcs: + graph += ' "%s"->"%s"\n' % (from_node, to_node) + graph += '}' + return graph + +def ancestry_graph(revision): + dot_data = ancestry_dot(revision) + # okay, let's output the graph + graph_sha = sha.new(dot_data).hexdigest() + 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) + debug("here we are..") + if filter(lambda fname: not os.access(fname, os.R_OK), must_exist): + debug("and here we are too") + 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: + def GET(self, revision): + output_png, output_imagemap = ancestry_graph(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: def GET(self, method, data): print "Bah."