source: timingandestimationplugin/branches/trac0.11-Permissions/scripts/trac-post-commit.py

Last change on this file was 5370, checked in by Russ Tyndall, 15 years ago

Applied patch from Greg Troxel to bring post commit hook inline with upstream trac (THANKS!)

File size: 10.9 KB
RevLine 
[5368]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
[5370]26### Changes for the Timing and Estimation plugin
[5368]27#
[5370]28# This script is very similar to "trac-post-commit" included with trac
29# itself.  This section explains functional changes relative to that
30# script, and comments throughout the code explain other differences.
31#
32## Logging support for debugging this hook
33#
34## Support for specifying time spent in commit messages.
35#
[5368]36# "Blah refs #12 (1)" will add 1h to the spent time for issue #12
37# "Blah refs #12 (spent 1.5)" will add 1h to the spent time for issue #12
38#
39# As above it is possible to use complicated messages:
40#
[5370]41# "Changed blah and foo to do this or that. Fixes #10 (1) and #12 (2),
42# and refs #13 (0.5)."
[5368]43#
[5370]44# This will close #10 and #12, and add a note to #13 and also add 1h
45# spent time to #10, add 2h spent time to #12 and add 30m spent time
46# to #13.
[5368]47#
48# Note that:
49#     (spent 2), (sp 2) or simply (2) may be used for spent
50#     ' ', ',', '&' or 'and' may be used references
51
52# This Subversion post-commit hook script is meant to interface to the
53# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc
54# system.
55#
56# It should be called from the 'post-commit' script in Subversion, such as
57# via:
58#
59# REPOS="$1"
60# REV="$2"
[5370]61# TRAC_ENV="/path/to/tracenv"
[5368]62#
63# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
64#  -p "$TRAC_ENV" -r "$REV"
65#
66# (all the other arguments are now deprecated and not needed anymore)
67#
68# It searches commit messages for text in the form of:
69#   command #1
70#   command #1, #2
71#   command #1 & #2
72#   command #1 and #2
73#
74# Instead of the short-hand syntax "#1", "ticket:1" can be used as well, e.g.:
75#   command ticket:1
76#   command ticket:1, ticket:2
77#   command ticket:1 & ticket:2
78#   command ticket:1 and ticket:2
79#
80# In addition, the ':' character can be omitted and issue or bug can be used
81# instead of ticket.
82#
83# You can have more then one command in a message. The following commands
84# are supported. There is more then one spelling for each command, to make
85# this as user-friendly as possible.
86#
87#   close, closed, closes, fix, fixed, fixes
88#     The specified issue numbers are closed with the contents of this
89#     commit message being added to it.
90#   references, refs, addresses, re, see
91#     The specified issue numbers are left in their current status, but
92#     the contents of this commit message are added to their notes.
93#
94# A fairly complicated example of what you can do is with a commit message
95# of:
96#
97#    Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
98#
99# This will close #10 and #12, and add a note to #12.
100
101import re
102import os
103import sys
104from datetime import datetime
[5370]105from optparse import OptionParser
[5368]106
[5370]107parser = OptionParser()
108depr = '(not used anymore)'
109parser.add_option('-e', '--require-envelope', dest='envelope', default='',
110                  help="""
111Require commands to be enclosed in an envelope.
112If -e[], then commands must be in the form of [closes #4].
113Must be two characters.""")
114parser.add_option('-p', '--project', dest='project',
115                  help='Path to the Trac project.')
116parser.add_option('-r', '--revision', dest='rev',
117                  help='Repository revision number.')
118parser.add_option('-u', '--user', dest='user',
119                  help='The user who is responsible for this action '+depr)
120parser.add_option('-m', '--msg', dest='msg',
121                  help='The log message to search '+depr)
122parser.add_option('-c', '--encoding', dest='encoding',
123                  help='The encoding used by the log message '+depr)
124parser.add_option('-s', '--siteurl', dest='url',
125                  help=depr+' the base_url from trac.ini will always be used.')
126
127(options, args) = parser.parse_args(sys.argv[1:])
128
129if not 'PYTHON_EGG_CACHE' in os.environ:
130    os.environ['PYTHON_EGG_CACHE'] = os.path.join(options.project, '.egg-cache')
131
[5368]132from trac.env import open_environment
133from trac.ticket.notification import TicketNotifyEmail
134from trac.ticket import Ticket
135from trac.ticket.web_ui import TicketModule
136# TODO: move grouped_changelog_entries to model.py
137from trac.util.text import to_unicode
138from trac.util.datefmt import utc
139from trac.versioncontrol.api import NoSuchChangeset
140
[5370]141# Change logfile to point to someplace this script can write.
[5368]142logfile = "/var/trac/commithook.log"
143LOG = False
144
145if LOG:
146    f = open (logfile,"w")
147    f.write("Begin Log\n")
148    f.close()
149    def log (s, *params):
150        f = open (logfile,"a")
151        f.write(s % params)
152        f.write("\n")
153        f.close()
154else:
155    def log (s, *params):
156        pass
157
[5370]158# Relative to trac standard, this table is hoisted out of class
159# CommitHook so that it can be used in constructing a regexp that only
160# matches on supported commands.
[5368]161_supported_cmds = {'close':      '_cmdClose',
162                   'closed':     '_cmdClose',
163                   'closes':     '_cmdClose',
164                   'fix':        '_cmdClose',
165                   'fixed':      '_cmdClose',
166                   'fixes':      '_cmdClose',
167                   'addresses':  '_cmdRefs',
168                   're':         '_cmdRefs',
169                   'references': '_cmdRefs',
170                   'refs':       '_cmdRefs',
171                   'see':        '_cmdRefs'}
172
[5370]173# Regexps are extended to include "(1)" and "(spent 1)".
[5368]174ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
175time_pattern = r'[ ]?(?:\((?:(?:spent|sp)[ ]?)?(-?[0-9]*(?:\.[0-9]+)?)\))?'
[5370]176ticket_reference = ticket_prefix + '[0-9]+' + time_pattern
[5368]177support_cmds_pattern = '|'.join(_supported_cmds.keys())
[5370]178
179# Relative to upstream, only match command tokens (rather than
180# matching all words).
[5368]181ticket_command =  (r'(?P<action>(?:%s))[ ]*'
182                   '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
[5370]183                   (support_cmds_pattern, ticket_reference, ticket_reference))
[5368]184
185if options.envelope:
186    ticket_command = r'\%s%s\%s' % (options.envelope[0], ticket_command,
187                                    options.envelope[1])
188   
[5370]189# Because we build the regexp to recognize only supported commands,
190# ignore case here.
[5368]191command_re = re.compile(ticket_command, re.IGNORECASE)
[5370]192ticket_re = re.compile(ticket_prefix + '([0-9]+)' + time_pattern, re.IGNORECASE)
[5368]193
194class CommitHook:
195    def __init__(self, project=options.project, author=options.user,
196                 rev=options.rev, url=options.url):
[5370]197        self.env = open_environment(project)
[5368]198        repos = self.env.get_repository()
199        repos.sync()
[5370]200       
[5368]201        # Instead of bothering with the encoding, we'll use unicode data
202        # as provided by the Trac versioncontrol API (#1310).
203        try:
204            chgset = repos.get_changeset(rev)
205        except NoSuchChangeset:
206            return # out of scope changesets are not cached
207        self.author = chgset.author
208        self.rev = rev
209        self.msg = "(In [%s]) %s" % (rev, chgset.message)
210        self.now = datetime.now(utc)
211
212        cmd_groups = command_re.findall(self.msg)
[5370]213
[5368]214        log ("cmd_groups:%s", cmd_groups)
215        tickets = {}
[5370]216        # \todo Explain what xxx1 and xxx2 do; I can't see more params
217        # in command_re.
[5368]218        for cmd, tkts, xxx1, xxx2 in cmd_groups:
219            log ("cmd:%s, tkts%s ", cmd, tkts)
220            funcname = _supported_cmds.get(cmd.lower(), '')
221            if funcname:
222                for tkt_id, spent in ticket_re.findall(tkts):
223                    func = getattr(self, funcname)
[5370]224                    tickets.setdefault(tkt_id, []).append([func, spent])
[5368]225
226        for tkt_id, vals in tickets.iteritems():
227            log ("tkt_id:%s, vals%s ", tkt_id, vals)
228            spent_total = 0.0
229            try:
230                db = self.env.get_db_cnx()
231               
232                ticket = Ticket(self.env, int(tkt_id), db)
233                for (cmd, spent) in vals:
234                    cmd(ticket)
235                    if spent:
236                        spent_total += float(spent)
237
238                # determine sequence number...
239                cnum = 0
240                tm = TicketModule(self.env)
241                for change in tm.grouped_changelog_entries(ticket, db):
242                    if change['permanent']:
243                        cnum += 1
[5370]244               
[5368]245                if spent_total:
246                    self._setTimeTrackerFields(ticket, spent_total)
247                ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
248                db.commit()
[5370]249               
[5368]250                tn = TicketNotifyEmail(self.env)
251                tn.notify(ticket, newticket=0, modtime=self.now)
252            except Exception, e:
253                # import traceback
254                # traceback.print_exc(file=sys.stderr)
255                log('Unexpected error while processing ticket ' \
256                                   'ID %s: %s' % (tkt_id, e))
257                print>>sys.stderr, 'Unexpected error while processing ticket ' \
258                                   'ID %s: %s' % (tkt_id, e)
259           
260
261    def _cmdClose(self, ticket):
262        ticket['status'] = 'closed'
263        ticket['resolution'] = 'fixed'
264
265    def _cmdRefs(self, ticket):
266        pass
267
268    def _setTimeTrackerFields(self, ticket, spent):
269        log ("Setting ticket:%s spent: %s", ticket, spent)
270        if (spent != ''):
271            spentTime = float(spent)
[5370]272            # \bug If the ticket has not been modified since
273            # TimingAndEstimation was installed, then it might not
274            # have hours.  It should still get hours applied because
275            # estimating and recording are separate.
[5368]276            if (ticket.values.has_key('hours')):
277                ticket['hours'] = str(spentTime)
278
279if __name__ == "__main__":
280    if len(sys.argv) < 5:
281        print "For usage: %s --help" % (sys.argv[0])
282        print
283        print "Note that the deprecated options will be removed in Trac 0.12."
284    else:
285        CommitHook()
Note: See TracBrowser for help on using the repository browser.