Show pre-receive-stable stopping following commands if used via a shim.
[git-central.git] / trac-post-commit-hook.py
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 # TRAC_ENV='/somewhere/trac/project/'
36 #
37 # /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
38 #  -p "$TRAC_ENV"  \
39 #  -r "$REV"       
40 #
41 # It searches commit messages for text in the form of:
42 #   command #1
43 #   command #1, #2
44 #   command #1 & #2 
45 #   command #1 and #2
46 #
47 # You can have more then one command in a message. The following commands
48 # are supported. There is more then one spelling for each command, to make
49 # this as user-friendly as possible.
50 #
51 #   closes, fixes
52 #     The specified issue numbers are closed with the contents of this
53 #     commit message being added to it. 
54 #   references, refs, addresses, re 
55 #     The specified issue numbers are left in their current status, but 
56 #     the contents of this commit message are added to their notes. 
57 #
58 # A fairly complicated example of what you can do is with a commit message
59 # of:
60 #
61 #    Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
62 #
63 # This will close #10 and #12, and add a note to #12.
64
65 import re
66 import os
67 import sys
68 from datetime import datetime 
69
70 from trac.env import open_environment
71 from trac.ticket.notification import TicketNotifyEmail
72 from trac.ticket import Ticket
73 from trac.ticket.web_ui import TicketModule
74 # TODO: move grouped_changelog_entries to model.py
75 from trac.util.datefmt import utc
76 from trac.versioncontrol.api import NoSuchChangeset
77
78 from optparse import OptionParser
79
80 parser = OptionParser()
81 parser.add_option('-p', '--project', dest='project', help='Path to the Trac project.')
82 parser.add_option('-r', '--revision', dest='rev', help='Repository revision number.')
83
84 (options, args) = parser.parse_args(sys.argv[1:])
85
86 leftEnv = ''
87 rghtEnv = ''
88 commandPattern = re.compile(leftEnv + r'(?P<action>[A-Za-z]*).?(?P<ticket>#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv)
89 ticketPattern = re.compile(r'#([0-9]*)')
90
91 # Old commands:
92 #                           'close':      '_cmdClose',
93 #                       'closed':     '_cmdClose',
94 #                       'closes':     '_cmdClose',
95 #                       'fix':        '_cmdClose',
96 #                       'fixed':      '_cmdClose',
97 #                       'fixes':      '_cmdClose',
98 #                       'addresses':  '_cmdRefs',
99 #                       'references': '_cmdRefs',
100 #                       'see':        '_cmdRefs'}
101
102 class CommitHook:
103
104     _supported_cmds = {
105                        're':         '_cmdRefs',
106                        'refs':       '_cmdRefs',
107                        'qa':         '_cmdQa'
108                       }
109
110     def __init__(self, project=options.project, rev=options.rev):
111         self.env = open_environment(project)
112         repos = self.env.get_repository()
113         repos.sync()
114
115         # Instead of bothering with the encoding, we'll use unicode data
116         # as provided by the Trac versioncontrol API (#1310).
117         try:
118             chgset = repos.get_changeset(rev)
119         except NoSuchChangeset:
120             return # out of scope changesets are not cached
121         self.author = chgset.author
122         self.rev = rev
123         self.msg = "(In [%s]) %s" % (rev, chgset.message)
124         self.now = datetime.now(utc)
125
126         cmdGroups = commandPattern.findall(self.msg)
127
128         tickets = {}
129         for cmd, tkts in cmdGroups:
130             funcname = CommitHook._supported_cmds.get(cmd.lower(), '')
131             if funcname:
132                 for tkt_id in ticketPattern.findall(tkts):
133                     func = getattr(self, funcname)
134                     tickets.setdefault(tkt_id, []).append(func)
135
136         for tkt_id, cmds in tickets.iteritems():
137             try:
138                 db = self.env.get_db_cnx()
139
140                 ticket = Ticket(self.env, int(tkt_id), db)
141                 for cmd in cmds:
142                     cmd(ticket)
143
144                 # determine sequence number... 
145                 cnum = 0
146                 tm = TicketModule(self.env)
147                 for change in tm.grouped_changelog_entries(ticket, db):
148                     if change['permanent']:
149                         cnum += 1
150
151                 ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
152                 db.commit()
153
154                 tn = TicketNotifyEmail(self.env)
155                 tn.notify(ticket, newticket=0, modtime=self.now)
156             except Exception, e:
157                 # import traceback
158                 # traceback.print_exc(file=sys.stderr)
159                 print>>sys.stderr, 'Unexpected error while processing ticket ' \
160                                    'ID %s: %s' % (tkt_id, e)
161
162     def _cmdClose(self, ticket):
163         ticket['status'] = 'closed'
164         ticket['resolution'] = 'fixed'
165
166     def _cmdRefs(self, ticket):
167         pass
168
169     def _cmdQa(self, ticket):
170         ticket['phase'] = 'Initial QA'
171         ticket['owner'] = ''
172         ticket['status'] = 'new'
173
174 if __name__ == "__main__":
175     if len(sys.argv) < 5:
176         print "For usage: %s --help" % (sys.argv[0])
177         print
178         print "Note that the deprecated options will be removed in Trac 0.12."
179     else:
180         CommitHook()