source: timingandestimationplugin/branches/trac1.0-Permissions/scripts/trac-post-commit.py

Last change on this file was 14061, checked in by Russ Tyndall, 9 years ago

branched for trac1.0

File size: 11.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.')
[10336]118parser.add_option('-R', '--repository', dest='repos',
[10840]119                  help='Repository name (or default if not set).')
[5370]120parser.add_option('-u', '--user', dest='user',
121                  help='The user who is responsible for this action '+depr)
122parser.add_option('-m', '--msg', dest='msg',
123                  help='The log message to search '+depr)
124parser.add_option('-c', '--encoding', dest='encoding',
125                  help='The encoding used by the log message '+depr)
126parser.add_option('-s', '--siteurl', dest='url',
127                  help=depr+' the base_url from trac.ini will always be used.')
128
129(options, args) = parser.parse_args(sys.argv[1:])
130
131if not 'PYTHON_EGG_CACHE' in os.environ:
132    os.environ['PYTHON_EGG_CACHE'] = os.path.join(options.project, '.egg-cache')
133
[5368]134from trac.env import open_environment
135from trac.ticket.notification import TicketNotifyEmail
136from trac.ticket import Ticket
137from trac.ticket.web_ui import TicketModule
138# TODO: move grouped_changelog_entries to model.py
139from trac.util.text import to_unicode
140from trac.util.datefmt import utc
141from trac.versioncontrol.api import NoSuchChangeset
142
[9633]143from trac.ticket.default_workflow import ConfigurableTicketWorkflow
144from trac.ticket import TicketSystem
145
146def get_available_actions(env, action='resolve'):
147    # The list should not have duplicates.
148    ts = TicketSystem(env)
149    for controller in ts.action_controllers:
150        if isinstance(controller, ConfigurableTicketWorkflow):
151            return controller.actions.get(action)
152    return None
153
154def get_next_status(env,action='resolve'):
155    action = get_available_actions(env,action)
156    return action['newstate']
157
158
159
[5370]160# Change logfile to point to someplace this script can write.
[5368]161logfile = "/var/trac/commithook.log"
162LOG = False
163
164if LOG:
165    f = open (logfile,"w")
166    f.write("Begin Log\n")
167    f.close()
168    def log (s, *params):
169        f = open (logfile,"a")
170        f.write(s % params)
171        f.write("\n")
172        f.close()
173else:
174    def log (s, *params):
175        pass
176
[5370]177# Relative to trac standard, this table is hoisted out of class
178# CommitHook so that it can be used in constructing a regexp that only
179# matches on supported commands.
[5368]180_supported_cmds = {'close':      '_cmdClose',
181                   'closed':     '_cmdClose',
182                   'closes':     '_cmdClose',
183                   'fix':        '_cmdClose',
184                   'fixed':      '_cmdClose',
185                   'fixes':      '_cmdClose',
186                   'addresses':  '_cmdRefs',
187                   're':         '_cmdRefs',
188                   'references': '_cmdRefs',
189                   'refs':       '_cmdRefs',
190                   'see':        '_cmdRefs'}
191
[5370]192# Regexps are extended to include "(1)" and "(spent 1)".
[5368]193ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
194time_pattern = r'[ ]?(?:\((?:(?:spent|sp)[ ]?)?(-?[0-9]*(?:\.[0-9]+)?)\))?'
[5370]195ticket_reference = ticket_prefix + '[0-9]+' + time_pattern
[5368]196support_cmds_pattern = '|'.join(_supported_cmds.keys())
[5370]197
198# Relative to upstream, only match command tokens (rather than
199# matching all words).
[5368]200ticket_command =  (r'(?P<action>(?:%s))[ ]*'
201                   '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
[5370]202                   (support_cmds_pattern, ticket_reference, ticket_reference))
[5368]203
204if options.envelope:
205    ticket_command = r'\%s%s\%s' % (options.envelope[0], ticket_command,
206                                    options.envelope[1])
207   
[5370]208# Because we build the regexp to recognize only supported commands,
209# ignore case here.
[5368]210command_re = re.compile(ticket_command, re.IGNORECASE)
[5370]211ticket_re = re.compile(ticket_prefix + '([0-9]+)' + time_pattern, re.IGNORECASE)
[5368]212
213class CommitHook:
214    def __init__(self, project=options.project, author=options.user,
[10336]215                 rev=options.rev, url=options.url, reponame=options.repos):
[5370]216        self.env = open_environment(project)
[10336]217        self.reponame = reponame
218        if reponame:
219            repos = self.env.get_repository(reponame)
220            revstring = rev + '/' + reponame
221        else:
[10840]222            repos = self.env.get_repository()
[10336]223            revstring = rev
[5368]224        repos.sync()
[5370]225       
[5368]226        # Instead of bothering with the encoding, we'll use unicode data
227        # as provided by the Trac versioncontrol API (#1310).
228        try:
229            chgset = repos.get_changeset(rev)
230        except NoSuchChangeset:
231            return # out of scope changesets are not cached
232        self.author = chgset.author
233        self.rev = rev
[10336]234        self.msg = "(In [%s]) %s" % (revstring, chgset.message)
[5368]235        self.now = datetime.now(utc)
236
237        cmd_groups = command_re.findall(self.msg)
[5370]238
[5368]239        log ("cmd_groups:%s", cmd_groups)
240        tickets = {}
[5370]241        # \todo Explain what xxx1 and xxx2 do; I can't see more params
242        # in command_re.
[5368]243        for cmd, tkts, xxx1, xxx2 in cmd_groups:
244            log ("cmd:%s, tkts%s ", cmd, tkts)
245            funcname = _supported_cmds.get(cmd.lower(), '')
246            if funcname:
247                for tkt_id, spent in ticket_re.findall(tkts):
248                    func = getattr(self, funcname)
[5370]249                    tickets.setdefault(tkt_id, []).append([func, spent])
[5368]250
251        for tkt_id, vals in tickets.iteritems():
252            log ("tkt_id:%s, vals%s ", tkt_id, vals)
253            spent_total = 0.0
254            try:
255                db = self.env.get_db_cnx()
256               
257                ticket = Ticket(self.env, int(tkt_id), db)
258                for (cmd, spent) in vals:
259                    cmd(ticket)
260                    if spent:
261                        spent_total += float(spent)
262
263                # determine sequence number...
264                cnum = 0
265                tm = TicketModule(self.env)
266                for change in tm.grouped_changelog_entries(ticket, db):
267                    if change['permanent']:
268                        cnum += 1
[5370]269               
[5368]270                if spent_total:
271                    self._setTimeTrackerFields(ticket, spent_total)
272                ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
273                db.commit()
[5370]274               
[5368]275                tn = TicketNotifyEmail(self.env)
276                tn.notify(ticket, newticket=0, modtime=self.now)
277            except Exception, e:
278                # import traceback
279                # traceback.print_exc(file=sys.stderr)
280                log('Unexpected error while processing ticket ' \
281                                   'ID %s: %s' % (tkt_id, e))
282                print>>sys.stderr, 'Unexpected error while processing ticket ' \
283                                   'ID %s: %s' % (tkt_id, e)
284           
285
286    def _cmdClose(self, ticket):
[9633]287        status = get_next_status(ticket.env, 'resolve') or 'closed'
288        ticket['status'] = status
[5368]289        ticket['resolution'] = 'fixed'
290
291    def _cmdRefs(self, ticket):
292        pass
293
294    def _setTimeTrackerFields(self, ticket, spent):
295        log ("Setting ticket:%s spent: %s", ticket, spent)
296        if (spent != ''):
297            spentTime = float(spent)
[5370]298            # \bug If the ticket has not been modified since
299            # TimingAndEstimation was installed, then it might not
300            # have hours.  It should still get hours applied because
301            # estimating and recording are separate.
[5368]302            if (ticket.values.has_key('hours')):
303                ticket['hours'] = str(spentTime)
304
305if __name__ == "__main__":
306    if len(sys.argv) < 5:
307        print "For usage: %s --help" % (sys.argv[0])
308        print
309        print "Note that the deprecated options will be removed in Trac 0.12."
310    else:
[9706]311        try:
312            CommitHook()
313        except Exception, e:
314            log('ERROR while processing: %s' % str(e) )
315            sys.exit(1)
[10336]316
Note: See TracBrowser for help on using the repository browser.