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