Source

trac-ticketlinks / contrib / trac-post-commit-hook

Full commit
#!/usr/bin/env python

# trac-post-commit-hook
# ----------------------------------------------------------------------------
# Copyright (c) 2004 Stephen Hansen 
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#   The above copyright notice and this permission notice shall be included in
#   all copies or substantial portions of the Software. 
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# ----------------------------------------------------------------------------

# This Subversion post-commit hook script is meant to interface to the
# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc 
# system.
# 
# It should be called from the 'post-commit' script in Subversion, such as
# via:
#
# REPOS="$1"
# REV="$2"
# LOG=`/usr/bin/svnlook log -r $REV $REPOS`
# AUTHOR=`/usr/bin/svnlook author -r $REV $REPOS`
# TRAC_ENV='/somewhere/trac/project/'
#
# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
#  -p "$TRAC_ENV"  \
#  -r "$REV"       \
#  -u "$AUTHOR"    \
#  -m "$LOG"
#
# It searches commit messages for text in the form of:
#   command #1
#   command #1, #2
#   command #1 & #2 
#   command #1 and #2
#
# You can have more then one command in a message. The following commands
# are supported. There is more then one spelling for each command, to make
# this as user-friendly as possible.
#
#   closes, fixes
#     The specified issue numbers are closed with the contents of this
#     commit message being added to it. 
#   references, refs, addresses, re 
#     The specified issue numbers are left in their current status, but 
#     the contents of this commit message are added to their notes. 
#
# A fairly complicated example of what you can do is with a commit message
# of:
#
#    Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
#
# This will close #10 and #12, and add a note to #12.

import re
import os
import sys
import time 

import sqlite

try:
    from optparse import OptionParser
except ImportError:
    try:
        from optik import OptionParser
    except ImportError:
        raise ImportError, 'Requires Python 2.3 or the Optik option parsing library.'

parser = OptionParser()
parser.add_option('-e', '--require-envelope', dest='env', default='',
                  help='Require commands to be enclosed in an envelope. If -e[], then commands must be in the form of [closes #4]. Must be two characters.')
parser.add_option('-p', '--project', dest='project', help='Path to the Trac project.')
parser.add_option('-r', '--revision', dest='rev', help='Repository revision number.')
parser.add_option('-u', '--user', dest='user', help='The user who is responsible for this action')
parser.add_option('-m', '--msg', dest='msg', help='The log message to search.')

(options, args) = parser.parse_args(sys.argv[1:])

print options.env

if options.env:
    leftEnv = '\\' + options.env[0]
    rghtEnv = '\\' + options.env[1]
else:
    leftEnv = ''
    rghtEnv = ''
    
commandPattern = re.compile(leftEnv + r'(?P<action>[A-Za-z]*).?(?P<ticket>#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv)
ticketPattern = re.compile(r'#([0-9]*)')

class CommitHook:
    _supported_cmds = { "closes":     '_cmdClose',
                        "fixes":      '_cmdClose',
                        "addresses":  '_cmdRefs',
                        "references": '_cmdRefs',
                        "refs":       '_cmdRefs',
                        "re":         '_cmdRefs',
                      } 
    def __init__(self, project=options.project, author=options.user, rev=options.rev, msg=options.msg):
        self.author = author
        self.rev = rev
        self.msg = "(In [%s]) %s" % (rev, msg)
        self.now = int(time.time()) 
        self.con = sqlite.connect(os.path.join(project, 'db', 'trac.db'), autocommit=0) 

        self.verifyDatabaseVersion()

        cmdGroups = commandPattern.findall(msg) 
        for cmd, tkts in cmdGroups:
            try:
                getattr(self, CommitHook._supported_cmds[cmd.lower()])(ticketPattern.findall(tkts))
            except: 
                self.con.rollback()
        self.con.commit()
        
    def _cmdClose(self, tickets):
        cur = self.con.cursor() 
        for tkt in tickets: 
            cur.execute("SELECT status FROM ticket WHERE id=%d", int(tkt))
            row = cur.fetchone()
            oldStatus = row[0]   
            cur.execute("UPDATE ticket SET status='closed', resolution='fixed', changetime=%s WHERE id=%s", self.now, int(tkt)) 
            cur.execute("INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES (%s, %s, %s, %s, %s, %s)",
                        int(tkt), self.now, self.author, 'comment', '', self.msg)
            cur.execute("INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES (%s, %s, %s, %s, %s, %s)",
                        int(tkt), self.now, self.author, 'status', oldStatus, 'closed') 
            cur.execute("INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES (%s, %s, %s, %s, %s, %s)",
                        int(tkt), self.now, self.author, 'resolution', '', 'fixed') 
            
    def _cmdRefs(self, tickets):
        cur = self.con.cursor() 
        now = int(time.time())
        for tkt in tickets: 
            cur.execute("UPDATE ticket SET changetime=%s WHERE id=%s", self.now, int(tkt))
            cur.execute("INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES (%s, %s, %s, %s, %s, %s)",
                        int(tkt), self.now, self.author, 'comment', '', self.msg)

    def verifyDatabaseVersion(self):
        """Make sure the database uses a known schema version"""
        cursor = self.con.cursor()
        cursor.execute('SELECT value FROM system WHERE '
                       'name=%s AND value=%s', 'database_version', '3')
        if not cursor.fetchone():
            raise Exception('Expected Trac database version 3')
                        
if __name__ == "__main__":
    if len(sys.argv) < 5:
        print "For usage: %s --help" % (sys.argv[0])
    else:
        CommitHook()