The unified diff between revisions [92eb8752..] and [14e5fc76..] is displayed below. It can also be downloaded as a raw diff.

#
#
# add_file "urls.py"
#  content [16a510f175f983e3853bc0d61029a95e59ae0bbe]
#
# patch "branchdiv.py"
#  from [5fbaaeb9d3050ab0f510c34b4ccfe18e7fda89a1]
#    to [49a419788c7b54eed260b770c7a89066da499c1f]
#
# patch "handlers.py"
#  from [59c9ba315b35d9ffe2667872266876b511188802]
#    to [fa13ff0c027a94aeec104c8b07f63d605ad78db4]
#
# patch "render.py"
#  from [032026fe3b80f63a098218b7c77c90c4d7a2e518]
#    to [c1223ddbb5396f63101b981838cd8296d0cc82ba]
#
# patch "viewmtn.py"
#  from [a3a3446ad7ea43e0ff0ceb6daa04dac0b7719bae]
#    to [6629b45724cc639d2555c41248ccadf35838b83e]
#
============================================================
--- urls.py	16a510f175f983e3853bc0d61029a95e59ae0bbe
+++ urls.py	16a510f175f983e3853bc0d61029a95e59ae0bbe
@@ -0,0 +1,60 @@
+# Copyright (C) 2005 Grahame Bowland <grahame@angrygoats.net>
+#
+# This program is made available under the GNU GPL version 2.0 or
+# greater. See the accompanying file COPYING for details.
+#
+# This program is distributed WITHOUT ANY WARRANTY; without even the
+# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+# PURPOSE.
+
+from mtn import revision_re
+
+#
+# these are the same, regardless of which database the user wishes
+# to view. These handles do not receive a RequestContext instance
+# as their first argument.
+#
+common_urls = (
+    r'about', 'About',
+    r'help', 'Help',
+    r'robots.txt', 'RobotsTxt',  ## FIXME needs o exclude per-db paths
+    r'mimeicon/([A-Za-z0-9][a-z0-9\-\+\.]*)/([A-Za-z0-9][a-z0-9\-\+\.]*)', 'MimeIcon',
+)
+
+#
+# all of these should have GET handler that takes a per-DB context as an argument.
+# They get a RequestContext instance as their first argument to methods, eg. GET
+#
+perdb_urls = (
+    r'', 'Index',
+    r'tags', 'Tags',
+    r'json/([A-Za-z]+)/(.*)', 'Json',
+
+    r'([a-zA-Z]/)?revision/browse/('+revision_re+')/(.*)', 'RevisionBrowse',
+    r'revision/browse/('+revision_re+')()', 'RevisionBrowse',
+    r'revision/diff/('+revision_re+')/with/('+revision_re+')', 'RevisionDiff',
+    r'revision/diff/('+revision_re+')/with/('+revision_re+')'+'/(.*)', 'RevisionDiff',
+    r'revision/rawdiff/('+revision_re+')/with/('+revision_re+')', 'RevisionRawDiff',
+    r'revision/rawdiff/('+revision_re+')/with/('+revision_re+')'+'/(.*)', 'RevisionRawDiff',
+    r'revision/file/('+revision_re+')/(.*)', 'RevisionFile',
+    r'revision/filechanges/()()('+revision_re+')/(.*)', 'RevisionFileChanges',
+    r'revision/filechanges/from/(\d+)/to/(\d+)/('+revision_re+')/(.*)', 'RevisionFileChanges',
+    r'revision/filechanges/rss/()()('+revision_re+')/(.*)', 'RevisionFileChangesRSS',
+    r'revision/filechanges/rss/from/(\d+)/to/(\d+)/('+revision_re+')/(.*)', 'RevisionFileChangesRSS',
+    r'revision/downloadfile/('+revision_re+')/(.*)', 'RevisionDownloadFile',
+    r'revision/info/('+revision_re+')', 'RevisionInfo',
+    r'revision/tar/('+revision_re+')', 'RevisionTar',
+    r'revision/graph/('+revision_re+')', 'RevisionGraph',
+
+    r'branch/changes/(.*)/from/(\d+)/to/(\d+)', 'HTMLBranchChanges',
+    r'branch/changes/([^/]+)()()', 'HTMLBranchChanges',
+    r'branch/changes/(.*)/from/(\d+)/to/(\d+)/rss', 'RSSBranchChanges',
+    r'branch/changes/([^/]+)()()/rss', 'RSSBranchChanges',
+    r'branch/tags/([^/]+)', 'Tags',
+
+    # let's make it possible to access any function on the head revision
+    # through this proxy method; it'll return a redirect to the head revision
+    # with the specified function
+    r'branch/(head)/([A-Za-z]+)/([^/]+)(.*)', 'BranchHead',
+    r'branch/(anyhead)/([A-Za-z]+)/([^/]+)(.*)', 'BranchHead',
+)
============================================================
--- branchdiv.py	5fbaaeb9d3050ab0f510c34b4ccfe18e7fda89a1
+++ branchdiv.py	49a419788c7b54eed260b770c7a89066da499c1f
@@ -8,14 +8,21 @@ from mk2 import MarkovChain
 # PURPOSE.

 from mk2 import MarkovChain
