Commits

Anonymous committed 42dba5a

[svn r4247] initial import of RepositoryHooksSystem

Comments (0)

Files changed (12)

0.11/repository_hook_system/__init__.py

+# API:  code available for export
+from admin import RepositoryHookAdmin
+from interface import IRepositoryChangeListener
+from interface import IRepositoryHookSubscriber
+from listener import RepositoryChangeListener
+from listener import command_line
+from svnhooksystem import SVNHookSystem
+from ticketchanger import TicketChanger

0.11/repository_hook_system/admin.py

+"""
+RepositoryHookAdmin:
+admin panel interface for controlling hook setup and listeners
+"""
+
+from repository_hook_system.interface import IRepositoryHookSystem
+from repository_hook_system.interface import IRepositoryHookSubscriber
+from trac.admin.api import IAdminPanelProvider
+from trac.config import Option
+from trac.core import *
+from trac.web.chrome import ITemplateProvider
+
+class RepositoryHookAdmin(Component):
+    """webadmin panel for hook configuration"""
+    
+    implements(ITemplateProvider, IAdminPanelProvider)
+    listeners = ExtensionPoint(IRepositoryHookSubscriber)
+
+    systems = ExtensionPoint(IRepositoryHookSystem) 
+    # XXX maybe should be IRepositoryHookSetup?
+    # or perhaps thes IRepositoryHookSetup and IRepositoryChangeListener
+    # interfaces should be combined
+
+    def system(self):
+        """returns the IRepositoryHookSystem appropriate to the repository"""
+        # XXX could abstract this, as this is not specific to TTW functionality
+        for system in self.systems:
+            if self.env.config.get('trac', 'repository_type') in system.type():
+                return system            
+
+
+    ### methods for ITemplateProvider
+
+    """Extension point interface for components that provide their own
+    ClearSilver templates and accompanying static resources.
+    """
+
+    def get_htdocs_dirs(self):
+        """Return a list of directories with static resources (such as style
+        sheets, images, etc.)
+
+        Each item in the list must be a `(prefix, abspath)` tuple. The
+        `prefix` part defines the path in the URL that requests to these
+        resources are prefixed with.
+        
+        The `abspath` is the absolute path to the directory containing the
+        resources on the local file system.
+        """
+        return []
+
+    def get_templates_dirs(self):
+        """Return a list of directories containing the provided template
+        files.
+        """
+        from pkg_resources import resource_filename
+        return [resource_filename(__name__, 'templates')]
+
+    ### methods for IAdminPanelProvider
+
+    """Extension point interface for adding panels to the web-based
+    administration interface.
+    """
+
+    def get_admin_panels(self, req):
+        """Return a list of available admin panels.
+        
+        The items returned by this function must be tuples of the form
+        `(category, category_label, page, page_label)`.
+        """
+        if req.perm.has_permission('TRAC_ADMIN'): # XXX needed?
+            system = self.system()
+            if system is not None:
+                for hook in system.available_hooks():
+                    yield ('repository_hooks', 'Repository Hooks', hook, hook)
+
+    def render_admin_panel(self, req, category, page, path_info):
+        """Process a request for an admin panel.
+        
+        This function should return a tuple of the form `(template, data)`,
+        where `template` is the name of the template to use and `data` is the
+        data to be passed to the template.
+        """
+        hookname = page
+        system = self.system()
+        data = {}
+        data['hook'] = hookname
+
+        if req.method == 'POST':
+
+            # implementation-specific post-processing
+            # XXX should probably handle errors, etc
+            system.process_post(hookname, req)
+
+            # toggle invocation of the hook
+            if req.args.get('enable'):
+                system.enable(hookname)
+            else:
+                system.disable(hookname)
+
+            # set available listeners on a hook
+            listeners = req.args.get('listeners', [])
+            if isinstance(listeners, basestring):
+                listeners = [ listeners ] # XXX ', '.join ?
+            self.env.config.set('repository-hooks', hookname, 
+                                ', '.join(listeners))
+
+            # process posted options to configuration
+            for listener in self.listeners:
+                name = listener.__class__.__name__
+                options = self.options(listener)
+                args = dict([(key.split('%s-' % name, 1)[1], value) 
+                             for key, value in req.args.items()
+                             if key.startswith('%s-' % name)])
+                for option in options:
+                    option_type = options[option]['type']
+                    section = options[option]['section']
+                    value = args.get(option, '')
+                    if option_type == 'bool':
+                        value = value == "on" and "true" or "false"
+                    self.env.config.set(section, option, value)
+
+            self.env.config.save()
+            
+
+        data['enabled'] = system.is_enabled(hookname)
+        activated = [ i.__class__.__name__ for i in system.subscribers(hookname) ]
+        data['snippet'] = system.render(hookname, req)
+
+        data['listeners'] = []
+        for listener in self.listeners:
+            _cls = listener.__class__
+            data['listeners'].append(dict(name=_cls.__name__, 
+                                          activated=(_cls.__name__ in activated),
+                                          description=listener.__doc__,
+                                          options=self.options(listener)))
+
+        return ('repositoryhooks.html', data)
+
+    def options(self, listener):
+        _cls = listener.__class__
+        options = [ (i, getattr(_cls, i)) for i in dir(_cls) 
+                    if isinstance(getattr(_cls, i), Option) ]
+        options = dict([(option.name, dict(section=option.section,
+                                         type=option.__class__.__name__.lower()[:-6] or 'text',
+                                         value=getattr(listener, attr),
+                                         description=option.__doc__))
+                        for attr, option in options ])
+        return options

