##// END OF EJS Templates
bugzilla: more documentation formatting fixups...
Jim Hague -
r13896:3b4025dc default
parent child Browse files
Show More
@@ -1,756 +1,756 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 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 2. Writing directly to the Bugzilla database. Only Bugzilla installations
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 templates as {hgweb}.
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 See also the [usermap] section.
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`` wityh 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 258 import re, time, 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.explain_exit(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 476 class CookieSafeTransport(xmlrpclib.SafeTransport):
477 477 """A SafeTransport 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 483 So this is a SafeTransport which looks for cookies being set
484 484 in responses and saves them to add to all future requests.
485 485 It appears a SafeTransport can do both HTTP and HTTPS sessions,
486 486 which saves us having to do a CookieTransport too.
487 487 """
488 488
489 489 # Inspiration drawn from
490 490 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
491 491 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
492 492
493 493 cookies = []
494 494 def send_cookies(self, connection):
495 495 if self.cookies:
496 496 for cookie in self.cookies:
497 497 connection.putheader("Cookie", cookie)
498 498
499 499 def request(self, host, handler, request_body, verbose=0):
500 500 self.verbose = verbose
501 501
502 502 # issue XML-RPC request
503 503 h = self.make_connection(host)
504 504 if verbose:
505 505 h.set_debuglevel(1)
506 506
507 507 self.send_request(h, handler, request_body)
508 508 self.send_host(h, host)
509 509 self.send_cookies(h)
510 510 self.send_user_agent(h)
511 511 self.send_content(h, request_body)
512 512
513 513 # Deal with differences between Python 2.4-2.6 and 2.7.
514 514 # In the former h is a HTTP(S). In the latter it's a
515 515 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
516 516 # HTTP(S) has an underlying HTTP(S)Connection, so extract
517 517 # that and use it.
518 518 try:
519 519 response = h.getresponse()
520 520 except AttributeError:
521 521 response = h._conn.getresponse()
522 522
523 523 # Add any cookie definitions to our list.
524 524 for header in response.msg.getallmatchingheaders("Set-Cookie"):
525 525 val = header.split(": ", 1)[1]
526 526 cookie = val.split(";", 1)[0]
527 527 self.cookies.append(cookie)
528 528
529 529 if response.status != 200:
530 530 raise xmlrpclib.ProtocolError(host + handler, response.status,
531 531 response.reason, response.msg.headers)
532 532
533 533 payload = response.read()
534 534 parser, unmarshaller = self.getparser()
535 535 parser.feed(payload)
536 536 parser.close()
537 537
538 538 return unmarshaller.close()
539 539
540 540 class bzxmlrpc(bzaccess):
541 541 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
542 542
543 543 Requires a minimum Bugzilla version 3.4.
544 544 """
545 545
546 546 def __init__(self, ui):
547 547 bzaccess.__init__(self, ui)
548 548
549 549 bzweb = self.ui.config('bugzilla', 'bzurl',
550 550 'http://localhost/bugzilla/')
551 551 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
552 552
553 553 user = self.ui.config('bugzilla', 'user', 'bugs')
554 554 passwd = self.ui.config('bugzilla', 'password')
555 555
556 556 self.bzproxy = xmlrpclib.ServerProxy(bzweb, CookieSafeTransport())
557 557 self.bzproxy.User.login(dict(login=user, password=passwd))
558 558
559 559 def get_bug_comments(self, id):
560 560 """Return a string with all comment text for a bug."""
561 561 c = self.bzproxy.Bug.comments(dict(ids=[id]))
562 562 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
563 563
564 564 def filter_real_bug_ids(self, ids):
565 565 res = set()
566 566 bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True))
567 567 for bug in bugs['bugs']:
568 568 res.add(bug['id'])
569 569 return res
570 570
571 571 def filter_cset_known_bug_ids(self, node, ids):
572 572 for id in sorted(ids):
573 573 if self.get_bug_comments(id).find(short(node)) != -1:
574 574 self.ui.status(_('bug %d already knows about changeset %s\n') %
575 575 (id, short(node)))
576 576 ids.discard(id)
577 577 return ids
578 578
579 579 def add_comment(self, bugid, text, committer):
580 580 self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text))
581 581
582 582 class bzxmlrpcemail(bzxmlrpc):
583 583 """Read data from Bugzilla via XMLRPC, send updates via email.
584 584
585 585 Advantages of sending updates via email:
586 586 1. Comments can be added as any user, not just logged in user.
587 587 2. Bug statuses and other fields not accessible via XMLRPC can
588 588 be updated. This is not currently used.
589 589 """
590 590
591 591 def __init__(self, ui):
592 592 bzxmlrpc.__init__(self, ui)
593 593
594 594 self.bzemail = self.ui.config('bugzilla', 'bzemail')
595 595 if not self.bzemail:
596 596 raise util.Abort(_("configuration 'bzemail' missing"))
597 597 mail.validateconfig(self.ui)
598 598
599 599 def send_bug_modify_email(self, bugid, commands, comment, committer):
600 600 '''send modification message to Bugzilla bug via email.
601 601
602 602 The message format is documented in the Bugzilla email_in.pl
603 603 specification. commands is a list of command lines, comment is the
604 604 comment text.
605 605
606 606 To stop users from crafting commit comments with
607 607 Bugzilla commands, specify the bug ID via the message body, rather
608 608 than the subject line, and leave a blank line after it.
609 609 '''
610 610 user = self.map_committer(committer)
611 611 matches = self.bzproxy.User.get(dict(match=[user]))
612 612 if not matches['users']:
613 613 user = self.ui.config('bugzilla', 'user', 'bugs')
614 614 matches = self.bzproxy.User.get(dict(match=[user]))
615 615 if not matches['users']:
616 616 raise util.Abort(_("default bugzilla user %s email not found") %
617 617 user)
618 618 user = matches['users'][0]['email']
619 619
620 620 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
621 621
622 622 _charsets = mail._charsets(self.ui)
623 623 user = mail.addressencode(self.ui, user, _charsets)
624 624 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
625 625 msg = mail.mimeencode(self.ui, text, _charsets)
626 626 msg['From'] = user
627 627 msg['To'] = bzemail
628 628 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
629 629 sendmail = mail.connect(self.ui)
630 630 sendmail(user, bzemail, msg.as_string())
631 631
632 632 def add_comment(self, bugid, text, committer):
633 633 self.send_bug_modify_email(bugid, [], text, committer)
634 634
635 635 class bugzilla(object):
636 636 # supported versions of bugzilla. different versions have
637 637 # different schemas.
638 638 _versions = {
639 639 '2.16': bzmysql,
640 640 '2.18': bzmysql_2_18,
641 641 '3.0': bzmysql_3_0,
642 642 'xmlrpc': bzxmlrpc,
643 643 'xmlrpc+email': bzxmlrpcemail
644 644 }
645 645
646 646 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
647 647 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
648 648
649 649 _bz = None
650 650
651 651 def __init__(self, ui, repo):
652 652 self.ui = ui
653 653 self.repo = repo
654 654
655 655 def bz(self):
656 656 '''return object that knows how to talk to bugzilla version in
657 657 use.'''
658 658
659 659 if bugzilla._bz is None:
660 660 bzversion = self.ui.config('bugzilla', 'version')
661 661 try:
662 662 bzclass = bugzilla._versions[bzversion]
663 663 except KeyError:
664 664 raise util.Abort(_('bugzilla version %s not supported') %
665 665 bzversion)
666 666 bugzilla._bz = bzclass(self.ui)
667 667 return bugzilla._bz
668 668
669 669 def __getattr__(self, key):
670 670 return getattr(self.bz(), key)
671 671
672 672 _bug_re = None
673 673 _split_re = None
674 674
675 675 def find_bug_ids(self, ctx):
676 676 '''return set of integer bug IDs from commit comment.
677 677
678 678 Extract bug IDs from changeset comments. Filter out any that are
679 679 not known to Bugzilla, and any that already have a reference to
680 680 the given changeset in their comments.
681 681 '''
682 682 if bugzilla._bug_re is None:
683 683 bugzilla._bug_re = re.compile(
684 684 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
685 685 re.IGNORECASE)
686 686 bugzilla._split_re = re.compile(r'\D+')
687 687 start = 0
688 688 ids = set()
689 689 while True:
690 690 m = bugzilla._bug_re.search(ctx.description(), start)
691 691 if not m:
692 692 break
693 693 start = m.end()
694 694 for id in bugzilla._split_re.split(m.group(1)):
695 695 if not id:
696 696 continue
697 697 ids.add(int(id))
698 698 if ids:
699 699 ids = self.filter_real_bug_ids(ids)
700 700 if ids:
701 701 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
702 702 return ids
703 703
704 704 def update(self, bugid, ctx):
705 705 '''update bugzilla bug with reference to changeset.'''
706 706
707 707 def webroot(root):
708 708 '''strip leading prefix of repo root and turn into
709 709 url-safe path.'''
710 710 count = int(self.ui.config('bugzilla', 'strip', 0))
711 711 root = util.pconvert(root)
712 712 while count > 0:
713 713 c = root.find('/')
714 714 if c == -1:
715 715 break
716 716 root = root[c + 1:]
717 717 count -= 1
718 718 return root
719 719
720 720 mapfile = self.ui.config('bugzilla', 'style')
721 721 tmpl = self.ui.config('bugzilla', 'template')
722 722 t = cmdutil.changeset_templater(self.ui, self.repo,
723 723 False, None, mapfile, False)
724 724 if not mapfile and not tmpl:
725 725 tmpl = _('changeset {node|short} in repo {root} refers '
726 726 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
727 727 if tmpl:
728 728 tmpl = templater.parsestring(tmpl, quoted=False)
729 729 t.use_template(tmpl)
730 730 self.ui.pushbuffer()
731 731 t.show(ctx, changes=ctx.changeset(),
732 732 bug=str(bugid),
733 733 hgweb=self.ui.config('web', 'baseurl'),
734 734 root=self.repo.root,
735 735 webroot=webroot(self.repo.root))
736 736 data = self.ui.popbuffer()
737 737 self.add_comment(bugid, data, util.email(ctx.user()))
738 738
739 739 def hook(ui, repo, hooktype, node=None, **kwargs):
740 740 '''add comment to bugzilla for each changeset that refers to a
741 741 bugzilla bug id. only add a comment once per bug, so same change
742 742 seen multiple times does not fill bug with duplicate data.'''
743 743 if node is None:
744 744 raise util.Abort(_('hook type %s does not pass a changeset id') %
745 745 hooktype)
746 746 try:
747 747 bz = bugzilla(ui, repo)
748 748 ctx = repo[node]
749 749 ids = bz.find_bug_ids(ctx)
750 750 if ids:
751 751 for id in ids:
752 752 bz.update(id, ctx)
753 753 bz.notify(ids, util.email(ctx.user()))
754 754 except Exception, e:
755 755 raise util.Abort(_('Bugzilla error: %s') % e)
756 756
General Comments 0
You need to be logged in to leave comments. Login now