source: repositoryhooksystemplugin/0.11/repository_hook_system/ticketchanger.py

Last change on this file was 7102, checked in by Jeff Hammel, 14 years ago

fix comments

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