0.11/repository_hook_system/filesystemhooks.py

+import os
+import repository_hook_system.listener as listener
+
+from repository_hook_system.interface import IRepositoryHookSetup
+from repository_hook_system.listener import command_line
+from repository_hook_system.listener import option_parser
+from trac.core import *
+from utils import command_line_args
+from utils import iswritable
+
+class FileSystemHooks(Component):
+    """
+    Implementation of IRepositoryHookSetup for hooks that live on the 
+    filesystem.  Currently, the filenames associated with the hooks must
+    be the same as the hook names
+    """
+    
+    implements(IRepositoryHookSetup)
+    abstract = True
+    mode = 0750 # mode to write hook files
+
+    ### these methods must be implemented by the provider class
+
+    def filename(self, hookname):
+        raise NotImplementedError
+
+    def args(self):
+        raise NotImplementedError
+
+    ### methods for manipulating the files
+
+    def file_contents(self, hookname):
+        """
+        return the lines of the file for a given hook,
+        or None if the file does not yet exist
+        """ 
+        filename = self.filename(hookname)
+        if not os.path.exists(filename):
+            return None
+
+        f = file(filename)
+        retval =  [ i.rstrip() for i in f.readlines() ]
+        f.close()
+        return retval
+
+    def marker(self):
+        """marker to place in the file to identify the hook"""
+        return "# trac repository hook system"
+
+    def projects_enabled(self, hookname):
+        """
+        returns enabled projects, or None if the stub is not found
+        returns a tuple of (lines, index, list_of_projects) when found
+        this won't work properly if the command line is used more than once 
+        in the file
+        """
+        lines = self.file_contents(hookname)
+        if lines is None:
+            return None
+
+        retval = None
+        invoker = listener.filename()
+        for index, line in enumerate(lines):
+            if ' %s ' % invoker in line and not line.strip().startswith('#'):
+                if retval is not None:
+                    # TODO: raise an error indicate that multiple invocations
+                    # detected in the hook file
+                    pass 
+                args = command_line_args(line)
+                parser = option_parser()
+                options, args = parser.parse_args(args)
+                retval = index, lines, options.projects
+
+        return retval
+
+    def create(self, hookname):
+        """create the stub for given hook and return the file object"""
+        
+        filename = self.filename(hookname)
+        os.mknod(filename, self.mode)
+        f = file(filename, 'w')
+        print >> f, "#!/bin/bash"
+        return f
+
+    ### methods for IRepositoryHookSetup
+
+    def enable(self, hookname):
+        # TODO:  remove multiple blank lines when writing
+
+        if self.is_enabled(hookname):
+            return # nothing to do
+
+        if not iswritable(hookname):
+            return # XXX err more gracefully
+
+        def print_hook(f):
+            print >> f, '%s%s' % (os.linesep, self.marker())
+            print >> f, command_line(self.env.path, hookname, *self.args())
+
+        filename = self.filename(hookname)
+        if os.path.exists(filename):
+            projects = self.projects_enabled(hookname)
+            if projects is None:            
+                f = file(filename, 'a')
+                print_hook(f)                
+            else:
+                project = os.path.realpath(self.env.path)
+                index, lines, projects = projects
+                projects.append(project)
+                lines[index] = command_line(projects, hookname, *self.args())
+                f = file(filename, 'w')
+                for line in lines:
+                    print >> f, line                    
+        else:
+            f = self.create(hookname)
+            print_hook(f)
+            
+        f.close()
+
+    def disable(self, hookname):
+        if not self.is_enabled(hookname):
+            return 
+        index, lines, projects = self.projects_enabled(hookname)
+        projects = [ os.path.realpath(project) 
+                     for project in projects ]
+        project = os.path.realpath(self.env.path)
+        
+        projects.remove(project)
+        if projects:
+            lines[index] = command_line(projects, hookname, *self.args())
+        else:
+            lines.pop(index)
+            # TODO: list bounds checking
+            if lines[index-1] == self.marker():
+                index = index-1
+                lines.pop(index)
+            if not lines[index-1].strip():
+                lines.pop(index-1)
+            
+        f = file(self.filename(hookname), 'w')
+        for line in lines:
+            print >> f, line
+        f.close()
+
+    def is_enabled(self, hookname):
+        if os.path.exists(self.filename(hookname)):
+            projects = self.projects_enabled(hookname)
+            if projects is not None:
+                index, lines, projects = projects
+                projects = [ os.path.realpath(project) for project in projects ]
+                project = os.path.realpath(self.env.path)
+                if project in projects: 
+                    return True
+        return False

