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