root/repositoryhooksystemplugin/0.11/repository_hook_system/ticketchanger.py

Revision 5079, 5.5 kB (checked in by k0s, 6 days ago)

use transaction instead of revision for pre-commit; closes #4327

Line 
1 """
2 annotes and closes tickets based on an SVN commit message;
3 port of http://trac.edgewall.org/browser/trunk/contrib/trac-post-commit-hook
4 """
5
6 import os
7 import re
8 import sys
9
10 from datetime import datetime
11 from repository_hook_system.interface import IRepositoryHookSubscriber
12 from trac.config import BoolOption
13 from trac.config import ListOption
14 from trac.config import Option
15 from trac.core import *
16 from trac.ticket import Ticket
17 from trac.ticket.notification import TicketNotifyEmail
18 from trac.ticket.web_ui import TicketModule
19 from trac.util.datefmt import utc
20
21 # TODO: look only for tickets that match
22 # `projectname:#|(ticket|issue|bug)`
23 # according to configuration
24 # (which also means moving the regex to the class TicketChanger)
25 # move more/all of configuration into the .ini file and therefor editable
26
27 class TicketChanger(Component):
28     """annotes and closes tickets on repository commit messages"""
29
30     implements(IRepositoryHookSubscriber)   
31
32     ### options
33     envelope_open = Option('ticket-changer', 'opener', default='',
34                            doc='must be present before the action taken to take effect')
35     envelope_close = Option('ticket-changer', 'closer', default='',
36                             doc='must be present after the action taken to take effect')
37     intertrac = BoolOption('ticket-changer', 'intertrac', default=False,
38                            doc='enforce using ticket prefix from intertrac linking')
39     cmd_close = ListOption('ticket-changer', 'close-commands',
40                            default=['close', 'closed', 'closes', 'fix', 'fixed', 'fixes'],
41                            doc='commit message tokens that indicate ticket close [e.g. "closes #123"]')
42     cmd_refs = ListOption('ticket-changer', 'references-commands',
43                           default=['addresses', 're', 'references', 'refs', 'see'],
44                           doc='commit message tokens that indicate ticket reference [e.g. "refs #123"]')
45    
46     def is_available(self, repository, hookname):
47         return True
48
49     def invoke(self, chgset):
50
51         # regular expressions       
52         ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
53         if self.intertrac:  # TODO: split to separate function?
54             # find intertrac links
55             intertrac = {}
56             aliases = {}
57             for key, value in self.env.config.options('intertrac'):
58                 if '.' in key:
59                     name, type_ = key.rsplit('.', 1)
60                     if type_ == 'url':
61                         intertrac[name] = value
62                 else:
63                     aliases.setdefault(value, []).append(key)
64             intertrac = dict([(value, [key] + aliases.get(key, [])) for key, value in intertrac.items()])
65             project = os.path.basename(self.env.path)
66
67             if '/%s' % project in intertrac: # TODO:  checking using base_url for full paths:
68                 ticket_prefix = '(?:%s):%s' % ( '|'.join(intertrac['/%s' % project]),
69                                               ticket_prefix )
70             else: # hopefully sesible default:
71                 ticket_prefix = '%s:%s' % (project, ticket_prefix)
72
73         ticket_reference = ticket_prefix + '[0-9]+'
74         ticket_command =  (r'(?P<action>[A-Za-z]*).?'
75                            '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
76                            (ticket_reference, ticket_reference))
77         ticket_command = r'%s%s%s' % (re.escape(self.envelope_open),
78                                       ticket_command,
79                                       re.escape(self.envelope_close))
80         command_re = re.compile(ticket_command, re.IGNORECASE)
81         ticket_re = re.compile(ticket_prefix + '([0-9]+)', re.IGNORECASE)
82
83         # other variables
84         msg = "(In [%s]) %s" % (chgset.rev, chgset.message)       
85         now = chgset.date
86         supported_cmds = {} # TODO: this could become an extension point
87         supported_cmds.update(dict([(key, self._cmdClose) for key in self.cmd_close]))
88         supported_cmds.update(dict([(key, self._cmdRefs) for key in self.cmd_refs]))
89
90         cmd_groups = command_re.findall(msg)
91
92         tickets = {}
93         for cmd, tkts in cmd_groups:
94             func = supported_cmds.get(cmd.lower(), None)
95             if func:
96                 for tkt_id in ticket_re.findall(tkts):
97                     tickets.setdefault(tkt_id, []).append(func)
98
99         for tkt_id, cmds in tickets.iteritems():
100             try:
101                 db = self.env.get_db_cnx()
102                
103                 ticket = Ticket(self.env, int(tkt_id), db)
104                 for cmd in cmds:
105                     cmd(ticket)
106
107                 # determine sequence number...
108                 cnum = 0
109                 tm = TicketModule(self.env)
110                 for change in tm.grouped_changelog_entries(ticket, db):
111                     if change['permanent']:
112                         cnum += 1
113                
114                 ticket.save_changes(chgset.author, msg, now, db, cnum+1)
115                 db.commit()
116                
117                 tn = TicketNotifyEmail(self.env)
118                 tn.notify(ticket, newticket=0, modtime=now)
119
120             except Exception, e:
121                 # import traceback
122                 # traceback.print_exc(file=sys.stderr)
123                 print>>sys.stderr, 'Unexpected error while processing ticket ' \
124                                    'ID %s: %s' % (tkt_id, e)
125            
126
127     def _cmdClose(self, ticket):
128         ticket['status'] = 'closed'
129         ticket['resolution'] = 'fixed'
130
131     def _cmdRefs(self, ticket):
132         pass
Note: See TracBrowser for help on using the browser.