0.11/repository_hook_system/interface.py

+"""
+interfaces for listening to repository changes
+and configuration of hooks
+"""
+
+from trac.core import Interface
+
+### interfaces for subscribers
+
+class IRepositoryHookSubscriber(Interface):
+    """
+    interface for subscribers to repository hooks
+    """
+
+    def is_available(repository, hookname):
+        """can this subscriber be invoked on this hook?"""        
+
+    def invoke(changeset):
+        """what to do on a commit"""
+
+### interfaces for the hook system
+
+class IRepositoryChangeListener(Interface):
+    """listeners to changes from repository hooks"""
+
+    def type():
+        """list of types of repository to listen for changes"""
+
+    def available_hooks():
+        """hooks available for the repository"""
+
+    def changeset(repo, hookname, *args):
+        """return the changeset as specified by the SCM-specific arguments"""
+
+    def subscribers(hookname): # XXX needed? -> elsewhere?
+        """returns activated subscribers for a given hook"""
+        # XXX this should probably be moved, as it puts
+        # the burden of knowing the subscriber on essentially
+        # the repository.  This is better done in infrastructure
+        # outside the repository;
+        # or maybe this isn't horrible if an abstract base class 
+        # is used for this interface
+
+    def invoke_hook(repo, hookname, *args):
+        """fires the given hook"""
+
+class IRepositoryHookSetup(Interface):
+    """participants capable of setting up hooks"""
+
+    def enable(hookname):
+        """enable the RepositoryChangeListener callback for a given hook"""
+
+    def disable(hookname):
+        """disable the RepositoryChangeListener callback for a given hook"""
+
+    def is_enabled(hookname):
+        """
+        whether the hook has been set up;  
+        contingent upon enable marking the hook in such a way that it can be identified as enabled
+        """
+
+class IRepositoryHookAdminContributer(Interface):
+    """
+    contributes to the webadmin panel for the RepositoryHookSystem
+    """
+    # XXX there should probably an equivalent on the level of IRepositoryHookSubscribers
+
+    def render(hookname, req):
+        """extra HTML to display in the webadmin panel for the hook"""
+        
+    def process_post(hookname, req):
+        """what to do on a POST request"""
+
+class IRepositoryHookSystem(IRepositoryChangeListener, IRepositoryHookSetup, IRepositoryHookAdminContributer):
+    """
+    mixed-in interface for a complete hook system;
+    implementers should be able to listen for changes (IRepositoryChangeListener)
+    as well as setup the hooks (IRepositoryHookSetup)
+    and contribute to the webadmin interface (IRepositoryHookAdminContributer)
+    """

