# # patch "config.py.example" # from [5b8ea8a19a27b2e7c4ff9ffe6f26547689ce4cb6] # to [d5aebe5b50d71328b03840c3df10f877465fdfc1] # # patch "monotone.py" # from [39dcefef2486e93bd971174f5d6ae75c51861fbe] # to [6f09c46dd18d11e37bc45f87b2d1eca164ec487f] # # patch "revision.psp" # from [3c361c7a765988f8db206b5bf5a610a6278fa8c9] # to [d3d64b7f76b32ceeb44527b5cd57f0178d43857d] # # patch "viewmtn.css" # from [32aff7b269d11ee1eb87187cc948e4435a8ce347] # to [7bcc3c9acbad50a673d66a1fe52cbda094378476] # # patch "wrapper.py" # from [83c5abf5583cadb41e9e10d1c517326599749045] # to [9b1c68fb36bb6c9fa6edee016a8b230e84a679b4] # --- config.py.example +++ config.py.example @@ -15,6 +15,9 @@ # the path to the 'monotone' binary monotone = '/opt/monotone/bin/monotone' +# the path to the 'dot' binary +dot = '/opt/graphviz/bin/dot' + # the monotone database to be shared out # obviously, everything in this database might # become public if something goes wrong; probably @@ -32,3 +35,11 @@ # end in a '/' character graphuri = 'graph/' +# options to use for nodes in the dot input file +# we generate. +nodeopts = { 'fontsize' : '12', + 'shape' : 'box', + 'height' : '0.3', + 'spline' : 'true', + 'style' : 'filled', + 'fillcolor' : '#dddddd' } --- monotone.py +++ monotone.py @@ -103,6 +103,14 @@ raise Exception("Unable to retrieve file: %s" % (result['childerr'])) else: return result['fromchild'] + + def annotate(self, id, file): + result = utility.run_command(self.base_command + " annotate --revision=%s %s" % (pipes.quote(id), pipes.quote(file))) + if result['exitcode'] != 0: + raise Exception("Unable to annotate file: %s using command '%s'" % (result['childerr'], result['run_command'])) + else: + return result['fromchild'] + def diff(self, rev_from, rev_to, files=None): command = self.base_command + " diff -r %s -r %s" % (pipes.quote(rev_from), pipes.quote(rev_to)) if files != None: command += ' ' + ' '.join(map(pipes.quote, files)) @@ -134,7 +142,8 @@ map(None, iterator) if entry: rv.append(entry) return rv - def ancestry_graph(self, graphdir, graphuri, id, limit=0): + + def ancestry_graph(self, graphdir, graphuri, id, nodeopts, limit=0): def dot_escape(s): # kinda paranoid, should probably revise later permitted=string.digits + string.letters + ' -<>-:,address@hidden&.+_~?/' @@ -153,8 +162,7 @@ if len(missing) == 0: rv['cached'] = True return rv - contents = "digraph ancestry {" - contents += "edge [dir=back];\n" + contents = "digraph ancestry {\nratio=compress\nnodesep=0.1\nranksep=0.2\nedge [dir=back];\n" revisions = {} for attrs in self.ancestry(id, limit): if not attrs.has_key("Revision") or not attrs.has_key("Ancestor"): @@ -166,20 +174,30 @@ if not revisions.has_key(ancestor): revisions[ancestor] = None contents += '"%s"->"%s"\n' % (revision, ancestor) for revision in revisions.keys(): - label = "%s" % (revision) + label = "%s..." % (revision[0:8]) attrs = revisions[revision] if attrs == None: # fill in the gaps; would be nice to clean this up. # shouldn't take long, anyway. attrs = self.ancestry(revision, 1)[0] + if attrs.has_key('Date'): + d = dot_escape(attrs['Date'][0]) + d = d[0:d.find("T")] + label += " on %s" % d if attrs.has_key('Author'): label += "\\n%s" % (dot_escape(attrs['Author'][0])) - if attrs.has_key('Date'): label += "\\n%s" % (dot_escape(attrs['Date'][0])) - opts = 'fontname=Windsor,fontsize=8,shape=box,href="revision.psp?id=%s",label="%s"' % (urllib.quote(revision), label) + #opts = 'fontname=Windsor,fontsize=8,shape=box,href="revision.psp?id=%s",label="%s"' % (urllib.quote(revision), label) + opts = 'label="%s"' % label #revision[0:8] + for opt in nodeopts: + opts += ',%s="%s"' % (opt, nodeopts[opt]) if revision == id: opts += ",color=blue" + opts += ',href="revision.psp?id=%s"' % urllib.quote(revision) + #opts += ',tooltip="by %s at %s on %s"' % (dot_escape(attrs['ChangeLog'][0]), + # dot_escape(attrs['Date'][0]), + # dot_escape(attrs['Branch'][0])) contents += '"%s" [%s]\n' % (revision, opts) contents += "}\n" open(rv['dot_file'], 'w').write(contents) - os.system("/usr/bin/dot -Tcmapx -o %s -Tpng -o %s %s" % (pipes.quote(rv['imagemap_file']), rv['image_file'], rv['dot_file'])) + os.system("/usr/local/bin/dot -Tcmapx -o %s -Tpng -o %s %s" % (pipes.quote(rv['imagemap_file']), rv['image_file'], rv['dot_file'])) rv['cached'] = False return rv --- revision.psp +++ revision.psp @@ -39,14 +39,18 @@ if ancestry_limit == 0 or ancestry_limit > ancestry_maximum: ancestry_limit = ancestry_maximum -ancestry_graph = mt.ancestry_graph(config.graphdir, config.graphuri, id, ancestry_limit) +ancestry_graph = mt.ancestry_graph(config.graphdir, config.graphuri, id, config.nodeopts, ancestry_limit) req.write(open(ancestry_graph['imagemap_file']).read()) %> + + + + + + -

