39
|
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 # LOG=`/usr/bin/svnlook log -r $REV $REPOS`
|
|
36 # AUTHOR=`/usr/bin/svnlook author -r $REV $REPOS`
|
|
37 # TRAC_ENV='/somewhere/trac/project/'
|
|
38 # TRAC_URL='http://trac.mysite.com/project/'
|
|
39 #
|
|
40 # /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
|
|
41 # -p "$TRAC_ENV" \
|
|
42 # -r "$REV" \
|
|
43 # -u "$AUTHOR" \
|
|
44 # -m "$LOG" \
|
|
45 # -s "$TRAC_URL"
|
|
46 #
|
|
47 # It searches commit messages for text in the form of:
|
|
48 # command #1
|
|
49 # command #1, #2
|
|
50 # command #1 & #2
|
|
51 # command #1 and #2
|
|
52 #
|
|
53 # You can have more then one command in a message. The following commands
|
|
54 # are supported. There is more then one spelling for each command, to make
|
|
55 # this as user-friendly as possible.
|
|
56 #
|
|
57 # closes, fixes
|
|
58 # The specified issue numbers are closed with the contents of this
|
|
59 # commit message being added to it.
|
|
60 # references, refs, addresses, re
|
|
61 # The specified issue numbers are left in their current status, but
|
|
62 # the contents of this commit message are added to their notes.
|
|
63 #
|
|
64 # A fairly complicated example of what you can do is with a commit message
|
|
65 # of:
|
|
66 #
|
|
67 # Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
|
|
68 #
|
|
69 # This will close #10 and #12, and add a note to #12.
|
|
70
|
|
71 import re
|
|
72 import os
|
|
73 import sys
|
|
74 import time
|
|
75
|
|
76 from trac.env import open_environment
|
|
77 from trac.ticket.notification import TicketNotifyEmail
|
|
78 from trac.ticket import Ticket
|
|
79 from trac.ticket.web_ui import TicketModule
|
|
80 # TODO: move grouped_changelog_entries to model.py
|
|
81 from trac.web.href import Href
|
|
82
|
|
83 try:
|
|
84 from optparse import OptionParser
|
|
85 except ImportError:
|
|
86 try:
|
|
87 from optik import OptionParser
|
|
88 except ImportError:
|
|
89 raise ImportError, 'Requires Python 2.3 or the Optik option parsing library.'
|
|
90
|
|
91 parser = OptionParser()
|
|
92 parser.add_option('-e', '--require-envelope', dest='env', default='',
|
|
93 help='Require commands to be enclosed in an envelope. If -e[], '
|
|
94 'then commands must be in the form of [closes #4]. Must '
|
|
95 'be two characters.')
|
|
96 parser.add_option('-p', '--project', dest='project',
|
|
97 help='Path to the Trac project.')
|
|
98 parser.add_option('-r', '--revision', dest='rev',
|
|
99 help='Repository revision number.')
|
|
100 parser.add_option('-u', '--user', dest='user',
|
|
101 help='The user who is responsible for this action')
|
|
102 parser.add_option('-m', '--msg', dest='msg',
|
|
103 help='The log message to search.')
|
|
104 parser.add_option('-s', '--siteurl', dest='url',
|
|
105 help='The base URL to the project\'s trac website (to which '
|
|
106 '/ticket/## is appended). If this is not specified, '
|
|
107 'the project URL from trac.ini will be used.')
|
|
108
|
|
109 (options, args) = parser.parse_args(sys.argv[1:])
|
|
110
|
|
111 if options.env:
|
|
112 leftEnv = '\\' + options.env[0]
|
|
113 rghtEnv = '\\' + options.env[1]
|
|
114 else:
|
|
115 leftEnv = ''
|
|
116 rghtEnv = ''
|
|
117
|
|
118 commandPattern = re.compile(leftEnv + r'(?P<action>[A-Za-z]*).?(?P<ticket>#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv)
|
|
119 ticketPattern = re.compile(r'#([0-9]*)')
|
|
120
|
|
121 class CommitHook:
|
|
122 _supported_cmds = {'close': '_cmdClose',
|
|
123 'closed': '_cmdClose',
|
|
124 'closes': '_cmdClose',
|
|
125 'fix': '_cmdClose',
|
|
126 'fixed': '_cmdClose',
|
|
127 'fixes': '_cmdClose',
|
|
128 'addresses': '_cmdRefs',
|
|
129 're': '_cmdRefs',
|
|
130 'references': '_cmdRefs',
|
|
131 'refs': '_cmdRefs',
|
|
132 'see': '_cmdRefs'}
|
|
133
|
|
134 def __init__(self, project=options.project, author=options.user,
|
|
135 rev=options.rev, msg=options.msg, url=options.url):
|
|
136 self.author = author
|
|
137 self.rev = rev
|
|
138 self.msg = "(In [%s]) %s" % (rev, msg)
|
|
139 self.now = int(time.time())
|
|
140 self.env = open_environment(project)
|
|
141 if url is None:
|
|
142 url = self.env.config.get('project', 'url')
|
|
143 class Request(object):
|
|
144 def __init__(self):
|
|
145 self.href = self.abs_href = Href(url)
|
|
146
|
|
147 cmdGroups = commandPattern.findall(msg)
|
|
148
|
|
149 tickets = {}
|
|
150 for cmd, tkts in cmdGroups:
|
|
151 funcname = CommitHook._supported_cmds.get(cmd.lower(), '')
|
|
152 if funcname:
|
|
153 for tkt_id in ticketPattern.findall(tkts):
|
|
154 func = getattr(self, funcname)
|
|
155 tickets.setdefault(tkt_id, []).append(func)
|
|
156
|
|
157 for tkt_id, cmds in tickets.iteritems():
|
|
158 try:
|
|
159 db = self.env.get_db_cnx()
|
|
160
|
|
161 ticket = Ticket(self.env, int(tkt_id), db)
|
|
162 for cmd in cmds:
|
|
163 cmd(ticket)
|
|
164
|
|
165 # determine sequence number...
|
|
166 cnum = 0
|
|
167 tm = TicketModule(self.env)
|
|
168 for change in tm.grouped_changelog_entries(ticket, db):
|
|
169 if change['permanent']:
|
|
170 cnum += 1
|
|
171
|
|
172 ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
|
|
173 db.commit()
|
|
174
|
|
175 tn = TicketNotifyEmail(self.env)
|
|
176 tn.notify(ticket, Request(), newticket=0, modtime=self.now)
|
|
177 except Exception, e:
|
|
178 # import traceback
|
|
179 # traceback.print_exc(file=sys.stderr)
|
|
180 print>>sys.stderr, 'Unexpected error while processing ticket ' \
|
|
181 'ID %s: %s' % (tkt_id, e)
|
|
182
|
|
183
|
|
184 def _cmdClose(self, ticket):
|
|
185 ticket['status'] = 'closed'
|
|
186 ticket['resolution'] = 'fixed'
|
|
187
|
|
188 def _cmdRefs(self, ticket):
|
|
189 pass
|
|
190
|
|
191
|
|
192 if __name__ == "__main__":
|
|
193 if len(sys.argv) < 5:
|
|
194 print "For usage: %s --help" % (sys.argv[0])
|
|
195 else:
|
|
196 CommitHook()
|