##// END OF EJS Templates
bugzilla: correct comment typo
Jim Hague -
r16226:674ecd23 stable
parent child Browse files
Show More
@@ -1,774 +1,774
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 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 If your Bugzilla is version 3.2 or above, you are strongly
303 If your Bugzilla is version 3.4 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 476 class cookietransportrequest(object):
477 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 483 So this is a helper for defining a Transport which looks for
484 484 cookies being set in responses and saves them to add to all future
485 485 requests.
486 486 """
487 487
488 488 # Inspiration drawn from
489 489 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
490 490 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
491 491
492 492 cookies = []
493 493 def send_cookies(self, connection):
494 494 if self.cookies:
495 495 for cookie in self.cookies:
496 496 connection.putheader("Cookie", cookie)
497 497
498 498 def request(self, host, handler, request_body, verbose=0):
499 499 self.verbose = verbose
500 500 self.accept_gzip_encoding = False
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 # The explicit calls to the underlying xmlrpclib __init__() methods are
541 541 # necessary. The xmlrpclib.Transport classes are old-style classes, and
542 542 # it turns out their __init__() doesn't get called when doing multiple
543 543 # inheritance with a new-style class.
544 544 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
545 545 def __init__(self, use_datetime=0):
546 546 xmlrpclib.Transport.__init__(self, use_datetime)
547 547
548 548 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
549 549 def __init__(self, use_datetime=0):
550 550 xmlrpclib.SafeTransport.__init__(self, use_datetime)
551 551
552 552 class bzxmlrpc(bzaccess):
553 553 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
554 554
555 555 Requires a minimum Bugzilla version 3.4.
556 556 """
557 557
558 558 def __init__(self, ui):
559 559 bzaccess.__init__(self, ui)
560 560
561 561 bzweb = self.ui.config('bugzilla', 'bzurl',
562 562 'http://localhost/bugzilla/')
563 563 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
564 564
565 565 user = self.ui.config('bugzilla', 'user', 'bugs')
566 566 passwd = self.ui.config('bugzilla', 'password')
567 567
568 568 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
569 569 self.bzproxy.User.login(dict(login=user, password=passwd))
570 570
571 571 def transport(self, uri):
572 572 if urlparse.urlparse(uri, "http")[0] == "https":
573 573 return cookiesafetransport()
574 574 else:
575 575 return cookietransport()
576 576
577 577 def get_bug_comments(self, id):
578 578 """Return a string with all comment text for a bug."""
579 579 c = self.bzproxy.Bug.comments(dict(ids=[id]))
580 580 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
581 581
582 582 def filter_real_bug_ids(self, ids):
583 583 res = set()
584 584 bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True))
585 585 for bug in bugs['bugs']:
586 586 res.add(bug['id'])
587 587 return res
588 588
589 589 def filter_cset_known_bug_ids(self, node, ids):
590 590 for id in sorted(ids):
591 591 if self.get_bug_comments(id).find(short(node)) != -1:
592 592 self.ui.status(_('bug %d already knows about changeset %s\n') %
593 593 (id, short(node)))
594 594 ids.discard(id)
595 595 return ids
596 596
597 597 def add_comment(self, bugid, text, committer):
598 598 self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text))
599 599
600 600 class bzxmlrpcemail(bzxmlrpc):
601 601 """Read data from Bugzilla via XMLRPC, send updates via email.
602 602
603 603 Advantages of sending updates via email:
604 604 1. Comments can be added as any user, not just logged in user.
605 605 2. Bug statuses and other fields not accessible via XMLRPC can
606 606 be updated. This is not currently used.
607 607 """
608 608
609 609 def __init__(self, ui):
610 610 bzxmlrpc.__init__(self, ui)
611 611
612 612 self.bzemail = self.ui.config('bugzilla', 'bzemail')
613 613 if not self.bzemail:
614 614 raise util.Abort(_("configuration 'bzemail' missing"))
615 615 mail.validateconfig(self.ui)
616 616
617 617 def send_bug_modify_email(self, bugid, commands, comment, committer):
618 618 '''send modification message to Bugzilla bug via email.
619 619
620 620 The message format is documented in the Bugzilla email_in.pl
621 621 specification. commands is a list of command lines, comment is the
622 622 comment text.
623 623
624 624 To stop users from crafting commit comments with
625 625 Bugzilla commands, specify the bug ID via the message body, rather
626 626 than the subject line, and leave a blank line after it.
627 627 '''
628 628 user = self.map_committer(committer)
629 629 matches = self.bzproxy.User.get(dict(match=[user]))
630 630 if not matches['users']:
631 631 user = self.ui.config('bugzilla', 'user', 'bugs')
632 632 matches = self.bzproxy.User.get(dict(match=[user]))
633 633 if not matches['users']:
634 634 raise util.Abort(_("default bugzilla user %s email not found") %
635 635 user)
636 636 user = matches['users'][0]['email']
637 637
638 638 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
639 639
640 640 _charsets = mail._charsets(self.ui)
641 641 user = mail.addressencode(self.ui, user, _charsets)
642 642 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
643 643 msg = mail.mimeencode(self.ui, text, _charsets)
644 644 msg['From'] = user
645 645 msg['To'] = bzemail
646 646 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
647 647 sendmail = mail.connect(self.ui)
648 648 sendmail(user, bzemail, msg.as_string())
649 649
650 650 def add_comment(self, bugid, text, committer):
651 651 self.send_bug_modify_email(bugid, [], text, committer)
652 652
653 653 class bugzilla(object):
654 654 # supported versions of bugzilla. different versions have
655 655 # different schemas.
656 656 _versions = {
657 657 '2.16': bzmysql,
658 658 '2.18': bzmysql_2_18,
659 659 '3.0': bzmysql_3_0,
660 660 'xmlrpc': bzxmlrpc,
661 661 'xmlrpc+email': bzxmlrpcemail
662 662 }
663 663
664 664 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
665 665 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
666 666
667 667 _bz = None
668 668
669 669 def __init__(self, ui, repo):
670 670 self.ui = ui
671 671 self.repo = repo
672 672
673 673 def bz(self):
674 674 '''return object that knows how to talk to bugzilla version in
675 675 use.'''
676 676
677 677 if bugzilla._bz is None:
678 678 bzversion = self.ui.config('bugzilla', 'version')
679 679 try:
680 680 bzclass = bugzilla._versions[bzversion]
681 681 except KeyError:
682 682 raise util.Abort(_('bugzilla version %s not supported') %
683 683 bzversion)
684 684 bugzilla._bz = bzclass(self.ui)
685 685 return bugzilla._bz
686 686
687 687 def __getattr__(self, key):
688 688 return getattr(self.bz(), key)
689 689
690 690 _bug_re = None
691 691 _split_re = None
692 692
693 693 def find_bug_ids(self, ctx):
694 694 '''return set of integer bug IDs from commit comment.
695 695
696 696 Extract bug IDs from changeset comments. Filter out any that are
697 697 not known to Bugzilla, and any that already have a reference to
698 698 the given changeset in their comments.
699 699 '''
700 700 if bugzilla._bug_re is None:
701 701 bugzilla._bug_re = re.compile(
702 702 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
703 703 re.IGNORECASE)
704 704 bugzilla._split_re = re.compile(r'\D+')
705 705 start = 0
706 706 ids = set()
707 707 while True:
708 708 m = bugzilla._bug_re.search(ctx.description(), start)
709 709 if not m:
710 710 break
711 711 start = m.end()
712 712 for id in bugzilla._split_re.split(m.group(1)):
713 713 if not id:
714 714 continue
715 715 ids.add(int(id))
716 716 if ids:
717 717 ids = self.filter_real_bug_ids(ids)
718 718 if ids:
719 719 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
720 720 return ids
721 721
722 722 def update(self, bugid, ctx):
723 723 '''update bugzilla bug with reference to changeset.'''
724 724
725 725 def webroot(root):
726 726 '''strip leading prefix of repo root and turn into
727 727 url-safe path.'''
728 728 count = int(self.ui.config('bugzilla', 'strip', 0))
729 729 root = util.pconvert(root)
730 730 while count > 0:
731 731 c = root.find('/')
732 732 if c == -1:
733 733 break
734 734 root = root[c + 1:]
735 735 count -= 1
736 736 return root
737 737
738 738 mapfile = self.ui.config('bugzilla', 'style')
739 739 tmpl = self.ui.config('bugzilla', 'template')
740 740 t = cmdutil.changeset_templater(self.ui, self.repo,
741 741 False, None, mapfile, False)
742 742 if not mapfile and not tmpl:
743 743 tmpl = _('changeset {node|short} in repo {root} refers '
744 744 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
745 745 if tmpl:
746 746 tmpl = templater.parsestring(tmpl, quoted=False)
747 747 t.use_template(tmpl)
748 748 self.ui.pushbuffer()
749 749 t.show(ctx, changes=ctx.changeset(),
750 750 bug=str(bugid),
751 751 hgweb=self.ui.config('web', 'baseurl'),
752 752 root=self.repo.root,
753 753 webroot=webroot(self.repo.root))
754 754 data = self.ui.popbuffer()
755 755 self.add_comment(bugid, data, util.email(ctx.user()))
756 756
757 757 def hook(ui, repo, hooktype, node=None, **kwargs):
758 758 '''add comment to bugzilla for each changeset that refers to a
759 759 bugzilla bug id. only add a comment once per bug, so same change
760 760 seen multiple times does not fill bug with duplicate data.'''
761 761 if node is None:
762 762 raise util.Abort(_('hook type %s does not pass a changeset id') %
763 763 hooktype)
764 764 try:
765 765 bz = bugzilla(ui, repo)
766 766 ctx = repo[node]
767 767 ids = bz.find_bug_ids(ctx)
768 768 if ids:
769 769 for id in ids:
770 770 bz.update(id, ctx)
771 771 bz.notify(ids, util.email(ctx.user()))
772 772 except Exception, e:
773 773 raise util.Abort(_('Bugzilla error: %s') % e)
774 774
General Comments 0
You need to be logged in to leave comments. Login now