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