0.11/repository_hook_system/listener.py

+#!/usr/bin/env python
+"""
+Repository Change Listener plugin for trac
+
+This module provides an entry point for trac callable 
+from the command line
+"""
+
+import os
+import sys
+
+from optparse import OptionParser
+from repository_hook_system.interface import IRepositoryChangeListener
+from trac.core import *
+from trac.env import open_environment
+from trac.versioncontrol.api import NoSuchChangeset
+
+class RepositoryChangeListener(object):
+    # XXX this doesn't need to be a class...yet!    
+
+    def __init__(self, project, hook, *args):
+        """
+        * project : path to the trac project environment
+        * hook : name of the hook called from
+        * args : arguments for the particular implementation of IRepositoryChangeListener
+        """
+
+        # open the trac environment 
+        env = open_environment(project)
+        repo = env.get_repository() # XXX multiproject branch
+        repo.sync()
+
+        # find the active listeners
+        listeners = ExtensionPoint(IRepositoryChangeListener).extensions(env)
+        
+        # find the listener for the given repository type and invoke the hook
+        for listener in listeners:
+            if env.config.get('trac', 'repository_type') in listener.type():
+                changeset = listener.changeset(repo, *args)
+                subscribers = listener.subscribers(hook)
+                for subscriber in subscribers:
+                    subscriber.invoke(changeset)
+        
+def filename():
+    return os.path.abspath(__file__.rstrip('c'))
+
+def command_line(projects, hook, *args):
+    """return a generic command line for invoking this file"""
+
+    # arguments to the command line
+    # XXX this could be returned as a list, if there is a reason to do so
+    retval = [ sys.executable, filename() ]
+    
+    # enable passing just one argument
+    if isinstance(projects, basestring):
+        projects = [ projects ]
+
+    # append the projects to the command line
+    for project in projects:
+        retval.extend(['-p', project])
+        
+    # add the hook
+    retval.extend(['--hook', hook])
+
+    # add the arguments
+    retval.extend(args)
+
+    return ' '.join(retval)
+
+def option_parser():
+    parser = OptionParser()
+    parser.add_option('-p', '--project', '--projects', 
+                      dest='projects', action='append', 
+                      default=[],
+                      help='projects to apply to',
+                      )
+    parser.add_option('--hook',
+                      dest='hook',
+                      help='hook called')
+    return parser
+
+if __name__ == '__main__':
+
+    # obtain command line options
+    # the arguments should be those needed for the particular
+    # implementation of IRepositoryChangeListener
+    parser = option_parser()
+    options, args = parser.parse_args()
+
+    # TODO: ensure --hook is passed
+
+    for project in options.projects:
+        RepositoryChangeListener(project, options.hook, *args)

0.11/repository_hook_system/svnhooksystem.py

