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