+import sys

+# it's hardly worth doing anything in this case..
+min_to_divide = 20
+
 class BranchDivisions(object):
     def __init__ (self):
         self.divisions = None

     def calculate_divisions (self, branches):
-        if self.divisions != None:
+        if not self.divisions is None:
             return
+        if len(branches) < 20:
+            self.divisions = []
+            return
         chain = MarkovChain (2, join_token='.', cutoff_func=MarkovChain.log_chunkable)
         for branch in branches:
             chain.update (branch.name.split ('.'))
============================================================
--- handlers.py	59c9ba315b35d9ffe2667872266876b511188802
+++ handlers.py	fa13ff0c027a94aeec104c8b07f63d605ad78db4
@@ -17,9 +17,8 @@ from fdo import sharedmimeinfo, iconthem
 # FreeDesktop.org share mime info and icon theme specification
 from fdo import sharedmimeinfo, icontheme
 # Other bits of ViewMTN
-import mtn, common, syntax, release
+import mtn, common, syntax, release, branchdiv
 from links import link, dynamic_join, static_join
-from branchdiv import BranchDivisions
 from ancestry import ancestry_graph
 from render import *
 hq = cgi.escape
@@ -38,30 +37,15 @@ else:
         raise e
 else:
     mimeicon = None
+
 # Renderer, sort out template inheritance, etc, etc.
 renderer = Renderer()
 # Figure out branch divisions
-divisions = BranchDivisions ()
+divisions = branchdiv.BranchDivisions ()

-class ComparisonRev:
-    def __init__(self, ops, revision):
-        self.revision = revision
-        self.certs = list(ops.certs(self.revision))
-        self.date = None
-        for cert in self.certs:
-            if cert[4] == 'name' and cert[5] == 'date':
-                self.date = common.parse_timecert(cert[7])
-    def __repr__(self):
-        return "ComparisonRev <%s>" % (repr(self.revision))
-    def __cmp__(self, other):
-        # irritating edge-case, heapq compares us to empty string if
-        # there's only one thing in the list
-        if not other: return 1
-        return cmp(other.date, self.date)
-
 class Index(object):
-    def GET(self, ops):
-        branches = list(ops.branches ())
+    def GET(self, ctxt):
+        branches = list(ctxt.ops.branches ())
         divisions.calculate_divisions (branches)
         def division_iter():
             bitter = iter(branches)
@@ -99,9 +83,9 @@ class Tags(object):
         renderer.render('about.html', page_title="About")

 class Tags(object):
-    def GET(self, ops, restrict_branch=None):
+    def GET(self, ctxt, restrict_branch=None):
         # otherwise we couldn't use automate again..
-        tags = list(ops.tags())
+        tags = list(ctxt.ops.tags())
         if restrict_branch != None:
             restrict_branch = mtn.Branch(restrict_branch)
             def tag_in(tag):
@@ -116,7 +100,7 @@ class Tags(object):
         tags.sort(lambda t1, t2: cmp(t1.name, t2.name))
         def revision_ago(rev):
             rv = ""
