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