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="/path/to/tracenv"
|
---|
36 | #
|
---|
37 | # /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
|
---|
38 | # -p "$TRAC_ENV" -r "$REV"
|
---|
39 | #
|
---|
40 | # (all the other arguments are now deprecated and not needed anymore)
|
---|
41 | #
|
---|
42 | # It searches commit messages for text in the form of:
|
---|
43 | # command #1
|
---|
44 | # command #1, #2
|
---|
45 | # command #1 & #2
|
---|
46 | # command #1 and #2
|
---|
47 | #
|
---|
48 | # Instead of the short-hand syntax "#1", "ticket:1" can be used as well, e.g.:
|
---|
49 | # command ticket:1
|
---|
50 | # command ticket:1, ticket:2
|
---|
51 | # command ticket:1 & ticket:2
|
---|
52 | # command ticket:1 and ticket:2
|
---|
53 | #
|
---|
54 | # In addition, the ':' character can be omitted and issue or bug can be used
|
---|
55 | # instead of ticket.
|
---|
56 | #
|
---|
57 | # You can have more than one command in a message. The following commands
|
---|
58 | # are supported. There is more than one spelling for each command, to make
|
---|
59 | # this as user-friendly as possible.
|
---|
60 | #
|
---|
61 | # close, closed, closes, fix, fixed, fixes
|
---|
62 | # The specified issue numbers are closed with the contents of this
|
---|
63 | # commit message being added to it.
|
---|
64 | # references, refs, addresses, re, see
|
---|
65 | # The specified issue numbers are left in their current status, but
|
---|
66 | # the contents of this commit message are added to their notes.
|
---|
67 | #
|
---|
68 | # A fairly complicated example of what you can do is with a commit message
|
---|
69 | # of:
|
---|
70 | #
|
---|
71 | # Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
|
---|
72 | #
|
---|
73 | # This will close #10 and #12, and add a note to #12.
|
---|
74 |
|
---|
75 | import re
|
---|
76 | import os
|
---|
77 | import sys
|
---|
78 | from datetime import datetime
|
---|
79 | from optparse import OptionParser
|
---|
80 |
|
---|
81 | parser = OptionParser()
|
---|
82 | depr = '(not used anymore)'
|
---|
83 | parser.add_option('-e', '--require-envelope', dest='envelope', default='',
|
---|
84 | help="""
|
---|
85 | Require commands to be enclosed in an envelope.
|
---|
86 | If -e[], then commands must be in the form of [closes #4].
|
---|
87 | Must be two characters.""")
|
---|
88 | parser.add_option('-p', '--project', dest='project',
|
---|
89 | help='Path to the Trac project.')
|
---|
90 | parser.add_option('-r', '--revision', dest='rev',
|
---|
91 | help='Repository revision number.')
|
---|
92 | parser.add_option('-u', '--user', dest='user',
|
---|
93 | help='The user who is responsible for this action '+depr)
|
---|
94 | parser.add_option('-m', '--msg', dest='msg',
|
---|
95 | help='The log message to search '+depr)
|
---|
96 | parser.add_option('-c', '--encoding', dest='encoding',
|
---|
97 | help='The encoding used by the log message '+depr)
|
---|
98 | parser.add_option('-s', '--siteurl', dest='url',
|
---|
99 | help=depr+' the base_url from trac.ini will always be used.')
|
---|
100 |
|
---|
101 | (options, args) = parser.parse_args(sys.argv[1:])
|
---|
102 |
|
---|
103 | if not 'PYTHON_EGG_CACHE' in os.environ:
|
---|
104 | os.environ['PYTHON_EGG_CACHE'] = os.path.join(options.project, '.egg-cache')
|
---|
105 |
|
---|
106 | from trac.env import open_environment
|
---|
107 | from trac.ticket.notification import TicketNotifyEmail
|
---|
108 | from trac.ticket import Ticket
|
---|
109 | from trac.ticket.web_ui import TicketModule
|
---|
110 | # TODO: move grouped_changelog_entries to model.py
|
---|
111 | from trac.util.text import to_unicode
|
---|
112 | from trac.util.datefmt import utc
|
---|
113 | from trac.versioncontrol.api import NoSuchChangeset
|
---|
114 |
|
---|
115 | ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
|
---|
116 | ticket_reference = ticket_prefix + '[0-9]+'
|
---|
117 | ticket_command = (r'(?P<action>[A-Za-z]*).?'
|
---|
118 | '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
|
---|
119 | (ticket_reference, ticket_reference))
|
---|
120 |
|
---|
121 | if options.envelope:
|
---|
122 | ticket_command = r'\%s%s\%s' % (options.envelope[0], ticket_command,
|
---|
123 | options.envelope[1])
|
---|
124 |
|
---|
125 | command_re = re.compile(ticket_command)
|
---|
126 | ticket_re = re.compile(ticket_prefix + '([0-9]+)')
|
---|
127 |
|
---|
128 | class CommitHook:
|
---|
129 | _supported_cmds = {'close': '_cmdClose',
|
---|
130 | 'closed': '_cmdClose',
|
---|
131 | 'closes': '_cmdClose',
|
---|
132 | 'fix': '_cmdClose',
|
---|
133 | 'fixed': '_cmdClose',
|
---|
134 | 'fixes': '_cmdClose',
|
---|
135 | 'addresses': '_cmdRefs',
|
---|
136 | 're': '_cmdRefs',
|
---|
137 | 'references': '_cmdRefs',
|
---|
138 | 'refs': '_cmdRefs',
|
---|
139 | 'see': '_cmdRefs'}
|
---|
140 |
|
---|
141 | def __init__(self, project=options.project, author=options.user,
|
---|
142 | rev=options.rev, url=options.url):
|
---|
143 | self.env = open_environment(project)
|
---|
144 | repos = self.env.get_repository()
|
---|
145 | repos.sync()
|
---|
146 |
|
---|
147 | # Instead of bothering with the encoding, we'll use unicode data
|
---|
148 | # as provided by the Trac versioncontrol API (#1310).
|
---|
149 | try:
|
---|
150 | chgset = repos.get_changeset(rev)
|
---|
151 | except NoSuchChangeset:
|
---|
152 | return # out of scope changesets are not cached
|
---|
153 | self.author = chgset.author
|
---|
154 | self.rev = rev
|
---|
155 | self.msg = "(In [%s]) %s" % (rev, chgset.message)
|
---|
156 | self.now = datetime.now(utc)
|
---|
157 |
|
---|
158 | cmd_groups = command_re.findall(self.msg)
|
---|
159 |
|
---|
160 | tickets = {}
|
---|
161 | for cmd, tkts in cmd_groups:
|
---|
162 | funcname = CommitHook._supported_cmds.get(cmd.lower(), '')
|
---|
163 | if funcname:
|
---|
164 | for tkt_id in ticket_re.findall(tkts):
|
---|
165 | func = getattr(self, funcname)
|
---|
166 | tickets.setdefault(tkt_id, []).append(func)
|
---|
167 |
|
---|
168 | for tkt_id, cmds in tickets.iteritems():
|
---|
169 | try:
|
---|
170 | db = self.env.get_db_cnx()
|
---|
171 |
|
---|
172 | ticket = Ticket(self.env, int(tkt_id), db)
|
---|
173 | for cmd in cmds:
|
---|
174 | cmd(ticket)
|
---|
175 |
|
---|
176 | # determine sequence number...
|
---|
177 | cnum = 0
|
---|
178 | tm = TicketModule(self.env)
|
---|
179 | for change in tm.grouped_changelog_entries(ticket, db):
|
---|
180 | if change['permanent']:
|
---|
181 | cnum += 1
|
---|
182 |
|
---|
183 | ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
|
---|
184 | db.commit()
|
---|
185 |
|
---|
186 | tn = TicketNotifyEmail(self.env)
|
---|
187 | tn.notify(ticket, newticket=0, modtime=self.now)
|
---|
188 | except Exception, e:
|
---|
189 | # import traceback
|
---|
190 | # traceback.print_exc(file=sys.stderr)
|
---|
191 | print>>sys.stderr, 'Unexpected error while processing ticket ' \
|
---|
192 | 'ID %s: %s' % (tkt_id, e)
|
---|
193 |
|
---|
194 |
|
---|
195 | def _cmdClose(self, ticket):
|
---|
196 | ticket['status'] = 'closed'
|
---|
197 | ticket['resolution'] = 'fixed'
|
---|
198 |
|
---|
199 | def _cmdRefs(self, ticket):
|
---|
200 | pass
|
---|
201 |
|
---|
202 |
|
---|
203 | if __name__ == "__main__":
|
---|
204 | if len(sys.argv) < 5:
|
---|
205 | print "For usage: %s --help" % (sys.argv[0])
|
---|
206 | print
|
---|
207 | print "Note that the deprecated options will be removed in Trac 0.12."
|
---|
208 | else:
|
---|
209 | CommitHook()
|
---|