##// END OF EJS Templates
bugzilla: remove superfluous pass statements
Augie Fackler -
r34367:00cf44b7 default
parent child Browse files
Show More
@@ -1,1130 +1,1126 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Four basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21 21
22 22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23 23
24 24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
26 26
27 27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 28 using MySQL are supported. Requires Python MySQLdb.
29 29
30 30 Writing directly to the database is susceptible to schema changes, and
31 31 relies on a Bugzilla contrib script to send out bug change
32 32 notification emails. This script runs as the user running Mercurial,
33 33 must be run on the host with the Bugzilla install, and requires
34 34 permission to read Bugzilla configuration details and the necessary
35 35 MySQL user and password to have full access rights to the Bugzilla
36 36 database. For these reasons this access mode is now considered
37 37 deprecated, and will not be updated for new Bugzilla versions going
38 38 forward. Only adding comments is supported in this access mode.
39 39
40 40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 41 in the configuration. Comments are added under that username. Since the
42 42 configuration must be readable by all Mercurial users, it is recommended
43 43 that the rights of that user are restricted in Bugzilla to the minimum
44 44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45 45
46 46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 47 email to the Bugzilla email interface to submit comments to bugs.
48 48 The From: address in the email is set to the email address of the Mercurial
49 49 user, so the comment appears to come from the Mercurial user. In the event
50 50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
51 51 user, the email associated with the Bugzilla username used to log into
52 52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
53 53 works on all supported Bugzilla versions.
54 54
55 55 Access via the REST-API needs either a Bugzilla username and password
56 56 or an apikey specified in the configuration. Comments are made under
57 57 the given username or the user associated with the apikey in Bugzilla.
58 58
59 59 Configuration items common to all access modes:
60 60
61 61 bugzilla.version
62 62 The access type to use. Values recognized are:
63 63
64 64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
69 69 including 3.0.
70 70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 71 including 2.18.
72 72
73 73 bugzilla.regexp
74 74 Regular expression to match bug IDs for update in changeset commit message.
75 75 It must contain one "()" named group ``<ids>`` containing the bug
76 76 IDs separated by non-digit characters. It may also contain
77 77 a named group ``<hours>`` with a floating-point number giving the
78 78 hours worked on the bug. If no named groups are present, the first
79 79 "()" group is assumed to contain the bug IDs, and work time is not
80 80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 82 variations thereof, followed by an hours number prefixed by ``h`` or
83 83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84 84
85 85 bugzilla.fixregexp
86 86 Regular expression to match bug IDs for marking fixed in changeset
87 87 commit message. This must contain a "()" named group ``<ids>` containing
88 88 the bug IDs separated by non-digit characters. It may also contain
89 89 a named group ``<hours>`` with a floating-point number giving the
90 90 hours worked on the bug. If no named groups are present, the first
91 91 "()" group is assumed to contain the bug IDs, and work time is not
92 92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 94 variations thereof, followed by an hours number prefixed by ``h`` or
95 95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96 96
97 97 bugzilla.fixstatus
98 98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99 99
100 100 bugzilla.fixresolution
101 101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102 102
103 103 bugzilla.style
104 104 The style file to use when formatting comments.
105 105
106 106 bugzilla.template
107 107 Template to use when formatting comments. Overrides style if
108 108 specified. In addition to the usual Mercurial keywords, the
109 109 extension specifies:
110 110
111 111 :``{bug}``: The Bugzilla bug ID.
112 112 :``{root}``: The full pathname of the Mercurial repository.
113 113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115 115
116 116 Default ``changeset {node|short} in repo {root} refers to bug
117 117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118 118
119 119 bugzilla.strip
120 120 The number of path separator characters to strip from the front of
121 121 the Mercurial repository path (``{root}`` in templates) to produce
122 122 ``{webroot}``. For example, a repository with ``{root}``
123 123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 124 ``{webroot}`` of ``my-project``. Default 0.
125 125
126 126 web.baseurl
127 127 Base URL for browsing Mercurial repositories. Referenced from
128 128 templates as ``{hgweb}``.
129 129
130 130 Configuration items common to XMLRPC+email and MySQL access modes:
131 131
132 132 bugzilla.usermap
133 133 Path of file containing Mercurial committer email to Bugzilla user email
134 134 mappings. If specified, the file should contain one mapping per
135 135 line::
136 136
137 137 committer = Bugzilla user
138 138
139 139 See also the ``[usermap]`` section.
140 140
141 141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 143 Contains entries of the form ``committer = Bugzilla user``.
144 144
145 145 XMLRPC and REST-API access mode configuration:
146 146
147 147 bugzilla.bzurl
148 148 The base URL for the Bugzilla installation.
149 149 Default ``http://localhost/bugzilla``.
150 150
151 151 bugzilla.user
152 152 The username to use to log into Bugzilla via XMLRPC. Default
153 153 ``bugs``.
154 154
155 155 bugzilla.password
156 156 The password for Bugzilla login.
157 157
158 158 REST-API access mode uses the options listed above as well as:
159 159
160 160 bugzilla.apikey
161 161 An apikey generated on the Bugzilla instance for api access.
162 162 Using an apikey removes the need to store the user and password
163 163 options.
164 164
165 165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 166 and also:
167 167
168 168 bugzilla.bzemail
169 169 The Bugzilla email address.
170 170
171 171 In addition, the Mercurial email settings must be configured. See the
172 172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173 173
174 174 MySQL access mode configuration:
175 175
176 176 bugzilla.host
177 177 Hostname of the MySQL server holding the Bugzilla database.
178 178 Default ``localhost``.
179 179
180 180 bugzilla.db
181 181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182 182
183 183 bugzilla.user
184 184 Username to use to access MySQL server. Default ``bugs``.
185 185
186 186 bugzilla.password
187 187 Password to use to access MySQL server.
188 188
189 189 bugzilla.timeout
190 190 Database connection timeout (seconds). Default 5.
191 191
192 192 bugzilla.bzuser
193 193 Fallback Bugzilla user name to record comments with, if changeset
194 194 committer cannot be found as a Bugzilla user.
195 195
196 196 bugzilla.bzdir
197 197 Bugzilla install directory. Used by default notify. Default
198 198 ``/var/www/html/bugzilla``.
199 199
200 200 bugzilla.notify
201 201 The command to run to get Bugzilla to send bug change notification
202 202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 203 id) and ``user`` (committer bugzilla email). Default depends on
204 204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 205 contrib/sendbugmail.pl %(id)s %(user)s".
206 206
207 207 Activating the extension::
208 208
209 209 [extensions]
210 210 bugzilla =
211 211
212 212 [hooks]
213 213 # run bugzilla hook on every change pulled or pushed in here
214 214 incoming.bugzilla = python:hgext.bugzilla.hook
215 215
216 216 Example configurations:
217 217
218 218 XMLRPC example configuration. This uses the Bugzilla at
219 219 ``http://my-project.org/bugzilla``, logging in as user
220 220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 222 with a web interface at ``http://my-project.org/hg``. ::
223 223
224 224 [bugzilla]
225 225 bzurl=http://my-project.org/bugzilla
226 226 user=bugmail@my-project.org
227 227 password=plugh
228 228 version=xmlrpc
229 229 template=Changeset {node|short} in {root|basename}.
230 230 {hgweb}/{webroot}/rev/{node|short}\\n
231 231 {desc}\\n
232 232 strip=5
233 233
234 234 [web]
235 235 baseurl=http://my-project.org/hg
236 236
237 237 XMLRPC+email example configuration. This uses the Bugzilla at
238 238 ``http://my-project.org/bugzilla``, logging in as user
239 239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 242 are sent to the Bugzilla email address
243 243 ``bugzilla@my-project.org``. ::
244 244
245 245 [bugzilla]
246 246 bzurl=http://my-project.org/bugzilla
247 247 user=bugmail@my-project.org
248 248 password=plugh
249 249 version=xmlrpc+email
250 250 bzemail=bugzilla@my-project.org
251 251 template=Changeset {node|short} in {root|basename}.
252 252 {hgweb}/{webroot}/rev/{node|short}\\n
253 253 {desc}\\n
254 254 strip=5
255 255
256 256 [web]
257 257 baseurl=http://my-project.org/hg
258 258
259 259 [usermap]
260 260 user@emaildomain.com=user.name@bugzilladomain.com
261 261
262 262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 264 the Bugzilla database name is ``bugs`` and MySQL is
265 265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 267 with a web interface at ``http://my-project.org/hg``. ::
268 268
269 269 [bugzilla]
270 270 host=localhost
271 271 password=XYZZY
272 272 version=3.0
273 273 bzuser=unknown@domain.com
274 274 bzdir=/opt/bugzilla-3.2
275 275 template=Changeset {node|short} in {root|basename}.
276 276 {hgweb}/{webroot}/rev/{node|short}\\n
277 277 {desc}\\n
278 278 strip=5
279 279
280 280 [web]
281 281 baseurl=http://my-project.org/hg
282 282
283 283 [usermap]
284 284 user@emaildomain.com=user.name@bugzilladomain.com
285 285
286 286 All the above add a comment to the Bugzilla bug record of the form::
287 287
288 288 Changeset 3b16791d6642 in repository-name.
289 289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290 290
291 291 Changeset commit comment. Bug 1234.
292 292 '''
293 293
294 294 from __future__ import absolute_import
295 295
296 296 import json
297 297 import re
298 298 import time
299 299
300 300 from mercurial.i18n import _
301 301 from mercurial.node import short
302 302 from mercurial import (
303 303 cmdutil,
304 304 configitems,
305 305 error,
306 306 mail,
307 307 registrar,
308 308 url,
309 309 util,
310 310 )
311 311
312 312 xmlrpclib = util.xmlrpclib
313 313
314 314 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
315 315 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
316 316 # be specifying the version(s) of Mercurial they are tested with, or
317 317 # leave the attribute unspecified.
318 318 testedwith = 'ships-with-hg-core'
319 319
320 320 configtable = {}
321 321 configitem = registrar.configitem(configtable)
322 322
323 323 configitem('bugzilla', 'apikey',
324 324 default='',
325 325 )
326 326 configitem('bugzilla', 'bzdir',
327 327 default='/var/www/html/bugzilla',
328 328 )
329 329 configitem('bugzilla', 'bzemail',
330 330 default=None,
331 331 )
332 332 configitem('bugzilla', 'bzurl',
333 333 default='http://localhost/bugzilla/',
334 334 )
335 335 configitem('bugzilla', 'bzuser',
336 336 default=None,
337 337 )
338 338 configitem('bugzilla', 'db',
339 339 default='bugs',
340 340 )
341 341 configitem('bugzilla', 'fixregexp',
342 342 default=(r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
343 343 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
344 344 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
345 345 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
346 346 )
347 347 configitem('bugzilla', 'fixresolution',
348 348 default='FIXED',
349 349 )
350 350 configitem('bugzilla', 'fixstatus',
351 351 default='RESOLVED',
352 352 )
353 353 configitem('bugzilla', 'host',
354 354 default='localhost',
355 355 )
356 356 configitem('bugzilla', 'notify',
357 357 default=configitems.dynamicdefault,
358 358 )
359 359 configitem('bugzilla', 'password',
360 360 default=None,
361 361 )
362 362 configitem('bugzilla', 'regexp',
363 363 default=(r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
364 364 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
365 365 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
366 366 )
367 367 configitem('bugzilla', 'strip',
368 368 default=0,
369 369 )
370 370 configitem('bugzilla', 'style',
371 371 default=None,
372 372 )
373 373 configitem('bugzilla', 'template',
374 374 default=None,
375 375 )
376 376 configitem('bugzilla', 'timeout',
377 377 default=5,
378 378 )
379 379 configitem('bugzilla', 'user',
380 380 default='bugs',
381 381 )
382 382 configitem('bugzilla', 'usermap',
383 383 default=None,
384 384 )
385 385 configitem('bugzilla', 'version',
386 386 default=None,
387 387 )
388 388
389 389 class bzaccess(object):
390 390 '''Base class for access to Bugzilla.'''
391 391
392 392 def __init__(self, ui):
393 393 self.ui = ui
394 394 usermap = self.ui.config('bugzilla', 'usermap')
395 395 if usermap:
396 396 self.ui.readconfig(usermap, sections=['usermap'])
397 397
398 398 def map_committer(self, user):
399 399 '''map name of committer to Bugzilla user name.'''
400 400 for committer, bzuser in self.ui.configitems('usermap'):
401 401 if committer.lower() == user.lower():
402 402 return bzuser
403 403 return user
404 404
405 405 # Methods to be implemented by access classes.
406 406 #
407 407 # 'bugs' is a dict keyed on bug id, where values are a dict holding
408 408 # updates to bug state. Recognized dict keys are:
409 409 #
410 410 # 'hours': Value, float containing work hours to be updated.
411 411 # 'fix': If key present, bug is to be marked fixed. Value ignored.
412 412
413 413 def filter_real_bug_ids(self, bugs):
414 414 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
415 pass
416 415
417 416 def filter_cset_known_bug_ids(self, node, bugs):
418 417 '''remove bug IDs where node occurs in comment text from bugs.'''
419 pass
420 418
421 419 def updatebug(self, bugid, newstate, text, committer):
422 420 '''update the specified bug. Add comment text and set new states.
423 421
424 422 If possible add the comment as being from the committer of
425 423 the changeset. Otherwise use the default Bugzilla user.
426 424 '''
427 pass
428 425
429 426 def notify(self, bugs, committer):
430 427 '''Force sending of Bugzilla notification emails.
431 428
432 429 Only required if the access method does not trigger notification
433 430 emails automatically.
434 431 '''
435 pass
436 432
437 433 # Bugzilla via direct access to MySQL database.
438 434 class bzmysql(bzaccess):
439 435 '''Support for direct MySQL access to Bugzilla.
440 436
441 437 The earliest Bugzilla version this is tested with is version 2.16.
442 438
443 439 If your Bugzilla is version 3.4 or above, you are strongly
444 440 recommended to use the XMLRPC access method instead.
445 441 '''
446 442
447 443 @staticmethod
448 444 def sql_buglist(ids):
449 445 '''return SQL-friendly list of bug ids'''
450 446 return '(' + ','.join(map(str, ids)) + ')'
451 447
452 448 _MySQLdb = None
453 449
454 450 def __init__(self, ui):
455 451 try:
456 452 import MySQLdb as mysql
457 453 bzmysql._MySQLdb = mysql
458 454 except ImportError as err:
459 455 raise error.Abort(_('python mysql support not available: %s') % err)
460 456
461 457 bzaccess.__init__(self, ui)
462 458
463 459 host = self.ui.config('bugzilla', 'host')
464 460 user = self.ui.config('bugzilla', 'user')
465 461 passwd = self.ui.config('bugzilla', 'password')
466 462 db = self.ui.config('bugzilla', 'db')
467 463 timeout = int(self.ui.config('bugzilla', 'timeout'))
468 464 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
469 465 (host, db, user, '*' * len(passwd)))
470 466 self.conn = bzmysql._MySQLdb.connect(host=host,
471 467 user=user, passwd=passwd,
472 468 db=db,
473 469 connect_timeout=timeout)
474 470 self.cursor = self.conn.cursor()
475 471 self.longdesc_id = self.get_longdesc_id()
476 472 self.user_ids = {}
477 473 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
478 474
479 475 def run(self, *args, **kwargs):
480 476 '''run a query.'''
481 477 self.ui.note(_('query: %s %s\n') % (args, kwargs))
482 478 try:
483 479 self.cursor.execute(*args, **kwargs)
484 480 except bzmysql._MySQLdb.MySQLError:
485 481 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
486 482 raise
487 483
488 484 def get_longdesc_id(self):
489 485 '''get identity of longdesc field'''
490 486 self.run('select fieldid from fielddefs where name = "longdesc"')
491 487 ids = self.cursor.fetchall()
492 488 if len(ids) != 1:
493 489 raise error.Abort(_('unknown database schema'))
494 490 return ids[0][0]
495 491
496 492 def filter_real_bug_ids(self, bugs):
497 493 '''filter not-existing bugs from set.'''
498 494 self.run('select bug_id from bugs where bug_id in %s' %
499 495 bzmysql.sql_buglist(bugs.keys()))
500 496 existing = [id for (id,) in self.cursor.fetchall()]
501 497 for id in bugs.keys():
502 498 if id not in existing:
503 499 self.ui.status(_('bug %d does not exist\n') % id)
504 500 del bugs[id]
505 501
506 502 def filter_cset_known_bug_ids(self, node, bugs):
507 503 '''filter bug ids that already refer to this changeset from set.'''
508 504 self.run('''select bug_id from longdescs where
509 505 bug_id in %s and thetext like "%%%s%%"''' %
510 506 (bzmysql.sql_buglist(bugs.keys()), short(node)))
511 507 for (id,) in self.cursor.fetchall():
512 508 self.ui.status(_('bug %d already knows about changeset %s\n') %
513 509 (id, short(node)))
514 510 del bugs[id]
515 511
516 512 def notify(self, bugs, committer):
517 513 '''tell bugzilla to send mail.'''
518 514 self.ui.status(_('telling bugzilla to send mail:\n'))
519 515 (user, userid) = self.get_bugzilla_user(committer)
520 516 for id in bugs.keys():
521 517 self.ui.status(_(' bug %s\n') % id)
522 518 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
523 519 bzdir = self.ui.config('bugzilla', 'bzdir')
524 520 try:
525 521 # Backwards-compatible with old notify string, which
526 522 # took one string. This will throw with a new format
527 523 # string.
528 524 cmd = cmdfmt % id
529 525 except TypeError:
530 526 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
531 527 self.ui.note(_('running notify command %s\n') % cmd)
532 528 fp = util.popen('(%s) 2>&1' % cmd)
533 529 out = fp.read()
534 530 ret = fp.close()
535 531 if ret:
536 532 self.ui.warn(out)
537 533 raise error.Abort(_('bugzilla notify command %s') %
538 534 util.explainexit(ret)[0])
539 535 self.ui.status(_('done\n'))
540 536
541 537 def get_user_id(self, user):
542 538 '''look up numeric bugzilla user id.'''
543 539 try:
544 540 return self.user_ids[user]
545 541 except KeyError:
546 542 try:
547 543 userid = int(user)
548 544 except ValueError:
549 545 self.ui.note(_('looking up user %s\n') % user)
550 546 self.run('''select userid from profiles
551 547 where login_name like %s''', user)
552 548 all = self.cursor.fetchall()
553 549 if len(all) != 1:
554 550 raise KeyError(user)
555 551 userid = int(all[0][0])
556 552 self.user_ids[user] = userid
557 553 return userid
558 554
559 555 def get_bugzilla_user(self, committer):
560 556 '''See if committer is a registered bugzilla user. Return
561 557 bugzilla username and userid if so. If not, return default
562 558 bugzilla username and userid.'''
563 559 user = self.map_committer(committer)
564 560 try:
565 561 userid = self.get_user_id(user)
566 562 except KeyError:
567 563 try:
568 564 defaultuser = self.ui.config('bugzilla', 'bzuser')
569 565 if not defaultuser:
570 566 raise error.Abort(_('cannot find bugzilla user id for %s') %
571 567 user)
572 568 userid = self.get_user_id(defaultuser)
573 569 user = defaultuser
574 570 except KeyError:
575 571 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
576 572 % (user, defaultuser))
577 573 return (user, userid)
578 574
579 575 def updatebug(self, bugid, newstate, text, committer):
580 576 '''update bug state with comment text.
581 577
582 578 Try adding comment as committer of changeset, otherwise as
583 579 default bugzilla user.'''
584 580 if len(newstate) > 0:
585 581 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
586 582
587 583 (user, userid) = self.get_bugzilla_user(committer)
588 584 now = time.strftime('%Y-%m-%d %H:%M:%S')
589 585 self.run('''insert into longdescs
590 586 (bug_id, who, bug_when, thetext)
591 587 values (%s, %s, %s, %s)''',
592 588 (bugid, userid, now, text))
593 589 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
594 590 values (%s, %s, %s, %s)''',
595 591 (bugid, userid, now, self.longdesc_id))
596 592 self.conn.commit()
597 593
598 594 class bzmysql_2_18(bzmysql):
599 595 '''support for bugzilla 2.18 series.'''
600 596
601 597 def __init__(self, ui):
602 598 bzmysql.__init__(self, ui)
603 599 self.default_notify = \
604 600 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
605 601
606 602 class bzmysql_3_0(bzmysql_2_18):
607 603 '''support for bugzilla 3.0 series.'''
608 604
609 605 def __init__(self, ui):
610 606 bzmysql_2_18.__init__(self, ui)
611 607
612 608 def get_longdesc_id(self):
613 609 '''get identity of longdesc field'''
614 610 self.run('select id from fielddefs where name = "longdesc"')
615 611 ids = self.cursor.fetchall()
616 612 if len(ids) != 1:
617 613 raise error.Abort(_('unknown database schema'))
618 614 return ids[0][0]
619 615
620 616 # Bugzilla via XMLRPC interface.
621 617
622 618 class cookietransportrequest(object):
623 619 """A Transport request method that retains cookies over its lifetime.
624 620
625 621 The regular xmlrpclib transports ignore cookies. Which causes
626 622 a bit of a problem when you need a cookie-based login, as with
627 623 the Bugzilla XMLRPC interface prior to 4.4.3.
628 624
629 625 So this is a helper for defining a Transport which looks for
630 626 cookies being set in responses and saves them to add to all future
631 627 requests.
632 628 """
633 629
634 630 # Inspiration drawn from
635 631 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
636 632 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
637 633
638 634 cookies = []
639 635 def send_cookies(self, connection):
640 636 if self.cookies:
641 637 for cookie in self.cookies:
642 638 connection.putheader("Cookie", cookie)
643 639
644 640 def request(self, host, handler, request_body, verbose=0):
645 641 self.verbose = verbose
646 642 self.accept_gzip_encoding = False
647 643
648 644 # issue XML-RPC request
649 645 h = self.make_connection(host)
650 646 if verbose:
651 647 h.set_debuglevel(1)
652 648
653 649 self.send_request(h, handler, request_body)
654 650 self.send_host(h, host)
655 651 self.send_cookies(h)
656 652 self.send_user_agent(h)
657 653 self.send_content(h, request_body)
658 654
659 655 # Deal with differences between Python 2.6 and 2.7.
660 656 # In the former h is a HTTP(S). In the latter it's a
661 657 # HTTP(S)Connection. Luckily, the 2.6 implementation of
662 658 # HTTP(S) has an underlying HTTP(S)Connection, so extract
663 659 # that and use it.
664 660 try:
665 661 response = h.getresponse()
666 662 except AttributeError:
667 663 response = h._conn.getresponse()
668 664
669 665 # Add any cookie definitions to our list.
670 666 for header in response.msg.getallmatchingheaders("Set-Cookie"):
671 667 val = header.split(": ", 1)[1]
672 668 cookie = val.split(";", 1)[0]
673 669 self.cookies.append(cookie)
674 670
675 671 if response.status != 200:
676 672 raise xmlrpclib.ProtocolError(host + handler, response.status,
677 673 response.reason, response.msg.headers)
678 674
679 675 payload = response.read()
680 676 parser, unmarshaller = self.getparser()
681 677 parser.feed(payload)
682 678 parser.close()
683 679
684 680 return unmarshaller.close()
685 681
686 682 # The explicit calls to the underlying xmlrpclib __init__() methods are
687 683 # necessary. The xmlrpclib.Transport classes are old-style classes, and
688 684 # it turns out their __init__() doesn't get called when doing multiple
689 685 # inheritance with a new-style class.
690 686 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
691 687 def __init__(self, use_datetime=0):
692 688 if util.safehasattr(xmlrpclib.Transport, "__init__"):
693 689 xmlrpclib.Transport.__init__(self, use_datetime)
694 690
695 691 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
696 692 def __init__(self, use_datetime=0):
697 693 if util.safehasattr(xmlrpclib.Transport, "__init__"):
698 694 xmlrpclib.SafeTransport.__init__(self, use_datetime)
699 695
700 696 class bzxmlrpc(bzaccess):
701 697 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
702 698
703 699 Requires a minimum Bugzilla version 3.4.
704 700 """
705 701
706 702 def __init__(self, ui):
707 703 bzaccess.__init__(self, ui)
708 704
709 705 bzweb = self.ui.config('bugzilla', 'bzurl')
710 706 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
711 707
712 708 user = self.ui.config('bugzilla', 'user')
713 709 passwd = self.ui.config('bugzilla', 'password')
714 710
715 711 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
716 712 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
717 713
718 714 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
719 715 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
720 716 self.bzvermajor = int(ver[0])
721 717 self.bzverminor = int(ver[1])
722 718 login = self.bzproxy.User.login({'login': user, 'password': passwd,
723 719 'restrict_login': True})
724 720 self.bztoken = login.get('token', '')
725 721
726 722 def transport(self, uri):
727 723 if util.urlreq.urlparse(uri, "http")[0] == "https":
728 724 return cookiesafetransport()
729 725 else:
730 726 return cookietransport()
731 727
732 728 def get_bug_comments(self, id):
733 729 """Return a string with all comment text for a bug."""
734 730 c = self.bzproxy.Bug.comments({'ids': [id],
735 731 'include_fields': ['text'],
736 732 'token': self.bztoken})
737 733 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
738 734
739 735 def filter_real_bug_ids(self, bugs):
740 736 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
741 737 'include_fields': [],
742 738 'permissive': True,
743 739 'token': self.bztoken,
744 740 })
745 741 for badbug in probe['faults']:
746 742 id = badbug['id']
747 743 self.ui.status(_('bug %d does not exist\n') % id)
748 744 del bugs[id]
749 745
750 746 def filter_cset_known_bug_ids(self, node, bugs):
751 747 for id in sorted(bugs.keys()):
752 748 if self.get_bug_comments(id).find(short(node)) != -1:
753 749 self.ui.status(_('bug %d already knows about changeset %s\n') %
754 750 (id, short(node)))
755 751 del bugs[id]
756 752
757 753 def updatebug(self, bugid, newstate, text, committer):
758 754 args = {}
759 755 if 'hours' in newstate:
760 756 args['work_time'] = newstate['hours']
761 757
762 758 if self.bzvermajor >= 4:
763 759 args['ids'] = [bugid]
764 760 args['comment'] = {'body' : text}
765 761 if 'fix' in newstate:
766 762 args['status'] = self.fixstatus
767 763 args['resolution'] = self.fixresolution
768 764 args['token'] = self.bztoken
769 765 self.bzproxy.Bug.update(args)
770 766 else:
771 767 if 'fix' in newstate:
772 768 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
773 769 "to mark bugs fixed\n"))
774 770 args['id'] = bugid
775 771 args['comment'] = text
776 772 self.bzproxy.Bug.add_comment(args)
777 773
778 774 class bzxmlrpcemail(bzxmlrpc):
779 775 """Read data from Bugzilla via XMLRPC, send updates via email.
780 776
781 777 Advantages of sending updates via email:
782 778 1. Comments can be added as any user, not just logged in user.
783 779 2. Bug statuses or other fields not accessible via XMLRPC can
784 780 potentially be updated.
785 781
786 782 There is no XMLRPC function to change bug status before Bugzilla
787 783 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
788 784 But bugs can be marked fixed via email from 3.4 onwards.
789 785 """
790 786
791 787 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
792 788 # in-email fields are specified as '@<fieldname> = <value>'. In
793 789 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
794 790 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
795 791 # compatibility, but rather than rely on this use the new format for
796 792 # 4.0 onwards.
797 793
798 794 def __init__(self, ui):
799 795 bzxmlrpc.__init__(self, ui)
800 796
801 797 self.bzemail = self.ui.config('bugzilla', 'bzemail')
802 798 if not self.bzemail:
803 799 raise error.Abort(_("configuration 'bzemail' missing"))
804 800 mail.validateconfig(self.ui)
805 801
806 802 def makecommandline(self, fieldname, value):
807 803 if self.bzvermajor >= 4:
808 804 return "@%s %s" % (fieldname, str(value))
809 805 else:
810 806 if fieldname == "id":
811 807 fieldname = "bug_id"
812 808 return "@%s = %s" % (fieldname, str(value))
813 809
814 810 def send_bug_modify_email(self, bugid, commands, comment, committer):
815 811 '''send modification message to Bugzilla bug via email.
816 812
817 813 The message format is documented in the Bugzilla email_in.pl
818 814 specification. commands is a list of command lines, comment is the
819 815 comment text.
820 816
821 817 To stop users from crafting commit comments with
822 818 Bugzilla commands, specify the bug ID via the message body, rather
823 819 than the subject line, and leave a blank line after it.
824 820 '''
825 821 user = self.map_committer(committer)
826 822 matches = self.bzproxy.User.get({'match': [user],
827 823 'token': self.bztoken})
828 824 if not matches['users']:
829 825 user = self.ui.config('bugzilla', 'user')
830 826 matches = self.bzproxy.User.get({'match': [user],
831 827 'token': self.bztoken})
832 828 if not matches['users']:
833 829 raise error.Abort(_("default bugzilla user %s email not found")
834 830 % user)
835 831 user = matches['users'][0]['email']
836 832 commands.append(self.makecommandline("id", bugid))
837 833
838 834 text = "\n".join(commands) + "\n\n" + comment
839 835
840 836 _charsets = mail._charsets(self.ui)
841 837 user = mail.addressencode(self.ui, user, _charsets)
842 838 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
843 839 msg = mail.mimeencode(self.ui, text, _charsets)
844 840 msg['From'] = user
845 841 msg['To'] = bzemail
846 842 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
847 843 sendmail = mail.connect(self.ui)
848 844 sendmail(user, bzemail, msg.as_string())
849 845
850 846 def updatebug(self, bugid, newstate, text, committer):
851 847 cmds = []
852 848 if 'hours' in newstate:
853 849 cmds.append(self.makecommandline("work_time", newstate['hours']))
854 850 if 'fix' in newstate:
855 851 cmds.append(self.makecommandline("bug_status", self.fixstatus))
856 852 cmds.append(self.makecommandline("resolution", self.fixresolution))
857 853 self.send_bug_modify_email(bugid, cmds, text, committer)
858 854
859 855 class NotFound(LookupError):
860 856 pass
861 857
862 858 class bzrestapi(bzaccess):
863 859 """Read and write bugzilla data using the REST API available since
864 860 Bugzilla 5.0.
865 861 """
866 862 def __init__(self, ui):
867 863 bzaccess.__init__(self, ui)
868 864 bz = self.ui.config('bugzilla', 'bzurl')
869 865 self.bzroot = '/'.join([bz, 'rest'])
870 866 self.apikey = self.ui.config('bugzilla', 'apikey')
871 867 self.user = self.ui.config('bugzilla', 'user')
872 868 self.passwd = self.ui.config('bugzilla', 'password')
873 869 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
874 870 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
875 871
876 872 def apiurl(self, targets, include_fields=None):
877 873 url = '/'.join([self.bzroot] + [str(t) for t in targets])
878 874 qv = {}
879 875 if self.apikey:
880 876 qv['api_key'] = self.apikey
881 877 elif self.user and self.passwd:
882 878 qv['login'] = self.user
883 879 qv['password'] = self.passwd
884 880 if include_fields:
885 881 qv['include_fields'] = include_fields
886 882 if qv:
887 883 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
888 884 return url
889 885
890 886 def _fetch(self, burl):
891 887 try:
892 888 resp = url.open(self.ui, burl)
893 889 return json.loads(resp.read())
894 890 except util.urlerr.httperror as inst:
895 891 if inst.code == 401:
896 892 raise error.Abort(_('authorization failed'))
897 893 if inst.code == 404:
898 894 raise NotFound()
899 895 else:
900 896 raise
901 897
902 898 def _submit(self, burl, data, method='POST'):
903 899 data = json.dumps(data)
904 900 if method == 'PUT':
905 901 class putrequest(util.urlreq.request):
906 902 def get_method(self):
907 903 return 'PUT'
908 904 request_type = putrequest
909 905 else:
910 906 request_type = util.urlreq.request
911 907 req = request_type(burl, data,
912 908 {'Content-Type': 'application/json'})
913 909 try:
914 910 resp = url.opener(self.ui).open(req)
915 911 return json.loads(resp.read())
916 912 except util.urlerr.httperror as inst:
917 913 if inst.code == 401:
918 914 raise error.Abort(_('authorization failed'))
919 915 if inst.code == 404:
920 916 raise NotFound()
921 917 else:
922 918 raise
923 919
924 920 def filter_real_bug_ids(self, bugs):
925 921 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
926 922 badbugs = set()
927 923 for bugid in bugs:
928 924 burl = self.apiurl(('bug', bugid), include_fields='status')
929 925 try:
930 926 self._fetch(burl)
931 927 except NotFound:
932 928 badbugs.add(bugid)
933 929 for bugid in badbugs:
934 930 del bugs[bugid]
935 931
936 932 def filter_cset_known_bug_ids(self, node, bugs):
937 933 '''remove bug IDs where node occurs in comment text from bugs.'''
938 934 sn = short(node)
939 935 for bugid in bugs.keys():
940 936 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
941 937 result = self._fetch(burl)
942 938 comments = result['bugs'][str(bugid)]['comments']
943 939 if any(sn in c['text'] for c in comments):
944 940 self.ui.status(_('bug %d already knows about changeset %s\n') %
945 941 (bugid, sn))
946 942 del bugs[bugid]
947 943
948 944 def updatebug(self, bugid, newstate, text, committer):
949 945 '''update the specified bug. Add comment text and set new states.
950 946
951 947 If possible add the comment as being from the committer of
952 948 the changeset. Otherwise use the default Bugzilla user.
953 949 '''
954 950 bugmod = {}
955 951 if 'hours' in newstate:
956 952 bugmod['work_time'] = newstate['hours']
957 953 if 'fix' in newstate:
958 954 bugmod['status'] = self.fixstatus
959 955 bugmod['resolution'] = self.fixresolution
960 956 if bugmod:
961 957 # if we have to change the bugs state do it here
962 958 bugmod['comment'] = {
963 959 'comment': text,
964 960 'is_private': False,
965 961 'is_markdown': False,
966 962 }
967 963 burl = self.apiurl(('bug', bugid))
968 964 self._submit(burl, bugmod, method='PUT')
969 965 self.ui.debug('updated bug %s\n' % bugid)
970 966 else:
971 967 burl = self.apiurl(('bug', bugid, 'comment'))
972 968 self._submit(burl, {
973 969 'comment': text,
974 970 'is_private': False,
975 971 'is_markdown': False,
976 972 })
977 973 self.ui.debug('added comment to bug %s\n' % bugid)
978 974
979 975 def notify(self, bugs, committer):
980 976 '''Force sending of Bugzilla notification emails.
981 977
982 978 Only required if the access method does not trigger notification
983 979 emails automatically.
984 980 '''
985 981 pass
986 982
987 983 class bugzilla(object):
988 984 # supported versions of bugzilla. different versions have
989 985 # different schemas.
990 986 _versions = {
991 987 '2.16': bzmysql,
992 988 '2.18': bzmysql_2_18,
993 989 '3.0': bzmysql_3_0,
994 990 'xmlrpc': bzxmlrpc,
995 991 'xmlrpc+email': bzxmlrpcemail,
996 992 'restapi': bzrestapi,
997 993 }
998 994
999 995 def __init__(self, ui, repo):
1000 996 self.ui = ui
1001 997 self.repo = repo
1002 998
1003 999 bzversion = self.ui.config('bugzilla', 'version')
1004 1000 try:
1005 1001 bzclass = bugzilla._versions[bzversion]
1006 1002 except KeyError:
1007 1003 raise error.Abort(_('bugzilla version %s not supported') %
1008 1004 bzversion)
1009 1005 self.bzdriver = bzclass(self.ui)
1010 1006
1011 1007 self.bug_re = re.compile(
1012 1008 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
1013 1009 self.fix_re = re.compile(
1014 1010 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
1015 1011 self.split_re = re.compile(r'\D+')
1016 1012
1017 1013 def find_bugs(self, ctx):
1018 1014 '''return bugs dictionary created from commit comment.
1019 1015
1020 1016 Extract bug info from changeset comments. Filter out any that are
1021 1017 not known to Bugzilla, and any that already have a reference to
1022 1018 the given changeset in their comments.
1023 1019 '''
1024 1020 start = 0
1025 1021 hours = 0.0
1026 1022 bugs = {}
1027 1023 bugmatch = self.bug_re.search(ctx.description(), start)
1028 1024 fixmatch = self.fix_re.search(ctx.description(), start)
1029 1025 while True:
1030 1026 bugattribs = {}
1031 1027 if not bugmatch and not fixmatch:
1032 1028 break
1033 1029 if not bugmatch:
1034 1030 m = fixmatch
1035 1031 elif not fixmatch:
1036 1032 m = bugmatch
1037 1033 else:
1038 1034 if bugmatch.start() < fixmatch.start():
1039 1035 m = bugmatch
1040 1036 else:
1041 1037 m = fixmatch
1042 1038 start = m.end()
1043 1039 if m is bugmatch:
1044 1040 bugmatch = self.bug_re.search(ctx.description(), start)
1045 1041 if 'fix' in bugattribs:
1046 1042 del bugattribs['fix']
1047 1043 else:
1048 1044 fixmatch = self.fix_re.search(ctx.description(), start)
1049 1045 bugattribs['fix'] = None
1050 1046
1051 1047 try:
1052 1048 ids = m.group('ids')
1053 1049 except IndexError:
1054 1050 ids = m.group(1)
1055 1051 try:
1056 1052 hours = float(m.group('hours'))
1057 1053 bugattribs['hours'] = hours
1058 1054 except IndexError:
1059 1055 pass
1060 1056 except TypeError:
1061 1057 pass
1062 1058 except ValueError:
1063 1059 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1064 1060
1065 1061 for id in self.split_re.split(ids):
1066 1062 if not id:
1067 1063 continue
1068 1064 bugs[int(id)] = bugattribs
1069 1065 if bugs:
1070 1066 self.bzdriver.filter_real_bug_ids(bugs)
1071 1067 if bugs:
1072 1068 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1073 1069 return bugs
1074 1070
1075 1071 def update(self, bugid, newstate, ctx):
1076 1072 '''update bugzilla bug with reference to changeset.'''
1077 1073
1078 1074 def webroot(root):
1079 1075 '''strip leading prefix of repo root and turn into
1080 1076 url-safe path.'''
1081 1077 count = int(self.ui.config('bugzilla', 'strip'))
1082 1078 root = util.pconvert(root)
1083 1079 while count > 0:
1084 1080 c = root.find('/')
1085 1081 if c == -1:
1086 1082 break
1087 1083 root = root[c + 1:]
1088 1084 count -= 1
1089 1085 return root
1090 1086
1091 1087 mapfile = None
1092 1088 tmpl = self.ui.config('bugzilla', 'template')
1093 1089 if not tmpl:
1094 1090 mapfile = self.ui.config('bugzilla', 'style')
1095 1091 if not mapfile and not tmpl:
1096 1092 tmpl = _('changeset {node|short} in repo {root} refers '
1097 1093 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1098 1094 spec = cmdutil.logtemplatespec(tmpl, mapfile)
1099 1095 t = cmdutil.changeset_templater(self.ui, self.repo, spec,
1100 1096 False, None, False)
1101 1097 self.ui.pushbuffer()
1102 1098 t.show(ctx, changes=ctx.changeset(),
1103 1099 bug=str(bugid),
1104 1100 hgweb=self.ui.config('web', 'baseurl'),
1105 1101 root=self.repo.root,
1106 1102 webroot=webroot(self.repo.root))
1107 1103 data = self.ui.popbuffer()
1108 1104 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
1109 1105
1110 1106 def notify(self, bugs, committer):
1111 1107 '''ensure Bugzilla users are notified of bug change.'''
1112 1108 self.bzdriver.notify(bugs, committer)
1113 1109
1114 1110 def hook(ui, repo, hooktype, node=None, **kwargs):
1115 1111 '''add comment to bugzilla for each changeset that refers to a
1116 1112 bugzilla bug id. only add a comment once per bug, so same change
1117 1113 seen multiple times does not fill bug with duplicate data.'''
1118 1114 if node is None:
1119 1115 raise error.Abort(_('hook type %s does not pass a changeset id') %
1120 1116 hooktype)
1121 1117 try:
1122 1118 bz = bugzilla(ui, repo)
1123 1119 ctx = repo[node]
1124 1120 bugs = bz.find_bugs(ctx)
1125 1121 if bugs:
1126 1122 for bug in bugs:
1127 1123 bz.update(bug, bugs[bug], ctx)
1128 1124 bz.notify(bugs, util.email(ctx.user()))
1129 1125 except Exception as e:
1130 1126 raise error.Abort(_('Bugzilla error: %s') % e)
General Comments 0
You need to be logged in to leave comments. Login now