-            for cert in ops.certs(rev):
+            for cert in ctxt.ops.certs(rev):
                 if cert[4] == 'name' and cert[5] == 'date':
                     revdate = common.parse_timecert(cert[7])
                     rv = common.ago(revdate)
@@ -128,6 +112,24 @@ class Changes(object):
         renderer.render('help.html', page_title="Help")

 class Changes(object):
+    class ComparisonRev:
+        def __init__(self, ops, revision):
+            self.revision = revision
+            self.certs = list(ops.certs(self.revision))
+            self.date = None
+            for cert in self.certs:
+                if cert[4] == 'name' and cert[5] == 'date':
+                    self.date = common.parse_timecert(cert[7])
+        def __repr__(self):
+            return "ComparisonRev <%s>" % (repr(self.revision))
+        def __cmp__(self, other):
+            # irritating edge-case, heapq compares us to empty string if
+            # there's only one thing in the list
+            if not other:
+                return 1
+            return cmp(other.date, self.date)
+
+
     def __get_last_changes(self, ops, start_from, parent_func, selection_func, n):
         """returns at least n revisions that are parents of the revisions in start_from,
         ordered by time (descending). selection_func is called for each revision, and
@@ -146,7 +148,7 @@ class Changes(object):
         result = []
         revq = []
         for rev in start_from:
-            heapq.heappush(revq, ComparisonRev(ops, rev))
+            heapq.heappush(revq, Changes.ComparisonRev(ops, rev))
         while len(result) < n:
         #           print >>sys.stderr, "start_revq state:", map(lambda x: (x.revision, x.date), revq)
             # update based on the last result we output
@@ -155,7 +157,7 @@ class Changes(object):
                 for parent_rev in parents:
                     if parent_rev == None:
                         continue
-                    heapq.heappush(revq, ComparisonRev(ops, parent_rev))
+                    heapq.heappush(revq, Changes.ComparisonRev(ops, parent_rev))

             # try and find something we haven't already output in the heap
             last_result = None
@@ -245,10 +247,10 @@ class Changes(object):
                                                               to_change)
         return changed, new_starting_point

-    def Branch_GET(self, ops, branch, from_change, to_change, template_name):
+    def Branch_GET(self, ctxt, branch, from_change, to_change, template_name):
         branch = mtn.Branch(branch)
         from_change, to_change, next_from, next_to, previous_from, previous_to = self.determine_bounds(from_change, to_change)
-        changed, new_starting_point = self.branch_get_last_changes(ops, branch, from_change, to_change)
+        changed, new_starting_point = self.branch_get_last_changes(ctxt.ops, branch, from_change, to_change)
         changed = changed[from_change:to_change]
         if len(changed) != to_change - from_change:
             next_from, next_to = None, None
@@ -263,7 +265,7 @@ class Changes(object):
                         previous_to=previous_to,
                         next_from=next_from,
                         next_to=next_to,
-                        display_revs=self.for_template(ops, changed))
+                        display_revs=self.for_template(ctxt.ops, changed))

     def file_get_last_changes(self, ops, from_change, to_change, revision, path):
         def content_changed_fn(start_revision, start_path, in_revision, pathinfo):
@@ -291,9 +293,9 @@ class Changes(object):
                                                               to_change)
         return changed, new_starting_point, pathinfo

-    def File_GET(self, ops, from_change, to_change, revision, path, template_name):
+    def File_GET(self, ctxt, from_change, to_change, revision, path, template_name):
         from_change, to_change, next_from, next_to, previous_from, previous_to = self.determine_bounds(from_change, to_change)
-        changed, new_starting_point, pathinfo = self.file_get_last_changes(ops, from_change, to_change, revision, path)
+        changed, new_starting_point, pathinfo = self.file_get_last_changes(ctxt.ops, from_change, to_change, revision, path)
         changed = changed[from_change:to_change]
         if len(changed) != to_change - from_change:
             next_from, next_to = None, None
@@ -309,15 +311,15 @@ class Changes(object):
                         previous_to=previous_to,
                         next_from=next_from,
                         next_to=next_to,
-                        display_revs=self.for_template(ops, changed, pathinfo=pathinfo, constrain_diff_to=revision))
+                        display_revs=self.for_template(ctxt.ops, changed, pathinfo=pathinfo, constrain_diff_to=revision))

 class HTMLBranchChanges(Changes):
-    def GET(self, ops, branch, from_change, to_change):
-        Changes.Branch_GET(self, ops, branch, from_change, to_change, "branchchanges.html")
+    def GET(self, ctxt, branch, from_change, to_change):
+        Changes.Branch_GET(self, ctxt, branch, from_change, to_change, "branchchanges.html")

 class RSSBranchChanges(Changes):
-    def GET(self, ops, branch, from_change, to_change):
-        Changes.Branch_GET(self, ops, branch, from_change, to_change, "branchchangesrss.html")
+    def GET(self, ctxt, branch, from_change, to_change):
+        Changes.Branch_GET(self, ctxt, branch, from_change, to_change, "branchchangesrss.html")

 class RevisionPage(object):
     def get_fileid(self, ops, revision, filename):
@@ -344,27 +346,27 @@ class RevisionFileChanges(Changes, Revis
         return rv

 class RevisionFileChanges(Changes, RevisionPage):
-    def GET(self, ops, from_change, to_change, revision, path):
+    def GET(self, ctxt, from_change, to_change, revision, path):
         revision = mtn.Revision(revision)
-        if not self.exists(ops, revision):
+        if not self.exists(ctxt.ops, revision):
             return web.notfound()
-        Changes.File_GET(self, ops, from_change, to_change, revision, path, "revisionfilechanges.html")
+        Changes.File_GET(self, ctxt, from_change, to_change, revision, path, "revisionfilechanges.html")

 class RevisionFileChangesRSS(Changes, RevisionPage):
-    def GET(self, ops, from_change, to_change, revision, path):
+    def GET(self, ctxt, from_change, to_change, revision, path):
         revision = mtn.Revision(revision)
-        if not self.exists(ops, revision):
+        if not self.exists(ctxt.ops, revision):
             return web.notfound()
-        Changes.File_GET(self, ops, from_change, to_change, revision, path, "revisionfilechangesrss.html")
+        Changes.File_GET(self, ctxt, from_change, to_change, revision, path, "revisionfilechangesrss.html")

 class RevisionInfo(RevisionPage):
-    def GET(self, ops, revision):
+    def GET(self, ctxt, revision):
         revision = mtn.Revision(revision)
-        if not self.exists(ops, revision):
+        if not self.exists(ctxt.ops, revision):
             return web.notfound()
-        certs = ops.certs(revision)
-        revisions = ops.get_revision(revision)
-        output_png, output_imagemap = ancestry_graph(ops, revision)
+        certs = ctxt.ops.certs(revision)
+        revisions = ctxt.ops.get_revision(revision)
+        output_png, output_imagemap = ancestry_graph(ctxt.ops, revision)
         if os.access(output_imagemap, os.R_OK):
             imagemap = open(output_imagemap).read().replace('\\n', ' by ')
             imageuri = dynamic_join('revision/graph/' + revision)
@@ -379,18 +381,18 @@ class RevisionDiff(RevisionPage):
                         revisions=revisions_for_template(revision, revisions))

 class RevisionDiff(RevisionPage):
-    def GET(self, ops, revision_from, revision_to, filename=None):
+    def GET(self, ctxt, revision_from, revision_to, filename=None):
         revision_from = mtn.Revision(revision_from)
         revision_to = mtn.Revision(revision_to)
-        if not self.exists(ops, revision_from):
+        if not self.exists(ctxt.ops, revision_from):
             return web.notfound()
-        if not self.exists(ops, revision_to):
+        if not self.exists(ctxt.ops, revision_to):
             return web.notfound()
         if filename != None:
             files = [filename]
         else:
             files = []
-        diff = ops.diff(revision_from, revision_to, files)
+        diff = ctxt.ops.diff(revision_from, revision_to, files)
         diff_obj = Diff(revision_from, revision_to, files)
         renderer.render('revisiondiff.html',
                         page_title="Diff from %s to %s" % (revision_from.abbrev(), revision_to.abbrev()),
@@ -402,33 +404,33 @@ class RevisionRawDiff(RevisionPage):
                         files=files)

 class RevisionRawDiff(RevisionPage):
-    def GET(self, ops, revision_from, revision_to, filename=None):
+    def GET(self, ctxt, revision_from, revision_to, filename=None):
         revision_from = mtn.Revision(revision_from)
         revision_to = mtn.Revision(revision_to)
-        if not self.exists(ops, revision_from):
+        if not self.exists(ctxt.ops, revision_from):
             return web.notfound()
-        if not self.exists(ops, revision_to):
+        if not self.exists(ctxt.ops, revision_to):
             return web.notfound()
         if filename != None:
             files = [filename]
         else:
             files = []
-        diff = ops.diff(revision_from, revision_to, files)
+        diff = ctxt.ops.diff(revision_from, revision_to, files)
         web.header('Content-Type', 'text/x-diff')
         for line in diff:
             sys.stdout.write (line)
         sys.stdout.flush()

 class RevisionFile(RevisionPage):
-    def GET(self, ops, revision, filename):
+    def GET(self, ctxt, revision, filename):
         revision = mtn.Revision(revision)
-        if not self.exists(ops, revision):
+        if not self.exists(ctxt.ops, revision):
             return web.notfound()
         language = filename.rsplit('.', 1)[-1]
-        fileid = RevisionPage.get_fileid(self, ops, revision, filename)
+        fileid = RevisionPage.get_fileid(self, ctxt.ops, revision, filename)
         if not fileid:
             return web.notfound()
-        contents = ops.get_file(fileid)
+        contents = ctxt.ops.get_file(fileid)
         mimetype = mimehelp.lookup(filename, '')
         mime_to_template = {
             'image/jpeg'           : 'revisionfileimg.html',
@@ -453,15 +455,15 @@ class RevisionDownloadFile(RevisionPage)
                         contents=syntax.highlight(contents, language))

 class RevisionDownloadFile(RevisionPage):
-    def GET(self, ops, revision, filename):
+    def GET(self, ctxt, revision, filename):
         web.header('Content-Disposition', 'attachment; filename=%s' % filename)
         revision = mtn.Revision(revision)
-        if not self.exists(ops, revision):
+        if not self.exists(ctxt.ops, revision):
             return web.notfound()
-        fileid = RevisionPage.get_fileid(self, ops, revision, filename)
+        fileid = RevisionPage.get_fileid(self, ctxt.ops, revision, filename)
         if not fileid:
             return web.notfound()
-        for idx, data in enumerate(ops.get_file(fileid)):
+        for idx, data in enumerate(ctxt.ops.get_file(fileid)):
             if idx == 0:
                 mimetype = mimehelp.lookup(filename, data)
                 web.header('Content-Type', mimetype)
@@ -469,16 +471,16 @@ class RevisionTar(RevisionPage):
         sys.stdout.flush()

 class RevisionTar(RevisionPage):
-    def GET(self, ops, revision):
+    def GET(self, ctxt, revision):
         # we'll output in the USTAR tar format; documentation taken from:
         # http://en.wikipedia.org/wiki/Tar_%28file_format%29
         revision = mtn.Revision(revision)
-        if not self.exists(ops, revision):
+        if not self.exists(ctxt.ops, revision):
             return web.notfound()
         filename = "%s.tar" % revision
         web.header('Content-Disposition', 'attachment; filename=%s' % filename)
         web.header('Content-Type', 'application/x-tar')
-        manifest = [stanza for stanza in ops.get_manifest_of(revision)]
+        manifest = [stanza for stanza in ctxt.ops.get_manifest_of(revision)]
         # for now; we might want to come up with something more interesting;
         # maybe the branch name (but there might be multiple branches?)
         basedirname = revision
@@ -492,7 +494,7 @@ class RevisionTar(RevisionPage):
             filename, fileid = stanza[1], stanza[3]
             filecontents = cStringIO.StringIO()
             filesize = 0
-            for data in ops.get_file(fileid):
+            for data in ctxt.ops.get_file(fileid):
                 filesize += len(data)
                 filecontents.write(data)
             ti = tarfile.TarInfo()
@@ -500,11 +502,11 @@ class RevisionTar(RevisionPage):
             ti.mode, ti.type = 00600, tarfile.REGTYPE
             ti.uid = ti.gid = 0
             # determine the most recent of the content marks
-            content_marks = [t[1] for t in ops.get_content_changed(revision, filename)]
+            content_marks = [t[1] for t in ctxt.ops.get_content_changed(revision, filename)]
             if len(content_marks) > 0:
                 # just pick one to make this faster
                 content_mark = content_marks[0]
-                since_epoch = timecert(ops.certs(content_mark)) - datetime.datetime.fromtimestamp(0)
+                since_epoch = timecert(ctxt.ops.certs(content_mark)) - datetime.datetime.fromtimestamp(0)
                 ti.mtime = since_epoch.days * 24 * 60 * 60 + since_epoch.seconds
             else:
                 ti.mtime = 0
@@ -513,12 +515,12 @@ class RevisionBrowse(RevisionPage):
             tarobj.addfile(ti, filecontents)

 class RevisionBrowse(RevisionPage):
-    def GET(self, ops, revision, path):
+    def GET(self, ctxt, revision, path):
         revision = mtn.Revision(revision)
-        if not self.exists(ops, revision):
+        if not self.exists(ctxt.ops, revision):
             return web.notfound()
-        branches = RevisionPage.branches_for_rev(self, ops, revision)
-        revisions = ops.get_revision(revision)
+        branches = RevisionPage.branches_for_rev(self, ctxt.ops, revision)
+        revisions = ctxt.ops.get_revision(revision)

         def components(path):
             # NB: mtn internally uses '/' for paths, so we shouldn't use os.path.join()
@@ -553,7 +555,7 @@ class RevisionBrowse(RevisionPage):
             page_title += " of %s %s" % (branch_plural, ', '.join(branches))

         def cut_manifest_to_subdir():
-            manifest = list(ops.get_manifest_of(revision))
+            manifest = list(ctxt.ops.get_manifest_of(revision))
             in_the_dir = False
             for stanza in manifest:
                 stanza_type = stanza[0]
@@ -593,7 +595,7 @@ class RevisionBrowse(RevisionPage):
                 if not certs.has_key(revision):
                     # subtle bug slipped in here; ops.cert() is a generator
                     # so we can't just store it in a cache!
-                    certs[revision] = list(ops.certs(revision))
+                    certs[revision] = list(ctxt.ops.certs(revision))
                 return certs[revision]

             def _get_certinfo(revision):
@@ -619,7 +621,7 @@ class RevisionBrowse(RevisionPage):

             for stanza_type, this_path in entry_iter:
                 # determine the most recent of the content marks
-                content_marks = [t[1] for t in ops.get_content_changed(revision, this_path)]
+                content_marks = [t[1] for t in ctxt.ops.get_content_changed(revision, this_path)]
                 for mark in content_marks:
                     get_cert(mark)
                 if len(content_marks):
@@ -664,8 +666,8 @@ class RevisionGraph(object):
                         entries=info_for_manifest(cut_manifest_to_subdir()))

 class RevisionGraph(object):
-    def GET(self, ops, revision):
-        output_png, output_imagemap = ancestry_graph(ops, revision)
+    def GET(self, ctxt, revision):
+        output_png, output_imagemap = ancestry_graph(ctxt.ops, revision)
         if os.access(output_png, os.R_OK):
             web.header('Content-Type', 'image/png')
             sys.stdout.write(open(output_png).read())
@@ -683,13 +685,13 @@ class Json(object):
                 revdate = common.parse_timecert(cert[7])
                 rv['ago'] = common.ago(revdate)

-    def BranchLink(self, ops, for_branch):
+    def BranchLink(self, ctxt, for_branch):
         rv = {
             'type' : 'branch',
             'branch' : for_branch,
         }
         branch = mtn.Branch(for_branch)
-        changes, new_starting_point = Changes().branch_get_last_changes(ops, branch, 0, 1)
+        changes, new_starting_point = Changes().branch_get_last_changes(ctxt.ops, branch, 0, 1)
         if len(changes) < 1:
             return web.notfound()
         if not changes:
@@ -699,34 +701,34 @@ class Json(object):
             self.fill_from_certs(rv, certs)
         return rv

-    def RevisionLink(self, ops, revision_id):
+    def RevisionLink(self, ctxt, revision_id):
         rv = {
             'type' : 'revision',
             'revision_id' : revision_id,
         }
         rev = mtn.Revision(revision_id)
-        certs = ops.certs(rev)
+        certs = ctxt.ops.certs(rev)
         self.fill_from_certs(rv, certs)
         return rv

-    def GET(self, ops, method, encoded_args):
+    def GET(self, ctxt, method, encoded_args):
         writer = json.JsonWriter()
         if not encoded_args.startswith('js_'):
             return web.notfound()
         args = json.read(binascii.unhexlify((encoded_args[3:])))
         if hasattr(self, method):
-            rv = getattr(self, method)(ops, *args)
+            rv = getattr(self, method)(ctxt, *args)
         else:
             return web.notfound()
         print writer.write(rv)

 class BranchHead(object):
-    def GET(self, ops, head_method, proxy_to, branch, extra_path):
+    def GET(self, ctxt, head_method, proxy_to, branch, extra_path):
         branch = mtn.Branch(branch)
         valid = ('browse', 'file', 'downloadfile', 'info', 'tar', 'graph')
         if not proxy_to in valid:
             return web.notfound()
-        heads = [head for head in ops.heads(branch.name)]
+        heads = [head for head in ctxt.ops.heads(branch.name)]
         if len(heads) == 0:
             return web.notfound()
         def proxyurl(revision):
@@ -740,7 +742,7 @@ class BranchHead(object):
             head_links = []
             for revision in heads:
                 author, date = '', ''
-                for cert in ops.certs(revision):
+                for cert in ctxt.ops.certs(revision):
                     if cert[4] == 'name' and cert[5] == 'date':
                         date = cert[7]
                     elif cert[4] == 'name' and cert[5] == 'author':
============================================================
--- render.py	032026fe3b80f63a098218b7c77c90c4d7a2e518
+++ render.py	c1223ddbb5396f63101b981838cd8296d0cc82ba
@@ -8,7 +8,7 @@ import cgi, urllib, web
 # PURPOSE.

 import cgi, urllib, web
-import mtn, release, config
+import mtn, release, config, common
 from links import link, dynamic_join, static_join

 hq = cgi.escape
============================================================
--- viewmtn.py	a3a3446ad7ea43e0ff0ceb6daa04dac0b7719bae
+++ viewmtn.py	6629b45724cc639d2555c41248ccadf35838b83e
@@ -11,21 +11,24 @@ from itertools import izip, chain, repea

 import os, sys
 from itertools import izip, chain, repeat
-# web.py
 import web
-# Other bits of ViewMTN
 import mtn, handlers
-# The user configuration file
+from urls import common_urls, perdb_urls
 import config

-debug = web.debug
-
 # purloined from: http://docs.python.org/lib/itertools-recipes.html
 def grouper(n, iterable, padvalue=None):
     "grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')"
     return izip(*[chain(iterable, repeat(padvalue, n-1))]*n)

-class OperationsFactory(object):
+class RequestContext(object):
+    def __init__ (self, dbname, ops):
+        self.dbname, self.ops = dbname, ops
+        # make sure that any unread automate output is flushed away
+        if not ops is None:
+            ops.per_request()
+
+class RequestContextFactory(object):
     def __init__ (self):
         # has the user specified a dbfiles hash? if so, use it
         self.ops_instances = {}
@@ -39,64 +42,14 @@ class OperationsFactory(object):
             self.ops_instances["legacy"] = mtn.Operations([config.monotone, config.dbfile])
             self.default = "legacy"

-    def get_ops(self, name):
-        ops = None
+    def __getitem__(self, name):
         if name is None:
             ops = self.ops_instances.get (self.default, None)
         else:
             name = name.rstrip('/')
             ops = self.ops_instances.get (name, None)
-        # technically it'd be better to do this before serving the
-        # request, however this is about the only per-request
-        # spot that runs for every handler..
-        if not ops is None:
-            ops.per_request()
-        return ops
+        return RequestContext(name, ops)

-op_fact = OperationsFactory()
-
-common_urls = (
-# these don't care about multiple databases specified via the URL
-    r'about', 'About',
-    r'help', 'Help',
-    r'robots.txt', 'RobotsTxt',  ## FIXME needs o exclude per-db paths
-    r'mimeicon/([A-Za-z0-9][a-z0-9\-\+\.]*)/([A-Za-z0-9][a-z0-9\-\+\.]*)', 'MimeIcon',
-)
-
-perdb_urls = (
-    r'', 'Index',
-    r'tags', 'Tags',
-    r'json/([A-Za-z]+)/(.*)', 'Json',
-
-    r'([a-zA-Z]/)?revision/browse/('+mtn.revision_re+')/(.*)', 'RevisionBrowse',
-    r'revision/browse/('+mtn.revision_re+')()', 'RevisionBrowse',
-    r'revision/diff/('+mtn.revision_re+')/with/('+mtn.revision_re+')', 'RevisionDiff',
-    r'revision/diff/('+mtn.revision_re+')/with/('+mtn.revision_re+')'+'/(.*)', 'RevisionDiff',
-    r'revision/rawdiff/('+mtn.revision_re+')/with/('+mtn.revision_re+')', 'RevisionRawDiff',
-    r'revision/rawdiff/('+mtn.revision_re+')/with/('+mtn.revision_re+')'+'/(.*)', 'RevisionRawDiff',
-    r'revision/file/('+mtn.revision_re+')/(.*)', 'RevisionFile',
-    r'revision/filechanges/()()('+mtn.revision_re+')/(.*)', 'RevisionFileChanges',
-    r'revision/filechanges/from/(\d+)/to/(\d+)/('+mtn.revision_re+')/(.*)', 'RevisionFileChanges',
-    r'revision/filechanges/rss/()()('+mtn.revision_re+')/(.*)', 'RevisionFileChangesRSS',
-    r'revision/filechanges/rss/from/(\d+)/to/(\d+)/('+mtn.revision_re+')/(.*)', 'RevisionFileChangesRSS',
-    r'revision/downloadfile/('+mtn.revision_re+')/(.*)', 'RevisionDownloadFile',
-    r'revision/info/('+mtn.revision_re+')', 'RevisionInfo',
-    r'revision/tar/('+mtn.revision_re+')', 'RevisionTar',
-    r'revision/graph/('+mtn.revision_re+')', 'RevisionGraph',
-
-    r'branch/changes/(.*)/from/(\d+)/to/(\d+)', 'HTMLBranchChanges',
-    r'branch/changes/([^/]+)()()', 'HTMLBranchChanges',
-    r'branch/changes/(.*)/from/(\d+)/to/(\d+)/rss', 'RSSBranchChanges',
-    r'branch/changes/([^/]+)()()/rss', 'RSSBranchChanges',
-    r'branch/tags/([^/]+)', 'Tags',
-
-    # let's make it possible to access any function on the head revision
-    # through this proxy method; it'll return a redirect to the head revision
-    # with the specified function
-    r'branch/(head)/([A-Za-z]+)/([^/]+)(.*)', 'BranchHead',
-    r'branch/(anyhead)/([A-Za-z]+)/([^/]+)(.*)', 'BranchHead',
-)
-
 def runfcgi_apache(func):
     web.wsgi.runfcgi(func, None)

@@ -112,7 +65,9 @@ if __name__ == '__main__':
     def assemble_urls():
         fvars = {}
         urls = ()
-
+
+        factory = RequestContextFactory()
+
         for url, fn in grouper (2, common_urls):
             url = r'^/' + url
             if hasattr(handlers, fn):
@@ -125,7 +80,7 @@ if __name__ == '__main__':
             class PerDBClosure(object):
                 def GET (self, *args, **kwargs):
                     db, other_args = args[0], args[1:]
-                    ops = op_fact.get_ops (db)
+                    ops = factory[db]
                     if ops is None:
                         return web.notfound()
                     return handler.GET (ops, *other_args, **kwargs)