+"""
+implementation of the RepositoryChangeListener interface for svn
+"""
+
+import os
+
+from genshi.builder import tag
+from repository_hook_system.filesystemhooks import FileSystemHooks
+from repository_hook_system.interface import IRepositoryChangeListener
+from repository_hook_system.interface import IRepositoryHookSubscriber
+from repository_hook_system.interface import IRepositoryHookSystem
+from trac.config import ListOption
+from trac.core import *
+from trac.util.text import CRLF
+from utils import iswritable
+
+class SVNHookSystem(FileSystemHooks):
+    """implementation of IRepositoryChangeListener for SVN repositories"""
+
+    implements(IRepositoryHookSystem, IRepositoryChangeListener)
+    listeners = ExtensionPoint(IRepositoryHookSubscriber)
+    hooks = [ 'pre-commit', 'post-commit', 'pre-revprop-change', 'post-revprop-change' ]
+
+    ### methods for FileSystemHooks
+
+    def filename(self, hookname):
+        location = self.env.config.get('trac', 'repository_dir')
+        return os.path.join(location, 'hooks', hookname)
+
+    def args(self):
+        return [ '$2' ]
+
+    ### methods for IRepositoryHookAdminContributer
+
+    def render(self, hookname, req):
+        filename = self.filename(hookname)
+        try:
+            contents = file(filename).read() # check for CRLF here too?
+            return tag.textarea(contents, rows='25', cols='80', name='hook-file-contents')
+        except IOError:
+            if iswritable(filename):
+                text = "No %s hook file yet exists;  enable this hook to create one" % hookname
+            else:
+                text = "The file, %s, is unwritable;  enabling this hook will have no effect"
+            return text
+
+    def process_post(self, hookname, req):
+        
+        contents = req.args.get('hook-file-contents', None)
+        if contents is None:
+            return
+        if os.linesep != CRLF:
+            contents = os.linesep.join(contents.split(CRLF)) # form contents will have this
+
+        filename = self.filename(hookname)
+        if not os.path.exists(filename):
+            if not iswritable(filename):
+                return # XXX error handling?
+            os.mknod(self.mode)
+        f = file(filename, 'w')
+        print >> f, contents
+
+    ### methods for IRepositoryChangeListener
+
+    def type(self):
+        return ['svn', 'svnsync']
+
+    def available_hooks(self):
+        return self.hooks
+
+    def subscribers(self, hookname):
+        """returns the active subscribers for a given hook name"""
+        
+        # XXX this is all SCM-agnostic;  should be moved out
+        return [ subscriber for subscriber in self.listeners 
+                 if subscriber.__class__.__name__ 
+                 in getattr(self, hookname, []) 
+                 and subscriber.is_available(self.type(), hookname) ]
+
+    def changeset(self, repo, revision):
+        """ 
+        return the changeset given the repository object and revision number
+        """
+        try:
+            chgset = repo.get_changeset(revision)
+        except NoSuchChangeset:
+            # XXX should probably throw an exception (same one?)
+            return # out of scope changesets are not cached
+        return chgset
+
+for hook in SVNHookSystem.hooks:
+    setattr(SVNHookSystem, hook, 
+            ListOption('repository-hooks', hook, default=[],
+                       doc="active listeners for SVN changes on the %s hook" % hook))

0.11/repository_hook_system/templates/repositoryhooks.html

+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+  <xi:include href="admin.html" />
+  <head>
+    <title>Ticket Submit Policy</title>
+  </head>
+  <body>
+    <h2>Repository Hooks</h2>
+    <form method="post">
+      <fieldset>
+	<legend>${hook}</legend>
+	<div class="field">
+	  <b>Enabled:</b>
+	    <input type="checkbox" name="enable" value="enable" 
+		   py:attrs="{'checked': enabled or None}"/>
+	</div>
+	<div>
+	  ${snippet}
+	</div>
+	<div class="field">
+	  <b>Listeners on this hook:</b>
+	  <py:for each="listener in listeners">
+	    <div>
+	      <fieldset>
+	      <legend>
+	      <input type="checkbox" id="${hook}:${listener['name']}" name="listeners" value="${listener['name']}"
+		     py:attrs="{'checked': listener['activated'] or None}"/>
+	      ${listener['name']}
+	      </legend>
+	      ${listener['description']}
+	      <py:for each="option_name, option in sorted(listener['options'].items())">
+	      <div>
+		<fieldset>
+		<legend>${option_name}</legend>
+		<py:choose test="option['type']">
+		  <py:when test="'bool'">
+		  <input type="checkbox" 
+			 name="${listener['name']}-${option_name}" 
+			 checked="${option['value'] and 'checked' or None}" />
+		  </py:when>
+		  <py:when text="'list'">
+		    <input type="text" 
+			   name="${listener['name']}-${option_name}"
+			   value="${', '.join(option['value'])}"/>
+		  </py:when>
+		  <py:otherwise>
+		    <input type="text" 
+			   name="${listener['name']}-${option_name}"
+			   value="${option['value']}"/>
+		  </py:otherwise>
+		</py:choose>
+		${option['description']}
+		</fieldset>
+	      </div>
+	      </py:for>
+	      </fieldset>
+	    </div>	    
+	  </py:for>
+	</div>
+      </fieldset>
+      <div class="buttons">
+        <input type="submit" value="Apply changes" />
+      </div>
+    </form>
+  </body>
+</html>

