##// END OF EJS Templates
bugzilla: rename filter_unknown_bug_ids to reflect its actual purpose....
Jim Hague -
r13798:9c9fa78f default
parent child Browse files
Show More
@@ -1,441 +1,441
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 MySQLdb = None
149
149
150 def buglist(ids):
150 def buglist(ids):
151 return '(' + ','.join(map(str, ids)) + ')'
151 return '(' + ','.join(map(str, ids)) + ')'
152
152
153 class bugzilla_2_16(object):
153 class bugzilla_2_16(object):
154 '''support for bugzilla version 2.16.'''
154 '''support for bugzilla version 2.16.'''
155
155
156 def __init__(self, ui):
156 def __init__(self, ui):
157 self.ui = ui
157 self.ui = ui
158 host = self.ui.config('bugzilla', 'host', 'localhost')
158 host = self.ui.config('bugzilla', 'host', 'localhost')
159 user = self.ui.config('bugzilla', 'user', 'bugs')
159 user = self.ui.config('bugzilla', 'user', 'bugs')
160 passwd = self.ui.config('bugzilla', 'password')
160 passwd = self.ui.config('bugzilla', 'password')
161 db = self.ui.config('bugzilla', 'db', 'bugs')
161 db = self.ui.config('bugzilla', 'db', 'bugs')
162 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
162 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
163 usermap = self.ui.config('bugzilla', 'usermap')
163 usermap = self.ui.config('bugzilla', 'usermap')
164 if usermap:
164 if usermap:
165 self.ui.readconfig(usermap, sections=['usermap'])
165 self.ui.readconfig(usermap, sections=['usermap'])
166 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
166 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
167 (host, db, user, '*' * len(passwd)))
167 (host, db, user, '*' * len(passwd)))
168 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
168 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
169 db=db, connect_timeout=timeout)
169 db=db, connect_timeout=timeout)
170 self.cursor = self.conn.cursor()
170 self.cursor = self.conn.cursor()
171 self.longdesc_id = self.get_longdesc_id()
171 self.longdesc_id = self.get_longdesc_id()
172 self.user_ids = {}
172 self.user_ids = {}
173 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
173 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
174
174
175 def run(self, *args, **kwargs):
175 def run(self, *args, **kwargs):
176 '''run a query.'''
176 '''run a query.'''
177 self.ui.note(_('query: %s %s\n') % (args, kwargs))
177 self.ui.note(_('query: %s %s\n') % (args, kwargs))
178 try:
178 try:
179 self.cursor.execute(*args, **kwargs)
179 self.cursor.execute(*args, **kwargs)
180 except MySQLdb.MySQLError:
180 except MySQLdb.MySQLError:
181 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
181 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
182 raise
182 raise
183
183
184 def get_longdesc_id(self):
184 def get_longdesc_id(self):
185 '''get identity of longdesc field'''
185 '''get identity of longdesc field'''
186 self.run('select fieldid from fielddefs where name = "longdesc"')
186 self.run('select fieldid from fielddefs where name = "longdesc"')
187 ids = self.cursor.fetchall()
187 ids = self.cursor.fetchall()
188 if len(ids) != 1:
188 if len(ids) != 1:
189 raise util.Abort(_('unknown database schema'))
189 raise util.Abort(_('unknown database schema'))
190 return ids[0][0]
190 return ids[0][0]
191
191
192 def filter_real_bug_ids(self, ids):
192 def filter_real_bug_ids(self, ids):
193 '''filter not-existing bug ids from list.'''
193 '''filter not-existing bug ids from list.'''
194 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
194 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
195 return sorted([c[0] for c in self.cursor.fetchall()])
195 return sorted([c[0] for c in self.cursor.fetchall()])
196
196
197 def filter_unknown_bug_ids(self, node, ids):
197 def filter_cset_known_bug_ids(self, node, ids):
198 '''filter bug ids from list that already refer to this changeset.'''
198 '''filter bug ids from list that already refer to this changeset.'''
199
199
200 self.run('''select bug_id from longdescs where
200 self.run('''select bug_id from longdescs where
201 bug_id in %s and thetext like "%%%s%%"''' %
201 bug_id in %s and thetext like "%%%s%%"''' %
202 (buglist(ids), short(node)))
202 (buglist(ids), short(node)))
203 unknown = set(ids)
203 unknown = set(ids)
204 for (id,) in self.cursor.fetchall():
204 for (id,) in self.cursor.fetchall():
205 self.ui.status(_('bug %d already knows about changeset %s\n') %
205 self.ui.status(_('bug %d already knows about changeset %s\n') %
206 (id, short(node)))
206 (id, short(node)))
207 unknown.discard(id)
207 unknown.discard(id)
208 return sorted(unknown)
208 return sorted(unknown)
209
209
210 def notify(self, ids, committer):
210 def notify(self, ids, committer):
211 '''tell bugzilla to send mail.'''
211 '''tell bugzilla to send mail.'''
212
212
213 self.ui.status(_('telling bugzilla to send mail:\n'))
213 self.ui.status(_('telling bugzilla to send mail:\n'))
214 (user, userid) = self.get_bugzilla_user(committer)
214 (user, userid) = self.get_bugzilla_user(committer)
215 for id in ids:
215 for id in ids:
216 self.ui.status(_(' bug %s\n') % id)
216 self.ui.status(_(' bug %s\n') % id)
217 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
217 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
218 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
218 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
219 try:
219 try:
220 # Backwards-compatible with old notify string, which
220 # Backwards-compatible with old notify string, which
221 # took one string. This will throw with a new format
221 # took one string. This will throw with a new format
222 # string.
222 # string.
223 cmd = cmdfmt % id
223 cmd = cmdfmt % id
224 except TypeError:
224 except TypeError:
225 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
225 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
226 self.ui.note(_('running notify command %s\n') % cmd)
226 self.ui.note(_('running notify command %s\n') % cmd)
227 fp = util.popen('(%s) 2>&1' % cmd)
227 fp = util.popen('(%s) 2>&1' % cmd)
228 out = fp.read()
228 out = fp.read()
229 ret = fp.close()
229 ret = fp.close()
230 if ret:
230 if ret:
231 self.ui.warn(out)
231 self.ui.warn(out)
232 raise util.Abort(_('bugzilla notify command %s') %
232 raise util.Abort(_('bugzilla notify command %s') %
233 util.explain_exit(ret)[0])
233 util.explain_exit(ret)[0])
234 self.ui.status(_('done\n'))
234 self.ui.status(_('done\n'))
235
235
236 def get_user_id(self, user):
236 def get_user_id(self, user):
237 '''look up numeric bugzilla user id.'''
237 '''look up numeric bugzilla user id.'''
238 try:
238 try:
239 return self.user_ids[user]
239 return self.user_ids[user]
240 except KeyError:
240 except KeyError:
241 try:
241 try:
242 userid = int(user)
242 userid = int(user)
243 except ValueError:
243 except ValueError:
244 self.ui.note(_('looking up user %s\n') % user)
244 self.ui.note(_('looking up user %s\n') % user)
245 self.run('''select userid from profiles
245 self.run('''select userid from profiles
246 where login_name like %s''', user)
246 where login_name like %s''', user)
247 all = self.cursor.fetchall()
247 all = self.cursor.fetchall()
248 if len(all) != 1:
248 if len(all) != 1:
249 raise KeyError(user)
249 raise KeyError(user)
250 userid = int(all[0][0])
250 userid = int(all[0][0])
251 self.user_ids[user] = userid
251 self.user_ids[user] = userid
252 return userid
252 return userid
253
253
254 def map_committer(self, user):
254 def map_committer(self, user):
255 '''map name of committer to bugzilla user name.'''
255 '''map name of committer to bugzilla user name.'''
256 for committer, bzuser in self.ui.configitems('usermap'):
256 for committer, bzuser in self.ui.configitems('usermap'):
257 if committer.lower() == user.lower():
257 if committer.lower() == user.lower():
258 return bzuser
258 return bzuser
259 return user
259 return user
260
260
261 def get_bugzilla_user(self, committer):
261 def get_bugzilla_user(self, committer):
262 '''see if committer is a registered bugzilla user. Return
262 '''see if committer is a registered bugzilla user. Return
263 bugzilla username and userid if so. If not, return default
263 bugzilla username and userid if so. If not, return default
264 bugzilla username and userid.'''
264 bugzilla username and userid.'''
265 user = self.map_committer(committer)
265 user = self.map_committer(committer)
266 try:
266 try:
267 userid = self.get_user_id(user)
267 userid = self.get_user_id(user)
268 except KeyError:
268 except KeyError:
269 try:
269 try:
270 defaultuser = self.ui.config('bugzilla', 'bzuser')
270 defaultuser = self.ui.config('bugzilla', 'bzuser')
271 if not defaultuser:
271 if not defaultuser:
272 raise util.Abort(_('cannot find bugzilla user id for %s') %
272 raise util.Abort(_('cannot find bugzilla user id for %s') %
273 user)
273 user)
274 userid = self.get_user_id(defaultuser)
274 userid = self.get_user_id(defaultuser)
275 user = defaultuser
275 user = defaultuser
276 except KeyError:
276 except KeyError:
277 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
277 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
278 (user, defaultuser))
278 (user, defaultuser))
279 return (user, userid)
279 return (user, userid)
280
280
281 def add_comment(self, bugid, text, committer):
281 def add_comment(self, bugid, text, committer):
282 '''add comment to bug. try adding comment as committer of
282 '''add comment to bug. try adding comment as committer of
283 changeset, otherwise as default bugzilla user.'''
283 changeset, otherwise as default bugzilla user.'''
284 (user, userid) = self.get_bugzilla_user(committer)
284 (user, userid) = self.get_bugzilla_user(committer)
285 now = time.strftime('%Y-%m-%d %H:%M:%S')
285 now = time.strftime('%Y-%m-%d %H:%M:%S')
286 self.run('''insert into longdescs
286 self.run('''insert into longdescs
287 (bug_id, who, bug_when, thetext)
287 (bug_id, who, bug_when, thetext)
288 values (%s, %s, %s, %s)''',
288 values (%s, %s, %s, %s)''',
289 (bugid, userid, now, text))
289 (bugid, userid, now, text))
290 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
290 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
291 values (%s, %s, %s, %s)''',
291 values (%s, %s, %s, %s)''',
292 (bugid, userid, now, self.longdesc_id))
292 (bugid, userid, now, self.longdesc_id))
293 self.conn.commit()
293 self.conn.commit()
294
294
295 class bugzilla_2_18(bugzilla_2_16):
295 class bugzilla_2_18(bugzilla_2_16):
296 '''support for bugzilla 2.18 series.'''
296 '''support for bugzilla 2.18 series.'''
297
297
298 def __init__(self, ui):
298 def __init__(self, ui):
299 bugzilla_2_16.__init__(self, ui)
299 bugzilla_2_16.__init__(self, ui)
300 self.default_notify = \
300 self.default_notify = \
301 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
301 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
302
302
303 class bugzilla_3_0(bugzilla_2_18):
303 class bugzilla_3_0(bugzilla_2_18):
304 '''support for bugzilla 3.0 series.'''
304 '''support for bugzilla 3.0 series.'''
305
305
306 def __init__(self, ui):
306 def __init__(self, ui):
307 bugzilla_2_18.__init__(self, ui)
307 bugzilla_2_18.__init__(self, ui)
308
308
309 def get_longdesc_id(self):
309 def get_longdesc_id(self):
310 '''get identity of longdesc field'''
310 '''get identity of longdesc field'''
311 self.run('select id from fielddefs where name = "longdesc"')
311 self.run('select id from fielddefs where name = "longdesc"')
312 ids = self.cursor.fetchall()
312 ids = self.cursor.fetchall()
313 if len(ids) != 1:
313 if len(ids) != 1:
314 raise util.Abort(_('unknown database schema'))
314 raise util.Abort(_('unknown database schema'))
315 return ids[0][0]
315 return ids[0][0]
316
316
317 class bugzilla(object):
317 class bugzilla(object):
318 # supported versions of bugzilla. different versions have
318 # supported versions of bugzilla. different versions have
319 # different schemas.
319 # different schemas.
320 _versions = {
320 _versions = {
321 '2.16': bugzilla_2_16,
321 '2.16': bugzilla_2_16,
322 '2.18': bugzilla_2_18,
322 '2.18': bugzilla_2_18,
323 '3.0': bugzilla_3_0
323 '3.0': bugzilla_3_0
324 }
324 }
325
325
326 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
326 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
327 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
327 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
328
328
329 _bz = None
329 _bz = None
330
330
331 def __init__(self, ui, repo):
331 def __init__(self, ui, repo):
332 self.ui = ui
332 self.ui = ui
333 self.repo = repo
333 self.repo = repo
334
334
335 def bz(self):
335 def bz(self):
336 '''return object that knows how to talk to bugzilla version in
336 '''return object that knows how to talk to bugzilla version in
337 use.'''
337 use.'''
338
338
339 if bugzilla._bz is None:
339 if bugzilla._bz is None:
340 bzversion = self.ui.config('bugzilla', 'version')
340 bzversion = self.ui.config('bugzilla', 'version')
341 try:
341 try:
342 bzclass = bugzilla._versions[bzversion]
342 bzclass = bugzilla._versions[bzversion]
343 except KeyError:
343 except KeyError:
344 raise util.Abort(_('bugzilla version %s not supported') %
344 raise util.Abort(_('bugzilla version %s not supported') %
345 bzversion)
345 bzversion)
346 bugzilla._bz = bzclass(self.ui)
346 bugzilla._bz = bzclass(self.ui)
347 return bugzilla._bz
347 return bugzilla._bz
348
348
349 def __getattr__(self, key):
349 def __getattr__(self, key):
350 return getattr(self.bz(), key)
350 return getattr(self.bz(), key)
351
351
352 _bug_re = None
352 _bug_re = None
353 _split_re = None
353 _split_re = None
354
354
355 def find_bug_ids(self, ctx):
355 def find_bug_ids(self, ctx):
356 '''find valid bug ids that are referred to in changeset
356 '''find valid bug ids that are referred to in changeset
357 comments and that do not already have references to this
357 comments and that do not already have references to this
358 changeset.'''
358 changeset.'''
359
359
360 if bugzilla._bug_re is None:
360 if bugzilla._bug_re is None:
361 bugzilla._bug_re = re.compile(
361 bugzilla._bug_re = re.compile(
362 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
362 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
363 re.IGNORECASE)
363 re.IGNORECASE)
364 bugzilla._split_re = re.compile(r'\D+')
364 bugzilla._split_re = re.compile(r'\D+')
365 start = 0
365 start = 0
366 ids = set()
366 ids = set()
367 while True:
367 while True:
368 m = bugzilla._bug_re.search(ctx.description(), start)
368 m = bugzilla._bug_re.search(ctx.description(), start)
369 if not m:
369 if not m:
370 break
370 break
371 start = m.end()
371 start = m.end()
372 for id in bugzilla._split_re.split(m.group(1)):
372 for id in bugzilla._split_re.split(m.group(1)):
373 if not id:
373 if not id:
374 continue
374 continue
375 ids.add(int(id))
375 ids.add(int(id))
376 if ids:
376 if ids:
377 ids = self.filter_real_bug_ids(ids)
377 ids = self.filter_real_bug_ids(ids)
378 if ids:
378 if ids:
379 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
379 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
380 return ids
380 return ids
381
381
382 def update(self, bugid, ctx):
382 def update(self, bugid, ctx):
383 '''update bugzilla bug with reference to changeset.'''
383 '''update bugzilla bug with reference to changeset.'''
384
384
385 def webroot(root):
385 def webroot(root):
386 '''strip leading prefix of repo root and turn into
386 '''strip leading prefix of repo root and turn into
387 url-safe path.'''
387 url-safe path.'''
388 count = int(self.ui.config('bugzilla', 'strip', 0))
388 count = int(self.ui.config('bugzilla', 'strip', 0))
389 root = util.pconvert(root)
389 root = util.pconvert(root)
390 while count > 0:
390 while count > 0:
391 c = root.find('/')
391 c = root.find('/')
392 if c == -1:
392 if c == -1:
393 break
393 break
394 root = root[c + 1:]
394 root = root[c + 1:]
395 count -= 1
395 count -= 1
396 return root
396 return root
397
397
398 mapfile = self.ui.config('bugzilla', 'style')
398 mapfile = self.ui.config('bugzilla', 'style')
399 tmpl = self.ui.config('bugzilla', 'template')
399 tmpl = self.ui.config('bugzilla', 'template')
400 t = cmdutil.changeset_templater(self.ui, self.repo,
400 t = cmdutil.changeset_templater(self.ui, self.repo,
401 False, None, mapfile, False)
401 False, None, mapfile, False)
402 if not mapfile and not tmpl:
402 if not mapfile and not tmpl:
403 tmpl = _('changeset {node|short} in repo {root} refers '
403 tmpl = _('changeset {node|short} in repo {root} refers '
404 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
404 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
405 if tmpl:
405 if tmpl:
406 tmpl = templater.parsestring(tmpl, quoted=False)
406 tmpl = templater.parsestring(tmpl, quoted=False)
407 t.use_template(tmpl)
407 t.use_template(tmpl)
408 self.ui.pushbuffer()
408 self.ui.pushbuffer()
409 t.show(ctx, changes=ctx.changeset(),
409 t.show(ctx, changes=ctx.changeset(),
410 bug=str(bugid),
410 bug=str(bugid),
411 hgweb=self.ui.config('web', 'baseurl'),
411 hgweb=self.ui.config('web', 'baseurl'),
412 root=self.repo.root,
412 root=self.repo.root,
413 webroot=webroot(self.repo.root))
413 webroot=webroot(self.repo.root))
414 data = self.ui.popbuffer()
414 data = self.ui.popbuffer()
415 self.add_comment(bugid, data, util.email(ctx.user()))
415 self.add_comment(bugid, data, util.email(ctx.user()))
416
416
417 def hook(ui, repo, hooktype, node=None, **kwargs):
417 def hook(ui, repo, hooktype, node=None, **kwargs):
418 '''add comment to bugzilla for each changeset that refers to a
418 '''add comment to bugzilla for each changeset that refers to a
419 bugzilla bug id. only add a comment once per bug, so same change
419 bugzilla bug id. only add a comment once per bug, so same change
420 seen multiple times does not fill bug with duplicate data.'''
420 seen multiple times does not fill bug with duplicate data.'''
421 try:
421 try:
422 import MySQLdb as mysql
422 import MySQLdb as mysql
423 global MySQLdb
423 global MySQLdb
424 MySQLdb = mysql
424 MySQLdb = mysql
425 except ImportError, err:
425 except ImportError, err:
426 raise util.Abort(_('python mysql support not available: %s') % err)
426 raise util.Abort(_('python mysql support not available: %s') % err)
427
427
428 if node is None:
428 if node is None:
429 raise util.Abort(_('hook type %s does not pass a changeset id') %
429 raise util.Abort(_('hook type %s does not pass a changeset id') %
430 hooktype)
430 hooktype)
431 try:
431 try:
432 bz = bugzilla(ui, repo)
432 bz = bugzilla(ui, repo)
433 ctx = repo[node]
433 ctx = repo[node]
434 ids = bz.find_bug_ids(ctx)
434 ids = bz.find_bug_ids(ctx)
435 if ids:
435 if ids:
436 for id in ids:
436 for id in ids:
437 bz.update(id, ctx)
437 bz.update(id, ctx)
438 bz.notify(ids, util.email(ctx.user()))
438 bz.notify(ids, util.email(ctx.user()))
439 except MySQLdb.MySQLError, err:
439 except MySQLdb.MySQLError, err:
440 raise util.Abort(_('database error: %s') % err.args[1])
440 raise util.Abort(_('database error: %s') % err.args[1])
441
441
General Comments 0
You need to be logged in to leave comments. Login now