# # # add_file "www/viewmtn/templates/help.html" # content [261b26dddeecdfc25505dc8bf5ce633c947b54d6] # # add_file "www/viewmtn/templates/revision.html" # content [e9eeb6212f211ce522b1db16156e7129e00993d1] # # add_file "www/viewmtn/templates/revisioninfo.html" # content [f78b63b0bea3ba8971396d33e0ce9e7b0477847d] # # add_file "www/viewmtn/templates/tags.html" # content [51d40e8786b2cdfb99d956a28583be36c49cc078] # # patch "www/viewmtn/mtn.py" # from [9aacc29cd3af6277ef411f7e582a450f58c727ee] # to [1f4c058e4f55b848c1c8195a38e6edaeb86310b6] # # patch "www/viewmtn/viewmtn.py" # from [574970773ca270f6282d13161905dae81274170b] # to [f64c08da43d7339e847ed1cddd44939c349bf9e7] # ============================================================ --- www/viewmtn/templates/help.html 261b26dddeecdfc25505dc8bf5ce633c947b54d6 +++ www/viewmtn/templates/help.html 261b26dddeecdfc25505dc8bf5ce633c947b54d6 @@ -0,0 +1,22 @@ +#extends base + +#def body +

+ViewMTN is a web interface to the Monotone revision control +system. These web pages provide various methods to access the data +controlled within a particular Monotone database. +

+ +

+To make full use of this web interface, it is recommended that you read +the Monotone +manual. +

+ +

+Feature suggestions, bug reports and patches are welcome. Please go +to the ViewMTN +software page and follow the contact instructions there. +

+#end def ============================================================ --- www/viewmtn/templates/revision.html e9eeb6212f211ce522b1db16156e7129e00993d1 +++ www/viewmtn/templates/revision.html e9eeb6212f211ce522b1db16156e7129e00993d1 @@ -0,0 +1,8 @@ +#extends base + +#def extramenu +Revision $revision.abbrev(): +Info | +Browse Files | +Download (tar)
+#end def ============================================================ --- www/viewmtn/templates/revisioninfo.html f78b63b0bea3ba8971396d33e0ce9e7b0477847d +++ www/viewmtn/templates/revisioninfo.html f78b63b0bea3ba8971396d33e0ce9e7b0477847d @@ -0,0 +1,43 @@ +#extends revision + +#def body + +

Certificates

+ + +#for cert in $certs + + + + +#end for +
+ #filter Filter + $cert['name'] + #filter WebSafe + + #filter Filter + $cert['value'] + #filter WebSafe +
+ +

Revision Details

+ + +#for stanza_type, descr, value in $revisions + + + +#end for +
+ #filter Filter + $descr + #filter WebSafe + + #filter Filter + $value + #filter WebSafe + +
+ +#end def ============================================================ --- www/viewmtn/templates/tags.html 51d40e8786b2cdfb99d956a28583be36c49cc078 +++ www/viewmtn/templates/tags.html 51d40e8786b2cdfb99d956a28583be36c49cc078 @@ -0,0 +1,29 @@ +#extends base + +#def body +

+A tag marks a particular revision that is in some way significant. +A common use of tags is to mark public release of a piece of software. +To view a particular tag, select it from the list below. +

