Mercurial > p > roundup > code
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> - - <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> - - </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> - - <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: </td> - <th class="required">highlighted</th> - <td> 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> + + <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> + + </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> + + <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: </td> + <th class="required">highlighted</th> + <td> 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 +#
