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