+ + + +#for tag in $tags + + + + + +#end for +
TagSigned byWhen
+ #filter Filter + $link($tag).html() + #filter WebSafe + + $tag.author + + +
+ +#end def ============================================================ --- www/viewmtn/mtn.py 9aacc29cd3af6277ef411f7e582a450f58c727ee +++ www/viewmtn/mtn.py 1f4c058e4f55b848c1c8195a38e6edaeb86310b6 @@ -1,10 +1,19 @@ import os import re +import fcntl import pipes import select import threading +import popen2 +debugging = True + +debug = None + +import web +debug = web.debug + # regular expressions that are of general use when # validating monotone output def group_compile(r): @@ -26,11 +35,29 @@ def abbrev(self): return '[' + self[:8] + '..]' +def set_nonblocking(fd): + fl = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NDELAY) + +def terminate_popen3(process): + debug("%s stopping %s" % (os.getpid(), process.pid)) + try: + process.tochild.close() + process.fromchild.close() + process.childerr.close() + if process.poll() == -1: + # the process is still running, so kill it. + os.kill(process.pid, signal.SIGKILL) + process.wait() + except: + debug("%s failed_to_stop %s" % (os.getpid(), self.process.pid)) + class Runner: def __init__(self, monotone, database): self.base_command = [monotone, "--db=%s" % pipes.quote(database)] packet_header_re = re.compile(r'^(\d+):(\d+):([lm]):(\d+):') +import web class Automate(Runner): """Runs commands via a particular monotone process. This @@ -45,36 +72,43 @@ self.lock = threading.Lock() self.process = None + def stop(self): + if not self.process: + return + terminate_popen3(self.process) + def __process_required(self): if self.process != None: return - from utility import set_nonblocking to_run = self.base_command + ['automate', 'stdio'] - self.child_stdin, self.child_stdout, self.child_stderr = os.popen3(to_run) - map (set_nonblocking, [ self.child_stdin, - self.child_stdout, - self.child_stderr ]) + self.process = popen2.Popen3(to_run, capturestderr=True) + map (set_nonblocking, [ self.process.fromchild, + self.process.tochild, + self.process.childerr ]) def run(self, command, args): if not self.lock.acquire(False): raise MonotoneException("Automate process can't be called: it is already locked.") self.__process_required() + debug(command, args) enc = "l%d:%s" % (len(command), command) enc += ''.join(map(lambda x: "%d:%s" % (len(x), x), args)) + 'e' - self.child_stdin.write(enc) - self.child_stdin.flush() + debug("wrote", enc) + self.process.tochild.write(enc) + self.process.tochild.flush() import sys def read_result_packets(): buffer = "" while True: - r_stdin, r_stdout, r_stderr = select.select([self.child_stdout], [], [], None) + r_stdin, r_stdout, r_stderr = select.select([self.process.fromchild], [], [], None) if not r_stdin and not r_stdout and not r_stderr: break - if self.child_stdout in r_stdin: - data = self.child_stdout.read() + if self.process.fromchild in r_stdin: + data = self.process.fromchild.read() + debug("data", data) if data == "": break buffer += data @@ -88,7 +122,7 @@ break in_packet = True cmdnum, errnum, pstate, length = m.groups() - errnum = int(cmdnum) + errnum = int(errnum) length = int(length) header_length = m.end(m.lastindex) + 1 # the '1' is the colon @@ -113,7 +147,7 @@ for line in data.split('\n'): yield line + '\n' if code_max > 0: - raise MonotoneException("error code %d in automate packet." % code_max) + raise MonotoneException("error code %d in automate packet." % (code_max)) self.lock.release() class Standalone(Runner): @@ -125,13 +159,15 @@ # arguments - and does not pass them through the shell according # to help(os.popen3) to_run = self.base_command + [command] + args - child_stdin, child_stdout, child_stderr = os.popen3(to_run) - for line in child_stdout: + process = popen2.Popen3(to_run, capturestderr=True) + for line in process.fromchild: yield line - stderr_data = child_stderr.read() + stderr_data = process.childerr.read() if len(stderr_data) > 0: raise MonotoneException("data on stderr for command '%s': %s" % (command, stderr_data)) + terminate_popen3(process) + class MtnObject: def __init__(self, obj_type): self.obj_type = obj_type @@ -223,8 +259,10 @@ consumer = choose_consume current_stanza = [] for line in gen: -# print "read line:", [line] - if (line == '' or line == '\n') and current_stanza: + # if we're not in an actual consumer (which we shouldn't be, unless + # we're parsing some sort of multi-line token) and we have a blank + # line, it indicates the end of any current stanza + if (consumer == choose_consume) and (line == '' or line == '\n') and current_stanza: yield current_stanza current_stanza = [] continue @@ -239,6 +277,9 @@ self.standalone = apply(Standalone, runner_args) self.automate = apply(Automate, runner_args) + def __del__(self): + debug("deleting Operations instance.") + def tags(self): for line in (t.strip() for t in self.standalone.run('ls', ['tags'])): if not line: ============================================================ --- www/viewmtn/viewmtn.py 574970773ca270f6282d13161905dae81274170b +++ www/viewmtn/viewmtn.py f64c08da43d7339e847ed1cddd44939c349bf9e7 @@ -1,9 +1,10 @@ #!/usr/bin/env python import cgi import mtn import web import config +import urllib import urlparse hq = cgi.escape @@ -52,12 +53,73 @@ class BranchLink(Link): def __init__(self, branch, **kwargs): Link.__init__(*(self, ), **kwargs) - self.relative_uri = 'branch/changes/' + hq(branch.name) + self.relative_uri = 'branch/changes/' + urllib.quote(branch.name) self.description = hq(branch.name) +class DiffLink(Link): + def __init__(self, diff, **kwargs): + Link.__init__(*(self, ), **kwargs) + self.relative_uri = 'revision/diff/' + diff.from_rev + '/with/' + diff.to_rev + self.description = "diff" + +class Diff: + def __init__(self, from_rev, to_rev, file): + self.obj_type = 'diff' + self.file = file + self.from_rev = from_rev + self.to_rev = to_rev + +def prettify(s): + return ' '.join( + map(lambda x: hq(x[0].upper() + x[1:]), + 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 correcly 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(rev_gen): + for stanza in rev_gen: + stanza_type = stanza[0] + description, value = prettify(stanza_type), None + + 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 + value = "Patch file %s (%s)" % ("not yet", + link(Diff(from_id, to_id, fname)).html()) + else: + value = "(this stanza type is not explicitly rendered; please report this.)\n%s" % hq(str(stanza)) + + if description != None: + yield stanza_type, description, value + type_to_link_class = { 'tag' : TagLink, - 'branch' : BranchLink + 'branch' : BranchLink, + 'diff' : DiffLink, } def link(obj): @@ -117,9 +179,13 @@ class RevisionInfo: def GET(self, revision): revision = mtn.Revision(revision) + certs = ops.certs(revision) + revisions = ops.get_revision(revision) renderer.render('revisioninfo.html', page_title="Revision %s" % revision.abbrev(), - revision=revision) + revision=revision, + certs=certs_for_template(certs), + revisions=revisions_for_template(revisions)) class RevisionDiff: def GET(self, revision_from, revision_to):