#
#
# 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
+
+
+
+
+
#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."