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