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