Edgewall Software

Ticket #2845: trac-post-commit-hook

File trac-post-commit-hook, 7.9 KB (added by anonymous, 3 years ago)
Line 
1#!/usr/bin/env python
2
3# trac-post-commit-hook
4# ----------------------------------------------------------------------------
5# Copyright (c) 2004 Stephen Hansen
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to
9# deal in the Software without restriction, including without limitation the
10# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11# sell copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14#   The above copyright notice and this permission notice shall be included in
15#   all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23# IN THE SOFTWARE.
24# ----------------------------------------------------------------------------
25
26# This Subversion post-commit hook script is meant to interface to the
27# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc
28# system.
29#
30# It should be called from the 'post-commit' script in Subversion, such as
31# via:
32#
33# REPOS="$1"
34# REV="$2"
35# LOG=`/usr/bin/svnlook log -r $REV $REPOS`
36# AUTHOR=`/usr/bin/svnlook author -r $REV $REPOS`
37# TRAC_ENV='/somewhere/trac/project/'
38# TRAC_URL='http://trac.mysite.com/project/'
39#
40# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
41#  -p "$TRAC_ENV"  \
42#  -r "$REV"       \
43#  -u "$AUTHOR"    \
44#  -m "$LOG"       \
45#  -s "$TRAC_URL"
46#
47# It searches commit messages for text in the form of:
48#   command #1
49#   command #1, #2
50#   command #1 & #2
51#   command #1 and #2
52#
53# You can have more then one command in a message. The following commands
54# are supported. There is more then one spelling for each command, to make
55# this as user-friendly as possible.
56#
57#   closes, fixes
58#     The specified issue numbers are closed with the contents of this
59#     commit message being added to it.
60#   references, refs, addresses, re
61#     The specified issue numbers are left in their current status, but
62#     the contents of this commit message are added to their notes.
63#
64# A fairly complicated example of what you can do is with a commit message
65# of:
66#
67#    Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
68#
69# This will close #10 and #12, and add a note to #12.
70
71import re
72import os
73import sys
74import time 
75
76from trac.env import open_environment
77from trac.Notify import TicketNotifyEmail
78from trac.ticket import Ticket
79from trac.web.href import Href
80
81try:
82    from optparse import OptionParser
83except ImportError:
84    try:
85        from optik import OptionParser
86    except ImportError:
87        raise ImportError, 'Requires Python 2.3 or the Optik option parsing library.'
88
89parser = OptionParser()
90parser.add_option('-e', '--require-envelope', dest='env', default='',
91                  help='Require commands to be enclosed in an envelope. If -e[], '
92                       'then commands must be in the form of [closes #4]. Must '
93                       'be two characters.')
94parser.add_option('-p', '--project', dest='project',
95                  help='Path to the Trac project.')
96parser.add_option('-r', '--revision', dest='rev',
97                  help='Repository revision number.')
98parser.add_option('-u', '--user', dest='user',
99                  help='The user who is responsible for this action')
100parser.add_option('-m', '--msg', dest='msg',
101                  help='The log message to search.')
102parser.add_option('-s', '--siteurl', dest='url',
103                  help='The base URL to the project\'s trac website (to which '
104                       '/ticket/## is appended).  If this is not specified, '
105                       'the project URL from trac.ini will be used.')
106
107(options, args) = parser.parse_args(sys.argv[1:])
108
109if options.env:
110    leftEnv = '\\' + options.env[0]
111    rghtEnv = '\\' + options.env[1]
112else:
113    leftEnv = ''
114    rghtEnv = ''
115
116commandPattern = re.compile(leftEnv + r'(?P<action>[A-Za-z]*).?(?P<ticket>#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv)
117ticketPattern = re.compile(r'#([0-9]*)')
118
119class CommitHook:
120    _supported_cmds = {'close':      '_cmdClose',
121                       'closed':     '_cmdClose',
122                       'closes':     '_cmdClose',
123                       'fix':        '_cmdClose',
124                       'fixed':      '_cmdClose',
125                       'fixes':      '_cmdClose',
126                       'addresses':  '_cmdRefs',
127                       're':         '_cmdRefs',
128                       'references': '_cmdRefs',
129                       'refs':       '_cmdRefs',
130                       'see':        '_cmdRefs'}
131
132    def __init__(self, project=options.project, author=options.user,
133                 rev=options.rev, msg=options.msg, url=options.url):
134        self.author = author
135        self.rev = rev
136        msg = self._readFromFile( msg )
137        self.msg = "(In [%s]) %s" % (rev, msg)
138        self.now = int(time.time()) 
139        self.env = open_environment(project)
140        if url is None:
141            url = self.env.config.get('project', 'url')
142        self.env.href = Href(url)
143        self.env.abs_href = Href(url)
144        cmdGroups = commandPattern.findall(msg) 
145        for cmd, tkts in cmdGroups:
146            if CommitHook._supported_cmds.has_key(cmd.lower()):
147                func = getattr(self, CommitHook._supported_cmds[cmd.lower()])
148                func(ticketPattern.findall(tkts))
149
150    def _cmdClose(self, tickets):
151        for tkt_id in tickets:
152            try:
153                ticket = Ticket(self.env, tkt_id)
154                ticket['status'] = 'closed'
155                ticket['resolution'] = 'fixed'
156                ticket.save_changes(self.author, self.msg, self.now)
157                tn = TicketNotifyEmail(self.env)
158                tn.notify(ticket, newticket=0, modtime=self.now)
159            except Exception, e:
160                print>>sys.stderr, 'Unexpected error while processing ticket ' \
161                                   'ID %s: %s' % (tkt_id, e)
162
163    def _cmdRefs(self, tickets):
164        for tkt_id in tickets: 
165            try:
166                ticket = Ticket(self.env, tkt_id)
167                ticket.save_changes(self.author, self.msg, self.now)
168                tn = TicketNotifyEmail(self.env)
169                tn.notify(ticket, newticket=0, modtime=self.now)
170            except Exception, e:
171                print>>sys.stderr, 'Unexpected error while processing ticket ' \
172                                   'ID %s: %s' % (tkt_id, e)
173    def _readFromFile(self, message):
174        """ Ivan Melnychuk: SOLUTION FOR WINDOWS SYSTEMS:
175        Windows does not support returning values from script.
176        Therefore temporary files may be used to keep some information like commit message
177        If the 'message' starts with 'file:', and the file with indicated name exists, then read the text message from the file rather then using the text directly
178        Even though actual message also may start with such prefix, it is very unlikely that the file exists with such a name by accident
179        """
180        if message[:5] == 'file:' and os.path.exists( message[5:] ):
181            f = open( message[5:], 'r' )
182            message = f.read()
183            f.close()
184            return message
185        return message;
186
187
188
189if __name__ == "__main__":
190    if len(sys.argv) < 5:
191        print "For usage: %s --help" % (sys.argv[0])
192    else:
193        CommitHook()