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