+There are multiple head revisions of the branch
+#filter Filter
+$link($branch).html()
+#end filter
+. You can access the method
+'$proxy_to' on each of these revisions by clicking on the links provided below. If you are
+attempting to access this method in a script, perhaps consider using this
+#filter Filter
+$anyhead
+#end filter
+which will always go directly to one of the head revisions.
+
+
+
+#for $head_link in $head_links
+ #filter Filter
+
+Current directory:
+#for $l in $path_links
+$link($l).html()
+#end for
+
+
+These files are in a revision of
+#if len($branches) == 1
+branch
+#end if
+#if len($branches) > 1
+branches
+#end if
+#if $branches
+$branch_links
+#end if
+
+Below is the file '$filename.name' from this revision. You can also
+#filter Filter
+$link($filename, for_download=True).html(override_description="download the file").
+#end filter
+
+Unfortunately, this ViewMTN has determined that this file (with
+MIME type $mimetype) is not suitable for display inside the web
+browser. If you feel this file could have been better displayed
+please inform the author.
+
+#filter Filter
+#for $line in $contents
+$line
+#end for
+#end filter
+
+#end def
+
============================================================
--- www/viewmtn/ChangeLog c16ac2013e466b7eafcdf54b808052056d2f3e89
+++ www/viewmtn/ChangeLog a90102fbf7b5d7be6c5d553f362389c465267832
@@ -1,3 +1,9 @@
+2006-10-26 Grahame Bowland
+
+ * release 0.06
+ * almost total rewrite
+ * see INSTALL for more details
+
2005-11-21 Grahame Bowland
* release 0.05
============================================================
--- www/viewmtn/TODO 6372d06a8696794b11e1f5dece588de4d1057896
+++ www/viewmtn/TODO d57f9d2d4d2acd8412cb3d9c090f424159eecb6a
@@ -1,7 +1,21 @@
+NEW VERSION
+
+ * Fix the RSS date fields
+ * Highlight -> content_type mapping (works at the moment, somehow)
+ * JSON
+ * Unit tests
+ * Read-only WebDAV support
+ * Put copyright notices on the files, mostly so I can keep track
+ of where they end up.
+ * remove the leading path from subdirectories in file browser
+ * page showing the tags for a given branch
+ * client side reorderable tables using JS
+
BUGS:
- * \n in title of the ancestry graph
+ * HEAD just does GET; we should really do HEAD properly. Also, we
+ should start issuing eTags
TODO:
@@ -20,16 +34,6 @@ TODO:
which updates with the commands that have been run - useful for debugging,
also useful for beginners to see how to do stuff.
- * Use monotone automate graph to do the ancestry graphing, and show some
- future information. Perhaps cache based on the mtime of the db viewmtn
- is looking at? Might be good enough.
-
- Show dotted lines for propogates (with that information on the arc)
-
- Show lines leading off from the boxes at either end of the graph (if
- applicable) to give context that the image being shown is part of a
- large overall graph.
-
* Show information when mousing over long hex strings.
* Magically make http:// ftp:// etc links inside files clickable
@@ -39,20 +43,10 @@ TODO:
explain what's happened. For bonus points, index into the diff so you
can click on a file that changed and jump to that section in the diff.
- * Improve the file listing; use data from the new rosters branch
- to get a better idea of when a given file was last touched.
-
* When viewing a file, give a list of revisions in which that file
was recently changed. Include in the list a link to diff with each
revision.
- * generally clean up table formatting code; things like
- list_of_branches()
- list_of_tags()
- revision_certs()
- revision_details()
- use a generic method to output the tables
-
* from [mrb]
support for multiple databases (perhaps some sort of dropdown to say which
database you want to look in) - perhaps also make the branches page show the
@@ -80,10 +74,3 @@ AJAX ideas:
* we need some concept of selecting two points revisions for diffs or other
comparison. This is the main strength ViewCVS seems to have over us.
-LONG TERM:
-
- * provide some option for people without apache2 / mod_python to run the
- thing; even if it's running the program from a standalone python webserver.
- Would settle for a solution that required a cgi capable webserver rather than
- specifically mod_python (and thus apache2) while still supporting mod_python
- acceleration if present.
============================================================
--- www/viewmtn/common.py 19a25a6ea138661ce01454ec58c1ea20fcd884aa
+++ www/viewmtn/common.py b5a9eb0d651eb4f1a41e94376d35b31f5c8efe5d
@@ -1,239 +1,48 @@ import datetime
import datetime
-import urllib
-import pydoc
import time
+import fcntl
+import os
+import signal
+from web import debug
+import traceback
-escape_function = pydoc.HTMLRepr().escape
-
-def type_wrapper(e, x):
- if x == None:
- return ""
- elif type(x) == type([]):
- return ' '.join(map(e, x))
- else:
- return e(x)
-
def parse_timecert(value):
return apply(datetime.datetime, time.strptime(value, "%Y-%m-%dT%H:%M:%S")[:6])
-def get_branch_links(mt, branches):
- if len(branches) > 1:
- branch_links = "branches "
- else:
- branch_links = "branch "
- links = []
- for branch in branches:
- links.append(link(mt, "branch", branch))
- return branch_links + ', '.join(links)
+def set_nonblocking(fd):
+ fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NDELAY)
-def extract_cert_from_certs(certs, certname, as_list=False):
- rv = []
- for cert in certs:
- name, value = None, None
- for k, v in cert:
- if k == "name": name = v
- elif k == "value": value = v
- if name == None or value == None: continue
- if name == certname:
- if not as_list:
- return value
- else:
- rv.append(value)
- return rv
+def terminate_popen3(process):
+ debug("[%s] stopping process: %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 (%s)" % (os.getpid(), process.pid, traceback.format_exc()))
-def determine_date(certs):
- dateval = extract_cert_from_certs(certs, "date")
- if dateval == None:
- return None
- else:
- return parse_timecert(dateval)
-
-def quicklog(value):
- hq = html_escape()
- rv = hq(value.strip().split('\n')[0])
- if rv.startswith('*'):
- rv = rv[1:].strip()
- return rv
-
-def ago_string(event, now):
+def ago(event):
def plural(v, singular, plural):
- if v == 1:
- return "%d %s" % (v, singular)
- else:
- return "%d %s" % (v, plural)
+ if v == 1:
+ return "%d %s" % (v, singular)
+ else:
+ return "%d %s" % (v, plural)
now = datetime.datetime.utcnow()
ago = now - event
if ago.days > 0:
- rv = "%s, %s" % (plural(ago.days, "day", "days"),
- plural(ago.seconds / 3600, "hour", "hours"))
+ rv = "%s" % (plural(ago.days, "day", "days"))
elif ago.seconds > 3600:
hours = ago.seconds / 3600
minutes = (ago.seconds - (hours * 3600)) / 60
- rv = "%s, %s" % (plural(hours, "hour", "hours"),
- plural(minutes, "minute", "minutes"))
+ rv = "%s" % (plural(hours, "hour", "hours"))
else:
minutes = ago.seconds / 60
seconds = (ago.seconds - (minutes * 60))
- rv = "%s, %s" % (plural(minutes, "minute", "minutes"),
- plural(seconds, "second", "seconds"))
+ rv = "%s" % (plural(minutes, "minute", "minutes"))
return rv
-
-def link(mt, link_type, link_to, description = None, no_quote = False):
- hq = html_escape()
- if not no_quote and description != None: description = hq(description)
- if link_type == "revision":
- rv = '' % (urllib.quote(link_to))
- if description != None: rv += description
- else: rv += hq(link_to[:8]) + ".."
- rv += ''
- if description == None: rv = '[' + rv + ']'
- elif link_type == "diff" or link_type == "download_diff":
- link_to = map(urllib.quote, filter(lambda x: x != None, link_to))
- if link_type == "diff":
- handler = "diff.psp"
- else:
- handler = "getdiff.py"
- uri = '%s?id1=%s&id2=%s' % (handler, link_to[0], link_to[1])
- if len(link_to) == 3:
- uri += '&fname=%s' % (link_to[2])
- rv = ''
- if description != None: rv += description
- else: rv += "diff"
- rv += ''
- elif link_type == "download":
- if type(link_to) == type([]):
- rv = '' % (urllib.quote(link_to[0]),
- urllib.quote(link_to[1]))
- link_id = link_to[0]
- else:
- rv = '' % (urllib.quote(link_to))
- link_id = link_to
- if description != None: rv += description + ""
- else: rv = "[" + rv + hq(link_id[:8]) + ".." + "]"
- elif link_type == "file":
- revision_id, path = link_to
- rv = '' % (urllib.quote(revision_id),
- urllib.quote(path))
- if description != None: rv += description + ""
- else: rv = "[" + rv + hq(path + '@' + revision_id[:8]) + ".." + "]"
- elif link_type == "fileinbranch":
- branch, path = link_to
- rv = '' % (urllib.quote(branch),
- urllib.quote(path))
- if description != None: rv += description + ""
- else: rv = "[" + rv + hq(path + '@' + branch) + "]"
- elif link_type == "branch":
- rv = '' % (urllib.quote(link_to))
- if description != None: rv += description
- else: rv += hq(link_to)
- rv += ''
- elif link_type == "tar":
- rv = '' % (urllib.quote(link_to))
- if description != None: rv += description
- else: rv = "tar of [" + rv + hq(link_to[:8]) + "..]" + "]"
- rv += ''
- elif link_type == "headofbranch":
- rv = '' % (urllib.quote(link_to))
- if description != None: rv += description
- else: rv += "head of " + hq(link_to)
- rv += ''
- elif link_type == "manifest":
- if type(link_to) == type([]):
- link_to, path = link_to
- rv = '' % (urllib.quote(link_to), urllib.quote(path))
- else:
- rv = '' % (urllib.quote(link_to))
- if description != None: rv += description
- else: rv += hq(link_to[:8]) + ".."
- rv += ''
- if description == None: rv = '[' + rv + ']'
- else:
- rv = 'Unknown link type: %s' % (hq(link_type))
- return '%s' % (hq(link_type+'Link'), rv)
-
-def html_escape():
- "returns a function stolen from pydoc that can be used to escape HTML"
- return lambda x: type_wrapper(escape_function, x)
-
-from enscriptlangs import enscript_langs
-from utility import run_command
-import mimetypes
-import config
-import pipes
-
-# is it binary?
-def is_binary(str):
- nontext_chars = "\x01\x02\x03\x04\x05\x06\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1c\x1d\x1e\x1f"
- check = {}
- for char in nontext_chars:
- check[char] = True
- for i in str:
- if check.has_key(i): return True
- return False
-
-# hm, later on might make this be some javascript that does an call back to the server.
-# then could have a pull down to let people choose which enscript encoding to use, and
-# just update the DOM with the new data.
-def colourise_code(req, hq, path, contents, filter=None):
- mime_type = mimetypes.guess_type(path)[0]
- if mime_type == None: mime_type = 'text/plain'
- if mime_type == 'image/png' or mime_type == 'image/jpeg' or mime_type == 'image/gif':
- display_as_image = True
- else: display_as_image = False
-
- # okay; can we guess a valid enscript filter to run this through?
- tsp = mime_type.split('/', 1)
- if filter == None and tsp[0] == 'text':
- candidate = tsp[1]
- if candidate.startswith('x-'): candidate = candidate[2:]
- if candidate.endswith('src'): candidate = candidate[:-3]
- if candidate.endswith('hdr'): candidate = candidate[:-3]
- if candidate == 'c++': candidate = 'cpp' # ugly
- if candidate in enscript_langs: filter = candidate
- if filter == None:
- # heh, will at least work for lua files
- last_dot = path.rfind('.')
- if last_dot == -1: last_dot = 0
- candidate = path[last_dot:]
- if candidate in enscript_langs: filter = candidate
-
- # if no filter then let's check if it's binary or not; if not binary
- # we'll just treat it as text; otherwise display a warning and a download
- # link
- if filter == None and not is_binary(contents):
- filter = 'text'
-
- req.write('''
''')
============================================================
--- www/viewmtn/config.py.example 9d8013696bf352853a5b9a9bce70386e9f8edb96
+++ www/viewmtn/config.py.example 343449d29b7b0e04c1e86f034c27a48bed094d5e
@@ -9,11 +9,6 @@
# If config changes are not picked up, reloading
# the web server should solve the issue.
#
-# If you want to run multiple viewmtn installs from
-# a single apache server, you might want to look at
-# giving them seperate python interpreter instances,
-# ie set PythonInterpreter viewmtn1, viewmtn2 etc
-# in .htaccess.
import sys
import os
@@ -42,13 +37,13 @@ for s in file_tokens(conffile):
lastkey = s
config_data[lastkey] = []
-
-# the base URL of this install 'http://%s%s/' % (req.hostname, os.path.dirname(req.uri))#
-def base_url(uri):
+def dynamic_uri_path(uri):
d = os.path.dirname(os.path.dirname(uri))
- return (config_data['base_url'][0] + '/projects/%s/viewmtn/') % os.path.basename(d)
+ return config_data['base_url'][0] + ('/projects/%s/viewmtn/' % os.path.basename(d))
+def static_uri_path(uri):
+ return config_data['base_url'][0] + '/viewmtn/static/'
-# the path to the 'monotone' binary
+# the path to the 'mtn' binary
monotone = config_data['monotone'][0]
# the monotone database to be shared out
@@ -61,37 +56,52 @@ gnome_mimetype_icon_path = config_data['
# where to find GNOME icons (used in manifest listing)
gnome_mimetype_icon_path = config_data['www_dir'][0] + '/viewmtn/mimetypes/' #'/usr/share/icons/gnome/'
+gnome_mimetype_icon_path = '/Users/grahame/mtn/viewmtn/mimetypes/'
# and where they are on the web
gnome_mimetype_uri = config_data['base_url'][0] + '/viewmtn/mimetypes/'
-# where to find GNU enscript
-enscript_path = '/usr/bin/enscript'
+# highlight from http://andre-simon.de/
+# if you don't have this available, just comment
+# the "highlight_command" line out
+highlight_command = '/opt/local/bin/highlight'
graphopts = {
- # a directory (must be writable by the web user)
- # in which viewmtn can output graph files
- # (you should set up a cronjob to delete old ones
- # periodically)
+ # a directory (must be writable by the web user)
+ # in which viewmtn can output graph files
+ # (you should set up a cronjob to delete old ones
+ # periodically)
'directory' : config_data['graph_dir'][0],
- # a URL, relative or absolute, at which the files
- # in the 'graphdir' directory can be found. Should
- # end in a '/' character
- 'uri' : 'graph/',
+ # a URL, relative or absolute, at which the files
+ # in the 'graphdir' directory can be found. Should
+ # end in a '/' character
+ 'uri' : 'graph/',
- # the path to the 'dot' program
- 'dot' : '/usr/bin/dot',
+ # the path to the 'dot' program
+ 'dot' : '/opt/local/bin/dot',
- # options to use for nodes in the dot input file
- # we generate.
- 'nodeopts' : { 'fontname' : 'Windsor',
- 'fontsize' : '8',
- 'shape' : 'box',
- 'height' : '0.3',
- 'spline' : 'true',
- 'style' : 'filled',
- 'fillcolor' : '#dddddd' }
+ # options to use for nodes in the dot input file
+ # we generate.
+ 'nodeopts' : { 'fontname' : 'Monaco',
+ 'fontsize' : '8',
+ 'shape' : 'box',
+ 'height' : '0.3',
+ 'spline' : 'true',
+ 'style' : 'filled',
+ 'fillcolor' : '#dddddd' }
}
+# Icon Theme to use for icons; 'gnome' is a safe value,
+# as is 'hicolor'. Note that icon_size must be a string,
+# not an integer
+icon_theme = 'gnome'
+icon_size = '16'
+# for tests/
+# don't worry about these unless you are going to run
+# the tests
+test_branch = "net.angrygoats.viewmtn"
+#test_revision = "53b7a6866f0f7268a8eb721e8d74688de8567fb8"
+test_revision = "ea14ea3aadb3a02ffe5041e0a98db15306cbcd81"
+
============================================================
--- www/viewmtn/mtn.py 1f4c058e4f55b848c1c8195a38e6edaeb86310b6
+++ www/viewmtn/mtn.py 10bae73eaa4b6b891d434bfcffa8ca66968b204b
@@ -6,19 +6,20 @@ import popen2
import select
import threading
import popen2
+from common import set_nonblocking, terminate_popen3
+from traceback import format_exc
+import genproxy
-debugging = True
-
-debug = None
-
import web
-debug = web.debug
+from web import debug
# regular expressions that are of general use when
# validating monotone output
def group_compile(r):
return re.compile('('+r+')')
+hex_re = r'[A-Fa-f0-9]*'
+hex_re_c = group_compile(hex_re)
revision_re = r'[A-Fa-f0-9]{40}'
revision_re_c = group_compile(revision_re)
name_re = r'^[\S]+'
@@ -29,32 +30,22 @@ class Revision(str):
class Revision(str):
def __init__(self, v):
- str.__init__(v)
- if not revision_re_c.match(self):
- raise MonotoneException("Not a valid revision ID")
+ # special case that must be handled: empty (initial) revision ID ''
+ str.__init__(v)
+ self.obj_type = "revision"
+ if v != '' and not revision_re_c.match(self):
+ raise MonotoneException("Not a valid revision ID: %s" % (v))
def abbrev(self):
- return '[' + self[:8] + '..]'
+ 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 Author(str):
+ def __init__(self, v):
+ str.__init__(v)
+ self.obj_type = "author"
+
class Runner:
def __init__(self, monotone, database):
- self.base_command = [monotone, "--db=%s" % pipes.quote(database)]
+ self.base_command = [monotone, "--db=%s" % pipes.quote(database)]
packet_header_re = re.compile(r'^(\d+):(\d+):([lm]):(\d+):')
import web
@@ -62,126 +53,191 @@ class Automate(Runner):
class Automate(Runner):
"""Runs commands via a particular monotone process. This
process is started the first time run() is called, and
- stopped when this class instance is deleted.
+ stopped when this class instance is deleted or the stop()
+ method is called.
If an error occurs, the monotone process may need to be
stopped and a new one created.
"""
def __init__(self, *args, **kwargs):
- Runner.__init__(*[self] + list(args), **kwargs)
- self.lock = threading.Lock()
- self.process = None
+ Runner.__init__(*[self] + list(args), **kwargs)
+ self.lock = threading.Lock()
+ self.process = None
def stop(self):
if not self.process:
- return
- terminate_popen3(self.process)
+ return
+ terminate_popen3(self.process)
+ self.process = None
+
+ def __process_required(self):
+ if self.process != None:
+ return
+ to_run = self.base_command + ['automate', 'stdio']
+ self.process = popen2.Popen3(to_run, capturestderr=True)
+ map (set_nonblocking, [ self.process.fromchild,
+ self.process.tochild,
+ self.process.childerr ])
- def __process_required(self):
- if self.process != None:
- return
- to_run = self.base_command + ['automate', 'stdio']
- self.process = popen2.Popen3(to_run, capturestderr=True)
- map (set_nonblocking, [ self.process.fromchild,
- self.process.tochild,
- self.process.childerr ])
+ def run(self, *args, **kwargs):
+# debug(("automate is running:", args, kwargs))
- 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()
+ lock = self.lock
+ stop = self.stop
+ class CleanRequest(genproxy.GeneratorProxy):
+ def __init__(self, *args, **kwargs):
+ genproxy.GeneratorProxy.__init__(self, *args, **kwargs)
- debug(command, args)
+ # nb; this used to be False, but True seems to behave more sensibly.
+ # in particular, if someone holds down Refresh sometimes the code
+ # gets here before __del__ is called on the previous iterator,
+ # causing a pointless error to occur
+ if not lock.acquire(True):
+ # I've checked; this exception does _not_ cause __del__ to run, so
+ # we don't accidentally unlock a lock below
+ raise MonotoneException("Automate request cannot be called: it is already locked! This indicates a logic error in ViewMTN; please report.")
+
+ def __del__(self):
+ def read_any_unread_output():
+ try:
+ # this'll raise StopIteration if we're done
+ self.next()
+ # okay, we're not done..
+ debug("warning: Automate output not completely read; reading manually.")
+ for stanza in self:
+ pass
+ except StopIteration:
+ pass
+
+ try:
+ read_any_unread_output()
+ lock.release()
+ except:
+ debug("exception cleaning up after Automation; calling stop()!")
+ stop()
+
+ return CleanRequest(self.__run(*args, **kwargs))
+
+ def __run(self, command, args):
enc = "l%d:%s" % (len(command), command)
- enc += ''.join(map(lambda x: "%d:%s" % (len(x), x), args)) + 'e'
- debug("wrote", enc)
- self.process.tochild.write(enc)
- self.process.tochild.flush()
+ enc += ''.join(["%d:%s" % (len(x), x) for x in args]) + 'e'
- import sys
- def read_result_packets():
- buffer = ""
- while True:
- 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
+ # number of tries to get a working mtn going..
+ for i in xrange(2):
+ self.__process_required()
+ try:
+ self.process.tochild.write(enc)
+ self.process.tochild.flush()
+ break
+ except:
+ # mtn has died underneath the automate; restart it
+ debug("exception writing to child process; attempting restart: %s" % format_exc())
+ self.stop()
- if self.process.fromchild in r_stdin:
- data = self.process.fromchild.read()
- debug("data", data)
- if data == "":
- break
- buffer += data
+ import sys
+ def read_result_packets():
+ buffer = ""
+ while True:
+ 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
- # loop, trying to get complete packets out of our buffer
- complete, in_packet = False, False
- while not complete and buffer != '':
- if not in_packet:
- m = packet_header_re.match(buffer)
- if not m:
- break
- in_packet = True
- cmdnum, errnum, pstate, length = m.groups()
- errnum = int(errnum)
- length = int(length)
- header_length = m.end(m.lastindex) + 1 # the '1' is the colon
+ if self.process.fromchild in r_stdin:
+ data = self.process.fromchild.read()
+ if data == "":
+ break
+ buffer += data
- if len(buffer) < length + header_length:
- # not enough data read from client yet; go round
- break
- else:
- result = buffer[header_length:header_length+length]
- buffer = buffer[header_length+length:]
- complete = pstate == 'l'
- in_packet = False
- yield errnum, complete, result
+ # loop, trying to get complete packets out of our buffer
+ complete, in_packet = False, False
+ while not complete and buffer != '':
+ if not in_packet:
+ m = packet_header_re.match(buffer)
+ if not m:
+ break
+ in_packet = True
+ cmdnum, errnum, pstate, length = m.groups()
+ errnum = int(errnum)
+ length = int(length)
+ header_length = m.end(m.lastindex) + 1 # the '1' is the colon
- if complete:
- break
-
- # get our response, and yield() it back one line at a time
- code_max = -1
- for code, is_last, data in read_result_packets():
- if code and code > code_max:
- code_max = code
- for line in data.split('\n'):
- yield line + '\n'
- if code_max > 0:
- raise MonotoneException("error code %d in automate packet." % (code_max))
- self.lock.release()
+ if len(buffer) < length + header_length:
+ # not enough data read from client yet; go round
+ break
+ else:
+ result = buffer[header_length:header_length+length]
+ buffer = buffer[header_length+length:]
+ complete = pstate == 'l'
+ in_packet = False
+ yield errnum, complete, result
+ if complete:
+ break
+
+ # get our response, and yield() it back one line at a time
+ code_max = -1
+ data_buf = ''
+ for code, is_last, data in read_result_packets():
+ if code and code > code_max:
+ code_max = code
+ data_buf += data
+ while True:
+ nl_idx = data_buf.find('\n')
+ if nl_idx == -1:
+ break
+ yield data_buf[:nl_idx+1]
+ data_buf = data_buf[nl_idx+1:]
+ # left over data?
+ if data_buf:
+ yield data_buf
+ if code_max > 0:
+ raise MonotoneException("error code %d in automate packet." % (code_max))
+
class Standalone(Runner):
"""Runs commands by running monotone. One monotone process
per command"""
def run(self, command, args):
- # as we pass popen3 as sequence, it executes monotone with these
- # arguments - and does not pass them through the shell according
- # to help(os.popen3)
- to_run = self.base_command + [command] + args
- process = popen2.Popen3(to_run, capturestderr=True)
- for line in process.fromchild:
- yield line
- 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)
+ # as we pass popen3 as sequence, it executes monotone with these
+ # arguments - and does not pass them through the shell according
+ # to help(os.popen3)
+# debug(("standalone is running:", command, args))
+ to_run = self.base_command + [command] + args
+ process = popen2.Popen3(to_run, capturestderr=True)
+ for line in process.fromchild:
+ yield line
+ 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
+ self.obj_type = obj_type
class Tag(MtnObject):
- def __init__(self, name, revision, author):
- MtnObject.__init__(self, "tag")
- self.name, self.revision, self.author = name, Revision(revision), author
+ def __init__(self, name, revision, author, branches):
+ MtnObject.__init__(self, "tag")
+ self.name, self.revision, self.author, self.branches = name, Revision(revision), author, branches
class Branch(MtnObject):
def __init__(self, name):
- MtnObject.__init__(self, "branch")
- self.name = name
+ MtnObject.__init__(self, "branch")
+ self.name = name
+class File(MtnObject):
+ def __init__(self, name, in_revision):
+ MtnObject.__init__(self, "file")
+ self.name = name
+ self.in_revision = in_revision
+
+class Dir(MtnObject):
+ def __init__(self, name, in_revision):
+ MtnObject.__init__(self, "dir")
+ self.name = name
+ self.in_revision = in_revision
+
basic_io_name_tok = re.compile(r'^(\S+)')
def basic_io_from_stream(gen):
@@ -190,116 +246,162 @@ def basic_io_from_stream(gen):
# new value of line (eg. with consumed tokens removed)
def hex_consume(line):
- m = revision_re_c.match(line[1:])
- if line[0] != '[' or not m:
- raise MonotoneException("This is not a hex token: %s" % line)
- end_of_match = m.end(m.lastindex)
- if line[end_of_match+1] != ']':
- raise MonotoneException("Hex token ends in character other than ']': %s" % line)
- return Revision(m.groups()[0]), choose_consume, line[end_of_match+2:]
+ m = hex_re_c.match(line[1:])
+ if line[0] != '[' or not m:
+ raise MonotoneException("This is not a hex token: %s" % line)
+ end_of_match = m.end(m.lastindex)
+ if line[end_of_match+1] != ']':
+ raise MonotoneException("Hex token ends in character other than ']': %s" % line)
+ return Revision(m.groups()[0]), choose_consume, line[end_of_match+2:]
def name_consume(line):
- m = name_re_c.match(line)
- if not m:
- raise MonotoneException("Not a name: %s" % line)
- end_of_match = m.end(m.lastindex)
- return m.groups()[0], choose_consume, line[end_of_match:]
+ m = name_re_c.match(line)
+ if not m:
+ raise MonotoneException("Not a name: %s" % line)
+ end_of_match = m.end(m.lastindex)
+ return m.groups()[0], choose_consume, line[end_of_match:]
def choose_consume(line):
- line = line.lstrip()
- if line == '':
- consumer = choose_consume
- elif line[0] == '[':
- consumer = hex_consume
- elif line[0] == '"':
- consumer = string_consume
- else:
- consumer = name_consume
- return None, consumer, line
+ line = line.lstrip()
+ if line == '':
+ consumer = choose_consume
+ elif line[0] == '[':
+ consumer = hex_consume
+ elif line[0] == '"':
+ consumer = string_consume
+ else:
+ consumer = name_consume
+ return None, consumer, line
class StringState:
- def __init__(self):
- self.in_escape = False
- self.has_started = False
- self.has_ended = False
- self.value = ''
+ def __init__(self):
+ self.in_escape = False
+ self.has_started = False
+ self.has_ended = False
+ self.value = ''
def string_consume(line, state=None):
- if not state:
- state = StringState()
+ if not state:
+ state = StringState()
- if not state.has_started:
- if line[0] != '"':
- raise MonotoneException("Not a string: %s" % line)
- line = line[1:]
- state.has_started = True
+ if not state.has_started:
+ if line[0] != '"':
+ raise MonotoneException("Not a string: %s" % line)
+ line = line[1:]
+ state.has_started = True
- for idx, c in enumerate(line):
- if state.in_escape:
- if c != '\\' or c != '\"':
- raise MonotoneException("Invalid escape code: %s\n" % line)
- state.value += c
- state.in_escape = False
- else:
- if c == '\\':
- state.in_escape = True
- elif c == '"':
- state.has_ended = True
- break
- else:
- state.value += c
+ idx = 0
+ for idx, c in enumerate(line):
+ if state.in_escape:
+ if c != '\\' and c != '"':
+ raise MonotoneException("Invalid escape code: %s in %s\n" % (c, line))
+ state.value += c
+ state.in_escape = False
+ else:
+ if c == '\\':
+ state.in_escape = True
+ elif c == '"':
+ state.has_ended = True
+ break
+ else:
+ state.value += c
- if state.has_ended:
- return state.value, choose_consume, line[idx+1:]
- else:
- return (None,
- lambda s: string_consume(s, state),
- line[idx+1:])
+ if state.has_ended:
+ return state.value, choose_consume, line[idx+1:]
+ else:
+ return (None,
+ lambda s: string_consume(s, state),
+ line[idx+1:])
consumer = choose_consume
current_stanza = []
for line in gen:
- # 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
+ # 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
- while line != '' and line != '\n':
- new_token, consumer, line = consumer(line)
- if new_token != None:
- current_stanza.append(new_token)
-
+ while line != '' and line != '\n':
+ new_token, consumer, line = consumer(line)
+ if new_token != None:
+ current_stanza.append(new_token)
+ if current_stanza:
+ yield current_stanza
+
class Operations:
def __init__(self, runner_args):
- self.standalone = apply(Standalone, runner_args)
- self.automate = apply(Automate, runner_args)
+ 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:
- continue
- yield apply(Tag, line.split(' ', 2))
+ for stanza in basic_io_from_stream(self.automate.run('tags', [])):
+ if stanza[0] == 'tag':
+ branches = []
+ for branch in stanza[7:]:
+ branches.append(Branch(branch))
+ yield Tag(stanza[1], stanza[3], stanza[5], branches)
def branches(self):
- for line in (t.strip() for t in self.standalone.run('ls', ['branches'])):
- if not line:
- continue
- yield apply(Branch, (line,))
+ for line in (t.strip() for t in self.automate.run('branches', [])):
+ if not line:
+ continue
+ yield apply(Branch, (line,))
def graph(self):
- for line in self.automate.run('graph', []):
- yield line
+ for line in self.automate.run('graph', []):
+ yield line
+ def parents(self, revision):
+ if revision != "":
+ for line in (t.strip() for t in self.automate.run('parents', [revision])):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def children(self, revision):
+ if revision != "":
+ for line in (t.strip() for t in self.automate.run('children', [revision])):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def toposort(self, revisions):
+ for line in (t.strip() for t in self.automate.run('toposort', revisions)):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def heads(self, branch):
+ for line in (t.strip() for t in self.automate.run('heads', [branch])):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def get_content_changed(self, revision, path):
+ for stanza in basic_io_from_stream(self.automate.run('get_content_changed', [revision, path])):
+ yield stanza
+
def get_revision(self, revision):
- for stanza in basic_io_from_stream(self.automate.run('get_revision', [revision])):
- yield stanza
+ for stanza in basic_io_from_stream(self.automate.run('get_revision', [revision])):
+ yield stanza
+ def get_manifest_of(self, revision):
+ for stanza in basic_io_from_stream(self.automate.run('get_manifest_of', [revision])):
+ yield stanza
+
+ def get_file(self, fileid):
+ for stanza in self.automate.run('get_file', [fileid]):
+ yield stanza
+
def certs(self, revision):
+ for stanza in basic_io_from_stream(self.automate.run('certs', [revision])):
+ yield stanza
+
+ def diff(self, revision_from, revision_to, files=[]):
+ args = ['-r', revision_from, '-r', revision_to] + files
+ for line in self.standalone.run('diff', args):
+ yield line
+
- for stanza in basic_io_from_stream(self.automate.run('certs', [revision])):
- yield stanza
============================================================
--- www/viewmtn/version.py a071b2192e2092ec222ab0d59ef95efd1c9c81e6
+++ www/viewmtn/release.py 9606208d767e8ed4999ca37fa5e20d13f3fa7be5
@@ -1,3 +1,12 @@
+version='0.06beta'
+authors='''Authors:
+Grahame Bowland
+Contributors:
+Matt Johnston
+Nathaniel Smith
+Bruce Stephens
+Lapo Luchini
+David Reiss
+
+'''
-# the latest release; make sure to update this (note for Grahame)
-release = "0.05"
============================================================
--- www/viewmtn/builtpython.sh c1d18a362622ec209ea6818957a5778d5374de45
+++ www/viewmtn/release.sh 65fe70e241d026a9e09530245362cb09287b6608
@@ -1,16 +1,11 @@
#!/bin/sh
-# generate the list of enscript formatting options
-LANGS=enscriptlangs.py
-echo -n 'enscript_langs = [' > "$LANGS"
-for i in `enscript --help-highlight | grep Name | awk {'print $2'}`; do
- echo -n "'$i', " >> "$LANGS"
-done; echo ']' >> "$LANGS"
-
# generate the help file data
-AUTHORS=authors.py
-echo -n "authors='''" > "$AUTHORS"
-cat AUTHORS >> "$AUTHORS"
-echo "'''" >> "$AUTHORS"
+OUT="release.py"
+RELEASE="0.06beta"
+echo -n > "$OUT"
+echo "version='$RELEASE'" > "$OUT"
+echo -n "authors='''" >> "$OUT"
+cat AUTHORS >> "$OUT"
+echo "'''" >> "$OUT"
-
============================================================
--- www/viewmtn/viewmtn.css 6efcadac0d56fb3d77a22786f477ba621f4af33d
+++ www/viewmtn/static/viewmtn.css 8981409323f375efedb9599c206ac0b34bdad739
@@ -47,11 +47,11 @@ TABLE.pretty TH {
padding-right: 0.5em;
}
-TR.odd {
+TR.even {
background-color: #eeeeee;
}
-TR.even {
+TR.odd {
background-color: #ffffff;
}
@@ -91,3 +91,10 @@ DIV#popupBox {
padding: 2px;
z-index: 10;
}
+
+PRE.code {
+ border-left-style: solid;
+ border-left-width: 3px;
+ border-left-color: #A0A0A0;
+ padding-left: 3px;
+}
============================================================
--- www/viewmtn/templates/about.html 6b1c55564ae50e2de59d5eda418bfad86f0b596d
+++ www/viewmtn/templates/about.html fea245cb17ab46a7766e03293800361db0b325b2
@@ -5,18 +5,18 @@
@@ -42,26 +42,12 @@ Foundation, Inc., 59 Temple Place, Suite
Dependencies
-ViewMTN is written in Python and
-runs under mod_python.
+ViewMTN is written in Python and runs under web.py. Code highlighting via Highlight.
+Graphing via GraphViz. Graph colour generation algorithm
+from monotone-viz with modifications from Matt Johnston. AJAX funtionality uses the MochiKit Javascript library.