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