##// END OF EJS Templates
bugzilla: pass the url to xmlrpclib.ServerProxy as str
Mads Kiilerich -
r46695:734d051d stable
parent child Browse files
Show More
@@ -1,1214 +1,1216
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 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 bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Four basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21 21
22 22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23 23
24 24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
26 26
27 27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 28 using MySQL are supported. Requires Python MySQLdb.
29 29
30 30 Writing directly to the database is susceptible to schema changes, and
31 31 relies on a Bugzilla contrib script to send out bug change
32 32 notification emails. This script runs as the user running Mercurial,
33 33 must be run on the host with the Bugzilla install, and requires
34 34 permission to read Bugzilla configuration details and the necessary
35 35 MySQL user and password to have full access rights to the Bugzilla
36 36 database. For these reasons this access mode is now considered
37 37 deprecated, and will not be updated for new Bugzilla versions going
38 38 forward. Only adding comments is supported in this access mode.
39 39
40 40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 41 in the configuration. Comments are added under that username. Since the
42 42 configuration must be readable by all Mercurial users, it is recommended
43 43 that the rights of that user are restricted in Bugzilla to the minimum
44 44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45 45
46 46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 47 email to the Bugzilla email interface to submit comments to bugs.
48 48 The From: address in the email is set to the email address of the Mercurial
49 49 user, so the comment appears to come from the Mercurial user. In the event
50 50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
51 51 user, the email associated with the Bugzilla username used to log into
52 52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
53 53 works on all supported Bugzilla versions.
54 54
55 55 Access via the REST-API needs either a Bugzilla username and password
56 56 or an apikey specified in the configuration. Comments are made under
57 57 the given username or the user associated with the apikey in Bugzilla.
58 58
59 59 Configuration items common to all access modes:
60 60
61 61 bugzilla.version
62 62 The access type to use. Values recognized are:
63 63
64 64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
69 69 including 3.0.
70 70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 71 including 2.18.
72 72
73 73 bugzilla.regexp
74 74 Regular expression to match bug IDs for update in changeset commit message.
75 75 It must contain one "()" named group ``<ids>`` containing the bug
76 76 IDs separated by non-digit characters. It may also contain
77 77 a named group ``<hours>`` with a floating-point number giving the
78 78 hours worked on the bug. If no named groups are present, the first
79 79 "()" group is assumed to contain the bug IDs, and work time is not
80 80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 82 variations thereof, followed by an hours number prefixed by ``h`` or
83 83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84 84
85 85 bugzilla.fixregexp
86 86 Regular expression to match bug IDs for marking fixed in changeset
87 87 commit message. This must contain a "()" named group ``<ids>` containing
88 88 the bug IDs separated by non-digit characters. It may also contain
89 89 a named group ``<hours>`` with a floating-point number giving the
90 90 hours worked on the bug. If no named groups are present, the first
91 91 "()" group is assumed to contain the bug IDs, and work time is not
92 92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 94 variations thereof, followed by an hours number prefixed by ``h`` or
95 95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96 96
97 97 bugzilla.fixstatus
98 98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99 99
100 100 bugzilla.fixresolution
101 101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102 102
103 103 bugzilla.style
104 104 The style file to use when formatting comments.
105 105
106 106 bugzilla.template
107 107 Template to use when formatting comments. Overrides style if
108 108 specified. In addition to the usual Mercurial keywords, the
109 109 extension specifies:
110 110
111 111 :``{bug}``: The Bugzilla bug ID.
112 112 :``{root}``: The full pathname of the Mercurial repository.
113 113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115 115
116 116 Default ``changeset {node|short} in repo {root} refers to bug
117 117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118 118
119 119 bugzilla.strip
120 120 The number of path separator characters to strip from the front of
121 121 the Mercurial repository path (``{root}`` in templates) to produce
122 122 ``{webroot}``. For example, a repository with ``{root}``
123 123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 124 ``{webroot}`` of ``my-project``. Default 0.
125 125
126 126 web.baseurl
127 127 Base URL for browsing Mercurial repositories. Referenced from
128 128 templates as ``{hgweb}``.
129 129
130 130 Configuration items common to XMLRPC+email and MySQL access modes:
131 131
132 132 bugzilla.usermap
133 133 Path of file containing Mercurial committer email to Bugzilla user email
134 134 mappings. If specified, the file should contain one mapping per
135 135 line::
136 136
137 137 committer = Bugzilla user
138 138
139 139 See also the ``[usermap]`` section.
140 140
141 141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 143 Contains entries of the form ``committer = Bugzilla user``.
144 144
145 145 XMLRPC and REST-API access mode configuration:
146 146
147 147 bugzilla.bzurl
148 148 The base URL for the Bugzilla installation.
149 149 Default ``http://localhost/bugzilla``.
150 150
151 151 bugzilla.user
152 152 The username to use to log into Bugzilla via XMLRPC. Default
153 153 ``bugs``.
154 154
155 155 bugzilla.password
156 156 The password for Bugzilla login.
157 157
158 158 REST-API access mode uses the options listed above as well as:
159 159
160 160 bugzilla.apikey
161 161 An apikey generated on the Bugzilla instance for api access.
162 162 Using an apikey removes the need to store the user and password
163 163 options.
164 164
165 165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 166 and also:
167 167
168 168 bugzilla.bzemail
169 169 The Bugzilla email address.
170 170
171 171 In addition, the Mercurial email settings must be configured. See the
172 172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173 173
174 174 MySQL access mode configuration:
175 175
176 176 bugzilla.host
177 177 Hostname of the MySQL server holding the Bugzilla database.
178 178 Default ``localhost``.
179 179
180 180 bugzilla.db
181 181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182 182
183 183 bugzilla.user
184 184 Username to use to access MySQL server. Default ``bugs``.
185 185
186 186 bugzilla.password
187 187 Password to use to access MySQL server.
188 188
189 189 bugzilla.timeout
190 190 Database connection timeout (seconds). Default 5.
191 191
192 192 bugzilla.bzuser
193 193 Fallback Bugzilla user name to record comments with, if changeset
194 194 committer cannot be found as a Bugzilla user.
195 195
196 196 bugzilla.bzdir
197 197 Bugzilla install directory. Used by default notify. Default
198 198 ``/var/www/html/bugzilla``.
199 199
200 200 bugzilla.notify
201 201 The command to run to get Bugzilla to send bug change notification
202 202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 203 id) and ``user`` (committer bugzilla email). Default depends on
204 204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 205 contrib/sendbugmail.pl %(id)s %(user)s".
206 206
207 207 Activating the extension::
208 208
209 209 [extensions]
210 210 bugzilla =
211 211
212 212 [hooks]
213 213 # run bugzilla hook on every change pulled or pushed in here
214 214 incoming.bugzilla = python:hgext.bugzilla.hook
215 215
216 216 Example configurations:
217 217
218 218 XMLRPC example configuration. This uses the Bugzilla at
219 219 ``http://my-project.org/bugzilla``, logging in as user
220 220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 222 with a web interface at ``http://my-project.org/hg``. ::
223 223
224 224 [bugzilla]
225 225 bzurl=http://my-project.org/bugzilla
226 226 user=bugmail@my-project.org
227 227 password=plugh
228 228 version=xmlrpc
229 229 template=Changeset {node|short} in {root|basename}.
230 230 {hgweb}/{webroot}/rev/{node|short}\\n
231 231 {desc}\\n
232 232 strip=5
233 233
234 234 [web]
235 235 baseurl=http://my-project.org/hg
236 236
237 237 XMLRPC+email example configuration. This uses the Bugzilla at
238 238 ``http://my-project.org/bugzilla``, logging in as user
239 239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 242 are sent to the Bugzilla email address
243 243 ``bugzilla@my-project.org``. ::
244 244
245 245 [bugzilla]
246 246 bzurl=http://my-project.org/bugzilla
247 247 user=bugmail@my-project.org
248 248 password=plugh
249 249 version=xmlrpc+email
250 250 bzemail=bugzilla@my-project.org
251 251 template=Changeset {node|short} in {root|basename}.
252 252 {hgweb}/{webroot}/rev/{node|short}\\n
253 253 {desc}\\n
254 254 strip=5
255 255
256 256 [web]
257 257 baseurl=http://my-project.org/hg
258 258
259 259 [usermap]
260 260 user@emaildomain.com=user.name@bugzilladomain.com
261 261
262 262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 264 the Bugzilla database name is ``bugs`` and MySQL is
265 265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 267 with a web interface at ``http://my-project.org/hg``. ::
268 268
269 269 [bugzilla]
270 270 host=localhost
271 271 password=XYZZY
272 272 version=3.0
273 273 bzuser=unknown@domain.com
274 274 bzdir=/opt/bugzilla-3.2
275 275 template=Changeset {node|short} in {root|basename}.
276 276 {hgweb}/{webroot}/rev/{node|short}\\n
277 277 {desc}\\n
278 278 strip=5
279 279
280 280 [web]
281 281 baseurl=http://my-project.org/hg
282 282
283 283 [usermap]
284 284 user@emaildomain.com=user.name@bugzilladomain.com
285 285
286 286 All the above add a comment to the Bugzilla bug record of the form::
287 287
288 288 Changeset 3b16791d6642 in repository-name.
289 289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290 290
291 291 Changeset commit comment. Bug 1234.
292 292 '''
293 293
294 294 from __future__ import absolute_import
295 295
296 296 import json
297 297 import re
298 298 import time
299 299
300 300 from mercurial.i18n import _
301 301 from mercurial.node import short
302 302 from mercurial import (
303 303 error,
304 304 logcmdutil,
305 305 mail,
306 306 pycompat,
307 307 registrar,
308 308 url,
309 309 util,
310 310 )
311 311 from mercurial.utils import (
312 312 procutil,
313 313 stringutil,
314 314 )
315 315
316 316 xmlrpclib = util.xmlrpclib
317 317
318 318 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
319 319 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
320 320 # be specifying the version(s) of Mercurial they are tested with, or
321 321 # leave the attribute unspecified.
322 322 testedwith = b'ships-with-hg-core'
323 323
324 324 configtable = {}
325 325 configitem = registrar.configitem(configtable)
326 326
327 327 configitem(
328 328 b'bugzilla', b'apikey', default=b'',
329 329 )
330 330 configitem(
331 331 b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla',
332 332 )
333 333 configitem(
334 334 b'bugzilla', b'bzemail', default=None,
335 335 )
336 336 configitem(
337 337 b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/',
338 338 )
339 339 configitem(
340 340 b'bugzilla', b'bzuser', default=None,
341 341 )
342 342 configitem(
343 343 b'bugzilla', b'db', default=b'bugs',
344 344 )
345 345 configitem(
346 346 b'bugzilla',
347 347 b'fixregexp',
348 348 default=(
349 349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
350 350 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
351 351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
352 352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
353 353 ),
354 354 )
355 355 configitem(
356 356 b'bugzilla', b'fixresolution', default=b'FIXED',
357 357 )
358 358 configitem(
359 359 b'bugzilla', b'fixstatus', default=b'RESOLVED',
360 360 )
361 361 configitem(
362 362 b'bugzilla', b'host', default=b'localhost',
363 363 )
364 364 configitem(
365 365 b'bugzilla', b'notify', default=configitem.dynamicdefault,
366 366 )
367 367 configitem(
368 368 b'bugzilla', b'password', default=None,
369 369 )
370 370 configitem(
371 371 b'bugzilla',
372 372 b'regexp',
373 373 default=(
374 374 br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
375 375 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
376 376 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
377 377 ),
378 378 )
379 379 configitem(
380 380 b'bugzilla', b'strip', default=0,
381 381 )
382 382 configitem(
383 383 b'bugzilla', b'style', default=None,
384 384 )
385 385 configitem(
386 386 b'bugzilla', b'template', default=None,
387 387 )
388 388 configitem(
389 389 b'bugzilla', b'timeout', default=5,
390 390 )
391 391 configitem(
392 392 b'bugzilla', b'user', default=b'bugs',
393 393 )
394 394 configitem(
395 395 b'bugzilla', b'usermap', default=None,
396 396 )
397 397 configitem(
398 398 b'bugzilla', b'version', default=None,
399 399 )
400 400
401 401
402 402 class bzaccess(object):
403 403 '''Base class for access to Bugzilla.'''
404 404
405 405 def __init__(self, ui):
406 406 self.ui = ui
407 407 usermap = self.ui.config(b'bugzilla', b'usermap')
408 408 if usermap:
409 409 self.ui.readconfig(usermap, sections=[b'usermap'])
410 410
411 411 def map_committer(self, user):
412 412 '''map name of committer to Bugzilla user name.'''
413 413 for committer, bzuser in self.ui.configitems(b'usermap'):
414 414 if committer.lower() == user.lower():
415 415 return bzuser
416 416 return user
417 417
418 418 # Methods to be implemented by access classes.
419 419 #
420 420 # 'bugs' is a dict keyed on bug id, where values are a dict holding
421 421 # updates to bug state. Recognized dict keys are:
422 422 #
423 423 # 'hours': Value, float containing work hours to be updated.
424 424 # 'fix': If key present, bug is to be marked fixed. Value ignored.
425 425
426 426 def filter_real_bug_ids(self, bugs):
427 427 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
428 428
429 429 def filter_cset_known_bug_ids(self, node, bugs):
430 430 '''remove bug IDs where node occurs in comment text from bugs.'''
431 431
432 432 def updatebug(self, bugid, newstate, text, committer):
433 433 '''update the specified bug. Add comment text and set new states.
434 434
435 435 If possible add the comment as being from the committer of
436 436 the changeset. Otherwise use the default Bugzilla user.
437 437 '''
438 438
439 439 def notify(self, bugs, committer):
440 440 '''Force sending of Bugzilla notification emails.
441 441
442 442 Only required if the access method does not trigger notification
443 443 emails automatically.
444 444 '''
445 445
446 446
447 447 # Bugzilla via direct access to MySQL database.
448 448 class bzmysql(bzaccess):
449 449 '''Support for direct MySQL access to Bugzilla.
450 450
451 451 The earliest Bugzilla version this is tested with is version 2.16.
452 452
453 453 If your Bugzilla is version 3.4 or above, you are strongly
454 454 recommended to use the XMLRPC access method instead.
455 455 '''
456 456
457 457 @staticmethod
458 458 def sql_buglist(ids):
459 459 '''return SQL-friendly list of bug ids'''
460 460 return b'(' + b','.join(map(str, ids)) + b')'
461 461
462 462 _MySQLdb = None
463 463
464 464 def __init__(self, ui):
465 465 try:
466 466 import MySQLdb as mysql
467 467
468 468 bzmysql._MySQLdb = mysql
469 469 except ImportError as err:
470 470 raise error.Abort(
471 471 _(b'python mysql support not available: %s') % err
472 472 )
473 473
474 474 bzaccess.__init__(self, ui)
475 475
476 476 host = self.ui.config(b'bugzilla', b'host')
477 477 user = self.ui.config(b'bugzilla', b'user')
478 478 passwd = self.ui.config(b'bugzilla', b'password')
479 479 db = self.ui.config(b'bugzilla', b'db')
480 480 timeout = int(self.ui.config(b'bugzilla', b'timeout'))
481 481 self.ui.note(
482 482 _(b'connecting to %s:%s as %s, password %s\n')
483 483 % (host, db, user, b'*' * len(passwd))
484 484 )
485 485 self.conn = bzmysql._MySQLdb.connect(
486 486 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
487 487 )
488 488 self.cursor = self.conn.cursor()
489 489 self.longdesc_id = self.get_longdesc_id()
490 490 self.user_ids = {}
491 491 self.default_notify = b"cd %(bzdir)s && ./processmail %(id)s %(user)s"
492 492
493 493 def run(self, *args, **kwargs):
494 494 '''run a query.'''
495 495 self.ui.note(_(b'query: %s %s\n') % (args, kwargs))
496 496 try:
497 497 self.cursor.execute(*args, **kwargs)
498 498 except bzmysql._MySQLdb.MySQLError:
499 499 self.ui.note(_(b'failed query: %s %s\n') % (args, kwargs))
500 500 raise
501 501
502 502 def get_longdesc_id(self):
503 503 '''get identity of longdesc field'''
504 504 self.run(b'select fieldid from fielddefs where name = "longdesc"')
505 505 ids = self.cursor.fetchall()
506 506 if len(ids) != 1:
507 507 raise error.Abort(_(b'unknown database schema'))
508 508 return ids[0][0]
509 509
510 510 def filter_real_bug_ids(self, bugs):
511 511 '''filter not-existing bugs from set.'''
512 512 self.run(
513 513 b'select bug_id from bugs where bug_id in %s'
514 514 % bzmysql.sql_buglist(bugs.keys())
515 515 )
516 516 existing = [id for (id,) in self.cursor.fetchall()]
517 517 for id in bugs.keys():
518 518 if id not in existing:
519 519 self.ui.status(_(b'bug %d does not exist\n') % id)
520 520 del bugs[id]
521 521
522 522 def filter_cset_known_bug_ids(self, node, bugs):
523 523 '''filter bug ids that already refer to this changeset from set.'''
524 524 self.run(
525 525 '''select bug_id from longdescs where
526 526 bug_id in %s and thetext like "%%%s%%"'''
527 527 % (bzmysql.sql_buglist(bugs.keys()), short(node))
528 528 )
529 529 for (id,) in self.cursor.fetchall():
530 530 self.ui.status(
531 531 _(b'bug %d already knows about changeset %s\n')
532 532 % (id, short(node))
533 533 )
534 534 del bugs[id]
535 535
536 536 def notify(self, bugs, committer):
537 537 '''tell bugzilla to send mail.'''
538 538 self.ui.status(_(b'telling bugzilla to send mail:\n'))
539 539 (user, userid) = self.get_bugzilla_user(committer)
540 540 for id in bugs.keys():
541 541 self.ui.status(_(b' bug %s\n') % id)
542 542 cmdfmt = self.ui.config(b'bugzilla', b'notify', self.default_notify)
543 543 bzdir = self.ui.config(b'bugzilla', b'bzdir')
544 544 try:
545 545 # Backwards-compatible with old notify string, which
546 546 # took one string. This will throw with a new format
547 547 # string.
548 548 cmd = cmdfmt % id
549 549 except TypeError:
550 550 cmd = cmdfmt % {b'bzdir': bzdir, b'id': id, b'user': user}
551 551 self.ui.note(_(b'running notify command %s\n') % cmd)
552 552 fp = procutil.popen(b'(%s) 2>&1' % cmd, b'rb')
553 553 out = util.fromnativeeol(fp.read())
554 554 ret = fp.close()
555 555 if ret:
556 556 self.ui.warn(out)
557 557 raise error.Abort(
558 558 _(b'bugzilla notify command %s') % procutil.explainexit(ret)
559 559 )
560 560 self.ui.status(_(b'done\n'))
561 561
562 562 def get_user_id(self, user):
563 563 '''look up numeric bugzilla user id.'''
564 564 try:
565 565 return self.user_ids[user]
566 566 except KeyError:
567 567 try:
568 568 userid = int(user)
569 569 except ValueError:
570 570 self.ui.note(_(b'looking up user %s\n') % user)
571 571 self.run(
572 572 '''select userid from profiles
573 573 where login_name like %s''',
574 574 user,
575 575 )
576 576 all = self.cursor.fetchall()
577 577 if len(all) != 1:
578 578 raise KeyError(user)
579 579 userid = int(all[0][0])
580 580 self.user_ids[user] = userid
581 581 return userid
582 582
583 583 def get_bugzilla_user(self, committer):
584 584 '''See if committer is a registered bugzilla user. Return
585 585 bugzilla username and userid if so. If not, return default
586 586 bugzilla username and userid.'''
587 587 user = self.map_committer(committer)
588 588 try:
589 589 userid = self.get_user_id(user)
590 590 except KeyError:
591 591 try:
592 592 defaultuser = self.ui.config(b'bugzilla', b'bzuser')
593 593 if not defaultuser:
594 594 raise error.Abort(
595 595 _(b'cannot find bugzilla user id for %s') % user
596 596 )
597 597 userid = self.get_user_id(defaultuser)
598 598 user = defaultuser
599 599 except KeyError:
600 600 raise error.Abort(
601 601 _(b'cannot find bugzilla user id for %s or %s')
602 602 % (user, defaultuser)
603 603 )
604 604 return (user, userid)
605 605
606 606 def updatebug(self, bugid, newstate, text, committer):
607 607 '''update bug state with comment text.
608 608
609 609 Try adding comment as committer of changeset, otherwise as
610 610 default bugzilla user.'''
611 611 if len(newstate) > 0:
612 612 self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n"))
613 613
614 614 (user, userid) = self.get_bugzilla_user(committer)
615 615 now = time.strftime('%Y-%m-%d %H:%M:%S')
616 616 self.run(
617 617 '''insert into longdescs
618 618 (bug_id, who, bug_when, thetext)
619 619 values (%s, %s, %s, %s)''',
620 620 (bugid, userid, now, text),
621 621 )
622 622 self.run(
623 623 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
624 624 values (%s, %s, %s, %s)''',
625 625 (bugid, userid, now, self.longdesc_id),
626 626 )
627 627 self.conn.commit()
628 628
629 629
630 630 class bzmysql_2_18(bzmysql):
631 631 '''support for bugzilla 2.18 series.'''
632 632
633 633 def __init__(self, ui):
634 634 bzmysql.__init__(self, ui)
635 635 self.default_notify = (
636 636 b"cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
637 637 )
638 638
639 639
640 640 class bzmysql_3_0(bzmysql_2_18):
641 641 '''support for bugzilla 3.0 series.'''
642 642
643 643 def __init__(self, ui):
644 644 bzmysql_2_18.__init__(self, ui)
645 645
646 646 def get_longdesc_id(self):
647 647 '''get identity of longdesc field'''
648 648 self.run(b'select id from fielddefs where name = "longdesc"')
649 649 ids = self.cursor.fetchall()
650 650 if len(ids) != 1:
651 651 raise error.Abort(_(b'unknown database schema'))
652 652 return ids[0][0]
653 653
654 654
655 655 # Bugzilla via XMLRPC interface.
656 656
657 657
658 658 class cookietransportrequest(object):
659 659 """A Transport request method that retains cookies over its lifetime.
660 660
661 661 The regular xmlrpclib transports ignore cookies. Which causes
662 662 a bit of a problem when you need a cookie-based login, as with
663 663 the Bugzilla XMLRPC interface prior to 4.4.3.
664 664
665 665 So this is a helper for defining a Transport which looks for
666 666 cookies being set in responses and saves them to add to all future
667 667 requests.
668 668 """
669 669
670 670 # Inspiration drawn from
671 671 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
672 672 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
673 673
674 674 cookies = []
675 675
676 676 def send_cookies(self, connection):
677 677 if self.cookies:
678 678 for cookie in self.cookies:
679 679 connection.putheader(b"Cookie", cookie)
680 680
681 681 def request(self, host, handler, request_body, verbose=0):
682 682 self.verbose = verbose
683 683 self.accept_gzip_encoding = False
684 684
685 685 # issue XML-RPC request
686 686 h = self.make_connection(host)
687 687 if verbose:
688 688 h.set_debuglevel(1)
689 689
690 690 self.send_request(h, handler, request_body)
691 691 self.send_host(h, host)
692 692 self.send_cookies(h)
693 693 self.send_user_agent(h)
694 694 self.send_content(h, request_body)
695 695
696 696 # Deal with differences between Python 2.6 and 2.7.
697 697 # In the former h is a HTTP(S). In the latter it's a
698 698 # HTTP(S)Connection. Luckily, the 2.6 implementation of
699 699 # HTTP(S) has an underlying HTTP(S)Connection, so extract
700 700 # that and use it.
701 701 try:
702 702 response = h.getresponse()
703 703 except AttributeError:
704 704 response = h._conn.getresponse()
705 705
706 706 # Add any cookie definitions to our list.
707 707 for header in response.msg.getallmatchingheaders(b"Set-Cookie"):
708 708 val = header.split(b": ", 1)[1]
709 709 cookie = val.split(b";", 1)[0]
710 710 self.cookies.append(cookie)
711 711
712 712 if response.status != 200:
713 713 raise xmlrpclib.ProtocolError(
714 714 host + handler,
715 715 response.status,
716 716 response.reason,
717 717 response.msg.headers,
718 718 )
719 719
720 720 payload = response.read()
721 721 parser, unmarshaller = self.getparser()
722 722 parser.feed(payload)
723 723 parser.close()
724 724
725 725 return unmarshaller.close()
726 726
727 727
728 728 # The explicit calls to the underlying xmlrpclib __init__() methods are
729 729 # necessary. The xmlrpclib.Transport classes are old-style classes, and
730 730 # it turns out their __init__() doesn't get called when doing multiple
731 731 # inheritance with a new-style class.
732 732 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
733 733 def __init__(self, use_datetime=0):
734 734 if util.safehasattr(xmlrpclib.Transport, "__init__"):
735 735 xmlrpclib.Transport.__init__(self, use_datetime)
736 736
737 737
738 738 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
739 739 def __init__(self, use_datetime=0):
740 740 if util.safehasattr(xmlrpclib.Transport, "__init__"):
741 741 xmlrpclib.SafeTransport.__init__(self, use_datetime)
742 742
743 743
744 744 class bzxmlrpc(bzaccess):
745 745 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
746 746
747 747 Requires a minimum Bugzilla version 3.4.
748 748 """
749 749
750 750 def __init__(self, ui):
751 751 bzaccess.__init__(self, ui)
752 752
753 753 bzweb = self.ui.config(b'bugzilla', b'bzurl')
754 754 bzweb = bzweb.rstrip(b"/") + b"/xmlrpc.cgi"
755 755
756 756 user = self.ui.config(b'bugzilla', b'user')
757 757 passwd = self.ui.config(b'bugzilla', b'password')
758 758
759 759 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
760 760 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
761 761
762 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
762 self.bzproxy = xmlrpclib.ServerProxy(
763 pycompat.strurl(bzweb), self.transport(bzweb)
764 )
763 765 ver = self.bzproxy.Bugzilla.version()[b'version'].split(b'.')
764 766 self.bzvermajor = int(ver[0])
765 767 self.bzverminor = int(ver[1])
766 768 login = self.bzproxy.User.login(
767 769 {b'login': user, b'password': passwd, b'restrict_login': True}
768 770 )
769 771 self.bztoken = login.get(b'token', b'')
770 772
771 773 def transport(self, uri):
772 774 if util.urlreq.urlparse(uri, b"http")[0] == b"https":
773 775 return cookiesafetransport()
774 776 else:
775 777 return cookietransport()
776 778
777 779 def get_bug_comments(self, id):
778 780 """Return a string with all comment text for a bug."""
779 781 c = self.bzproxy.Bug.comments(
780 782 {b'ids': [id], b'include_fields': [b'text'], b'token': self.bztoken}
781 783 )
782 784 return b''.join(
783 785 [t[b'text'] for t in c[b'bugs'][b'%d' % id][b'comments']]
784 786 )
785 787
786 788 def filter_real_bug_ids(self, bugs):
787 789 probe = self.bzproxy.Bug.get(
788 790 {
789 791 b'ids': sorted(bugs.keys()),
790 792 b'include_fields': [],
791 793 b'permissive': True,
792 794 b'token': self.bztoken,
793 795 }
794 796 )
795 797 for badbug in probe[b'faults']:
796 798 id = badbug[b'id']
797 799 self.ui.status(_(b'bug %d does not exist\n') % id)
798 800 del bugs[id]
799 801
800 802 def filter_cset_known_bug_ids(self, node, bugs):
801 803 for id in sorted(bugs.keys()):
802 804 if self.get_bug_comments(id).find(short(node)) != -1:
803 805 self.ui.status(
804 806 _(b'bug %d already knows about changeset %s\n')
805 807 % (id, short(node))
806 808 )
807 809 del bugs[id]
808 810
809 811 def updatebug(self, bugid, newstate, text, committer):
810 812 args = {}
811 813 if b'hours' in newstate:
812 814 args[b'work_time'] = newstate[b'hours']
813 815
814 816 if self.bzvermajor >= 4:
815 817 args[b'ids'] = [bugid]
816 818 args[b'comment'] = {b'body': text}
817 819 if b'fix' in newstate:
818 820 args[b'status'] = self.fixstatus
819 821 args[b'resolution'] = self.fixresolution
820 822 args[b'token'] = self.bztoken
821 823 self.bzproxy.Bug.update(args)
822 824 else:
823 825 if b'fix' in newstate:
824 826 self.ui.warn(
825 827 _(
826 828 b"Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
827 829 b"to mark bugs fixed\n"
828 830 )
829 831 )
830 832 args[b'id'] = bugid
831 833 args[b'comment'] = text
832 834 self.bzproxy.Bug.add_comment(args)
833 835
834 836
835 837 class bzxmlrpcemail(bzxmlrpc):
836 838 """Read data from Bugzilla via XMLRPC, send updates via email.
837 839
838 840 Advantages of sending updates via email:
839 841 1. Comments can be added as any user, not just logged in user.
840 842 2. Bug statuses or other fields not accessible via XMLRPC can
841 843 potentially be updated.
842 844
843 845 There is no XMLRPC function to change bug status before Bugzilla
844 846 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
845 847 But bugs can be marked fixed via email from 3.4 onwards.
846 848 """
847 849
848 850 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
849 851 # in-email fields are specified as '@<fieldname> = <value>'. In
850 852 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
851 853 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
852 854 # compatibility, but rather than rely on this use the new format for
853 855 # 4.0 onwards.
854 856
855 857 def __init__(self, ui):
856 858 bzxmlrpc.__init__(self, ui)
857 859
858 860 self.bzemail = self.ui.config(b'bugzilla', b'bzemail')
859 861 if not self.bzemail:
860 862 raise error.Abort(_(b"configuration 'bzemail' missing"))
861 863 mail.validateconfig(self.ui)
862 864
863 865 def makecommandline(self, fieldname, value):
864 866 if self.bzvermajor >= 4:
865 867 return b"@%s %s" % (fieldname, pycompat.bytestr(value))
866 868 else:
867 869 if fieldname == b"id":
868 870 fieldname = b"bug_id"
869 871 return b"@%s = %s" % (fieldname, pycompat.bytestr(value))
870 872
871 873 def send_bug_modify_email(self, bugid, commands, comment, committer):
872 874 '''send modification message to Bugzilla bug via email.
873 875
874 876 The message format is documented in the Bugzilla email_in.pl
875 877 specification. commands is a list of command lines, comment is the
876 878 comment text.
877 879
878 880 To stop users from crafting commit comments with
879 881 Bugzilla commands, specify the bug ID via the message body, rather
880 882 than the subject line, and leave a blank line after it.
881 883 '''
882 884 user = self.map_committer(committer)
883 885 matches = self.bzproxy.User.get(
884 886 {b'match': [user], b'token': self.bztoken}
885 887 )
886 888 if not matches[b'users']:
887 889 user = self.ui.config(b'bugzilla', b'user')
888 890 matches = self.bzproxy.User.get(
889 891 {b'match': [user], b'token': self.bztoken}
890 892 )
891 893 if not matches[b'users']:
892 894 raise error.Abort(
893 895 _(b"default bugzilla user %s email not found") % user
894 896 )
895 897 user = matches[b'users'][0][b'email']
896 898 commands.append(self.makecommandline(b"id", bugid))
897 899
898 900 text = b"\n".join(commands) + b"\n\n" + comment
899 901
900 902 _charsets = mail._charsets(self.ui)
901 903 user = mail.addressencode(self.ui, user, _charsets)
902 904 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
903 905 msg = mail.mimeencode(self.ui, text, _charsets)
904 906 msg[b'From'] = user
905 907 msg[b'To'] = bzemail
906 908 msg[b'Subject'] = mail.headencode(
907 909 self.ui, b"Bug modification", _charsets
908 910 )
909 911 sendmail = mail.connect(self.ui)
910 912 sendmail(user, bzemail, msg.as_string())
911 913
912 914 def updatebug(self, bugid, newstate, text, committer):
913 915 cmds = []
914 916 if b'hours' in newstate:
915 917 cmds.append(self.makecommandline(b"work_time", newstate[b'hours']))
916 918 if b'fix' in newstate:
917 919 cmds.append(self.makecommandline(b"bug_status", self.fixstatus))
918 920 cmds.append(self.makecommandline(b"resolution", self.fixresolution))
919 921 self.send_bug_modify_email(bugid, cmds, text, committer)
920 922
921 923
922 924 class NotFound(LookupError):
923 925 pass
924 926
925 927
926 928 class bzrestapi(bzaccess):
927 929 """Read and write bugzilla data using the REST API available since
928 930 Bugzilla 5.0.
929 931 """
930 932
931 933 def __init__(self, ui):
932 934 bzaccess.__init__(self, ui)
933 935 bz = self.ui.config(b'bugzilla', b'bzurl')
934 936 self.bzroot = b'/'.join([bz, b'rest'])
935 937 self.apikey = self.ui.config(b'bugzilla', b'apikey')
936 938 self.user = self.ui.config(b'bugzilla', b'user')
937 939 self.passwd = self.ui.config(b'bugzilla', b'password')
938 940 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
939 941 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
940 942
941 943 def apiurl(self, targets, include_fields=None):
942 944 url = b'/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets])
943 945 qv = {}
944 946 if self.apikey:
945 947 qv[b'api_key'] = self.apikey
946 948 elif self.user and self.passwd:
947 949 qv[b'login'] = self.user
948 950 qv[b'password'] = self.passwd
949 951 if include_fields:
950 952 qv[b'include_fields'] = include_fields
951 953 if qv:
952 954 url = b'%s?%s' % (url, util.urlreq.urlencode(qv))
953 955 return url
954 956
955 957 def _fetch(self, burl):
956 958 try:
957 959 resp = url.open(self.ui, burl)
958 960 return pycompat.json_loads(resp.read())
959 961 except util.urlerr.httperror as inst:
960 962 if inst.code == 401:
961 963 raise error.Abort(_(b'authorization failed'))
962 964 if inst.code == 404:
963 965 raise NotFound()
964 966 else:
965 967 raise
966 968
967 969 def _submit(self, burl, data, method=b'POST'):
968 970 data = json.dumps(data)
969 971 if method == b'PUT':
970 972
971 973 class putrequest(util.urlreq.request):
972 974 def get_method(self):
973 975 return b'PUT'
974 976
975 977 request_type = putrequest
976 978 else:
977 979 request_type = util.urlreq.request
978 980 req = request_type(burl, data, {b'Content-Type': b'application/json'})
979 981 try:
980 982 resp = url.opener(self.ui).open(req)
981 983 return pycompat.json_loads(resp.read())
982 984 except util.urlerr.httperror as inst:
983 985 if inst.code == 401:
984 986 raise error.Abort(_(b'authorization failed'))
985 987 if inst.code == 404:
986 988 raise NotFound()
987 989 else:
988 990 raise
989 991
990 992 def filter_real_bug_ids(self, bugs):
991 993 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
992 994 badbugs = set()
993 995 for bugid in bugs:
994 996 burl = self.apiurl((b'bug', bugid), include_fields=b'status')
995 997 try:
996 998 self._fetch(burl)
997 999 except NotFound:
998 1000 badbugs.add(bugid)
999 1001 for bugid in badbugs:
1000 1002 del bugs[bugid]
1001 1003
1002 1004 def filter_cset_known_bug_ids(self, node, bugs):
1003 1005 '''remove bug IDs where node occurs in comment text from bugs.'''
1004 1006 sn = short(node)
1005 1007 for bugid in bugs.keys():
1006 1008 burl = self.apiurl(
1007 1009 (b'bug', bugid, b'comment'), include_fields=b'text'
1008 1010 )
1009 1011 result = self._fetch(burl)
1010 1012 comments = result[b'bugs'][pycompat.bytestr(bugid)][b'comments']
1011 1013 if any(sn in c[b'text'] for c in comments):
1012 1014 self.ui.status(
1013 1015 _(b'bug %d already knows about changeset %s\n')
1014 1016 % (bugid, sn)
1015 1017 )
1016 1018 del bugs[bugid]
1017 1019
1018 1020 def updatebug(self, bugid, newstate, text, committer):
1019 1021 '''update the specified bug. Add comment text and set new states.
1020 1022
1021 1023 If possible add the comment as being from the committer of
1022 1024 the changeset. Otherwise use the default Bugzilla user.
1023 1025 '''
1024 1026 bugmod = {}
1025 1027 if b'hours' in newstate:
1026 1028 bugmod[b'work_time'] = newstate[b'hours']
1027 1029 if b'fix' in newstate:
1028 1030 bugmod[b'status'] = self.fixstatus
1029 1031 bugmod[b'resolution'] = self.fixresolution
1030 1032 if bugmod:
1031 1033 # if we have to change the bugs state do it here
1032 1034 bugmod[b'comment'] = {
1033 1035 b'comment': text,
1034 1036 b'is_private': False,
1035 1037 b'is_markdown': False,
1036 1038 }
1037 1039 burl = self.apiurl((b'bug', bugid))
1038 1040 self._submit(burl, bugmod, method=b'PUT')
1039 1041 self.ui.debug(b'updated bug %s\n' % bugid)
1040 1042 else:
1041 1043 burl = self.apiurl((b'bug', bugid, b'comment'))
1042 1044 self._submit(
1043 1045 burl,
1044 1046 {
1045 1047 b'comment': text,
1046 1048 b'is_private': False,
1047 1049 b'is_markdown': False,
1048 1050 },
1049 1051 )
1050 1052 self.ui.debug(b'added comment to bug %s\n' % bugid)
1051 1053
1052 1054 def notify(self, bugs, committer):
1053 1055 '''Force sending of Bugzilla notification emails.
1054 1056
1055 1057 Only required if the access method does not trigger notification
1056 1058 emails automatically.
1057 1059 '''
1058 1060 pass
1059 1061
1060 1062
1061 1063 class bugzilla(object):
1062 1064 # supported versions of bugzilla. different versions have
1063 1065 # different schemas.
1064 1066 _versions = {
1065 1067 b'2.16': bzmysql,
1066 1068 b'2.18': bzmysql_2_18,
1067 1069 b'3.0': bzmysql_3_0,
1068 1070 b'xmlrpc': bzxmlrpc,
1069 1071 b'xmlrpc+email': bzxmlrpcemail,
1070 1072 b'restapi': bzrestapi,
1071 1073 }
1072 1074
1073 1075 def __init__(self, ui, repo):
1074 1076 self.ui = ui
1075 1077 self.repo = repo
1076 1078
1077 1079 bzversion = self.ui.config(b'bugzilla', b'version')
1078 1080 try:
1079 1081 bzclass = bugzilla._versions[bzversion]
1080 1082 except KeyError:
1081 1083 raise error.Abort(
1082 1084 _(b'bugzilla version %s not supported') % bzversion
1083 1085 )
1084 1086 self.bzdriver = bzclass(self.ui)
1085 1087
1086 1088 self.bug_re = re.compile(
1087 1089 self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE
1088 1090 )
1089 1091 self.fix_re = re.compile(
1090 1092 self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE
1091 1093 )
1092 1094 self.split_re = re.compile(br'\D+')
1093 1095
1094 1096 def find_bugs(self, ctx):
1095 1097 '''return bugs dictionary created from commit comment.
1096 1098
1097 1099 Extract bug info from changeset comments. Filter out any that are
1098 1100 not known to Bugzilla, and any that already have a reference to
1099 1101 the given changeset in their comments.
1100 1102 '''
1101 1103 start = 0
1102 1104 bugs = {}
1103 1105 bugmatch = self.bug_re.search(ctx.description(), start)
1104 1106 fixmatch = self.fix_re.search(ctx.description(), start)
1105 1107 while True:
1106 1108 bugattribs = {}
1107 1109 if not bugmatch and not fixmatch:
1108 1110 break
1109 1111 if not bugmatch:
1110 1112 m = fixmatch
1111 1113 elif not fixmatch:
1112 1114 m = bugmatch
1113 1115 else:
1114 1116 if bugmatch.start() < fixmatch.start():
1115 1117 m = bugmatch
1116 1118 else:
1117 1119 m = fixmatch
1118 1120 start = m.end()
1119 1121 if m is bugmatch:
1120 1122 bugmatch = self.bug_re.search(ctx.description(), start)
1121 1123 if b'fix' in bugattribs:
1122 1124 del bugattribs[b'fix']
1123 1125 else:
1124 1126 fixmatch = self.fix_re.search(ctx.description(), start)
1125 1127 bugattribs[b'fix'] = None
1126 1128
1127 1129 try:
1128 1130 ids = m.group(b'ids')
1129 1131 except IndexError:
1130 1132 ids = m.group(1)
1131 1133 try:
1132 1134 hours = float(m.group(b'hours'))
1133 1135 bugattribs[b'hours'] = hours
1134 1136 except IndexError:
1135 1137 pass
1136 1138 except TypeError:
1137 1139 pass
1138 1140 except ValueError:
1139 1141 self.ui.status(_(b"%s: invalid hours\n") % m.group(b'hours'))
1140 1142
1141 1143 for id in self.split_re.split(ids):
1142 1144 if not id:
1143 1145 continue
1144 1146 bugs[int(id)] = bugattribs
1145 1147 if bugs:
1146 1148 self.bzdriver.filter_real_bug_ids(bugs)
1147 1149 if bugs:
1148 1150 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1149 1151 return bugs
1150 1152
1151 1153 def update(self, bugid, newstate, ctx):
1152 1154 '''update bugzilla bug with reference to changeset.'''
1153 1155
1154 1156 def webroot(root):
1155 1157 '''strip leading prefix of repo root and turn into
1156 1158 url-safe path.'''
1157 1159 count = int(self.ui.config(b'bugzilla', b'strip'))
1158 1160 root = util.pconvert(root)
1159 1161 while count > 0:
1160 1162 c = root.find(b'/')
1161 1163 if c == -1:
1162 1164 break
1163 1165 root = root[c + 1 :]
1164 1166 count -= 1
1165 1167 return root
1166 1168
1167 1169 mapfile = None
1168 1170 tmpl = self.ui.config(b'bugzilla', b'template')
1169 1171 if not tmpl:
1170 1172 mapfile = self.ui.config(b'bugzilla', b'style')
1171 1173 if not mapfile and not tmpl:
1172 1174 tmpl = _(
1173 1175 b'changeset {node|short} in repo {root} refers '
1174 1176 b'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1175 1177 )
1176 1178 spec = logcmdutil.templatespec(tmpl, mapfile)
1177 1179 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1178 1180 self.ui.pushbuffer()
1179 1181 t.show(
1180 1182 ctx,
1181 1183 changes=ctx.changeset(),
1182 1184 bug=pycompat.bytestr(bugid),
1183 1185 hgweb=self.ui.config(b'web', b'baseurl'),
1184 1186 root=self.repo.root,
1185 1187 webroot=webroot(self.repo.root),
1186 1188 )
1187 1189 data = self.ui.popbuffer()
1188 1190 self.bzdriver.updatebug(
1189 1191 bugid, newstate, data, stringutil.email(ctx.user())
1190 1192 )
1191 1193
1192 1194 def notify(self, bugs, committer):
1193 1195 '''ensure Bugzilla users are notified of bug change.'''
1194 1196 self.bzdriver.notify(bugs, committer)
1195 1197
1196 1198
1197 1199 def hook(ui, repo, hooktype, node=None, **kwargs):
1198 1200 '''add comment to bugzilla for each changeset that refers to a
1199 1201 bugzilla bug id. only add a comment once per bug, so same change
1200 1202 seen multiple times does not fill bug with duplicate data.'''
1201 1203 if node is None:
1202 1204 raise error.Abort(
1203 1205 _(b'hook type %s does not pass a changeset id') % hooktype
1204 1206 )
1205 1207 try:
1206 1208 bz = bugzilla(ui, repo)
1207 1209 ctx = repo[node]
1208 1210 bugs = bz.find_bugs(ctx)
1209 1211 if bugs:
1210 1212 for bug in bugs:
1211 1213 bz.update(bug, bugs[bug], ctx)
1212 1214 bz.notify(bugs, stringutil.email(ctx.user()))
1213 1215 except Exception as e:
1214 1216 raise error.Abort(_(b'Bugzilla error: %s') % stringutil.forcebytestr(e))
General Comments 0
You need to be logged in to leave comments. Login now