0.11/repository_hook_system/ticketchanger.py

+"""
+annotes and closes tickets based on an SVN commit message;
+port of http://trac.edgewall.org/browser/trunk/contrib/trac-post-commit-hook
+"""
+
+import os
+import re
+import sys
+
+from datetime import datetime
+from repository_hook_system.interface import IRepositoryHookSubscriber
+from trac.config import BoolOption
+from trac.config import ListOption
+from trac.config import Option
+from trac.core import *
+from trac.ticket import Ticket
+from trac.ticket.notification import TicketNotifyEmail
+from trac.ticket.web_ui import TicketModule
+from trac.util.datefmt import utc
+
+# TODO: look only for tickets that match 
+# `projectname:#|(ticket|issue|bug)`
+# according to configuration
+# (which also means moving the regex to the class TicketChanger)
+# move more/all of configuration into the .ini file and therefor editable
+
+class TicketChanger(Component):
+    """annotes and closes tickets on repository commit messages"""
+
+    implements(IRepositoryHookSubscriber)    
+
+    ### options
+    envelope_open = Option('ticket-changer', 'opener', default='',
+                           doc='must be present before the action taken to take effect')
+    envelope_close = Option('ticket-changer', 'closer', default='',
+                            doc='must be present after the action taken to take effect')
+    intertrac = BoolOption('ticket-changer', 'intertrac', default=False,
+                           doc='enforce using ticket prefix from intertrac linking')
+    cmd_close = ListOption('ticket-changer', 'close-commands',
+                           default=['close', 'closed', 'closes', 'fix', 'fixed', 'fixes'],
+                           doc='commit message tokens that indicate ticket close [e.g. "closes #123"]')
+    cmd_refs = ListOption('ticket-changer', 'references-commands',
+                          default=['addresses', 're', 'references', 'refs', 'see'],
+                          doc='commit message tokens that indicate ticket reference [e.g. "refs #123"]')
+    
+    def is_available(self, repository, hookname):
+        return True
+
+    def invoke(self, chgset):
+
+        # regular expressions        
+        ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
+        if self.intertrac:  # TODO: split to separate function?
+            # find intertrac links
+            intertrac = {}
+            aliases = {}
+            for key, value in self.env.config.options('intertrac'):
+                if '.' in key:
+                    name, type_ = key.rsplit('.', 1)
+                    if type_ == 'url':
+                        intertrac[name] = value
+                else:
+                    aliases.setdefault(value, []).append(key)
+            intertrac = dict([(value, [key] + aliases.get(key, [])) for key, value in intertrac.items()])
+            project = os.path.basename(self.env.path)
+
+            if '/%s' % project in intertrac: # TODO:  checking using base_url for full paths:
+                ticket_prefix = '(?:%s):%s' % ( '|'.join(intertrac['/%s' % project]),
+                                              ticket_prefix )
+            else: # hopefully sesible default:
+                ticket_prefix = '%s:%s' % (project, ticket_prefix)
+
+        ticket_reference = ticket_prefix + '[0-9]+'
+        ticket_command =  (r'(?P<action>[A-Za-z]*).?'
+                           '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
+                           (ticket_reference, ticket_reference))
+        ticket_command = r'%s%s%s' % (re.escape(self.envelope_open), 
+                                      ticket_command,
+                                      re.escape(self.envelope_close))
+        command_re = re.compile(ticket_command)
+        ticket_re = re.compile(ticket_prefix + '([0-9]+)')
+
+        # other variables
+        msg = "(In [%s]) %s" % (chgset.rev, chgset.message)        
+        now = chgset.date
+        supported_cmds = {} # TODO: this could become an extension point
+        supported_cmds.update(dict([(key, self._cmdClose) for key in self.cmd_close]))
+        supported_cmds.update(dict([(key, self._cmdRefs) for key in self.cmd_refs]))
+
+        cmd_groups = command_re.findall(msg)
+
+        tickets = {}
+        for cmd, tkts in cmd_groups:
+            func = supported_cmds.get(cmd.lower(), None)
+            if func:
+                for tkt_id in ticket_re.findall(tkts):
+                    tickets.setdefault(tkt_id, []).append(func)
+
+        for tkt_id, cmds in tickets.iteritems():
+            try:
+                db = self.env.get_db_cnx()
+                
+                ticket = Ticket(self.env, int(tkt_id), db)
+                for cmd in cmds:
+                    cmd(ticket)
+
+                # determine sequence number... 
+                cnum = 0
+                tm = TicketModule(self.env)
+                for change in tm.grouped_changelog_entries(ticket, db):
+                    if change['permanent']:
+                        cnum += 1
+                
+                ticket.save_changes(chgset.author, msg, now, db, cnum+1)
+                db.commit()
+                
+                tn = TicketNotifyEmail(self.env)
+                tn.notify(ticket, newticket=0, modtime=now)
+            except Exception, e:
+                # import traceback
+                # traceback.print_exc(file=sys.stderr)
+                print>>sys.stderr, 'Unexpected error while processing ticket ' \
+                                   'ID %s: %s' % (tkt_id, e)
+            
+
+    def _cmdClose(self, ticket):
+        ticket['status'] = 'closed'
+        ticket['resolution'] = 'fixed'
+
+    def _cmdRefs(self, ticket):
+        pass

