##// END OF EJS Templates
bugzilla: localise all MySQL direct access inside access class....
Jim Hague -
r13800:c2ef8159 default
parent child Browse files
Show More
@@ -1,442 +1,478 b''
1 # bugzilla.py - bugzilla integration for mercurial
1 # bugzilla.py - bugzilla integration for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''hooks for integrating with the Bugzilla bug tracker
8 '''hooks for integrating with the Bugzilla bug tracker
9
9
10 This hook extension adds comments on bugs in Bugzilla when changesets
10 This hook extension adds comments on bugs in Bugzilla when changesets
11 that refer to bugs by Bugzilla ID are seen. The hook does not change
11 that refer to bugs by Bugzilla ID are seen. The hook does not change
12 bug status.
12 bug status.
13
13
14 The hook updates the Bugzilla database directly. Only Bugzilla
14 The hook updates the Bugzilla database directly. Only Bugzilla
15 installations using MySQL are supported.
15 installations using MySQL are supported.
16
16
17 The hook relies on a Bugzilla script to send bug change notification
17 The hook relies on a Bugzilla script to send bug change notification
18 emails. That script changes between Bugzilla versions; the
18 emails. That script changes between Bugzilla versions; the
19 'processmail' script used prior to 2.18 is replaced in 2.18 and
19 'processmail' script used prior to 2.18 is replaced in 2.18 and
20 subsequent versions by 'config/sendbugmail.pl'. Note that these will
20 subsequent versions by 'config/sendbugmail.pl'. Note that these will
21 be run by Mercurial as the user pushing the change; you will need to
21 be run by Mercurial as the user pushing the change; you will need to
22 ensure the Bugzilla install file permissions are set appropriately.
22 ensure the Bugzilla install file permissions are set appropriately.
23
23
24 The extension is configured through three different configuration
24 The extension is configured through three different configuration
25 sections. These keys are recognized in the [bugzilla] section:
25 sections. These keys are recognized in the [bugzilla] section:
26
26
27 host
27 host
28 Hostname of the MySQL server holding the Bugzilla database.
28 Hostname of the MySQL server holding the Bugzilla database.
29
29
30 db
30 db
31 Name of the Bugzilla database in MySQL. Default 'bugs'.
31 Name of the Bugzilla database in MySQL. Default 'bugs'.
32
32
33 user
33 user
34 Username to use to access MySQL server. Default 'bugs'.
34 Username to use to access MySQL server. Default 'bugs'.
35
35
36 password
36 password
37 Password to use to access MySQL server.
37 Password to use to access MySQL server.
38
38
39 timeout
39 timeout
40 Database connection timeout (seconds). Default 5.
40 Database connection timeout (seconds). Default 5.
41
41
42 version
42 version
43 Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,
43 Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,
44 '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior
44 '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior
45 to 2.18.
45 to 2.18.
46
46
47 bzuser
47 bzuser
48 Fallback Bugzilla user name to record comments with, if changeset
48 Fallback Bugzilla user name to record comments with, if changeset
49 committer cannot be found as a Bugzilla user.
49 committer cannot be found as a Bugzilla user.
50
50
51 bzdir
51 bzdir
52 Bugzilla install directory. Used by default notify. Default
52 Bugzilla install directory. Used by default notify. Default
53 '/var/www/html/bugzilla'.
53 '/var/www/html/bugzilla'.
54
54
55 notify
55 notify
56 The command to run to get Bugzilla to send bug change notification
56 The command to run to get Bugzilla to send bug change notification
57 emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
57 emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
58 and 'user' (committer bugzilla email). Default depends on version;
58 and 'user' (committer bugzilla email). Default depends on version;
59 from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
59 from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
60 %(id)s %(user)s".
60 %(id)s %(user)s".
61
61
62 regexp
62 regexp
63 Regular expression to match bug IDs in changeset commit message.
63 Regular expression to match bug IDs in changeset commit message.
64 Must contain one "()" group. The default expression matches 'Bug
64 Must contain one "()" group. The default expression matches 'Bug
65 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug
65 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug
66 1234 and 5678' and variations thereof. Matching is case insensitive.
66 1234 and 5678' and variations thereof. Matching is case insensitive.
67
67
68 style
68 style
69 The style file to use when formatting comments.
69 The style file to use when formatting comments.
70
70
71 template
71 template
72 Template to use when formatting comments. Overrides style if
72 Template to use when formatting comments. Overrides style if
73 specified. In addition to the usual Mercurial keywords, the
73 specified. In addition to the usual Mercurial keywords, the
74 extension specifies::
74 extension specifies::
75
75
76 {bug} The Bugzilla bug ID.
76 {bug} The Bugzilla bug ID.
77 {root} The full pathname of the Mercurial repository.
77 {root} The full pathname of the Mercurial repository.
78 {webroot} Stripped pathname of the Mercurial repository.
78 {webroot} Stripped pathname of the Mercurial repository.
79 {hgweb} Base URL for browsing Mercurial repositories.
79 {hgweb} Base URL for browsing Mercurial repositories.
80
80
81 Default 'changeset {node|short} in repo {root} refers '
81 Default 'changeset {node|short} in repo {root} refers '
82 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
82 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
83
83
84 strip
84 strip
85 The number of slashes to strip from the front of {root} to produce
85 The number of slashes to strip from the front of {root} to produce
86 {webroot}. Default 0.
86 {webroot}. Default 0.
87
87
88 usermap
88 usermap
89 Path of file containing Mercurial committer ID to Bugzilla user ID
89 Path of file containing Mercurial committer ID to Bugzilla user ID
90 mappings. If specified, the file should contain one mapping per
90 mappings. If specified, the file should contain one mapping per
91 line, "committer"="Bugzilla user". See also the [usermap] section.
91 line, "committer"="Bugzilla user". See also the [usermap] section.
92
92
93 The [usermap] section is used to specify mappings of Mercurial
93 The [usermap] section is used to specify mappings of Mercurial
94 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
94 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
95 "committer"="Bugzilla user"
95 "committer"="Bugzilla user"
96
96
97 Finally, the [web] section supports one entry:
97 Finally, the [web] section supports one entry:
98
98
99 baseurl
99 baseurl
100 Base URL for browsing Mercurial repositories. Reference from
100 Base URL for browsing Mercurial repositories. Reference from
101 templates as {hgweb}.
101 templates as {hgweb}.
102
102
103 Activating the extension::
103 Activating the extension::
104
104
105 [extensions]
105 [extensions]
106 bugzilla =
106 bugzilla =
107
107
108 [hooks]
108 [hooks]
109 # run bugzilla hook on every change pulled or pushed in here
109 # run bugzilla hook on every change pulled or pushed in here
110 incoming.bugzilla = python:hgext.bugzilla.hook
110 incoming.bugzilla = python:hgext.bugzilla.hook
111
111
112 Example configuration:
112 Example configuration:
113
113
114 This example configuration is for a collection of Mercurial
114 This example configuration is for a collection of Mercurial
115 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
115 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
116 installation in /opt/bugzilla-3.2. ::
116 installation in /opt/bugzilla-3.2. ::
117
117
118 [bugzilla]
118 [bugzilla]
119 host=localhost
119 host=localhost
120 password=XYZZY
120 password=XYZZY
121 version=3.0
121 version=3.0
122 bzuser=unknown@domain.com
122 bzuser=unknown@domain.com
123 bzdir=/opt/bugzilla-3.2
123 bzdir=/opt/bugzilla-3.2
124 template=Changeset {node|short} in {root|basename}.
124 template=Changeset {node|short} in {root|basename}.
125 {hgweb}/{webroot}/rev/{node|short}\\n
125 {hgweb}/{webroot}/rev/{node|short}\\n
126 {desc}\\n
126 {desc}\\n
127 strip=5
127 strip=5
128
128
129 [web]
129 [web]
130 baseurl=http://dev.domain.com/hg
130 baseurl=http://dev.domain.com/hg
131
131
132 [usermap]
132 [usermap]
133 user@emaildomain.com=user.name@bugzilladomain.com
133 user@emaildomain.com=user.name@bugzilladomain.com
134
134
135 Commits add a comment to the Bugzilla bug record of the form::
135 Commits add a comment to the Bugzilla bug record of the form::
136
136
137 Changeset 3b16791d6642 in repository-name.
137 Changeset 3b16791d6642 in repository-name.
138 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
138 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
139
139
140 Changeset commit comment. Bug 1234.
140 Changeset commit comment. Bug 1234.
141 '''
141 '''
142
142
143 from mercurial.i18n import _
143 from mercurial.i18n import _
144 from mercurial.node import short
144 from mercurial.node import short
145 from mercurial import cmdutil, templater, util
145 from mercurial import cmdutil, templater, util
146 import re, time
146 import re, time
147
147
148 MySQLdb = None
148 class bzaccess(object):
149
149 '''Base class for access to Bugzilla.'''
150 def buglist(ids):
151 return '(' + ','.join(map(str, ids)) + ')'
152
153 class bugzilla_2_16(object):
154 '''support for bugzilla version 2.16.'''
155
150
156 def __init__(self, ui):
151 def __init__(self, ui):
157 self.ui = ui
152 self.ui = ui
153 usermap = self.ui.config('bugzilla', 'usermap')
154 if usermap:
155 self.ui.readconfig(usermap, sections=['usermap'])
156
157 def map_committer(self, user):
158 '''map name of committer to Bugzilla user name.'''
159 for committer, bzuser in self.ui.configitems('usermap'):
160 if committer.lower() == user.lower():
161 return bzuser
162 return user
163
164 # Methods to be implemented by access classes.
165 def filter_real_bug_ids(self, ids):
166 '''remove bug IDs that do not exist in Bugzilla from set.'''
167 pass
168
169 def filter_cset_known_bug_ids(self, node, ids):
170 '''remove bug IDs where node occurs in comment text from set.'''
171 pass
172
173 def add_comment(self, bugid, text, committer):
174 '''add comment to bug.
175
176 If possible add the comment as being from the committer of
177 the changeset. Otherwise use the default Bugzilla user.
178 '''
179 pass
180
181 def notify(self, ids, committer):
182 '''Force sending of Bugzilla notification emails.'''
183 pass
184
185 # Bugzilla via direct access to MySQL database.
186 class bzmysql(bzaccess):
187 '''Support for direct MySQL access to Bugzilla.
188
189 The earliest Bugzilla version this is tested with is version 2.16.
190 '''
191
192 @staticmethod
193 def sql_buglist(ids):
194 '''return SQL-friendly list of bug ids'''
195 return '(' + ','.join(map(str, ids)) + ')'
196
197 _MySQLdb = None
198
199 def __init__(self, ui):
200 try:
201 import MySQLdb as mysql
202 bzmysql._MySQLdb = mysql
203 except ImportError, err:
204 raise util.Abort(_('python mysql support not available: %s') % err)
205
206 bzaccess.__init__(self, ui)
207
158 host = self.ui.config('bugzilla', 'host', 'localhost')
208 host = self.ui.config('bugzilla', 'host', 'localhost')
159 user = self.ui.config('bugzilla', 'user', 'bugs')
209 user = self.ui.config('bugzilla', 'user', 'bugs')
160 passwd = self.ui.config('bugzilla', 'password')
210 passwd = self.ui.config('bugzilla', 'password')
161 db = self.ui.config('bugzilla', 'db', 'bugs')
211 db = self.ui.config('bugzilla', 'db', 'bugs')
162 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
212 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
163 usermap = self.ui.config('bugzilla', 'usermap')
164 if usermap:
165 self.ui.readconfig(usermap, sections=['usermap'])
166 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
213 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
167 (host, db, user, '*' * len(passwd)))
214 (host, db, user, '*' * len(passwd)))
168 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
215 self.conn = bzmysql._MySQLdb.connect(host=host,
169 db=db, connect_timeout=timeout)
216 user=user, passwd=passwd,
217 db=db,
218 connect_timeout=timeout)
170 self.cursor = self.conn.cursor()
219 self.cursor = self.conn.cursor()
171 self.longdesc_id = self.get_longdesc_id()
220 self.longdesc_id = self.get_longdesc_id()
172 self.user_ids = {}
221 self.user_ids = {}
173 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
222 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
174
223
175 def run(self, *args, **kwargs):
224 def run(self, *args, **kwargs):
176 '''run a query.'''
225 '''run a query.'''
177 self.ui.note(_('query: %s %s\n') % (args, kwargs))
226 self.ui.note(_('query: %s %s\n') % (args, kwargs))
178 try:
227 try:
179 self.cursor.execute(*args, **kwargs)
228 self.cursor.execute(*args, **kwargs)
180 except MySQLdb.MySQLError:
229 except bzmysql._MySQLdb.MySQLError:
181 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
230 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
182 raise
231 raise
183
232
184 def get_longdesc_id(self):
233 def get_longdesc_id(self):
185 '''get identity of longdesc field'''
234 '''get identity of longdesc field'''
186 self.run('select fieldid from fielddefs where name = "longdesc"')
235 self.run('select fieldid from fielddefs where name = "longdesc"')
187 ids = self.cursor.fetchall()
236 ids = self.cursor.fetchall()
188 if len(ids) != 1:
237 if len(ids) != 1:
189 raise util.Abort(_('unknown database schema'))
238 raise util.Abort(_('unknown database schema'))
190 return ids[0][0]
239 return ids[0][0]
191
240
192 def filter_real_bug_ids(self, ids):
241 def filter_real_bug_ids(self, ids):
193 '''filter not-existing bug ids from set.'''
242 '''filter not-existing bug ids from set.'''
194 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
243 self.run('select bug_id from bugs where bug_id in %s' %
244 bzmysql.sql_buglist(ids))
195 return set([c[0] for c in self.cursor.fetchall()])
245 return set([c[0] for c in self.cursor.fetchall()])
196
246
197 def filter_cset_known_bug_ids(self, node, ids):
247 def filter_cset_known_bug_ids(self, node, ids):
198 '''filter bug ids that already refer to this changeset from set.'''
248 '''filter bug ids that already refer to this changeset from set.'''
199
249
200 self.run('''select bug_id from longdescs where
250 self.run('''select bug_id from longdescs where
201 bug_id in %s and thetext like "%%%s%%"''' %
251 bug_id in %s and thetext like "%%%s%%"''' %
202 (buglist(ids), short(node)))
252 (bzmysql.sql_buglist(ids), short(node)))
203 for (id,) in self.cursor.fetchall():
253 for (id,) in self.cursor.fetchall():
204 self.ui.status(_('bug %d already knows about changeset %s\n') %
254 self.ui.status(_('bug %d already knows about changeset %s\n') %
205 (id, short(node)))
255 (id, short(node)))
206 ids.discard(id)
256 ids.discard(id)
207 return ids
257 return ids
208
258
209 def notify(self, ids, committer):
259 def notify(self, ids, committer):
210 '''tell bugzilla to send mail.'''
260 '''tell bugzilla to send mail.'''
211
261
212 self.ui.status(_('telling bugzilla to send mail:\n'))
262 self.ui.status(_('telling bugzilla to send mail:\n'))
213 (user, userid) = self.get_bugzilla_user(committer)
263 (user, userid) = self.get_bugzilla_user(committer)
214 for id in ids:
264 for id in ids:
215 self.ui.status(_(' bug %s\n') % id)
265 self.ui.status(_(' bug %s\n') % id)
216 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
266 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
217 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
267 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
218 try:
268 try:
219 # Backwards-compatible with old notify string, which
269 # Backwards-compatible with old notify string, which
220 # took one string. This will throw with a new format
270 # took one string. This will throw with a new format
221 # string.
271 # string.
222 cmd = cmdfmt % id
272 cmd = cmdfmt % id
223 except TypeError:
273 except TypeError:
224 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
274 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
225 self.ui.note(_('running notify command %s\n') % cmd)
275 self.ui.note(_('running notify command %s\n') % cmd)
226 fp = util.popen('(%s) 2>&1' % cmd)
276 fp = util.popen('(%s) 2>&1' % cmd)
227 out = fp.read()
277 out = fp.read()
228 ret = fp.close()
278 ret = fp.close()
229 if ret:
279 if ret:
230 self.ui.warn(out)
280 self.ui.warn(out)
231 raise util.Abort(_('bugzilla notify command %s') %
281 raise util.Abort(_('bugzilla notify command %s') %
232 util.explain_exit(ret)[0])
282 util.explain_exit(ret)[0])
233 self.ui.status(_('done\n'))
283 self.ui.status(_('done\n'))
234
284
235 def get_user_id(self, user):
285 def get_user_id(self, user):
236 '''look up numeric bugzilla user id.'''
286 '''look up numeric bugzilla user id.'''
237 try:
287 try:
238 return self.user_ids[user]
288 return self.user_ids[user]
239 except KeyError:
289 except KeyError:
240 try:
290 try:
241 userid = int(user)
291 userid = int(user)
242 except ValueError:
292 except ValueError:
243 self.ui.note(_('looking up user %s\n') % user)
293 self.ui.note(_('looking up user %s\n') % user)
244 self.run('''select userid from profiles
294 self.run('''select userid from profiles
245 where login_name like %s''', user)
295 where login_name like %s''', user)
246 all = self.cursor.fetchall()
296 all = self.cursor.fetchall()
247 if len(all) != 1:
297 if len(all) != 1:
248 raise KeyError(user)
298 raise KeyError(user)
249 userid = int(all[0][0])
299 userid = int(all[0][0])
250 self.user_ids[user] = userid
300 self.user_ids[user] = userid
251 return userid
301 return userid
252
302
253 def map_committer(self, user):
254 '''map name of committer to bugzilla user name.'''
255 for committer, bzuser in self.ui.configitems('usermap'):
256 if committer.lower() == user.lower():
257 return bzuser
258 return user
259
260 def get_bugzilla_user(self, committer):
303 def get_bugzilla_user(self, committer):
261 '''see if committer is a registered bugzilla user. Return
304 '''see if committer is a registered bugzilla user. Return
262 bugzilla username and userid if so. If not, return default
305 bugzilla username and userid if so. If not, return default
263 bugzilla username and userid.'''
306 bugzilla username and userid.'''
264 user = self.map_committer(committer)
307 user = self.map_committer(committer)
265 try:
308 try:
266 userid = self.get_user_id(user)
309 userid = self.get_user_id(user)
267 except KeyError:
310 except KeyError:
268 try:
311 try:
269 defaultuser = self.ui.config('bugzilla', 'bzuser')
312 defaultuser = self.ui.config('bugzilla', 'bzuser')
270 if not defaultuser:
313 if not defaultuser:
271 raise util.Abort(_('cannot find bugzilla user id for %s') %
314 raise util.Abort(_('cannot find bugzilla user id for %s') %
272 user)
315 user)
273 userid = self.get_user_id(defaultuser)
316 userid = self.get_user_id(defaultuser)
274 user = defaultuser
317 user = defaultuser
275 except KeyError:
318 except KeyError:
276 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
319 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
277 (user, defaultuser))
320 (user, defaultuser))
278 return (user, userid)
321 return (user, userid)
279
322
280 def add_comment(self, bugid, text, committer):
323 def add_comment(self, bugid, text, committer):
281 '''add comment to bug. try adding comment as committer of
324 '''add comment to bug. try adding comment as committer of
282 changeset, otherwise as default bugzilla user.'''
325 changeset, otherwise as default bugzilla user.'''
283 (user, userid) = self.get_bugzilla_user(committer)
326 (user, userid) = self.get_bugzilla_user(committer)
284 now = time.strftime('%Y-%m-%d %H:%M:%S')
327 now = time.strftime('%Y-%m-%d %H:%M:%S')
285 self.run('''insert into longdescs
328 self.run('''insert into longdescs
286 (bug_id, who, bug_when, thetext)
329 (bug_id, who, bug_when, thetext)
287 values (%s, %s, %s, %s)''',
330 values (%s, %s, %s, %s)''',
288 (bugid, userid, now, text))
331 (bugid, userid, now, text))
289 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
332 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
290 values (%s, %s, %s, %s)''',
333 values (%s, %s, %s, %s)''',
291 (bugid, userid, now, self.longdesc_id))
334 (bugid, userid, now, self.longdesc_id))
292 self.conn.commit()
335 self.conn.commit()
293
336
294 class bugzilla_2_18(bugzilla_2_16):
337 class bzmysql_2_18(bzmysql):
295 '''support for bugzilla 2.18 series.'''
338 '''support for bugzilla 2.18 series.'''
296
339
297 def __init__(self, ui):
340 def __init__(self, ui):
298 bugzilla_2_16.__init__(self, ui)
341 bzmysql.__init__(self, ui)
299 self.default_notify = \
342 self.default_notify = \
300 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
343 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
301
344
302 class bugzilla_3_0(bugzilla_2_18):
345 class bzmysql_3_0(bzmysql_2_18):
303 '''support for bugzilla 3.0 series.'''
346 '''support for bugzilla 3.0 series.'''
304
347
305 def __init__(self, ui):
348 def __init__(self, ui):
306 bugzilla_2_18.__init__(self, ui)
349 bzmysql_2_18.__init__(self, ui)
307
350
308 def get_longdesc_id(self):
351 def get_longdesc_id(self):
309 '''get identity of longdesc field'''
352 '''get identity of longdesc field'''
310 self.run('select id from fielddefs where name = "longdesc"')
353 self.run('select id from fielddefs where name = "longdesc"')
311 ids = self.cursor.fetchall()
354 ids = self.cursor.fetchall()
312 if len(ids) != 1:
355 if len(ids) != 1:
313 raise util.Abort(_('unknown database schema'))
356 raise util.Abort(_('unknown database schema'))
314 return ids[0][0]
357 return ids[0][0]
315
358
316 class bugzilla(object):
359 class bugzilla(object):
317 # supported versions of bugzilla. different versions have
360 # supported versions of bugzilla. different versions have
318 # different schemas.
361 # different schemas.
319 _versions = {
362 _versions = {
320 '2.16': bugzilla_2_16,
363 '2.16': bzmysql,
321 '2.18': bugzilla_2_18,
364 '2.18': bzmysql_2_18,
322 '3.0': bugzilla_3_0
365 '3.0': bzmysql_3_0
323 }
366 }
324
367
325 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
368 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
326 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
369 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
327
370
328 _bz = None
371 _bz = None
329
372
330 def __init__(self, ui, repo):
373 def __init__(self, ui, repo):
331 self.ui = ui
374 self.ui = ui
332 self.repo = repo
375 self.repo = repo
333
376
334 def bz(self):
377 def bz(self):
335 '''return object that knows how to talk to bugzilla version in
378 '''return object that knows how to talk to bugzilla version in
336 use.'''
379 use.'''
337
380
338 if bugzilla._bz is None:
381 if bugzilla._bz is None:
339 bzversion = self.ui.config('bugzilla', 'version')
382 bzversion = self.ui.config('bugzilla', 'version')
340 try:
383 try:
341 bzclass = bugzilla._versions[bzversion]
384 bzclass = bugzilla._versions[bzversion]
342 except KeyError:
385 except KeyError:
343 raise util.Abort(_('bugzilla version %s not supported') %
386 raise util.Abort(_('bugzilla version %s not supported') %
344 bzversion)
387 bzversion)
345 bugzilla._bz = bzclass(self.ui)
388 bugzilla._bz = bzclass(self.ui)
346 return bugzilla._bz
389 return bugzilla._bz
347
390
348 def __getattr__(self, key):
391 def __getattr__(self, key):
349 return getattr(self.bz(), key)
392 return getattr(self.bz(), key)
350
393
351 _bug_re = None
394 _bug_re = None
352 _split_re = None
395 _split_re = None
353
396
354 def find_bug_ids(self, ctx):
397 def find_bug_ids(self, ctx):
355 '''return set of integer bug IDs from commit comment.
398 '''return set of integer bug IDs from commit comment.
356
399
357 Extract bug IDs from changeset comments. Filter out any that are
400 Extract bug IDs from changeset comments. Filter out any that are
358 not known to Bugzilla, and any that already have a reference to
401 not known to Bugzilla, and any that already have a reference to
359 the given changeset in their comments.
402 the given changeset in their comments.
360 '''
403 '''
361 if bugzilla._bug_re is None:
404 if bugzilla._bug_re is None:
362 bugzilla._bug_re = re.compile(
405 bugzilla._bug_re = re.compile(
363 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
406 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
364 re.IGNORECASE)
407 re.IGNORECASE)
365 bugzilla._split_re = re.compile(r'\D+')
408 bugzilla._split_re = re.compile(r'\D+')
366 start = 0
409 start = 0
367 ids = set()
410 ids = set()
368 while True:
411 while True:
369 m = bugzilla._bug_re.search(ctx.description(), start)
412 m = bugzilla._bug_re.search(ctx.description(), start)
370 if not m:
413 if not m:
371 break
414 break
372 start = m.end()
415 start = m.end()
373 for id in bugzilla._split_re.split(m.group(1)):
416 for id in bugzilla._split_re.split(m.group(1)):
374 if not id:
417 if not id:
375 continue
418 continue
376 ids.add(int(id))
419 ids.add(int(id))
377 if ids:
420 if ids:
378 ids = self.filter_real_bug_ids(ids)
421 ids = self.filter_real_bug_ids(ids)
379 if ids:
422 if ids:
380 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
423 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
381 return ids
424 return ids
382
425
383 def update(self, bugid, ctx):
426 def update(self, bugid, ctx):
384 '''update bugzilla bug with reference to changeset.'''
427 '''update bugzilla bug with reference to changeset.'''
385
428
386 def webroot(root):
429 def webroot(root):
387 '''strip leading prefix of repo root and turn into
430 '''strip leading prefix of repo root and turn into
388 url-safe path.'''
431 url-safe path.'''
389 count = int(self.ui.config('bugzilla', 'strip', 0))
432 count = int(self.ui.config('bugzilla', 'strip', 0))
390 root = util.pconvert(root)
433 root = util.pconvert(root)
391 while count > 0:
434 while count > 0:
392 c = root.find('/')
435 c = root.find('/')
393 if c == -1:
436 if c == -1:
394 break
437 break
395 root = root[c + 1:]
438 root = root[c + 1:]
396 count -= 1
439 count -= 1
397 return root
440 return root
398
441
399 mapfile = self.ui.config('bugzilla', 'style')
442 mapfile = self.ui.config('bugzilla', 'style')
400 tmpl = self.ui.config('bugzilla', 'template')
443 tmpl = self.ui.config('bugzilla', 'template')
401 t = cmdutil.changeset_templater(self.ui, self.repo,
444 t = cmdutil.changeset_templater(self.ui, self.repo,
402 False, None, mapfile, False)
445 False, None, mapfile, False)
403 if not mapfile and not tmpl:
446 if not mapfile and not tmpl:
404 tmpl = _('changeset {node|short} in repo {root} refers '
447 tmpl = _('changeset {node|short} in repo {root} refers '
405 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
448 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
406 if tmpl:
449 if tmpl:
407 tmpl = templater.parsestring(tmpl, quoted=False)
450 tmpl = templater.parsestring(tmpl, quoted=False)
408 t.use_template(tmpl)
451 t.use_template(tmpl)
409 self.ui.pushbuffer()
452 self.ui.pushbuffer()
410 t.show(ctx, changes=ctx.changeset(),
453 t.show(ctx, changes=ctx.changeset(),
411 bug=str(bugid),
454 bug=str(bugid),
412 hgweb=self.ui.config('web', 'baseurl'),
455 hgweb=self.ui.config('web', 'baseurl'),
413 root=self.repo.root,
456 root=self.repo.root,
414 webroot=webroot(self.repo.root))
457 webroot=webroot(self.repo.root))
415 data = self.ui.popbuffer()
458 data = self.ui.popbuffer()
416 self.add_comment(bugid, data, util.email(ctx.user()))
459 self.add_comment(bugid, data, util.email(ctx.user()))
417
460
418 def hook(ui, repo, hooktype, node=None, **kwargs):
461 def hook(ui, repo, hooktype, node=None, **kwargs):
419 '''add comment to bugzilla for each changeset that refers to a
462 '''add comment to bugzilla for each changeset that refers to a
420 bugzilla bug id. only add a comment once per bug, so same change
463 bugzilla bug id. only add a comment once per bug, so same change
421 seen multiple times does not fill bug with duplicate data.'''
464 seen multiple times does not fill bug with duplicate data.'''
422 try:
423 import MySQLdb as mysql
424 global MySQLdb
425 MySQLdb = mysql
426 except ImportError, err:
427 raise util.Abort(_('python mysql support not available: %s') % err)
428
429 if node is None:
465 if node is None:
430 raise util.Abort(_('hook type %s does not pass a changeset id') %
466 raise util.Abort(_('hook type %s does not pass a changeset id') %
431 hooktype)
467 hooktype)
432 try:
468 try:
433 bz = bugzilla(ui, repo)
469 bz = bugzilla(ui, repo)
434 ctx = repo[node]
470 ctx = repo[node]
435 ids = bz.find_bug_ids(ctx)
471 ids = bz.find_bug_ids(ctx)
436 if ids:
472 if ids:
437 for id in ids:
473 for id in ids:
438 bz.update(id, ctx)
474 bz.update(id, ctx)
439 bz.notify(ids, util.email(ctx.user()))
475 bz.notify(ids, util.email(ctx.user()))
440 except MySQLdb.MySQLError, err:
476 except Exception, e:
441 raise util.Abort(_('database error: %s') % err.args[1])
477 raise util.Abort(_('Bugzilla error: %s') % e)
442
478
General Comments 0
You need to be logged in to leave comments. Login now