changeset 4166:fe9b0fdb1790 gsoc-2009

Moved beta-notify to notify-roundup
author Pygi <pygi@users.sourceforge.net>
date Thu, 02 Jul 2009 18:02:46 +0000
parents 38dac0b488de
children 2b554b262d29
files scripts/beta-notify/detectors/svnauditor.py scripts/beta-notify/doc/README scripts/beta-notify/extensions/revision_info.py scripts/beta-notify/html/bug.item.html scripts/beta-notify/html/svn_rev.item.html scripts/beta-notify/html/user.item.html scripts/beta-notify/notify-roundup.ini scripts/beta-notify/notify-roundup.py scripts/notify-roundup/detectors/svnauditor.py scripts/notify-roundup/doc/README scripts/notify-roundup/extensions/revision_info.py scripts/notify-roundup/html/bug.item.html scripts/notify-roundup/html/svn_rev.item.html scripts/notify-roundup/html/user.item.html scripts/notify-roundup/notify-roundup.ini scripts/notify-roundup/notify-roundup.py
diffstat 16 files changed, 1119 insertions(+), 1119 deletions(-) [+]
line wrap: on
line diff
--- a/scripts/beta-notify/detectors/svnauditor.py	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-# Subversion integration auditor
-# 
-# Watches for messages formatted by the notify-roundup.py Subversion hook
-# script, and parses the meta-data out of them, removing it from the
-# message body in the process.
-#
-# Place this file in your tracker's "detectors" directory.
-#
-# See end of file for change history
-
-import re, sets
-
-import roundup.date
-
-import ConfigParser
-
-svn_msg = re.compile('^(revision|repos|host|date|summary)=(.*)$')
-ini_path = '/path/to/notify-roundup.ini'
-
-def parse_message(db, cl, nodeid, newvalues):
-    '''Parse an incoming message for Subversion information.
-    '''
-
-    # collect up our meta-data from the message
-    info = {}
-    content = []
-    for line in newvalues.get('content', '').splitlines():
-        m = svn_msg.match(line)
-        if not m:
-            content.append(line)
-            continue
-        info[m.group(1)] = m.group(2).strip()
-
-    # only continue if all five pieces of information are present
-    if len(info) != 5:
-        return
-
-    # look up the repository id
-    try:
-        svn_repo_id = db.svn_repo.stringFind(path=info['repos'],
-            host=info['host'])[0]
-    except IndexError:
-        #logger.error('no repository %s in tracker'%repos.repos_dir)
-        return
-
-    # create the subversion revision item
-    svn_rev_id = db.svn_rev.create(repository=svn_repo_id,
-        revision=int(info['revision']))
-
-    # minor bit of content cleaning - remove the single leading blank line
-    if content and not content[0].strip():
-        del content[0]
-
-    # set the info on the message
-    newvalues['content'] = '\n'.join(content)
-    newvalues['date'] = roundup.date.Date(info['date'])
-    newvalues['summary'] = info['summary']
-    newvalues['revision'] = svn_rev_id
-
-def undo_title(db, cl, nodeid, newvalues):
-    '''Don't change the title of issues to "SVN commit message..."'''
-    if newvalues.get('title', '').lower().startswith('svn commit message'):
-        del newvalues['title']
-
-
-def init(db):
-    db.msg.audit('create', parse_message)
-    cfg = ConfigParser.ConfigParser()
-    cfg.read(ini_path)
-    fetch_klass = cfg.get('main', 'item-class')
-    klass = db.getclass(fetch_klass)
-    klass.audit('set', undo_title)
-
-#
-# 2005-05-16 - 1.2
-# 
-#   - Status wasn't being set by ID in local mode
-#   - Wasn't catching errors in local changes, hence not cleaning up db
-#     correctly
-#   - svnauditor.py wasn't handling the fifth argument from notify-roundup.py
-#   - viewcvs_url formatting wasn't quite right
-#
-# 2005-05-04 - 1.1
-#   - Several fixes from  Ron Alford
-#   - Don't change issue titles to "SVN commit message..."
-# 
-# 2005-04-26 - 1.0
-#   - Initial version released
-#
--- a/scripts/beta-notify/doc/README	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-Roundup devel tracker in love with subversion
-=============================================
-
-This document serves a tutorial on setting up a Roundup instance
-with devel template featuring subversion integration. Several assumptions
-are made in this tutorial:
-
-1) You've checked out gsoc-2009 Roundup branch
-2) You know how to setup subversion repository
-3) You want to work with a local subversion repository
-
-Let's follow the steps:
-
-1) Setup subversion post-commit hook
-
-a) Rename post-commit.tmpl to post-commit
-b) Make sure its executable (chmod +x)
-c) Add the following to it:
-
-PYTHON=/usr/bin/python
-NOTIFY=/path/to/notify-roundup.py[1]
-CONFIG=/path/to/notify-roundup.ini[1]
-PYTHONPATH=/path/to/roundup/instance "$PYTHON" "$NOTIFY" "$CONFIG" "$REPOS" "$REV"
-
-[1] notify-roundup.py and notify-roundup.ini can be found in scripts/notify-roundup
-
-2) Modify notify-roundup.ini
-
-a) Set tracker home: tracker-home = /path/to/tracker-home
-b) Set address mappings
-
-3) Copy html templates from scripts/notify-roundup/html to share/roundup/templates/devel/html
-
-4) Copy revision_info.py extension from scripts/notify-roundup/extensions to share/roundup/templates/devel/extensions
-
-5) Now, now, this wasn't so hard :)
-
-How-to format subversion commit message
-=======================================
-
-By default, notify-roundup handles bugs.
-To change status of bug1, format your commit
-message like this:
-
-bug1 pending
-
-Current limitations
-===================
-
-1) Notify-roundup can work only with one item-class
-2) Notify-roundup can only modify Status
-
-We are aware of the those limitations, and have plans to alleviate them in the future.
--- a/scripts/beta-notify/extensions/revision_info.py	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-# Subversion integration information fetcher
-# 
-# Extracts information about a specific revision from a local repository.
-#
-# Place this file in your tracker's "extensions" directory.
-
-import sys, os, time
-from svn import core, fs, delta, repos
-
-def inner(pool, path, rev):
-    repos_ptr = repos.svn_repos_open(path, pool)
-    fs_ptr = repos.svn_repos_fs(repos_ptr)
-
-    root = fs.revision_root(fs_ptr, rev, pool)
-    base_rev = rev - 1
-
-    # get all changes
-    editor = repos.RevisionChangeCollector(fs_ptr, rev, pool)
-    e_ptr, e_baton = delta.make_editor(editor, pool)
-    repos.svn_repos_replay(root, e_ptr, e_baton, pool)
-
-    changelist = editor.changes.items()
-    changelist.sort()
-
-    base_root = fs.revision_root(fs_ptr, base_rev, pool)
-
-    l = []
-    for filepath, change in changelist:
-        d = {'path': filepath, 'info': ''}
-        if change.path:
-            if change.added:
-                d['action'] = 'new'
-            else:
-                d['action'] = 'modify'
-                differ = fs.FileDiff(base_root, change.path, root, filepath,
-                    pool, '-L \t(original) -L \t(new) -u'.split(' '))
-                d['info'] = differ.get_pipe().read()
-        else:
-            d['action'] = 'delete'
-        l.append(d)
-    return l
-
-
-def getRevisionInfo(revision):
-    #path = '/Users/richard/tmp/test_repo'
-    #rev = 2
-    return core.run_app(inner, str(revision['repository']['path']),
-        int(revision['revision']))
-
-def init(instance):
-    instance.registerUtil('getRevisionInfo', getRevisionInfo)
-
-if __name__ == '__main__':
-    print getRevision(1)
-
--- a/scripts/beta-notify/html/bug.item.html	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,275 +0,0 @@
-<tal:block metal:use-macro="templates/page/macros/frame">
-<title metal:fill-slot="head_title">
-<tal:block condition="context/id" i18n:translate=""
- >Bug <span tal:replace="context/id" i18n:name="id"
- />: <span tal:replace="context/title" i18n:name="title"
- /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
-/></tal:block>
-<tal:block condition="not:context/id" i18n:translate=""
- >New Bug report - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
-/></tal:block>
-</title>
-<tal:block metal:fill-slot="body_title">
- <span tal:condition="python: not (context.id or context.is_edit_ok())"
-  tal:omit-tag="python:1" i18n:translate="">New Bug</span>
- <span tal:condition="python: not context.id and context.is_edit_ok()"
-  tal:omit-tag="python:1" i18n:translate="">New Bug Editing</span>
- <span tal:condition="python: context.id and not context.is_edit_ok()"
-  tal:omit-tag="python:1" i18n:translate="">Bug <tal:x
-  replace="context/id" i18n:name="id" /></span>
- <span tal:condition="python: context.id and context.is_edit_ok()"
-  tal:omit-tag="python:1" i18n:translate="">Bug<tal:x
-  replace="context/id" i18n:name="id" /> Editing</span>
-</tal:block>
-
-<td class="content" metal:fill-slot="content">
-
-<p tal:condition="python:not (context.is_view_ok()
- or request.user.hasRole('Anonymous'))" i18n:translate="">
- You are not allowed to view this page.</p>
-
-<p tal:condition="python:not context.is_view_ok()
- and request.user.hasRole('Anonymous')" i18n:translate="">
- Please login with your username and password.</p>
-
-<div tal:condition="context/is_view_ok">
-
-<form method="POST" name="itemSynopsis"
-      onSubmit="return submit_once()" enctype="multipart/form-data"
-      tal:attributes="action context/designator">
-
-<fieldset><legend>classification</legend>
-<table class="form">
-<tr>
- <th class="required" i18n:translate="">Title:</th>
- <td colspan="3" tal:condition="context/title/is_edit_ok"
-     tal:content="structure python:context.title.field(size=40)">title</td>
- <td colspan="3" tal:condition="not:context/title/is_edit_ok">
-  <span tal:content="structure context/title/plain"/>
-  <input type="hidden" name="title" tal:attributes="value context/title">
- </td>
-</tr>
-
-<tr>
- <th class="required" i18n:translate="">
-   <span tal:condition="context/type/is_edit_ok" 
-         tal:replace="structure python:db.bug_type.classhelp('id,name,description',property='type',label='Type')" />
-   <span tal:condition="not:context/type/is_edit_ok">Type</span>:
- </th>
- <td tal:content="structure context/type/menu">type</td>
- <th i18n:translate="">
-   <span tal:condition="context/severity/is_edit_ok"
-         tal:replace="structure python:db.severity.classhelp('id,name,description',property='severity',label='Severity')" />
-   <span tal:condition="not:context/severity/is_edit_ok">Severity</span>:
- </th>
- <td tal:content="structure context/severity/menu">severity</td>
-</tr>
-
-<tr>
- <th i18n:translate="">
-   <span tal:condition="context/components/is_edit_ok" 
-         tal:replace="structure python:db.component.classhelp('id,name,description',property='components',label='Components')" />
-   <span tal:condition="not:context/components/is_edit_ok">Components</span>:
- </th>
- <td tal:content="structure context/components/menu">components</td>
- <th i18n:translate="">
-   <span tal:condition="context/versions/is_edit_ok" 
-         tal:replace="structure python:db.version.classhelp('id,name,description',property='versions',label='Versions')" />
-   <span tal:condition="not:context/versions/is_edit_ok">Versions</span>:
- </th>
- <td tal:content="structure context/versions/menu">versions</td>
-</tr>
-</table>
-</fieldset>
-
-<fieldset><legend>process</legend>
-<table class="form">
-<tr tal:condition="context/id">
- <th i18n:translate="">
-   <span tal:condition="context/status/is_edit_ok" 
-         tal:replace="structure python:db.status.classhelp('id,name,description',property='status', label='Status')" />
-   <span tal:condition="not:context/status/is_edit_ok">Status</span>:
- </th>
- <td tal:content="structure context/status/menu">status</td>
- <th i18n:translate="">
-   <span tal:condition="context/resolution/is_edit_ok" 
-         tal:replace="structure python:db.resolution.classhelp('id,name,description',property='resolution', label='Resolution')" />
-   <span tal:condition="not:context/resolution/is_edit_ok">Resolution</span>:
- </th>
- <td tal:content="structure context/resolution/menu">resolution</td>
-</tr>
-
-<tr tal:condition="context/id">
- <th>
-  <tal:block i18n:translate="">Dependencies</tal:block>:
-  <span tal:condition="context/dependencies/is_edit_ok"
-        tal:replace="structure python:db.bug.classhelp('id,title', filter='status=0,1', property='dependencies')" />
- </th>
- <td>
-  <span tal:replace="structure python:context.dependencies.field(showid=1,size=20)" />
-  <span tal:condition="context/dependencies" tal:repeat="d python:context.dependencies.sorted('creation')">
-   <br/>View: <a tal:attributes="href string:bug${d/id}" tal:content="d/id"></a>
-  </span>
- </td>
- <th i18n:translate="">
-  <tal:block i18n:translate="">Superseder</tal:block>:
-  <span tal:condition="context/superseder/is_edit_ok"
-        tal:replace="structure python:db.bug.classhelp('id,title', filter='status=0,1', property='superseder')" />
- </th>
- <td>
-  <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
- <span tal:condition="context/superseder">
-<!--   <br><span i18n:translate="">View</span>:
-     <a tal:repeat="sup context/superseder"
-        tal:content="python:sup['id'] + ', '*(not repeat['sup'].end)"
-        tal:attributes="href string:bug${sup/id}; title sup/title;"></a> -->
-   <br><span i18n:translate="">View</span>:
-     <a tal:content="context/superseder/id"
-        tal:attributes="href string:bug${context/superseder/id}; title context/superseder/title;"></a> 
-  </span> 
- </td>
- </tr>
- <tr>
- <th><tal:block i18n:translate="">Assigned To</tal:block>:</th>
- <td tal:content="structure context/assignee/menu">assignedto menu</td>
- <th><tal:block i18n:translate="">Nosy List</tal:block>:
-  <span tal:condition="context/nosy/is_edit_ok"
-        tal:replace="structure python:db.user.classhelp('username,realname,address', property='nosy')" />
- </th>
- <td>
-  <span tal:replace="structure context/nosy/field" />
- </td>
-</tr>
-<tr>
- <th>
-   <span tal:condition="context/priority/is_edit_ok"
-         tal:replace="structure python:db.priority.classhelp('id,name,description',property='priority',label='Priority')" />
-   <span tal:condition="not:context/priority/is_edit_ok">Priority</span>:
- </th>
- <td tal:content="structure context/priority/menu">priority</td>
- <th i18n:translate="">Keywords:</th>
- <td tal:content="structure python:context['keywords'].menu(height=5)">keywords</td>
-
-
-</tr>
-<tr tal:condition="context/is_edit_ok">
- <th><tal:block i18n:translate="">Comment</tal:block>:</th>
- <td colspan="3">
-  <textarea tal:content="request/form/@note/value | default"
-            name="@note" wrap="hard" rows="10" cols="60"></textarea>
- </td>
-</tr>
-
-<tr tal:condition="context/is_edit_ok">
- <th><tal:block i18n:translate="">File</tal:block>:</th>
- <td colspan="3">
-   <input type="hidden" name="@link@files" value="file-1">
-   <input type="file" name="file-1@content" size="35">
- </td>
-</tr>
-<tr tal:condition="context/is_edit_ok">
- <th><tal:block i18n:translate="">File Description</tal:block>:</th>
- <td colspan=3><input type="edit" name="file-1@description" size="40"></td>
-</tr>
-</table>
-</fieldset>
-<table class="form">
-<tr tal:condition="context/is_edit_ok">
- <td>
-  &nbsp;
-  <input type="hidden" name="@template" value="item">
-  <input type="hidden" name="@required" value="title">
- </td>
- <td colspan=3>
-  <span tal:replace="structure context/submit">submit button</span>
-  <a tal:condition="context/id" tal:attributes="href context/copy_url"
-   i18n:translate="">Make a copy</a>
- </td>
-</tr>
-</table>
-</form>
-
-<p tal:condition="context/id" i18n:translate="">
- Created on <b><tal:x replace="python:context.creation.pretty('%Y-%m-%d %H:%M')" i18n:name="creation" /></b>
- by <b><tal:x replace="context/creator" i18n:name="creator" /></b>,
- last changed <b><tal:x replace="python:context.activity.pretty('%Y-%m-%d %H:%M')" i18n:name="activity" /></b>
- by <b><tal:x replace="context/actor" i18n:name="actor" /></b>.
-</p>
-
-<table class="files" tal:condition="context/files">
- <tr><th colspan="5" class="header" i18n:translate="">Files</th></tr>
- <tr>
-  <th i18n:translate="">File name</th>
-  <th i18n:translate="">Uploaded</th>
-  <th i18n:translate="">Description</th>
-  <th i18n:translate="">Edit</th>
-  <th i18n:translate="">Remove</th>
- </tr>
- <tr tal:repeat="file python:context.files.sorted('creation')">
-  <td>
-   <a tal:attributes="href file/download_url"
-      tal:content="file/name">dld link</a>
-  </td>
-  <td>
-   <span tal:content="file/creator">creator's name</span>,
-   <span tal:content="python:file.creation.pretty('%Y-%m-%d %H:%M')">creation date</span>
-  </td>
-  <td tal:content="file/description" />
-  <td><a tal:condition="file/is_edit_ok"
-          tal:attributes="href string:file${file/id}">edit</a>
-  </td>
-  <td>
-   <form style="padding:0" tal:condition="file/is_edit_ok"
-         tal:attributes="action string:bug${context/id}">
-    <input type="hidden" name="@remove@files" tal:attributes="value file/id">
-    <input type="hidden" name="@action" value="edit">
-    <input type="submit" value="remove" i18n:attributes="value">
-   </form>
-  </td>
- </tr>
-</table>
-
-<table class="messages" tal:condition="context/messages">
- <tr><th colspan="4" class="header" i18n:translate="">Messages</th></tr>
- <tal:block tal:repeat="msg context/messages">
-
-  <tr>
-   <th><a tal:attributes="href string:msg${msg/id}"
-    i18n:translate="">msg<tal:x replace="msg/id" i18n:name="id" /></a></th>
-   <th i18n:translate="">Author: <tal:x replace="python:msg.author.realname.plain()"
-       i18n:name="author" /> (<tal:x replace="msg/author"/>)</th>
-   <th i18n:translate="">Date: <tal:x replace="python:msg.date.pretty('%Y-%m-%d %H:%M')"
-       i18n:name="date" /></th>
-    <form style="padding:0" tal:condition="msg/is_edit_ok"
-          tal:attributes="action string:bug${context/id}">
-     <input type="hidden" name="@remove@messages" tal:attributes="value msg/id">
-     <input type="hidden" name="@action" value="edit">
-     <input type="submit" value="remove" i18n:attributes="value">
-    </form>
-  </tr>
-
-
-   <tr tal:condition="msg/revision">
-   <th tal:define="r msg/revision" colspan="4">
-    <a tal:attributes="href string:svn_rev${r/id}"
-       tal:content="string:Subversion revision ${r/revision}" />
-   </th>
-  </tr>
-  
-  <tr>
-
-   <td colspan="4" class="content">
-    <pre tal:condition="python:msg.content.is_view_ok()"
-         tal:content="structure python:utils.localReplace(msg.content.hyperlinked())">content</pre>
-   </td>
-  </tr>
- </tal:block>
-</table>
-
-<tal:block tal:condition="context/id" tal:replace="structure context/history" />
-
-</div>
-
-</td>
-
-</tal:block>
--- a/scripts/beta-notify/html/svn_rev.item.html	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-<tal:block metal:use-macro="templates/page/macros/frame">
-<title metal:fill-slot="head_title">
- SVN Revision <span tal:replace="context/revision" />
-</title>
-<tal:block metal:fill-slot="body_title">
- SVN Revision <span tal:replace="context/revision" />
-</tal:block>
-
-<td class="content" metal:fill-slot="content">
-
-<p tal:condition="not:context/is_view_ok" i18n:translate="">You are not
-    allowed to view this page.</p>
-
-<div tal:condition="context/is_view_ok">
-
-<table class="messages">
-<tal:block repeat="file python:utils.getRevisionInfo(context)">
- <tr>
-  <th tal:content="string: ${file/action} ${file/path}" />
- </tr>
- <tr tal:condition="file/info"><td><pre tal:content="file/info" /></td></tr>
-</tal:block>
-</table>
-
-<tal:block tal:condition="context/id" tal:replace="structure context/history" />
-
-</div>
-
-</td>
-
-</tal:block>
--- a/scripts/beta-notify/html/user.item.html	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-<tal:doc metal:use-macro="templates/page/macros/frame"
-define="edit_ok context/is_edit_ok"
->
-<title metal:fill-slot="head_title">
-<tal:if condition="context/id" i18n:translate=""
- >User <span tal:replace="context/id" i18n:name="id"
- />: <span tal:replace="context/username" i18n:name="title"
- /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
-/></tal:if>
-<tal:if condition="not:context/id" i18n:translate=""
- >New User - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
-/></tal:if>
-</title>
-<metal:slot fill-slot="more-javascript">
-<script metal:use-macro="templates/page/macros/user_utils"></script>
-<script type="text/javascript" src="@@file/help_controls.js"></script>
-</metal:slot>
-<tal:block metal:fill-slot="body_title"
-  define="edit_ok context/is_edit_ok">
- <span tal:condition="python: not (context.id or edit_ok)"
-  tal:omit-tag="python:1" i18n:translate="">New User</span>
- <span tal:condition="python: not context.id and edit_ok"
-  tal:omit-tag="python:1" i18n:translate="">New User Editing</span>
- <span tal:condition="python: context.id and not edit_ok"
-  tal:omit-tag="python:1" i18n:translate="">User<tal:x
-  replace="context/id" i18n:name="id" /></span>
- <span tal:condition="python: context.id and edit_ok"
-  tal:omit-tag="python:1" i18n:translate="">User<tal:x
-  replace="context/id" i18n:name="id" /> Editing</span>
-</tal:block>
-
-<td class="content" metal:fill-slot="content">
-
-<p tal:condition="python:not (context.is_view_ok()
- or request.user.hasRole('Anonymous'))" i18n:translate="">
- You are not allowed to view this page.</p>
-
-<p tal:condition="python:not context.is_view_ok()
- and request.user.hasRole('Anonymous')" i18n:translate="">
- Please login with your username and password.</p>
-
-<div tal:condition="context/is_view_ok">
-
-<form method="POST"
-      tal:define="required python:'username address'.split()"
-      enctype="multipart/form-data"
-      tal:attributes="action context/designator;
-      onSubmit python:'return checkRequiredFields(\'%s\')'%'\', \''.join(required);
-      ">
-<table class="form" tal:define="
-  th_label templates/page/macros/th_label;
-  src_input templates/page/macros/user_src_input;
-  normal_input templates/page/macros/user_normal_input;
-  pw_input templates/page/macros/user_pw_input;
-  confirm_input templates/page/macros/user_confirm_input;
-  edit_ok context/is_edit_ok;
-  ">
- <tr tal:define="name string:realname; label string:Name; value context/realname; edit_ok edit_ok">
-  <th metal:use-macro="th_label">Name</th>
-  <td><input name="realname" metal:use-macro="src_input"></td>
- </tr>
- <tr tal:define="name string:username; label string:Login Name; value context/username">
-   <th metal:use-macro="th_label">Login Name</th>
-   <td><input metal:use-macro="src_input"></td>
- </tr>
-  <tal:if condition="edit_ok">
- <tr tal:define="name string:password; label string:Login Password">
-  <th metal:use-macro="th_label">Login Password</th>
-  <td><input metal:use-macro="pw_input" type="password"></td>
- </tr>
- <tr tal:define="name string:password; label string:Confirm Password">
-  <th metal:use-macro="th_label">Confirm Password</th>
-  <td><input metal:use-macro="confirm_input" type="password"></td>
- </tr>
-  </tal:if>
- <tr>
- <th i18n:translate="">Subversion login</th>
- <td tal:content="structure context/svn_name/field">svn_name</td>
- </tr>
-  <tal:if condition="python:request.user.hasPermission('Web Roles')">
- <tr tal:define="name string:roles; label string:Roles;">
-  <th><label for="roles" i18n:translate="">Roles</label></th>
-  <td tal:define="gips context/id">
-    <tal:subif condition=gips define="value context/roles">
-      <input metal:use-macro="normal_input">
-    </tal:subif>
-    <tal:subif condition="not:gips" define="value db/config/NEW_WEB_USER_ROLES">
-      <input metal:use-macro="normal_input">
-    </tal:subif>
-   <tal:block i18n:translate="">(to give the user more than one role,
-    enter a comma,separated,list)</tal:block>
-  </td>
- </tr>
- </tal:if>
-
- <tr tal:define="name string:phone; label string:Phone; value context/phone">
-  <th metal:use-macro="th_label">Phone</th>
-  <td><input name="phone" metal:use-macro="normal_input"></td>
- </tr>
-
- <tr tal:define="name string:organisation; label string:Organisation; value context/organisation">
-  <th metal:use-macro="th_label">Organisation</th>
-  <td><input name="organisation" metal:use-macro="normal_input"></td>
- </tr>
-
- <tr tal:condition="python:edit_ok or context.timezone"
-     tal:define="name string:timezone; label string:Timezone; value context/timezone">
-  <th metal:use-macro="th_label">Timezone</th>
-  <td><input tal:replace="structure python:
-       utils.tzfield(context.timezone, 'timezone', db.config.DEFAULT_TIMEZONE)"/>
-  </td>
- </tr>
-
- <tr tal:define="name string:address; label string:E-mail address; value context/address">
-  <th metal:use-macro="th_label">E-mail address</th>
-  <td tal:define="mailto python:context.address.field(id='address');
-	  mklink python:mailto and not edit_ok">
-      <a href="mailto:calvin@the-z.org"
-		  tal:attributes="href string:mailto:$value"
-		  tal:content="value"
-          tal:condition="python:mklink">calvin@the-z.org</a>
-      <tal:if condition=edit_ok>
-      <input metal:use-macro="src_input" value="calvin@the-z.org">
-      </tal:if>
-      &nbsp;
-  </td>
- </tr>
-
- <tr>
-  <th><label for="alternate_addresses" i18n:translate="">Alternate E-mail addresses<br>One address per line</label></th>
-  <td>
-    <textarea rows=5 cols=40 tal:replace="structure context/alternate_addresses/multiline">nobody@nowhere.org
-anybody@everywhere.net
-(alternate_addresses)
-    </textarea>
-  </td>
- </tr>
-
- <tr tal:condition="edit_ok">
-  <td>
-   &nbsp;
-   <input type="hidden" name="@template" value="item">
-   <input type="hidden" name="@required" value="username,address"
-          tal:attributes="value python:','.join(required)">
-  </td>
-  <td><input type="submit" value="save" tal:replace="structure context/submit"><!--submit button here-->
-    <input type="reset">
-  </td>
- </tr>
-</table>
-</form>
-
-<tal:block tal:condition="not:context/id" i18n:translate="">
-<table class="form">
-<tr>
- <td>Note:&nbsp;</td>
- <th class="required">highlighted</th>
- <td>&nbsp;fields are required.</td>
-</tr>
-</table>
-</tal:block>
-
-<tal:block tal:condition="context/id" tal:replace="structure context/history" />
-
-</div>
-
-</td>
-
-</tal:doc>
--- a/scripts/beta-notify/notify-roundup.ini	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-; notify-roundup.py configuration file
-
-[main]
-; notify a local or emailed tracker -- 'email' or 'local'
-;mode = email
-mode = local
-
-; change this to detect other issue types
-; multiple issue classes are possible (use regular expression "either" syntax)
-item-class = bug
-; item-class = system
-; item-class = dev|system|network
-
-; only set this if socket.gethostname() doesn't return the host's name as
-; registered with your tracker
-; host = host.name.example
-
-
-
-[local]
-; if notifying a local tracker, configure this variable
-tracker-home = /path/to/your/tracker-home
-
-[email]
-; if notifying a tracker by email, configure these variables
-smtp-host = smtp-host.example
-tracker-address = issues@host.example
-; email-domain is used in conjuntion with the address mappings below
-default-domain = @host.example
-
-[vcs]
-; choose a VCS type -- 'svn' or 'hg'
-;type = hg
-type = svn
-
-[address mappings]
-; map Subversion author names to email addresses that the tracker will
-; recognise. The "email :: default-domain" var will be appended if the
-; address doesn't specify a domain.
-richard = rjones
-; richard = ni@spam.example
-
-; If no mapping is defined for a particular author, we either:
-; 1. use the <Subversion author name>@<default-domain> address or,
-; 2. if a "*" entry is defined under address mappings, then we use
-;    that address as the from address.
-;* = unknown
-
--- a/scripts/beta-notify/notify-roundup.py	Thu Jul 02 18:02:12 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,399 +0,0 @@
-#!/usr/bin/python
-#
-# notify-roundup.py: call into a roundup tracker to notify it of commits
-#
-# USAGE: notify-roundup.py TRACKER-HOME REPOS-DIR REVISION
-#        notify-roundup.py TRACKER-HOME REPOS-DIR REVISION AUTHOR PROPNAME
-#
-#   TRACKER-HOME is the tracker to notify
-#
-# See end of file for change history
-
-import sys, os, time, cStringIO, re, logging, smtplib, ConfigParser, socket
-
-
-# configure logging
-logger = logging.getLogger('notify-roundup')
-hdlr = logging.FileHandler('/tmp/log')
-formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
-hdlr.setFormatter(formatter)
-logger.addHandler(hdlr)
-logger.propogate = False
-logger.setLevel(logging.DEBUG)
-
-#print sys.argv
-# now try to import stuff that might not work
-try:
-    import roundup.instance, roundup.date
-
-    import svn.fs
-    import svn.delta
-    import svn.repos
-    import svn.core
-except:
-    logger.exception('Exception while importing Roundup and SVN')
-    sys.exit(1)
-
-class Failed(Exception):
-    pass
-class Unauthorised(Failed):
-    pass
-
-def main(pool):
-    '''Handle the commit revision.
-    '''
-    # command-line args
-    cfg = ConfigParser.ConfigParser()
-    cfg.read(sys.argv[1])
-    repos_dir = sys.argv[2]
-    revision = int(sys.argv[3])
-    
-    vcs_type = cfg.get('vcs', 'type')
-    
-    # get a handle on the revision in the VCS repository
-    if vcs_type == 'svn':
-        repos = SVNRepository(repos_dir, revision, pool)
-    elif vcs_type == 'hg':
-        repos = HGRepository(repos_dir, revision, pool)
-    else:
-        logging.error('we currently don\'t support %s VCS type'%vcs_type)
-      
-    repos.klass = cfg.get('main', 'item-class')
-    if not repos.extract_info():
-        return
-
-    if cfg.has_option('main', 'host'):
-        repos.host = cfg.get('main', 'host')
-    else:
-        repos.host = socket.gethostname()
-
-    mode = cfg.get('main', 'mode')
-    if mode == 'local':
-        notify_local(cfg.get('local', 'tracker-home'), repos)
-    elif mode == 'email':
-        tracker_address = cfg.get('email', 'tracker-address')
-        domain = cfg.get('email', 'default-domain')
-        smtp_host = cfg.get('email', 'smtp-host')
-        if cfg.has_option('address mappings', repos.author):
-            mapped_email = cfg.get('address mappings', repos.author)
-        elif cfg.has_option('address mappings', '*'):
-            mapped_email = cfg.get('address mappings', '*')
-        else:
-            mapped_email = repos.author
-        if '@' not in mapped_email:
-            mapped_email += domain
-        notify_email(tracker_address, mapped_email, smtp_host, repos)
-    else:
-        logging.error('invalid mode %s in config file'%mode)
-
-
-def notify_email(tracker_address, from_address, smtp_host, repos):
-    subject = '[%s%s] SVN commit message'%(repos.klass, repos.itemid)
-    if repos.status:
-        subject += ' [status=%s]'%repos.status
-    date = time.strftime('%Y-%m-%d %H:%M:%S', repos.date)
-    message = '''From: %s
-To: %s
-Subject: %s
-
-revision=%s
-host=%s
-repos=%s
-date=%s
-summary=%s
-
-%s'''%(from_address, tracker_address, subject, repos.rev, repos.host,
-    repos.repos_dir, date, repos.summary, repos.message)
-
-    logger.debug('MESSAGE TO SEND\n%s'%message)
-
-    smtp = smtplib.SMTP(smtp_host)
-    try:
-        smtp.sendmail(from_address, [tracker_address], message)
-    except:
-        logging.exception('mail to %r from %r via %r'%(tracker_address,
-            from_address, smtp_host))
-
-def notify_local(tracker_home, repos):
-    # get a handle on the tracker db
-    tracker = roundup.instance.open(tracker_home)
-    db = tracker.open('admin')
-    try:
-        notify_local_inner(db, tracker_home, repos)
-    except:
-        db.rollback()
-        db.close()
-        raise
-
-def notify_local_inner(db, tracker_home, repos):
-    # sanity check
-    try:
-        db.getclass(repos.klass)
-    except KeyError:
-        logger.error('no such tracker class %s'%repos.klass)
-        raise Failed
-    if not db.getclass(repos.klass).hasnode(repos.itemid):
-        logger.error('no such %s item %s'%(repos.klass, repos.itemid))
-        raise Failed
-    if repos.status:
-        try:
-            status_id = db.status.lookup(repos.status)
-        except KeyError:
-            logger.error('no such status %s'%repos.status)
-            raise Failed
-
-    print repos.host, repos.repos_dir
-    # get the svn repo information from the tracker
-    try:
-        svn_repo_id = db.svn_repo.stringFind(host=repos.host,
-            path=repos.repos_dir)[0]
-    except IndexError:
-        logger.error('no repository %s in tracker'%repos.repos_dir)
-        raise Failed
-
-    # log in as the appropriate user
-    try:
-        matches = db.user.stringFind(svn_name=repos.author)
-    except KeyError:
-        # the user class has no property "svn_name"
-        matches = []
-    if matches:
-        userid = matches[0]
-    else:
-        try:
-            userid = db.user.lookup(repos.author)
-        except KeyError:
-            raise Failed, 'no Roundup user matching %s'%repos.author
-    username = db.user.get(userid, 'username')
-    db.close()
-
-    # tell Roundup
-    tracker = roundup.instance.open(tracker_home)
-    db = tracker.open(username)
-
-    # check perms
-    if not db.security.hasPermission('Create', userid, 'svn_rev'):
-        raise Unauthorised, "Can't create items of class 'svn_rev'"
-    if not db.security.hasPermission('Create', userid, 'msg'):
-        raise Unauthorised, "Can't create items of class 'msg'"
-    if not db.security.hasPermission('Edit', userid, repos.klass,
-            'messages', repos.itemid):
-        raise Unauthorised, "Can't edit items of class '%s'"%repos.klass
-    if repos.status and not db.security.hasPermission('Edit', userid,
-            repos.klass, 'status', repos.itemid):
-        raise Unauthorised, "Can't edit items of class '%s'"%repos.klass
-
-    # create the revision
-    svn_rev_id = db.svn_rev.create(repository=svn_repo_id, revision=repos.rev)
-
-    # add the message to the spool
-    date = roundup.date.Date(repos.date)
-    msgid = db.msg.create(content=repos.message, summary=repos.summary,
-        author=userid, date=date, revision=svn_rev_id)
-    klass = db.getclass(repos.klass)
-    messages = klass.get(repos.itemid, 'messages')
-    messages.append(msgid)
-    klass.set(repos.itemid, messages=messages)
-    
-    # and set the status
-    if repos.status:
-        klass.set(repos.itemid, status=status_id)
-
-    db.commit()
-    logger.debug('Roundup modification complete')
-    db.close()
-
-
-def _select_adds(change):
-  return change.added
-def _select_deletes(change):
-  return change.path is None
-def _select_modifies(change):
-  return not change.added and change.path is not None
-
-
-def generate_list(output, header, changelist, selection):
-    items = [ ]
-    for path, change in changelist:
-      if selection(change):
-        items.append((path, change))
-    if not items:
-      return
-
-    output.write('%s:\n' % header)
-    for fname, change in items:
-      if change.item_kind == svn.core.svn_node_dir:
-        is_dir = '/'
-      else:
-        is_dir = ''
-      if change.prop_changes:
-        if change.text_changed:
-          props = '   (contents, props changed)'
-        else:
-          props = '   (props changed)'
-      else:
-        props = ''
-      output.write('   %s%s%s\n' % (fname, is_dir, props))
-      if change.added and change.base_path:
-        if is_dir:
-          text = ''
-        elif change.text_changed:
-          text = ', changed'
-        else:
-          text = ' unchanged'
-        output.write('      - copied%s from r%d, %s%s\n'
-                     % (text, change.base_rev, change.base_path[1:], is_dir))
-
-class HGRepository:
-    '''Holds roots and other information about the hg repository.'
-    '''
-    
-    def __init__(self,repos_dir,rev,pool):
-        pass
-        
-class SVNRepository:
-    '''Hold roots and other information about the svn repository. From mailer.py
-    '''
-    def __init__(self, repos_dir, rev, pool):
-        self.repos_dir = repos_dir
-        self.rev = rev
-        self.pool = pool
-
-        self.repos_ptr = svn.repos.svn_repos_open(repos_dir, pool)
-        self.fs_ptr = svn.repos.svn_repos_fs(self.repos_ptr)
-
-        self.roots = {}
-
-        self.root_this = self.roots[rev] = svn.fs.revision_root(self.fs_ptr,
-            rev, self.pool)
-
-        self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR)
-
-    def get_rev_prop(self, propname):
-        return svn.fs.revision_prop(self.fs_ptr, self.rev, propname, self.pool)
-
-    def extract_info(self):
-        issue_re = re.compile('^\s*(%s)\s*(\d+)(\s+(\S+))?\s*$'%self.klass,
-            re.I)
-
-        # parse for Roundup item information
-        log = self.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or ''
-        for line in log.splitlines():
-            m = issue_re.match(line)
-            if m:
-                break
-        else:
-            # nothing to do
-            return
-
-        # parse out the issue information
-        klass = m.group(1)
-        self.itemid = m.group(2)
-
-        issue = klass + self.itemid
-        self.status = m.group(4)
-
-        logger.debug('Roundup info item=%r, status=%r'%(issue, self.status))
-
-        # get all the changes and sort by path
-        editor = svn.repos.RevisionChangeCollector(self.fs_ptr, self.rev,
-            self.pool)
-        e_ptr, e_baton = svn.delta.make_editor(editor, self.pool)
-        svn.repos.svn_repos_replay(self.root_this, e_ptr, e_baton, self.pool)
-
-        changelist = editor.changes.items()
-        changelist.sort()
-
-        # figure out the changed directories
-        dirs = { }
-        for path, change in changelist:
-            if change.item_kind == svn.core.svn_node_dir:
-                dirs[path] = None
-            else:
-                idx = path.rfind('/')
-                if idx == -1:
-                    dirs[''] = None
-                else:
-                    dirs[path[:idx]] = None
-
-        dirlist = dirs.keys()
-
-        # figure out the common portion of all the dirs. note that there is
-        # no "common" if only a single dir was changed, or the root was changed.
-        if len(dirs) == 1 or dirs.has_key(''):
-            commondir = ''
-        else:
-            common = dirlist.pop().split('/')
-            for d in dirlist:
-                parts = d.split('/')
-                for i in range(len(common)):
-                    if i == len(parts) or common[i] != parts[i]:
-                        del common[i:]
-                        break
-            commondir = '/'.join(common)
-            if commondir:
-                # strip the common portion from each directory
-                l = len(commondir) + 1
-                dirlist = [ ]
-                for d in dirs.keys():
-                    if d == commondir:
-                        dirlist.append('.')
-                    else:
-                        dirlist.append(d[l:])
-            else:
-                # nothing in common, so reset the list of directories
-                dirlist = dirs.keys()
-
-        # compose the basic subject line. later, we can prefix it.
-        dirlist.sort()
-        dirlist = ' '.join(dirlist)
-
-        if commondir:
-            self.summary = 'r%d - in %s: %s' % (self.rev, commondir, dirlist)
-        else:
-            self.summary = 'r%d - %s' % (self.rev, dirlist)
-
-        # Generate email for the various groups and option-params.
-        output = cStringIO.StringIO()
-
-        # print summary sections
-        generate_list(output, 'Added', changelist, _select_adds)
-        generate_list(output, 'Removed', changelist, _select_deletes)
-        generate_list(output, 'Modified', changelist, _select_modifies)
-
-        output.write('Log:\n%s\n'%log)
-
-        self.message = output.getvalue()
-
-        svndate = self.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE)
-        self.date = time.localtime(svn.core.secs_from_timestr(svndate,
-            self.pool))
-
-        return True
-
-if __name__ == '__main__':
-    try:
-        svn.core.run_app(main)
-    except Failed, message:
-        logger.error(message)
-        sys.exit(1)
-    except:
-        logger.exception('top level')
-        sys.exit(1)
-
-#
-# 2005-05-16 - 1.2
-# 
-#   - Status wasn't being set by ID in local mode
-#   - Wasn't catching errors in local changes, hence not cleaning up db
-#     correctly
-#   - svnauditor.py wasn't handling the fifth argument from notify-roundup.py
-#   - viewcvs_url formatting wasn't quite right
-#
-# 2005-05-04 - 1.1
-#   - Several fixes from  Ron Alford
-#   - Don't change issue titles to "SVN commit message..."
-# 
-# 2005-04-26 - 1.0
-#   - Initial version released
-#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/detectors/svnauditor.py	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,89 @@
+# Subversion integration auditor
+# 
+# Watches for messages formatted by the notify-roundup.py Subversion hook
+# script, and parses the meta-data out of them, removing it from the
+# message body in the process.
+#
+# Place this file in your tracker's "detectors" directory.
+#
+# See end of file for change history
+
+import re, sets
+
+import roundup.date
+
+import ConfigParser
+
+svn_msg = re.compile('^(revision|repos|host|date|summary)=(.*)$')
+ini_path = '/path/to/notify-roundup.ini'
+
+def parse_message(db, cl, nodeid, newvalues):
+    '''Parse an incoming message for Subversion information.
+    '''
+
+    # collect up our meta-data from the message
+    info = {}
+    content = []
+    for line in newvalues.get('content', '').splitlines():
+        m = svn_msg.match(line)
+        if not m:
+            content.append(line)
+            continue
+        info[m.group(1)] = m.group(2).strip()
+
+    # only continue if all five pieces of information are present
+    if len(info) != 5:
+        return
+
+    # look up the repository id
+    try:
+        svn_repo_id = db.svn_repo.stringFind(path=info['repos'],
+            host=info['host'])[0]
+    except IndexError:
+        #logger.error('no repository %s in tracker'%repos.repos_dir)
+        return
+
+    # create the subversion revision item
+    svn_rev_id = db.svn_rev.create(repository=svn_repo_id,
+        revision=int(info['revision']))
+
+    # minor bit of content cleaning - remove the single leading blank line
+    if content and not content[0].strip():
+        del content[0]
+
+    # set the info on the message
+    newvalues['content'] = '\n'.join(content)
+    newvalues['date'] = roundup.date.Date(info['date'])
+    newvalues['summary'] = info['summary']
+    newvalues['revision'] = svn_rev_id
+
+def undo_title(db, cl, nodeid, newvalues):
+    '''Don't change the title of issues to "SVN commit message..."'''
+    if newvalues.get('title', '').lower().startswith('svn commit message'):
+        del newvalues['title']
+
+
+def init(db):
+    db.msg.audit('create', parse_message)
+    cfg = ConfigParser.ConfigParser()
+    cfg.read(ini_path)
+    fetch_klass = cfg.get('main', 'item-class')
+    klass = db.getclass(fetch_klass)
+    klass.audit('set', undo_title)
+
+#
+# 2005-05-16 - 1.2
+# 
+#   - Status wasn't being set by ID in local mode
+#   - Wasn't catching errors in local changes, hence not cleaning up db
+#     correctly
+#   - svnauditor.py wasn't handling the fifth argument from notify-roundup.py
+#   - viewcvs_url formatting wasn't quite right
+#
+# 2005-05-04 - 1.1
+#   - Several fixes from  Ron Alford
+#   - Don't change issue titles to "SVN commit message..."
+# 
+# 2005-04-26 - 1.0
+#   - Initial version released
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/doc/README	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,53 @@
+Roundup devel tracker in love with subversion
+=============================================
+
+This document serves a tutorial on setting up a Roundup instance
+with devel template featuring subversion integration. Several assumptions
+are made in this tutorial:
+
+1) You've checked out gsoc-2009 Roundup branch
+2) You know how to setup subversion repository
+3) You want to work with a local subversion repository
+
+Let's follow the steps:
+
+1) Setup subversion post-commit hook
+
+a) Rename post-commit.tmpl to post-commit
+b) Make sure its executable (chmod +x)
+c) Add the following to it:
+
+PYTHON=/usr/bin/python
+NOTIFY=/path/to/notify-roundup.py[1]
+CONFIG=/path/to/notify-roundup.ini[1]
+PYTHONPATH=/path/to/roundup/instance "$PYTHON" "$NOTIFY" "$CONFIG" "$REPOS" "$REV"
+
+[1] notify-roundup.py and notify-roundup.ini can be found in scripts/notify-roundup
+
+2) Modify notify-roundup.ini
+
+a) Set tracker home: tracker-home = /path/to/tracker-home
+b) Set address mappings
+
+3) Copy html templates from scripts/notify-roundup/html to share/roundup/templates/devel/html
+
+4) Copy revision_info.py extension from scripts/notify-roundup/extensions to share/roundup/templates/devel/extensions
+
+5) Now, now, this wasn't so hard :)
+
+How-to format subversion commit message
+=======================================
+
+By default, notify-roundup handles bugs.
+To change status of bug1, format your commit
+message like this:
+
+bug1 pending
+
+Current limitations
+===================
+
+1) Notify-roundup can work only with one item-class
+2) Notify-roundup can only modify Status
+
+We are aware of the those limitations, and have plans to alleviate them in the future.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/extensions/revision_info.py	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,55 @@
+# Subversion integration information fetcher
+# 
+# Extracts information about a specific revision from a local repository.
+#
+# Place this file in your tracker's "extensions" directory.
+
+import sys, os, time
+from svn import core, fs, delta, repos
+
+def inner(pool, path, rev):
+    repos_ptr = repos.svn_repos_open(path, pool)
+    fs_ptr = repos.svn_repos_fs(repos_ptr)
+
+    root = fs.revision_root(fs_ptr, rev, pool)
+    base_rev = rev - 1
+
+    # get all changes
+    editor = repos.RevisionChangeCollector(fs_ptr, rev, pool)
+    e_ptr, e_baton = delta.make_editor(editor, pool)
+    repos.svn_repos_replay(root, e_ptr, e_baton, pool)
+
+    changelist = editor.changes.items()
+    changelist.sort()
+
+    base_root = fs.revision_root(fs_ptr, base_rev, pool)
+
+    l = []
+    for filepath, change in changelist:
+        d = {'path': filepath, 'info': ''}
+        if change.path:
+            if change.added:
+                d['action'] = 'new'
+            else:
+                d['action'] = 'modify'
+                differ = fs.FileDiff(base_root, change.path, root, filepath,
+                    pool, '-L \t(original) -L \t(new) -u'.split(' '))
+                d['info'] = differ.get_pipe().read()
+        else:
+            d['action'] = 'delete'
+        l.append(d)
+    return l
+
+
+def getRevisionInfo(revision):
+    #path = '/Users/richard/tmp/test_repo'
+    #rev = 2
+    return core.run_app(inner, str(revision['repository']['path']),
+        int(revision['revision']))
+
+def init(instance):
+    instance.registerUtil('getRevisionInfo', getRevisionInfo)
+
+if __name__ == '__main__':
+    print getRevision(1)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/html/bug.item.html	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,275 @@
+<tal:block metal:use-macro="templates/page/macros/frame">
+<title metal:fill-slot="head_title">
+<tal:block condition="context/id" i18n:translate=""
+ >Bug <span tal:replace="context/id" i18n:name="id"
+ />: <span tal:replace="context/title" i18n:name="title"
+ /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+<tal:block condition="not:context/id" i18n:translate=""
+ >New Bug report - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+</title>
+<tal:block metal:fill-slot="body_title">
+ <span tal:condition="python: not (context.id or context.is_edit_ok())"
+  tal:omit-tag="python:1" i18n:translate="">New Bug</span>
+ <span tal:condition="python: not context.id and context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">New Bug Editing</span>
+ <span tal:condition="python: context.id and not context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">Bug <tal:x
+  replace="context/id" i18n:name="id" /></span>
+ <span tal:condition="python: context.id and context.is_edit_ok()"
+  tal:omit-tag="python:1" i18n:translate="">Bug<tal:x
+  replace="context/id" i18n:name="id" /> Editing</span>
+</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<form method="POST" name="itemSynopsis"
+      onSubmit="return submit_once()" enctype="multipart/form-data"
+      tal:attributes="action context/designator">
+
+<fieldset><legend>classification</legend>
+<table class="form">
+<tr>
+ <th class="required" i18n:translate="">Title:</th>
+ <td colspan="3" tal:condition="context/title/is_edit_ok"
+     tal:content="structure python:context.title.field(size=40)">title</td>
+ <td colspan="3" tal:condition="not:context/title/is_edit_ok">
+  <span tal:content="structure context/title/plain"/>
+  <input type="hidden" name="title" tal:attributes="value context/title">
+ </td>
+</tr>
+
+<tr>
+ <th class="required" i18n:translate="">
+   <span tal:condition="context/type/is_edit_ok" 
+         tal:replace="structure python:db.bug_type.classhelp('id,name,description',property='type',label='Type')" />
+   <span tal:condition="not:context/type/is_edit_ok">Type</span>:
+ </th>
+ <td tal:content="structure context/type/menu">type</td>
+ <th i18n:translate="">
+   <span tal:condition="context/severity/is_edit_ok"
+         tal:replace="structure python:db.severity.classhelp('id,name,description',property='severity',label='Severity')" />
+   <span tal:condition="not:context/severity/is_edit_ok">Severity</span>:
+ </th>
+ <td tal:content="structure context/severity/menu">severity</td>
+</tr>
+
+<tr>
+ <th i18n:translate="">
+   <span tal:condition="context/components/is_edit_ok" 
+         tal:replace="structure python:db.component.classhelp('id,name,description',property='components',label='Components')" />
+   <span tal:condition="not:context/components/is_edit_ok">Components</span>:
+ </th>
+ <td tal:content="structure context/components/menu">components</td>
+ <th i18n:translate="">
+   <span tal:condition="context/versions/is_edit_ok" 
+         tal:replace="structure python:db.version.classhelp('id,name,description',property='versions',label='Versions')" />
+   <span tal:condition="not:context/versions/is_edit_ok">Versions</span>:
+ </th>
+ <td tal:content="structure context/versions/menu">versions</td>
+</tr>
+</table>
+</fieldset>
+
+<fieldset><legend>process</legend>
+<table class="form">
+<tr tal:condition="context/id">
+ <th i18n:translate="">
+   <span tal:condition="context/status/is_edit_ok" 
+         tal:replace="structure python:db.status.classhelp('id,name,description',property='status', label='Status')" />
+   <span tal:condition="not:context/status/is_edit_ok">Status</span>:
+ </th>
+ <td tal:content="structure context/status/menu">status</td>
+ <th i18n:translate="">
+   <span tal:condition="context/resolution/is_edit_ok" 
+         tal:replace="structure python:db.resolution.classhelp('id,name,description',property='resolution', label='Resolution')" />
+   <span tal:condition="not:context/resolution/is_edit_ok">Resolution</span>:
+ </th>
+ <td tal:content="structure context/resolution/menu">resolution</td>
+</tr>
+
+<tr tal:condition="context/id">
+ <th>
+  <tal:block i18n:translate="">Dependencies</tal:block>:
+  <span tal:condition="context/dependencies/is_edit_ok"
+        tal:replace="structure python:db.bug.classhelp('id,title', filter='status=0,1', property='dependencies')" />
+ </th>
+ <td>
+  <span tal:replace="structure python:context.dependencies.field(showid=1,size=20)" />
+  <span tal:condition="context/dependencies" tal:repeat="d python:context.dependencies.sorted('creation')">
+   <br/>View: <a tal:attributes="href string:bug${d/id}" tal:content="d/id"></a>
+  </span>
+ </td>
+ <th i18n:translate="">
+  <tal:block i18n:translate="">Superseder</tal:block>:
+  <span tal:condition="context/superseder/is_edit_ok"
+        tal:replace="structure python:db.bug.classhelp('id,title', filter='status=0,1', property='superseder')" />
+ </th>
+ <td>
+  <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
+ <span tal:condition="context/superseder">
+<!--   <br><span i18n:translate="">View</span>:
+     <a tal:repeat="sup context/superseder"
+        tal:content="python:sup['id'] + ', '*(not repeat['sup'].end)"
+        tal:attributes="href string:bug${sup/id}; title sup/title;"></a> -->
+   <br><span i18n:translate="">View</span>:
+     <a tal:content="context/superseder/id"
+        tal:attributes="href string:bug${context/superseder/id}; title context/superseder/title;"></a> 
+  </span> 
+ </td>
+ </tr>
+ <tr>
+ <th><tal:block i18n:translate="">Assigned To</tal:block>:</th>
+ <td tal:content="structure context/assignee/menu">assignedto menu</td>
+ <th><tal:block i18n:translate="">Nosy List</tal:block>:
+  <span tal:condition="context/nosy/is_edit_ok"
+        tal:replace="structure python:db.user.classhelp('username,realname,address', property='nosy')" />
+ </th>
+ <td>
+  <span tal:replace="structure context/nosy/field" />
+ </td>
+</tr>
+<tr>
+ <th>
+   <span tal:condition="context/priority/is_edit_ok"
+         tal:replace="structure python:db.priority.classhelp('id,name,description',property='priority',label='Priority')" />
+   <span tal:condition="not:context/priority/is_edit_ok">Priority</span>:
+ </th>
+ <td tal:content="structure context/priority/menu">priority</td>
+ <th i18n:translate="">Keywords:</th>
+ <td tal:content="structure python:context['keywords'].menu(height=5)">keywords</td>
+
+
+</tr>
+<tr tal:condition="context/is_edit_ok">
+ <th><tal:block i18n:translate="">Comment</tal:block>:</th>
+ <td colspan="3">
+  <textarea tal:content="request/form/@note/value | default"
+            name="@note" wrap="hard" rows="10" cols="60"></textarea>
+ </td>
+</tr>
+
+<tr tal:condition="context/is_edit_ok">
+ <th><tal:block i18n:translate="">File</tal:block>:</th>
+ <td colspan="3">
+   <input type="hidden" name="@link@files" value="file-1">
+   <input type="file" name="file-1@content" size="35">
+ </td>
+</tr>
+<tr tal:condition="context/is_edit_ok">
+ <th><tal:block i18n:translate="">File Description</tal:block>:</th>
+ <td colspan=3><input type="edit" name="file-1@description" size="40"></td>
+</tr>
+</table>
+</fieldset>
+<table class="form">
+<tr tal:condition="context/is_edit_ok">
+ <td>
+  &nbsp;
+  <input type="hidden" name="@template" value="item">
+  <input type="hidden" name="@required" value="title">
+ </td>
+ <td colspan=3>
+  <span tal:replace="structure context/submit">submit button</span>
+  <a tal:condition="context/id" tal:attributes="href context/copy_url"
+   i18n:translate="">Make a copy</a>
+ </td>
+</tr>
+</table>
+</form>
+
+<p tal:condition="context/id" i18n:translate="">
+ Created on <b><tal:x replace="python:context.creation.pretty('%Y-%m-%d %H:%M')" i18n:name="creation" /></b>
+ by <b><tal:x replace="context/creator" i18n:name="creator" /></b>,
+ last changed <b><tal:x replace="python:context.activity.pretty('%Y-%m-%d %H:%M')" i18n:name="activity" /></b>
+ by <b><tal:x replace="context/actor" i18n:name="actor" /></b>.
+</p>
+
+<table class="files" tal:condition="context/files">
+ <tr><th colspan="5" class="header" i18n:translate="">Files</th></tr>
+ <tr>
+  <th i18n:translate="">File name</th>
+  <th i18n:translate="">Uploaded</th>
+  <th i18n:translate="">Description</th>
+  <th i18n:translate="">Edit</th>
+  <th i18n:translate="">Remove</th>
+ </tr>
+ <tr tal:repeat="file python:context.files.sorted('creation')">
+  <td>
+   <a tal:attributes="href file/download_url"
+      tal:content="file/name">dld link</a>
+  </td>
+  <td>
+   <span tal:content="file/creator">creator's name</span>,
+   <span tal:content="python:file.creation.pretty('%Y-%m-%d %H:%M')">creation date</span>
+  </td>
+  <td tal:content="file/description" />
+  <td><a tal:condition="file/is_edit_ok"
+          tal:attributes="href string:file${file/id}">edit</a>
+  </td>
+  <td>
+   <form style="padding:0" tal:condition="file/is_edit_ok"
+         tal:attributes="action string:bug${context/id}">
+    <input type="hidden" name="@remove@files" tal:attributes="value file/id">
+    <input type="hidden" name="@action" value="edit">
+    <input type="submit" value="remove" i18n:attributes="value">
+   </form>
+  </td>
+ </tr>
+</table>
+
+<table class="messages" tal:condition="context/messages">
+ <tr><th colspan="4" class="header" i18n:translate="">Messages</th></tr>
+ <tal:block tal:repeat="msg context/messages">
+
+  <tr>
+   <th><a tal:attributes="href string:msg${msg/id}"
+    i18n:translate="">msg<tal:x replace="msg/id" i18n:name="id" /></a></th>
+   <th i18n:translate="">Author: <tal:x replace="python:msg.author.realname.plain()"
+       i18n:name="author" /> (<tal:x replace="msg/author"/>)</th>
+   <th i18n:translate="">Date: <tal:x replace="python:msg.date.pretty('%Y-%m-%d %H:%M')"
+       i18n:name="date" /></th>
+    <form style="padding:0" tal:condition="msg/is_edit_ok"
+          tal:attributes="action string:bug${context/id}">
+     <input type="hidden" name="@remove@messages" tal:attributes="value msg/id">
+     <input type="hidden" name="@action" value="edit">
+     <input type="submit" value="remove" i18n:attributes="value">
+    </form>
+  </tr>
+
+
+   <tr tal:condition="msg/revision">
+   <th tal:define="r msg/revision" colspan="4">
+    <a tal:attributes="href string:svn_rev${r/id}"
+       tal:content="string:Subversion revision ${r/revision}" />
+   </th>
+  </tr>
+  
+  <tr>
+
+   <td colspan="4" class="content">
+    <pre tal:condition="python:msg.content.is_view_ok()"
+         tal:content="structure python:utils.localReplace(msg.content.hyperlinked())">content</pre>
+   </td>
+  </tr>
+ </tal:block>
+</table>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:block>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/html/svn_rev.item.html	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,31 @@
+<tal:block metal:use-macro="templates/page/macros/frame">
+<title metal:fill-slot="head_title">
+ SVN Revision <span tal:replace="context/revision" />
+</title>
+<tal:block metal:fill-slot="body_title">
+ SVN Revision <span tal:replace="context/revision" />
+</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="not:context/is_view_ok" i18n:translate="">You are not
+    allowed to view this page.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<table class="messages">
+<tal:block repeat="file python:utils.getRevisionInfo(context)">
+ <tr>
+  <th tal:content="string: ${file/action} ${file/path}" />
+ </tr>
+ <tr tal:condition="file/info"><td><pre tal:content="file/info" /></td></tr>
+</tal:block>
+</table>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:block>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/html/user.item.html	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,169 @@
+<tal:doc metal:use-macro="templates/page/macros/frame"
+define="edit_ok context/is_edit_ok"
+>
+<title metal:fill-slot="head_title">
+<tal:if condition="context/id" i18n:translate=""
+ >User <span tal:replace="context/id" i18n:name="id"
+ />: <span tal:replace="context/username" i18n:name="title"
+ /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:if>
+<tal:if condition="not:context/id" i18n:translate=""
+ >New User - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:if>
+</title>
+<metal:slot fill-slot="more-javascript">
+<script metal:use-macro="templates/page/macros/user_utils"></script>
+<script type="text/javascript" src="@@file/help_controls.js"></script>
+</metal:slot>
+<tal:block metal:fill-slot="body_title"
+  define="edit_ok context/is_edit_ok">
+ <span tal:condition="python: not (context.id or edit_ok)"
+  tal:omit-tag="python:1" i18n:translate="">New User</span>
+ <span tal:condition="python: not context.id and edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">New User Editing</span>
+ <span tal:condition="python: context.id and not edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">User<tal:x
+  replace="context/id" i18n:name="id" /></span>
+ <span tal:condition="python: context.id and edit_ok"
+  tal:omit-tag="python:1" i18n:translate="">User<tal:x
+  replace="context/id" i18n:name="id" /> Editing</span>
+</tal:block>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<form method="POST"
+      tal:define="required python:'username address'.split()"
+      enctype="multipart/form-data"
+      tal:attributes="action context/designator;
+      onSubmit python:'return checkRequiredFields(\'%s\')'%'\', \''.join(required);
+      ">
+<table class="form" tal:define="
+  th_label templates/page/macros/th_label;
+  src_input templates/page/macros/user_src_input;
+  normal_input templates/page/macros/user_normal_input;
+  pw_input templates/page/macros/user_pw_input;
+  confirm_input templates/page/macros/user_confirm_input;
+  edit_ok context/is_edit_ok;
+  ">
+ <tr tal:define="name string:realname; label string:Name; value context/realname; edit_ok edit_ok">
+  <th metal:use-macro="th_label">Name</th>
+  <td><input name="realname" metal:use-macro="src_input"></td>
+ </tr>
+ <tr tal:define="name string:username; label string:Login Name; value context/username">
+   <th metal:use-macro="th_label">Login Name</th>
+   <td><input metal:use-macro="src_input"></td>
+ </tr>
+  <tal:if condition="edit_ok">
+ <tr tal:define="name string:password; label string:Login Password">
+  <th metal:use-macro="th_label">Login Password</th>
+  <td><input metal:use-macro="pw_input" type="password"></td>
+ </tr>
+ <tr tal:define="name string:password; label string:Confirm Password">
+  <th metal:use-macro="th_label">Confirm Password</th>
+  <td><input metal:use-macro="confirm_input" type="password"></td>
+ </tr>
+  </tal:if>
+ <tr>
+ <th i18n:translate="">Subversion login</th>
+ <td tal:content="structure context/svn_name/field">svn_name</td>
+ </tr>
+  <tal:if condition="python:request.user.hasPermission('Web Roles')">
+ <tr tal:define="name string:roles; label string:Roles;">
+  <th><label for="roles" i18n:translate="">Roles</label></th>
+  <td tal:define="gips context/id">
+    <tal:subif condition=gips define="value context/roles">
+      <input metal:use-macro="normal_input">
+    </tal:subif>
+    <tal:subif condition="not:gips" define="value db/config/NEW_WEB_USER_ROLES">
+      <input metal:use-macro="normal_input">
+    </tal:subif>
+   <tal:block i18n:translate="">(to give the user more than one role,
+    enter a comma,separated,list)</tal:block>
+  </td>
+ </tr>
+ </tal:if>
+
+ <tr tal:define="name string:phone; label string:Phone; value context/phone">
+  <th metal:use-macro="th_label">Phone</th>
+  <td><input name="phone" metal:use-macro="normal_input"></td>
+ </tr>
+
+ <tr tal:define="name string:organisation; label string:Organisation; value context/organisation">
+  <th metal:use-macro="th_label">Organisation</th>
+  <td><input name="organisation" metal:use-macro="normal_input"></td>
+ </tr>
+
+ <tr tal:condition="python:edit_ok or context.timezone"
+     tal:define="name string:timezone; label string:Timezone; value context/timezone">
+  <th metal:use-macro="th_label">Timezone</th>
+  <td><input tal:replace="structure python:
+       utils.tzfield(context.timezone, 'timezone', db.config.DEFAULT_TIMEZONE)"/>
+  </td>
+ </tr>
+
+ <tr tal:define="name string:address; label string:E-mail address; value context/address">
+  <th metal:use-macro="th_label">E-mail address</th>
+  <td tal:define="mailto python:context.address.field(id='address');
+	  mklink python:mailto and not edit_ok">
+      <a href="mailto:calvin@the-z.org"
+		  tal:attributes="href string:mailto:$value"
+		  tal:content="value"
+          tal:condition="python:mklink">calvin@the-z.org</a>
+      <tal:if condition=edit_ok>
+      <input metal:use-macro="src_input" value="calvin@the-z.org">
+      </tal:if>
+      &nbsp;
+  </td>
+ </tr>
+
+ <tr>
+  <th><label for="alternate_addresses" i18n:translate="">Alternate E-mail addresses<br>One address per line</label></th>
+  <td>
+    <textarea rows=5 cols=40 tal:replace="structure context/alternate_addresses/multiline">nobody@nowhere.org
+anybody@everywhere.net
+(alternate_addresses)
+    </textarea>
+  </td>
+ </tr>
+
+ <tr tal:condition="edit_ok">
+  <td>
+   &nbsp;
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="username,address"
+          tal:attributes="value python:','.join(required)">
+  </td>
+  <td><input type="submit" value="save" tal:replace="structure context/submit"><!--submit button here-->
+    <input type="reset">
+  </td>
+ </tr>
+</table>
+</form>
+
+<tal:block tal:condition="not:context/id" i18n:translate="">
+<table class="form">
+<tr>
+ <td>Note:&nbsp;</td>
+ <th class="required">highlighted</th>
+ <td>&nbsp;fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:doc>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/notify-roundup.ini	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,48 @@
+; notify-roundup.py configuration file
+
+[main]
+; notify a local or emailed tracker -- 'email' or 'local'
+;mode = email
+mode = local
+
+; change this to detect other issue types
+; multiple issue classes are possible (use regular expression "either" syntax)
+item-class = bug
+; item-class = system
+; item-class = dev|system|network
+
+; only set this if socket.gethostname() doesn't return the host's name as
+; registered with your tracker
+; host = host.name.example
+
+
+
+[local]
+; if notifying a local tracker, configure this variable
+tracker-home = /path/to/your/tracker-home
+
+[email]
+; if notifying a tracker by email, configure these variables
+smtp-host = smtp-host.example
+tracker-address = issues@host.example
+; email-domain is used in conjuntion with the address mappings below
+default-domain = @host.example
+
+[vcs]
+; choose a VCS type -- 'svn' or 'hg'
+;type = hg
+type = svn
+
+[address mappings]
+; map Subversion author names to email addresses that the tracker will
+; recognise. The "email :: default-domain" var will be appended if the
+; address doesn't specify a domain.
+richard = rjones
+; richard = ni@spam.example
+
+; If no mapping is defined for a particular author, we either:
+; 1. use the <Subversion author name>@<default-domain> address or,
+; 2. if a "*" entry is defined under address mappings, then we use
+;    that address as the from address.
+;* = unknown
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/notify-roundup/notify-roundup.py	Thu Jul 02 18:02:46 2009 +0000
@@ -0,0 +1,399 @@
+#!/usr/bin/python
+#
+# notify-roundup.py: call into a roundup tracker to notify it of commits
+#
+# USAGE: notify-roundup.py TRACKER-HOME REPOS-DIR REVISION
+#        notify-roundup.py TRACKER-HOME REPOS-DIR REVISION AUTHOR PROPNAME
+#
+#   TRACKER-HOME is the tracker to notify
+#
+# See end of file for change history
+
+import sys, os, time, cStringIO, re, logging, smtplib, ConfigParser, socket
+
+
+# configure logging
+logger = logging.getLogger('notify-roundup')
+hdlr = logging.FileHandler('/tmp/log')
+formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+hdlr.setFormatter(formatter)
+logger.addHandler(hdlr)
+logger.propogate = False
+logger.setLevel(logging.DEBUG)
+
+#print sys.argv
+# now try to import stuff that might not work
+try:
+    import roundup.instance, roundup.date
+
+    import svn.fs
+    import svn.delta
+    import svn.repos
+    import svn.core
+except:
+    logger.exception('Exception while importing Roundup and SVN')
+    sys.exit(1)
+
+class Failed(Exception):
+    pass
+class Unauthorised(Failed):
+    pass
+
+def main(pool):
+    '''Handle the commit revision.
+    '''
+    # command-line args
+    cfg = ConfigParser.ConfigParser()
+    cfg.read(sys.argv[1])
+    repos_dir = sys.argv[2]
+    revision = int(sys.argv[3])
+    
+    vcs_type = cfg.get('vcs', 'type')
+    
+    # get a handle on the revision in the VCS repository
+    if vcs_type == 'svn':
+        repos = SVNRepository(repos_dir, revision, pool)
+    elif vcs_type == 'hg':
+        repos = HGRepository(repos_dir, revision, pool)
+    else:
+        logging.error('we currently don\'t support %s VCS type'%vcs_type)
+      
+    repos.klass = cfg.get('main', 'item-class')
+    if not repos.extract_info():
+        return
+
+    if cfg.has_option('main', 'host'):
+        repos.host = cfg.get('main', 'host')
+    else:
+        repos.host = socket.gethostname()
+
+    mode = cfg.get('main', 'mode')
+    if mode == 'local':
+        notify_local(cfg.get('local', 'tracker-home'), repos)
+    elif mode == 'email':
+        tracker_address = cfg.get('email', 'tracker-address')
+        domain = cfg.get('email', 'default-domain')
+        smtp_host = cfg.get('email', 'smtp-host')
+        if cfg.has_option('address mappings', repos.author):
+            mapped_email = cfg.get('address mappings', repos.author)
+        elif cfg.has_option('address mappings', '*'):
+            mapped_email = cfg.get('address mappings', '*')
+        else:
+            mapped_email = repos.author
+        if '@' not in mapped_email:
+            mapped_email += domain
+        notify_email(tracker_address, mapped_email, smtp_host, repos)
+    else:
+        logging.error('invalid mode %s in config file'%mode)
+
+
+def notify_email(tracker_address, from_address, smtp_host, repos):
+    subject = '[%s%s] SVN commit message'%(repos.klass, repos.itemid)
+    if repos.status:
+        subject += ' [status=%s]'%repos.status
+    date = time.strftime('%Y-%m-%d %H:%M:%S', repos.date)
+    message = '''From: %s
+To: %s
+Subject: %s
+
+revision=%s
+host=%s
+repos=%s
+date=%s
+summary=%s
+
+%s'''%(from_address, tracker_address, subject, repos.rev, repos.host,
+    repos.repos_dir, date, repos.summary, repos.message)
+
+    logger.debug('MESSAGE TO SEND\n%s'%message)
+
+    smtp = smtplib.SMTP(smtp_host)
+    try:
+        smtp.sendmail(from_address, [tracker_address], message)
+    except:
+        logging.exception('mail to %r from %r via %r'%(tracker_address,
+            from_address, smtp_host))
+
+def notify_local(tracker_home, repos):
+    # get a handle on the tracker db
+    tracker = roundup.instance.open(tracker_home)
+    db = tracker.open('admin')
+    try:
+        notify_local_inner(db, tracker_home, repos)
+    except:
+        db.rollback()
+        db.close()
+        raise
+
+def notify_local_inner(db, tracker_home, repos):
+    # sanity check
+    try:
+        db.getclass(repos.klass)
+    except KeyError:
+        logger.error('no such tracker class %s'%repos.klass)
+        raise Failed
+    if not db.getclass(repos.klass).hasnode(repos.itemid):
+        logger.error('no such %s item %s'%(repos.klass, repos.itemid))
+        raise Failed
+    if repos.status:
+        try:
+            status_id = db.status.lookup(repos.status)
+        except KeyError:
+            logger.error('no such status %s'%repos.status)
+            raise Failed
+
+    print repos.host, repos.repos_dir
+    # get the svn repo information from the tracker
+    try:
+        svn_repo_id = db.svn_repo.stringFind(host=repos.host,
+            path=repos.repos_dir)[0]
+    except IndexError:
+        logger.error('no repository %s in tracker'%repos.repos_dir)
+        raise Failed
+
+    # log in as the appropriate user
+    try:
+        matches = db.user.stringFind(svn_name=repos.author)
+    except KeyError:
+        # the user class has no property "svn_name"
+        matches = []
+    if matches:
+        userid = matches[0]
+    else:
+        try:
+            userid = db.user.lookup(repos.author)
+        except KeyError:
+            raise Failed, 'no Roundup user matching %s'%repos.author
+    username = db.user.get(userid, 'username')
+    db.close()
+
+    # tell Roundup
+    tracker = roundup.instance.open(tracker_home)
+    db = tracker.open(username)
+
+    # check perms
+    if not db.security.hasPermission('Create', userid, 'svn_rev'):
+        raise Unauthorised, "Can't create items of class 'svn_rev'"
+    if not db.security.hasPermission('Create', userid, 'msg'):
+        raise Unauthorised, "Can't create items of class 'msg'"
+    if not db.security.hasPermission('Edit', userid, repos.klass,
+            'messages', repos.itemid):
+        raise Unauthorised, "Can't edit items of class '%s'"%repos.klass
+    if repos.status and not db.security.hasPermission('Edit', userid,
+            repos.klass, 'status', repos.itemid):
+        raise Unauthorised, "Can't edit items of class '%s'"%repos.klass
+
+    # create the revision
+    svn_rev_id = db.svn_rev.create(repository=svn_repo_id, revision=repos.rev)
+
+    # add the message to the spool
+    date = roundup.date.Date(repos.date)
+    msgid = db.msg.create(content=repos.message, summary=repos.summary,
+        author=userid, date=date, revision=svn_rev_id)
+    klass = db.getclass(repos.klass)
+    messages = klass.get(repos.itemid, 'messages')
+    messages.append(msgid)
+    klass.set(repos.itemid, messages=messages)
+    
+    # and set the status
+    if repos.status:
+        klass.set(repos.itemid, status=status_id)
+
+    db.commit()
+    logger.debug('Roundup modification complete')
+    db.close()
+
+
+def _select_adds(change):
+  return change.added
+def _select_deletes(change):
+  return change.path is None
+def _select_modifies(change):
+  return not change.added and change.path is not None
+
+
+def generate_list(output, header, changelist, selection):
+    items = [ ]
+    for path, change in changelist:
+      if selection(change):
+        items.append((path, change))
+    if not items:
+      return
+
+    output.write('%s:\n' % header)
+    for fname, change in items:
+      if change.item_kind == svn.core.svn_node_dir:
+        is_dir = '/'
+      else:
+        is_dir = ''
+      if change.prop_changes:
+        if change.text_changed:
+          props = '   (contents, props changed)'
+        else:
+          props = '   (props changed)'
+      else:
+        props = ''
+      output.write('   %s%s%s\n' % (fname, is_dir, props))
+      if change.added and change.base_path:
+        if is_dir:
+          text = ''
+        elif change.text_changed:
+          text = ', changed'
+        else:
+          text = ' unchanged'
+        output.write('      - copied%s from r%d, %s%s\n'
+                     % (text, change.base_rev, change.base_path[1:], is_dir))
+
+class HGRepository:
+    '''Holds roots and other information about the hg repository.'
+    '''
+    
+    def __init__(self,repos_dir,rev,pool):
+        pass
+        
+class SVNRepository:
+    '''Hold roots and other information about the svn repository. From mailer.py
+    '''
+    def __init__(self, repos_dir, rev, pool):
+        self.repos_dir = repos_dir
+        self.rev = rev
+        self.pool = pool
+
+        self.repos_ptr = svn.repos.svn_repos_open(repos_dir, pool)
+        self.fs_ptr = svn.repos.svn_repos_fs(self.repos_ptr)
+
+        self.roots = {}
+
+        self.root_this = self.roots[rev] = svn.fs.revision_root(self.fs_ptr,
+            rev, self.pool)
+
+        self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR)
+
+    def get_rev_prop(self, propname):
+        return svn.fs.revision_prop(self.fs_ptr, self.rev, propname, self.pool)
+
+    def extract_info(self):
+        issue_re = re.compile('^\s*(%s)\s*(\d+)(\s+(\S+))?\s*$'%self.klass,
+            re.I)
+
+        # parse for Roundup item information
+        log = self.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or ''
+        for line in log.splitlines():
+            m = issue_re.match(line)
+            if m:
+                break
+        else:
+            # nothing to do
+            return
+
+        # parse out the issue information
+        klass = m.group(1)
+        self.itemid = m.group(2)
+
+        issue = klass + self.itemid
+        self.status = m.group(4)
+
+        logger.debug('Roundup info item=%r, status=%r'%(issue, self.status))
+
+        # get all the changes and sort by path
+        editor = svn.repos.RevisionChangeCollector(self.fs_ptr, self.rev,
+            self.pool)
+        e_ptr, e_baton = svn.delta.make_editor(editor, self.pool)
+        svn.repos.svn_repos_replay(self.root_this, e_ptr, e_baton, self.pool)
+
+        changelist = editor.changes.items()
+        changelist.sort()
+
+        # figure out the changed directories
+        dirs = { }
+        for path, change in changelist:
+            if change.item_kind == svn.core.svn_node_dir:
+                dirs[path] = None
+            else:
+                idx = path.rfind('/')
+                if idx == -1:
+                    dirs[''] = None
+                else:
+                    dirs[path[:idx]] = None
+
+        dirlist = dirs.keys()
+
+        # figure out the common portion of all the dirs. note that there is
+        # no "common" if only a single dir was changed, or the root was changed.
+        if len(dirs) == 1 or dirs.has_key(''):
+            commondir = ''
+        else:
+            common = dirlist.pop().split('/')
+            for d in dirlist:
+                parts = d.split('/')
+                for i in range(len(common)):
+                    if i == len(parts) or common[i] != parts[i]:
+                        del common[i:]
+                        break
+            commondir = '/'.join(common)
+            if commondir:
+                # strip the common portion from each directory
+                l = len(commondir) + 1
+                dirlist = [ ]
+                for d in dirs.keys():
+                    if d == commondir:
+                        dirlist.append('.')
+                    else:
+                        dirlist.append(d[l:])
+            else:
+                # nothing in common, so reset the list of directories
+                dirlist = dirs.keys()
+
+        # compose the basic subject line. later, we can prefix it.
+        dirlist.sort()
+        dirlist = ' '.join(dirlist)
+
+        if commondir:
+            self.summary = 'r%d - in %s: %s' % (self.rev, commondir, dirlist)
+        else:
+            self.summary = 'r%d - %s' % (self.rev, dirlist)
+
+        # Generate email for the various groups and option-params.
+        output = cStringIO.StringIO()
+
+        # print summary sections
+        generate_list(output, 'Added', changelist, _select_adds)
+        generate_list(output, 'Removed', changelist, _select_deletes)
+        generate_list(output, 'Modified', changelist, _select_modifies)
+
+        output.write('Log:\n%s\n'%log)
+
+        self.message = output.getvalue()
+
+        svndate = self.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE)
+        self.date = time.localtime(svn.core.secs_from_timestr(svndate,
+            self.pool))
+
+        return True
+
+if __name__ == '__main__':
+    try:
+        svn.core.run_app(main)
+    except Failed, message:
+        logger.error(message)
+        sys.exit(1)
+    except:
+        logger.exception('top level')
+        sys.exit(1)
+
+#
+# 2005-05-16 - 1.2
+# 
+#   - Status wasn't being set by ID in local mode
+#   - Wasn't catching errors in local changes, hence not cleaning up db
+#     correctly
+#   - svnauditor.py wasn't handling the fifth argument from notify-roundup.py
+#   - viewcvs_url formatting wasn't quite right
+#
+# 2005-05-04 - 1.1
+#   - Several fixes from  Ron Alford
+#   - Don't change issue titles to "SVN commit message..."
+# 
+# 2005-04-26 - 1.0
+#   - Initial version released
+#

Roundup Issue Tracker: http://roundup-tracker.org/