Manifest

+

Certificates

- +
<% certs = mt.certs(id) for cert in certs: @@ -61,7 +65,7 @@

Revision details

-
+
<% revision = mt.revision(id) old_revision = None @@ -72,12 +76,12 @@ if type == "patch": fname, from_id, to_id = stanza[0][1], stanza[1][1], stanza[2][1] if not from_id: - value += 'Add file %s with revision %s
' % (hq(fname), urllib.quote(to_id), urllib.quote(fname), hq(to_id)) + value += 'Add file %s with revision %s
' % (hq(fname), urllib.quote(to_id), hq(to_id)[0:8]) else: - value += 'Patch file %s from %s to %s (diff)
' % (hq(fname), urllib.quote(from_id), urllib.quote(fname), hq(from_id), urllib.quote(to_id), urllib.quote(fname), hq(to_id), urllib.quote(old_revision), urllib.quote(id), urllib.quote(fname)) + value += 'Patch file %s from %s to %s (diff)
' % (hq(fname), urllib.quote(from_id), hq(from_id)[0:8], urllib.quote(to_id), hq(to_id)[0:8], urllib.quote(old_revision), urllib.quote(id), urllib.quote(fname)) elif type == "old_revision": old_revision, old_manifest = stanza[0][1], stanza[1][1] - value += 'Old revision is: %s (diff)
Old manifest: %s
' % (urllib.quote(old_revision), hq(old_revision), urllib.quote(old_revision), urllib.quote(id), hq(old_manifest)) + value += 'Old revision is: %s (diff)
Old manifest: %s
' % (urllib.quote(old_revision), hq(old_revision)[0:8], urllib.quote(old_revision), urllib.quote(id), hq(old_manifest)) elif type == "new_manifest": new_manifest = stanza[0][1] value += 'New manifest is: %s
' % (hq(old_manifest)) @@ -90,8 +94,43 @@ req.write('' % (hq(prettify(key)), value)) %> +
%s%s
+

Manifest

+ +<% +if not revision.has_key('new_manifest'): +%> +

No manifest is associated with this revision.

+<% +else: + # ugh, need to wrap things nicer + manifest_id = revision['new_manifest'][0][0][1] + manifest = mt.manifest(manifest_id) + gettar='gettar.py?id=%s' % (urllib.quote(manifest_id)) +%> + +

+All <%=len(manifest)%> files in this manifest can be downloaded together in a tar archive. +

+ + + +<% + for fid, filename in manifest: +%> + + + +<% + +%> + +
Filename
<%='(annotate) %s' % (urllib.quote(id), urllib.quote(filename), urllib.quote(fid), urllib.quote(filename), hq(filename))%>
+

Ancestry Graph

<% @@ -120,42 +159,13 @@ %> -Ancestry of <%= hq(id) %> +Ancestry of <%= hq(id) %>

+
<% -if not revision.has_key('new_manifest'): -%> -

No manifest is associated with this revision.

-<% -else: - # ugh, need to wrap things nicer - manifest_id = revision['new_manifest'][0][0][1] - manifest = mt.manifest(manifest_id) - gettar='gettar.py?id=%s' % (urllib.quote(manifest_id)) -%> - -

-All <%=len(manifest)%> files in this manifest can be downloaded together in a tar archive. -

- - - -<% - for id, filename in manifest: -%> - - - -<% - -%> - -
Filename
<%='%s' % (urllib.quote(id), urllib.quote(filename), hq(filename))%>
- -<% req.write(footer(info)) %> --- viewmtn.css +++ viewmtn.css @@ -30,6 +30,13 @@ text-align: right; } +TABLE.containertable { + position: relative; + border-width=0px; + border-style: none; + font-size: 100%; +} + TABLE { position: relative; border-width: 1px; --- wrapper.py +++ wrapper.py @@ -15,9 +15,9 @@ # restart the web server. # -#reload(monotone) -#reload(config) -#reload(template) +reload(monotone) +reload(config) +reload(template) # paranoid sane_uri_re = re.compile('^\w+$') @@ -38,6 +38,22 @@ req.write(mt.file(id)) return apache.OK +def get_annotate(req): + mt = Monotone(config.monotone, config.dbfile) + form = util.FieldStorage(req) + if not form.has_key('id'): + return apache.HTTP_BAD_REQUEST + if not form.has_key('path'): + return apache.HTTP_BAD_REQUEST + id = form['id'] + if not monotone.is_valid_id(id): + return apache.HTTP_BAD_REQUEST + filepath = form['path'] + mime_type = "text/plain" + req.content_type = mime_type + req.write(mt.annotate(id, filepath)) + return apache.OK + def get_diff(req): mt = Monotone(config.monotone, config.dbfile) form = util.FieldStorage(req) @@ -93,6 +109,7 @@ handlers = { 'getfile.py' : get_file, + 'getannotate.py': get_annotate, 'getdiff.py' : get_diff, 'gettar.py' : get_tar }