0.11/repository_hook_system/utils.py

+"""
+generic utilities needed for the RepositoryHookSystem package;
+ideally, these would be part of python's stdlib, but until then,
+roll one's own
+"""
+
+import os
+import subprocess
+import sys
+
+def iswritable(filename):
+    """
+    returns whether or not a filename is writable,
+    irregardless of its existance
+    """
+
+    if os.path.exists(filename):
+        return os.access(filename, os.W_OK)
+    else:
+        
+        # XXX try to make the file and delete it,
+        # as this is easier than figuring out permissions
+        try:
+            file(filename, 'w').close()
+        except IOError:
+            return False
+
+        os.remove(filename) # remove the file stub
+        return True
+
+def command_line_args(string):
+    p = subprocess.Popen('%s %s %s' % (sys.executable, 
+                                       os.path.abspath(__file__),
+                                       string),
+                         shell=True, stdout=subprocess.PIPE)
+    stdout = p.communicate()[0]
+    args = stdout.split('\n')[:-1]
+    return args
+
+if __name__ == '__main__':
+    for i in sys.argv[1:]:
+        print i

0.11/repositoryhooksystem.dia

Binary file added.

0.11/repositoryhooksystem.png

Added
New image
+from setuptools import find_packages, setup
+
+version='0.0'
+
+setup(name='RepositoryHookSystem',
+      version=version,
+      description="plugin to make repository commit events iterable and accessible to other plugins",
+      author='Jeff Hammel',
+      author_email='jhammel@openplans.org',
+      url='http://trac-hacks.org/wiki/k0s',
+      keywords='trac plugin',
+      license="GPL",
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests*']),
+      include_package_data=True,
+      zip_safe=False,
+      entry_points = """
+      [trac.plugins]
+      repositoryhooksystem = repository_hook_system
+      """,
+      )
+