317 |
317 |
318 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
318 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
319 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
319 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
320 # be specifying the version(s) of Mercurial they are tested with, or |
320 # be specifying the version(s) of Mercurial they are tested with, or |
321 # leave the attribute unspecified. |
321 # leave the attribute unspecified. |
322 testedwith = 'ships-with-hg-core' |
322 testedwith = b'ships-with-hg-core' |
323 |
323 |
324 configtable = {} |
324 configtable = {} |
325 configitem = registrar.configitem(configtable) |
325 configitem = registrar.configitem(configtable) |
326 |
326 |
327 configitem( |
327 configitem( |
328 'bugzilla', 'apikey', default='', |
328 b'bugzilla', b'apikey', default=b'', |
329 ) |
329 ) |
330 configitem( |
330 configitem( |
331 'bugzilla', 'bzdir', default='/var/www/html/bugzilla', |
331 b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla', |
332 ) |
332 ) |
333 configitem( |
333 configitem( |
334 'bugzilla', 'bzemail', default=None, |
334 b'bugzilla', b'bzemail', default=None, |
335 ) |
335 ) |
336 configitem( |
336 configitem( |
337 'bugzilla', 'bzurl', default='http://localhost/bugzilla/', |
337 b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/', |
338 ) |
338 ) |
339 configitem( |
339 configitem( |
340 'bugzilla', 'bzuser', default=None, |
340 b'bugzilla', b'bzuser', default=None, |
341 ) |
341 ) |
342 configitem( |
342 configitem( |
343 'bugzilla', 'db', default='bugs', |
343 b'bugzilla', b'db', default=b'bugs', |
344 ) |
344 ) |
345 configitem( |
345 configitem( |
346 'bugzilla', |
346 b'bugzilla', |
347 'fixregexp', |
347 b'fixregexp', |
348 default=( |
348 default=( |
349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*' |
349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*' |
350 br'(?:nos?\.?|num(?:ber)?s?)?\s*' |
350 br'(?:nos?\.?|num(?:ber)?s?)?\s*' |
351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)' |
351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)' |
352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' |
352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' |
353 ), |
353 ), |
354 ) |
354 ) |
355 configitem( |
355 configitem( |
356 'bugzilla', 'fixresolution', default='FIXED', |
356 b'bugzilla', b'fixresolution', default=b'FIXED', |
357 ) |
357 ) |
358 configitem( |
358 configitem( |
359 'bugzilla', 'fixstatus', default='RESOLVED', |
359 b'bugzilla', b'fixstatus', default=b'RESOLVED', |
360 ) |
360 ) |
361 configitem( |
361 configitem( |
362 'bugzilla', 'host', default='localhost', |
362 b'bugzilla', b'host', default=b'localhost', |
363 ) |
363 ) |
364 configitem( |
364 configitem( |
365 'bugzilla', 'notify', default=configitem.dynamicdefault, |
365 b'bugzilla', b'notify', default=configitem.dynamicdefault, |
366 ) |
366 ) |
367 configitem( |
367 configitem( |
368 'bugzilla', 'password', default=None, |
368 b'bugzilla', b'password', default=None, |
369 ) |
369 ) |
370 configitem( |
370 configitem( |
371 'bugzilla', |
371 b'bugzilla', |
372 'regexp', |
372 b'regexp', |
373 default=( |
373 default=( |
374 br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' |
374 br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' |
375 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)' |
375 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)' |
376 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' |
376 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' |
377 ), |
377 ), |
378 ) |
378 ) |
379 configitem( |
379 configitem( |
380 'bugzilla', 'strip', default=0, |
380 b'bugzilla', b'strip', default=0, |
381 ) |
381 ) |
382 configitem( |
382 configitem( |
383 'bugzilla', 'style', default=None, |
383 b'bugzilla', b'style', default=None, |
384 ) |
384 ) |
385 configitem( |
385 configitem( |
386 'bugzilla', 'template', default=None, |
386 b'bugzilla', b'template', default=None, |
387 ) |
387 ) |
388 configitem( |
388 configitem( |
389 'bugzilla', 'timeout', default=5, |
389 b'bugzilla', b'timeout', default=5, |
390 ) |
390 ) |
391 configitem( |
391 configitem( |
392 'bugzilla', 'user', default='bugs', |
392 b'bugzilla', b'user', default=b'bugs', |
393 ) |
393 ) |
394 configitem( |
394 configitem( |
395 'bugzilla', 'usermap', default=None, |
395 b'bugzilla', b'usermap', default=None, |
396 ) |
396 ) |
397 configitem( |
397 configitem( |
398 'bugzilla', 'version', default=None, |
398 b'bugzilla', b'version', default=None, |
399 ) |
399 ) |
400 |
400 |
401 |
401 |
402 class bzaccess(object): |
402 class bzaccess(object): |
403 '''Base class for access to Bugzilla.''' |
403 '''Base class for access to Bugzilla.''' |
404 |
404 |
405 def __init__(self, ui): |
405 def __init__(self, ui): |
406 self.ui = ui |
406 self.ui = ui |
407 usermap = self.ui.config('bugzilla', 'usermap') |
407 usermap = self.ui.config(b'bugzilla', b'usermap') |
408 if usermap: |
408 if usermap: |
409 self.ui.readconfig(usermap, sections=['usermap']) |
409 self.ui.readconfig(usermap, sections=[b'usermap']) |
410 |
410 |
411 def map_committer(self, user): |
411 def map_committer(self, user): |
412 '''map name of committer to Bugzilla user name.''' |
412 '''map name of committer to Bugzilla user name.''' |
413 for committer, bzuser in self.ui.configitems('usermap'): |
413 for committer, bzuser in self.ui.configitems(b'usermap'): |
414 if committer.lower() == user.lower(): |
414 if committer.lower() == user.lower(): |
415 return bzuser |
415 return bzuser |
416 return user |
416 return user |
417 |
417 |
418 # Methods to be implemented by access classes. |
418 # Methods to be implemented by access classes. |
455 ''' |
455 ''' |
456 |
456 |
457 @staticmethod |
457 @staticmethod |
458 def sql_buglist(ids): |
458 def sql_buglist(ids): |
459 '''return SQL-friendly list of bug ids''' |
459 '''return SQL-friendly list of bug ids''' |
460 return '(' + ','.join(map(str, ids)) + ')' |
460 return b'(' + b','.join(map(str, ids)) + b')' |
461 |
461 |
462 _MySQLdb = None |
462 _MySQLdb = None |
463 |
463 |
464 def __init__(self, ui): |
464 def __init__(self, ui): |
465 try: |
465 try: |
466 import MySQLdb as mysql |
466 import MySQLdb as mysql |
467 |
467 |
468 bzmysql._MySQLdb = mysql |
468 bzmysql._MySQLdb = mysql |
469 except ImportError as err: |
469 except ImportError as err: |
470 raise error.Abort(_('python mysql support not available: %s') % err) |
470 raise error.Abort( |
|
471 _(b'python mysql support not available: %s') % err |
|
472 ) |
471 |
473 |
472 bzaccess.__init__(self, ui) |
474 bzaccess.__init__(self, ui) |
473 |
475 |
474 host = self.ui.config('bugzilla', 'host') |
476 host = self.ui.config(b'bugzilla', b'host') |
475 user = self.ui.config('bugzilla', 'user') |
477 user = self.ui.config(b'bugzilla', b'user') |
476 passwd = self.ui.config('bugzilla', 'password') |
478 passwd = self.ui.config(b'bugzilla', b'password') |
477 db = self.ui.config('bugzilla', 'db') |
479 db = self.ui.config(b'bugzilla', b'db') |
478 timeout = int(self.ui.config('bugzilla', 'timeout')) |
480 timeout = int(self.ui.config(b'bugzilla', b'timeout')) |
479 self.ui.note( |
481 self.ui.note( |
480 _('connecting to %s:%s as %s, password %s\n') |
482 _(b'connecting to %s:%s as %s, password %s\n') |
481 % (host, db, user, '*' * len(passwd)) |
483 % (host, db, user, b'*' * len(passwd)) |
482 ) |
484 ) |
483 self.conn = bzmysql._MySQLdb.connect( |
485 self.conn = bzmysql._MySQLdb.connect( |
484 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout |
486 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout |
485 ) |
487 ) |
486 self.cursor = self.conn.cursor() |
488 self.cursor = self.conn.cursor() |
487 self.longdesc_id = self.get_longdesc_id() |
489 self.longdesc_id = self.get_longdesc_id() |
488 self.user_ids = {} |
490 self.user_ids = {} |
489 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s" |
491 self.default_notify = b"cd %(bzdir)s && ./processmail %(id)s %(user)s" |
490 |
492 |
491 def run(self, *args, **kwargs): |
493 def run(self, *args, **kwargs): |
492 '''run a query.''' |
494 '''run a query.''' |
493 self.ui.note(_('query: %s %s\n') % (args, kwargs)) |
495 self.ui.note(_(b'query: %s %s\n') % (args, kwargs)) |
494 try: |
496 try: |
495 self.cursor.execute(*args, **kwargs) |
497 self.cursor.execute(*args, **kwargs) |
496 except bzmysql._MySQLdb.MySQLError: |
498 except bzmysql._MySQLdb.MySQLError: |
497 self.ui.note(_('failed query: %s %s\n') % (args, kwargs)) |
499 self.ui.note(_(b'failed query: %s %s\n') % (args, kwargs)) |
498 raise |
500 raise |
499 |
501 |
500 def get_longdesc_id(self): |
502 def get_longdesc_id(self): |
501 '''get identity of longdesc field''' |
503 '''get identity of longdesc field''' |
502 self.run('select fieldid from fielddefs where name = "longdesc"') |
504 self.run(b'select fieldid from fielddefs where name = "longdesc"') |
503 ids = self.cursor.fetchall() |
505 ids = self.cursor.fetchall() |
504 if len(ids) != 1: |
506 if len(ids) != 1: |
505 raise error.Abort(_('unknown database schema')) |
507 raise error.Abort(_(b'unknown database schema')) |
506 return ids[0][0] |
508 return ids[0][0] |
507 |
509 |
508 def filter_real_bug_ids(self, bugs): |
510 def filter_real_bug_ids(self, bugs): |
509 '''filter not-existing bugs from set.''' |
511 '''filter not-existing bugs from set.''' |
510 self.run( |
512 self.run( |
511 'select bug_id from bugs where bug_id in %s' |
513 b'select bug_id from bugs where bug_id in %s' |
512 % bzmysql.sql_buglist(bugs.keys()) |
514 % bzmysql.sql_buglist(bugs.keys()) |
513 ) |
515 ) |
514 existing = [id for (id,) in self.cursor.fetchall()] |
516 existing = [id for (id,) in self.cursor.fetchall()] |
515 for id in bugs.keys(): |
517 for id in bugs.keys(): |
516 if id not in existing: |
518 if id not in existing: |
517 self.ui.status(_('bug %d does not exist\n') % id) |
519 self.ui.status(_(b'bug %d does not exist\n') % id) |
518 del bugs[id] |
520 del bugs[id] |
519 |
521 |
520 def filter_cset_known_bug_ids(self, node, bugs): |
522 def filter_cset_known_bug_ids(self, node, bugs): |
521 '''filter bug ids that already refer to this changeset from set.''' |
523 '''filter bug ids that already refer to this changeset from set.''' |
522 self.run( |
524 self.run( |
524 bug_id in %s and thetext like "%%%s%%"''' |
526 bug_id in %s and thetext like "%%%s%%"''' |
525 % (bzmysql.sql_buglist(bugs.keys()), short(node)) |
527 % (bzmysql.sql_buglist(bugs.keys()), short(node)) |
526 ) |
528 ) |
527 for (id,) in self.cursor.fetchall(): |
529 for (id,) in self.cursor.fetchall(): |
528 self.ui.status( |
530 self.ui.status( |
529 _('bug %d already knows about changeset %s\n') |
531 _(b'bug %d already knows about changeset %s\n') |
530 % (id, short(node)) |
532 % (id, short(node)) |
531 ) |
533 ) |
532 del bugs[id] |
534 del bugs[id] |
533 |
535 |
534 def notify(self, bugs, committer): |
536 def notify(self, bugs, committer): |
535 '''tell bugzilla to send mail.''' |
537 '''tell bugzilla to send mail.''' |
536 self.ui.status(_('telling bugzilla to send mail:\n')) |
538 self.ui.status(_(b'telling bugzilla to send mail:\n')) |
537 (user, userid) = self.get_bugzilla_user(committer) |
539 (user, userid) = self.get_bugzilla_user(committer) |
538 for id in bugs.keys(): |
540 for id in bugs.keys(): |
539 self.ui.status(_(' bug %s\n') % id) |
541 self.ui.status(_(b' bug %s\n') % id) |
540 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify) |
542 cmdfmt = self.ui.config(b'bugzilla', b'notify', self.default_notify) |
541 bzdir = self.ui.config('bugzilla', 'bzdir') |
543 bzdir = self.ui.config(b'bugzilla', b'bzdir') |
542 try: |
544 try: |
543 # Backwards-compatible with old notify string, which |
545 # Backwards-compatible with old notify string, which |
544 # took one string. This will throw with a new format |
546 # took one string. This will throw with a new format |
545 # string. |
547 # string. |
546 cmd = cmdfmt % id |
548 cmd = cmdfmt % id |
547 except TypeError: |
549 except TypeError: |
548 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user} |
550 cmd = cmdfmt % {b'bzdir': bzdir, b'id': id, b'user': user} |
549 self.ui.note(_('running notify command %s\n') % cmd) |
551 self.ui.note(_(b'running notify command %s\n') % cmd) |
550 fp = procutil.popen('(%s) 2>&1' % cmd, 'rb') |
552 fp = procutil.popen(b'(%s) 2>&1' % cmd, b'rb') |
551 out = util.fromnativeeol(fp.read()) |
553 out = util.fromnativeeol(fp.read()) |
552 ret = fp.close() |
554 ret = fp.close() |
553 if ret: |
555 if ret: |
554 self.ui.warn(out) |
556 self.ui.warn(out) |
555 raise error.Abort( |
557 raise error.Abort( |
556 _('bugzilla notify command %s') % procutil.explainexit(ret) |
558 _(b'bugzilla notify command %s') % procutil.explainexit(ret) |
557 ) |
559 ) |
558 self.ui.status(_('done\n')) |
560 self.ui.status(_(b'done\n')) |
559 |
561 |
560 def get_user_id(self, user): |
562 def get_user_id(self, user): |
561 '''look up numeric bugzilla user id.''' |
563 '''look up numeric bugzilla user id.''' |
562 try: |
564 try: |
563 return self.user_ids[user] |
565 return self.user_ids[user] |
564 except KeyError: |
566 except KeyError: |
565 try: |
567 try: |
566 userid = int(user) |
568 userid = int(user) |
567 except ValueError: |
569 except ValueError: |
568 self.ui.note(_('looking up user %s\n') % user) |
570 self.ui.note(_(b'looking up user %s\n') % user) |
569 self.run( |
571 self.run( |
570 '''select userid from profiles |
572 '''select userid from profiles |
571 where login_name like %s''', |
573 where login_name like %s''', |
572 user, |
574 user, |
573 ) |
575 ) |
585 user = self.map_committer(committer) |
587 user = self.map_committer(committer) |
586 try: |
588 try: |
587 userid = self.get_user_id(user) |
589 userid = self.get_user_id(user) |
588 except KeyError: |
590 except KeyError: |
589 try: |
591 try: |
590 defaultuser = self.ui.config('bugzilla', 'bzuser') |
592 defaultuser = self.ui.config(b'bugzilla', b'bzuser') |
591 if not defaultuser: |
593 if not defaultuser: |
592 raise error.Abort( |
594 raise error.Abort( |
593 _('cannot find bugzilla user id for %s') % user |
595 _(b'cannot find bugzilla user id for %s') % user |
594 ) |
596 ) |
595 userid = self.get_user_id(defaultuser) |
597 userid = self.get_user_id(defaultuser) |
596 user = defaultuser |
598 user = defaultuser |
597 except KeyError: |
599 except KeyError: |
598 raise error.Abort( |
600 raise error.Abort( |
599 _('cannot find bugzilla user id for %s or %s') |
601 _(b'cannot find bugzilla user id for %s or %s') |
600 % (user, defaultuser) |
602 % (user, defaultuser) |
601 ) |
603 ) |
602 return (user, userid) |
604 return (user, userid) |
603 |
605 |
604 def updatebug(self, bugid, newstate, text, committer): |
606 def updatebug(self, bugid, newstate, text, committer): |
605 '''update bug state with comment text. |
607 '''update bug state with comment text. |
606 |
608 |
607 Try adding comment as committer of changeset, otherwise as |
609 Try adding comment as committer of changeset, otherwise as |
608 default bugzilla user.''' |
610 default bugzilla user.''' |
609 if len(newstate) > 0: |
611 if len(newstate) > 0: |
610 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n")) |
612 self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n")) |
611 |
613 |
612 (user, userid) = self.get_bugzilla_user(committer) |
614 (user, userid) = self.get_bugzilla_user(committer) |
613 now = time.strftime(r'%Y-%m-%d %H:%M:%S') |
615 now = time.strftime(r'%Y-%m-%d %H:%M:%S') |
614 self.run( |
616 self.run( |
615 '''insert into longdescs |
617 '''insert into longdescs |
727 # necessary. The xmlrpclib.Transport classes are old-style classes, and |
729 # necessary. The xmlrpclib.Transport classes are old-style classes, and |
728 # it turns out their __init__() doesn't get called when doing multiple |
730 # it turns out their __init__() doesn't get called when doing multiple |
729 # inheritance with a new-style class. |
731 # inheritance with a new-style class. |
730 class cookietransport(cookietransportrequest, xmlrpclib.Transport): |
732 class cookietransport(cookietransportrequest, xmlrpclib.Transport): |
731 def __init__(self, use_datetime=0): |
733 def __init__(self, use_datetime=0): |
732 if util.safehasattr(xmlrpclib.Transport, "__init__"): |
734 if util.safehasattr(xmlrpclib.Transport, b"__init__"): |
733 xmlrpclib.Transport.__init__(self, use_datetime) |
735 xmlrpclib.Transport.__init__(self, use_datetime) |
734 |
736 |
735 |
737 |
736 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport): |
738 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport): |
737 def __init__(self, use_datetime=0): |
739 def __init__(self, use_datetime=0): |
738 if util.safehasattr(xmlrpclib.Transport, "__init__"): |
740 if util.safehasattr(xmlrpclib.Transport, b"__init__"): |
739 xmlrpclib.SafeTransport.__init__(self, use_datetime) |
741 xmlrpclib.SafeTransport.__init__(self, use_datetime) |
740 |
742 |
741 |
743 |
742 class bzxmlrpc(bzaccess): |
744 class bzxmlrpc(bzaccess): |
743 """Support for access to Bugzilla via the Bugzilla XMLRPC API. |
745 """Support for access to Bugzilla via the Bugzilla XMLRPC API. |
746 """ |
748 """ |
747 |
749 |
748 def __init__(self, ui): |
750 def __init__(self, ui): |
749 bzaccess.__init__(self, ui) |
751 bzaccess.__init__(self, ui) |
750 |
752 |
751 bzweb = self.ui.config('bugzilla', 'bzurl') |
753 bzweb = self.ui.config(b'bugzilla', b'bzurl') |
752 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi" |
754 bzweb = bzweb.rstrip(b"/") + b"/xmlrpc.cgi" |
753 |
755 |
754 user = self.ui.config('bugzilla', 'user') |
756 user = self.ui.config(b'bugzilla', b'user') |
755 passwd = self.ui.config('bugzilla', 'password') |
757 passwd = self.ui.config(b'bugzilla', b'password') |
756 |
758 |
757 self.fixstatus = self.ui.config('bugzilla', 'fixstatus') |
759 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus') |
758 self.fixresolution = self.ui.config('bugzilla', 'fixresolution') |
760 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution') |
759 |
761 |
760 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb)) |
762 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb)) |
761 ver = self.bzproxy.Bugzilla.version()['version'].split('.') |
763 ver = self.bzproxy.Bugzilla.version()[b'version'].split(b'.') |
762 self.bzvermajor = int(ver[0]) |
764 self.bzvermajor = int(ver[0]) |
763 self.bzverminor = int(ver[1]) |
765 self.bzverminor = int(ver[1]) |
764 login = self.bzproxy.User.login( |
766 login = self.bzproxy.User.login( |
765 {'login': user, 'password': passwd, 'restrict_login': True} |
767 {b'login': user, b'password': passwd, b'restrict_login': True} |
766 ) |
768 ) |
767 self.bztoken = login.get('token', '') |
769 self.bztoken = login.get(b'token', b'') |
768 |
770 |
769 def transport(self, uri): |
771 def transport(self, uri): |
770 if util.urlreq.urlparse(uri, "http")[0] == "https": |
772 if util.urlreq.urlparse(uri, b"http")[0] == b"https": |
771 return cookiesafetransport() |
773 return cookiesafetransport() |
772 else: |
774 else: |
773 return cookietransport() |
775 return cookietransport() |
774 |
776 |
775 def get_bug_comments(self, id): |
777 def get_bug_comments(self, id): |
776 """Return a string with all comment text for a bug.""" |
778 """Return a string with all comment text for a bug.""" |
777 c = self.bzproxy.Bug.comments( |
779 c = self.bzproxy.Bug.comments( |
778 {'ids': [id], 'include_fields': ['text'], 'token': self.bztoken} |
780 {b'ids': [id], b'include_fields': [b'text'], b'token': self.bztoken} |
779 ) |
781 ) |
780 return ''.join([t['text'] for t in c['bugs']['%d' % id]['comments']]) |
782 return b''.join( |
|
783 [t[b'text'] for t in c[b'bugs'][b'%d' % id][b'comments']] |
|
784 ) |
781 |
785 |
782 def filter_real_bug_ids(self, bugs): |
786 def filter_real_bug_ids(self, bugs): |
783 probe = self.bzproxy.Bug.get( |
787 probe = self.bzproxy.Bug.get( |
784 { |
788 { |
785 'ids': sorted(bugs.keys()), |
789 b'ids': sorted(bugs.keys()), |
786 'include_fields': [], |
790 b'include_fields': [], |
787 'permissive': True, |
791 b'permissive': True, |
788 'token': self.bztoken, |
792 b'token': self.bztoken, |
789 } |
793 } |
790 ) |
794 ) |
791 for badbug in probe['faults']: |
795 for badbug in probe[b'faults']: |
792 id = badbug['id'] |
796 id = badbug[b'id'] |
793 self.ui.status(_('bug %d does not exist\n') % id) |
797 self.ui.status(_(b'bug %d does not exist\n') % id) |
794 del bugs[id] |
798 del bugs[id] |
795 |
799 |
796 def filter_cset_known_bug_ids(self, node, bugs): |
800 def filter_cset_known_bug_ids(self, node, bugs): |
797 for id in sorted(bugs.keys()): |
801 for id in sorted(bugs.keys()): |
798 if self.get_bug_comments(id).find(short(node)) != -1: |
802 if self.get_bug_comments(id).find(short(node)) != -1: |
799 self.ui.status( |
803 self.ui.status( |
800 _('bug %d already knows about changeset %s\n') |
804 _(b'bug %d already knows about changeset %s\n') |
801 % (id, short(node)) |
805 % (id, short(node)) |
802 ) |
806 ) |
803 del bugs[id] |
807 del bugs[id] |
804 |
808 |
805 def updatebug(self, bugid, newstate, text, committer): |
809 def updatebug(self, bugid, newstate, text, committer): |
806 args = {} |
810 args = {} |
807 if 'hours' in newstate: |
811 if b'hours' in newstate: |
808 args['work_time'] = newstate['hours'] |
812 args[b'work_time'] = newstate[b'hours'] |
809 |
813 |
810 if self.bzvermajor >= 4: |
814 if self.bzvermajor >= 4: |
811 args['ids'] = [bugid] |
815 args[b'ids'] = [bugid] |
812 args['comment'] = {'body': text} |
816 args[b'comment'] = {b'body': text} |
813 if 'fix' in newstate: |
817 if b'fix' in newstate: |
814 args['status'] = self.fixstatus |
818 args[b'status'] = self.fixstatus |
815 args['resolution'] = self.fixresolution |
819 args[b'resolution'] = self.fixresolution |
816 args['token'] = self.bztoken |
820 args[b'token'] = self.bztoken |
817 self.bzproxy.Bug.update(args) |
821 self.bzproxy.Bug.update(args) |
818 else: |
822 else: |
819 if 'fix' in newstate: |
823 if b'fix' in newstate: |
820 self.ui.warn( |
824 self.ui.warn( |
821 _( |
825 _( |
822 "Bugzilla/XMLRPC needs Bugzilla 4.0 or later " |
826 b"Bugzilla/XMLRPC needs Bugzilla 4.0 or later " |
823 "to mark bugs fixed\n" |
827 b"to mark bugs fixed\n" |
824 ) |
828 ) |
825 ) |
829 ) |
826 args['id'] = bugid |
830 args[b'id'] = bugid |
827 args['comment'] = text |
831 args[b'comment'] = text |
828 self.bzproxy.Bug.add_comment(args) |
832 self.bzproxy.Bug.add_comment(args) |
829 |
833 |
830 |
834 |
831 class bzxmlrpcemail(bzxmlrpc): |
835 class bzxmlrpcemail(bzxmlrpc): |
832 """Read data from Bugzilla via XMLRPC, send updates via email. |
836 """Read data from Bugzilla via XMLRPC, send updates via email. |
849 # 4.0 onwards. |
853 # 4.0 onwards. |
850 |
854 |
851 def __init__(self, ui): |
855 def __init__(self, ui): |
852 bzxmlrpc.__init__(self, ui) |
856 bzxmlrpc.__init__(self, ui) |
853 |
857 |
854 self.bzemail = self.ui.config('bugzilla', 'bzemail') |
858 self.bzemail = self.ui.config(b'bugzilla', b'bzemail') |
855 if not self.bzemail: |
859 if not self.bzemail: |
856 raise error.Abort(_("configuration 'bzemail' missing")) |
860 raise error.Abort(_(b"configuration 'bzemail' missing")) |
857 mail.validateconfig(self.ui) |
861 mail.validateconfig(self.ui) |
858 |
862 |
859 def makecommandline(self, fieldname, value): |
863 def makecommandline(self, fieldname, value): |
860 if self.bzvermajor >= 4: |
864 if self.bzvermajor >= 4: |
861 return "@%s %s" % (fieldname, pycompat.bytestr(value)) |
865 return b"@%s %s" % (fieldname, pycompat.bytestr(value)) |
862 else: |
866 else: |
863 if fieldname == "id": |
867 if fieldname == b"id": |
864 fieldname = "bug_id" |
868 fieldname = b"bug_id" |
865 return "@%s = %s" % (fieldname, pycompat.bytestr(value)) |
869 return b"@%s = %s" % (fieldname, pycompat.bytestr(value)) |
866 |
870 |
867 def send_bug_modify_email(self, bugid, commands, comment, committer): |
871 def send_bug_modify_email(self, bugid, commands, comment, committer): |
868 '''send modification message to Bugzilla bug via email. |
872 '''send modification message to Bugzilla bug via email. |
869 |
873 |
870 The message format is documented in the Bugzilla email_in.pl |
874 The message format is documented in the Bugzilla email_in.pl |
875 Bugzilla commands, specify the bug ID via the message body, rather |
879 Bugzilla commands, specify the bug ID via the message body, rather |
876 than the subject line, and leave a blank line after it. |
880 than the subject line, and leave a blank line after it. |
877 ''' |
881 ''' |
878 user = self.map_committer(committer) |
882 user = self.map_committer(committer) |
879 matches = self.bzproxy.User.get( |
883 matches = self.bzproxy.User.get( |
880 {'match': [user], 'token': self.bztoken} |
884 {b'match': [user], b'token': self.bztoken} |
881 ) |
885 ) |
882 if not matches['users']: |
886 if not matches[b'users']: |
883 user = self.ui.config('bugzilla', 'user') |
887 user = self.ui.config(b'bugzilla', b'user') |
884 matches = self.bzproxy.User.get( |
888 matches = self.bzproxy.User.get( |
885 {'match': [user], 'token': self.bztoken} |
889 {b'match': [user], b'token': self.bztoken} |
886 ) |
890 ) |
887 if not matches['users']: |
891 if not matches[b'users']: |
888 raise error.Abort( |
892 raise error.Abort( |
889 _("default bugzilla user %s email not found") % user |
893 _(b"default bugzilla user %s email not found") % user |
890 ) |
894 ) |
891 user = matches['users'][0]['email'] |
895 user = matches[b'users'][0][b'email'] |
892 commands.append(self.makecommandline("id", bugid)) |
896 commands.append(self.makecommandline(b"id", bugid)) |
893 |
897 |
894 text = "\n".join(commands) + "\n\n" + comment |
898 text = b"\n".join(commands) + b"\n\n" + comment |
895 |
899 |
896 _charsets = mail._charsets(self.ui) |
900 _charsets = mail._charsets(self.ui) |
897 user = mail.addressencode(self.ui, user, _charsets) |
901 user = mail.addressencode(self.ui, user, _charsets) |
898 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets) |
902 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets) |
899 msg = mail.mimeencode(self.ui, text, _charsets) |
903 msg = mail.mimeencode(self.ui, text, _charsets) |
900 msg['From'] = user |
904 msg[b'From'] = user |
901 msg['To'] = bzemail |
905 msg[b'To'] = bzemail |
902 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets) |
906 msg[b'Subject'] = mail.headencode( |
|
907 self.ui, b"Bug modification", _charsets |
|
908 ) |
903 sendmail = mail.connect(self.ui) |
909 sendmail = mail.connect(self.ui) |
904 sendmail(user, bzemail, msg.as_string()) |
910 sendmail(user, bzemail, msg.as_string()) |
905 |
911 |
906 def updatebug(self, bugid, newstate, text, committer): |
912 def updatebug(self, bugid, newstate, text, committer): |
907 cmds = [] |
913 cmds = [] |
908 if 'hours' in newstate: |
914 if b'hours' in newstate: |
909 cmds.append(self.makecommandline("work_time", newstate['hours'])) |
915 cmds.append(self.makecommandline(b"work_time", newstate[b'hours'])) |
910 if 'fix' in newstate: |
916 if b'fix' in newstate: |
911 cmds.append(self.makecommandline("bug_status", self.fixstatus)) |
917 cmds.append(self.makecommandline(b"bug_status", self.fixstatus)) |
912 cmds.append(self.makecommandline("resolution", self.fixresolution)) |
918 cmds.append(self.makecommandline(b"resolution", self.fixresolution)) |
913 self.send_bug_modify_email(bugid, cmds, text, committer) |
919 self.send_bug_modify_email(bugid, cmds, text, committer) |
914 |
920 |
915 |
921 |
916 class NotFound(LookupError): |
922 class NotFound(LookupError): |
917 pass |
923 pass |
922 Bugzilla 5.0. |
928 Bugzilla 5.0. |
923 """ |
929 """ |
924 |
930 |
925 def __init__(self, ui): |
931 def __init__(self, ui): |
926 bzaccess.__init__(self, ui) |
932 bzaccess.__init__(self, ui) |
927 bz = self.ui.config('bugzilla', 'bzurl') |
933 bz = self.ui.config(b'bugzilla', b'bzurl') |
928 self.bzroot = '/'.join([bz, 'rest']) |
934 self.bzroot = b'/'.join([bz, b'rest']) |
929 self.apikey = self.ui.config('bugzilla', 'apikey') |
935 self.apikey = self.ui.config(b'bugzilla', b'apikey') |
930 self.user = self.ui.config('bugzilla', 'user') |
936 self.user = self.ui.config(b'bugzilla', b'user') |
931 self.passwd = self.ui.config('bugzilla', 'password') |
937 self.passwd = self.ui.config(b'bugzilla', b'password') |
932 self.fixstatus = self.ui.config('bugzilla', 'fixstatus') |
938 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus') |
933 self.fixresolution = self.ui.config('bugzilla', 'fixresolution') |
939 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution') |
934 |
940 |
935 def apiurl(self, targets, include_fields=None): |
941 def apiurl(self, targets, include_fields=None): |
936 url = '/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets]) |
942 url = b'/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets]) |
937 qv = {} |
943 qv = {} |
938 if self.apikey: |
944 if self.apikey: |
939 qv['api_key'] = self.apikey |
945 qv[b'api_key'] = self.apikey |
940 elif self.user and self.passwd: |
946 elif self.user and self.passwd: |
941 qv['login'] = self.user |
947 qv[b'login'] = self.user |
942 qv['password'] = self.passwd |
948 qv[b'password'] = self.passwd |
943 if include_fields: |
949 if include_fields: |
944 qv['include_fields'] = include_fields |
950 qv[b'include_fields'] = include_fields |
945 if qv: |
951 if qv: |
946 url = '%s?%s' % (url, util.urlreq.urlencode(qv)) |
952 url = b'%s?%s' % (url, util.urlreq.urlencode(qv)) |
947 return url |
953 return url |
948 |
954 |
949 def _fetch(self, burl): |
955 def _fetch(self, burl): |
950 try: |
956 try: |
951 resp = url.open(self.ui, burl) |
957 resp = url.open(self.ui, burl) |
952 return json.loads(resp.read()) |
958 return json.loads(resp.read()) |
953 except util.urlerr.httperror as inst: |
959 except util.urlerr.httperror as inst: |
954 if inst.code == 401: |
960 if inst.code == 401: |
955 raise error.Abort(_('authorization failed')) |
961 raise error.Abort(_(b'authorization failed')) |
956 if inst.code == 404: |
962 if inst.code == 404: |
957 raise NotFound() |
963 raise NotFound() |
958 else: |
964 else: |
959 raise |
965 raise |
960 |
966 |
961 def _submit(self, burl, data, method='POST'): |
967 def _submit(self, burl, data, method=b'POST'): |
962 data = json.dumps(data) |
968 data = json.dumps(data) |
963 if method == 'PUT': |
969 if method == b'PUT': |
964 |
970 |
965 class putrequest(util.urlreq.request): |
971 class putrequest(util.urlreq.request): |
966 def get_method(self): |
972 def get_method(self): |
967 return 'PUT' |
973 return b'PUT' |
968 |
974 |
969 request_type = putrequest |
975 request_type = putrequest |
970 else: |
976 else: |
971 request_type = util.urlreq.request |
977 request_type = util.urlreq.request |
972 req = request_type(burl, data, {'Content-Type': 'application/json'}) |
978 req = request_type(burl, data, {b'Content-Type': b'application/json'}) |
973 try: |
979 try: |
974 resp = url.opener(self.ui).open(req) |
980 resp = url.opener(self.ui).open(req) |
975 return json.loads(resp.read()) |
981 return json.loads(resp.read()) |
976 except util.urlerr.httperror as inst: |
982 except util.urlerr.httperror as inst: |
977 if inst.code == 401: |
983 if inst.code == 401: |
978 raise error.Abort(_('authorization failed')) |
984 raise error.Abort(_(b'authorization failed')) |
979 if inst.code == 404: |
985 if inst.code == 404: |
980 raise NotFound() |
986 raise NotFound() |
981 else: |
987 else: |
982 raise |
988 raise |
983 |
989 |
984 def filter_real_bug_ids(self, bugs): |
990 def filter_real_bug_ids(self, bugs): |
985 '''remove bug IDs that do not exist in Bugzilla from bugs.''' |
991 '''remove bug IDs that do not exist in Bugzilla from bugs.''' |
986 badbugs = set() |
992 badbugs = set() |
987 for bugid in bugs: |
993 for bugid in bugs: |
988 burl = self.apiurl(('bug', bugid), include_fields='status') |
994 burl = self.apiurl((b'bug', bugid), include_fields=b'status') |
989 try: |
995 try: |
990 self._fetch(burl) |
996 self._fetch(burl) |
991 except NotFound: |
997 except NotFound: |
992 badbugs.add(bugid) |
998 badbugs.add(bugid) |
993 for bugid in badbugs: |
999 for bugid in badbugs: |
995 |
1001 |
996 def filter_cset_known_bug_ids(self, node, bugs): |
1002 def filter_cset_known_bug_ids(self, node, bugs): |
997 '''remove bug IDs where node occurs in comment text from bugs.''' |
1003 '''remove bug IDs where node occurs in comment text from bugs.''' |
998 sn = short(node) |
1004 sn = short(node) |
999 for bugid in bugs.keys(): |
1005 for bugid in bugs.keys(): |
1000 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text') |
1006 burl = self.apiurl( |
|
1007 (b'bug', bugid, b'comment'), include_fields=b'text' |
|
1008 ) |
1001 result = self._fetch(burl) |
1009 result = self._fetch(burl) |
1002 comments = result['bugs'][pycompat.bytestr(bugid)]['comments'] |
1010 comments = result[b'bugs'][pycompat.bytestr(bugid)][b'comments'] |
1003 if any(sn in c['text'] for c in comments): |
1011 if any(sn in c[b'text'] for c in comments): |
1004 self.ui.status( |
1012 self.ui.status( |
1005 _('bug %d already knows about changeset %s\n') % (bugid, sn) |
1013 _(b'bug %d already knows about changeset %s\n') |
|
1014 % (bugid, sn) |
1006 ) |
1015 ) |
1007 del bugs[bugid] |
1016 del bugs[bugid] |
1008 |
1017 |
1009 def updatebug(self, bugid, newstate, text, committer): |
1018 def updatebug(self, bugid, newstate, text, committer): |
1010 '''update the specified bug. Add comment text and set new states. |
1019 '''update the specified bug. Add comment text and set new states. |
1011 |
1020 |
1012 If possible add the comment as being from the committer of |
1021 If possible add the comment as being from the committer of |
1013 the changeset. Otherwise use the default Bugzilla user. |
1022 the changeset. Otherwise use the default Bugzilla user. |
1014 ''' |
1023 ''' |
1015 bugmod = {} |
1024 bugmod = {} |
1016 if 'hours' in newstate: |
1025 if b'hours' in newstate: |
1017 bugmod['work_time'] = newstate['hours'] |
1026 bugmod[b'work_time'] = newstate[b'hours'] |
1018 if 'fix' in newstate: |
1027 if b'fix' in newstate: |
1019 bugmod['status'] = self.fixstatus |
1028 bugmod[b'status'] = self.fixstatus |
1020 bugmod['resolution'] = self.fixresolution |
1029 bugmod[b'resolution'] = self.fixresolution |
1021 if bugmod: |
1030 if bugmod: |
1022 # if we have to change the bugs state do it here |
1031 # if we have to change the bugs state do it here |
1023 bugmod['comment'] = { |
1032 bugmod[b'comment'] = { |
1024 'comment': text, |
1033 b'comment': text, |
1025 'is_private': False, |
1034 b'is_private': False, |
1026 'is_markdown': False, |
1035 b'is_markdown': False, |
1027 } |
1036 } |
1028 burl = self.apiurl(('bug', bugid)) |
1037 burl = self.apiurl((b'bug', bugid)) |
1029 self._submit(burl, bugmod, method='PUT') |
1038 self._submit(burl, bugmod, method=b'PUT') |
1030 self.ui.debug('updated bug %s\n' % bugid) |
1039 self.ui.debug(b'updated bug %s\n' % bugid) |
1031 else: |
1040 else: |
1032 burl = self.apiurl(('bug', bugid, 'comment')) |
1041 burl = self.apiurl((b'bug', bugid, b'comment')) |
1033 self._submit( |
1042 self._submit( |
1034 burl, |
1043 burl, |
1035 {'comment': text, 'is_private': False, 'is_markdown': False,}, |
1044 { |
|
1045 b'comment': text, |
|
1046 b'is_private': False, |
|
1047 b'is_markdown': False, |
|
1048 }, |
1036 ) |
1049 ) |
1037 self.ui.debug('added comment to bug %s\n' % bugid) |
1050 self.ui.debug(b'added comment to bug %s\n' % bugid) |
1038 |
1051 |
1039 def notify(self, bugs, committer): |
1052 def notify(self, bugs, committer): |
1040 '''Force sending of Bugzilla notification emails. |
1053 '''Force sending of Bugzilla notification emails. |
1041 |
1054 |
1042 Only required if the access method does not trigger notification |
1055 Only required if the access method does not trigger notification |
1047 |
1060 |
1048 class bugzilla(object): |
1061 class bugzilla(object): |
1049 # supported versions of bugzilla. different versions have |
1062 # supported versions of bugzilla. different versions have |
1050 # different schemas. |
1063 # different schemas. |
1051 _versions = { |
1064 _versions = { |
1052 '2.16': bzmysql, |
1065 b'2.16': bzmysql, |
1053 '2.18': bzmysql_2_18, |
1066 b'2.18': bzmysql_2_18, |
1054 '3.0': bzmysql_3_0, |
1067 b'3.0': bzmysql_3_0, |
1055 'xmlrpc': bzxmlrpc, |
1068 b'xmlrpc': bzxmlrpc, |
1056 'xmlrpc+email': bzxmlrpcemail, |
1069 b'xmlrpc+email': bzxmlrpcemail, |
1057 'restapi': bzrestapi, |
1070 b'restapi': bzrestapi, |
1058 } |
1071 } |
1059 |
1072 |
1060 def __init__(self, ui, repo): |
1073 def __init__(self, ui, repo): |
1061 self.ui = ui |
1074 self.ui = ui |
1062 self.repo = repo |
1075 self.repo = repo |
1063 |
1076 |
1064 bzversion = self.ui.config('bugzilla', 'version') |
1077 bzversion = self.ui.config(b'bugzilla', b'version') |
1065 try: |
1078 try: |
1066 bzclass = bugzilla._versions[bzversion] |
1079 bzclass = bugzilla._versions[bzversion] |
1067 except KeyError: |
1080 except KeyError: |
1068 raise error.Abort( |
1081 raise error.Abort( |
1069 _('bugzilla version %s not supported') % bzversion |
1082 _(b'bugzilla version %s not supported') % bzversion |
1070 ) |
1083 ) |
1071 self.bzdriver = bzclass(self.ui) |
1084 self.bzdriver = bzclass(self.ui) |
1072 |
1085 |
1073 self.bug_re = re.compile( |
1086 self.bug_re = re.compile( |
1074 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE |
1087 self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE |
1075 ) |
1088 ) |
1076 self.fix_re = re.compile( |
1089 self.fix_re = re.compile( |
1077 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE |
1090 self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE |
1078 ) |
1091 ) |
1079 self.split_re = re.compile(br'\D+') |
1092 self.split_re = re.compile(br'\D+') |
1080 |
1093 |
1081 def find_bugs(self, ctx): |
1094 def find_bugs(self, ctx): |
1082 '''return bugs dictionary created from commit comment. |
1095 '''return bugs dictionary created from commit comment. |
1140 '''update bugzilla bug with reference to changeset.''' |
1153 '''update bugzilla bug with reference to changeset.''' |
1141 |
1154 |
1142 def webroot(root): |
1155 def webroot(root): |
1143 '''strip leading prefix of repo root and turn into |
1156 '''strip leading prefix of repo root and turn into |
1144 url-safe path.''' |
1157 url-safe path.''' |
1145 count = int(self.ui.config('bugzilla', 'strip')) |
1158 count = int(self.ui.config(b'bugzilla', b'strip')) |
1146 root = util.pconvert(root) |
1159 root = util.pconvert(root) |
1147 while count > 0: |
1160 while count > 0: |
1148 c = root.find('/') |
1161 c = root.find(b'/') |
1149 if c == -1: |
1162 if c == -1: |
1150 break |
1163 break |
1151 root = root[c + 1 :] |
1164 root = root[c + 1 :] |
1152 count -= 1 |
1165 count -= 1 |
1153 return root |
1166 return root |
1154 |
1167 |
1155 mapfile = None |
1168 mapfile = None |
1156 tmpl = self.ui.config('bugzilla', 'template') |
1169 tmpl = self.ui.config(b'bugzilla', b'template') |
1157 if not tmpl: |
1170 if not tmpl: |
1158 mapfile = self.ui.config('bugzilla', 'style') |
1171 mapfile = self.ui.config(b'bugzilla', b'style') |
1159 if not mapfile and not tmpl: |
1172 if not mapfile and not tmpl: |
1160 tmpl = _( |
1173 tmpl = _( |
1161 'changeset {node|short} in repo {root} refers ' |
1174 b'changeset {node|short} in repo {root} refers ' |
1162 'to bug {bug}.\ndetails:\n\t{desc|tabindent}' |
1175 b'to bug {bug}.\ndetails:\n\t{desc|tabindent}' |
1163 ) |
1176 ) |
1164 spec = logcmdutil.templatespec(tmpl, mapfile) |
1177 spec = logcmdutil.templatespec(tmpl, mapfile) |
1165 t = logcmdutil.changesettemplater(self.ui, self.repo, spec) |
1178 t = logcmdutil.changesettemplater(self.ui, self.repo, spec) |
1166 self.ui.pushbuffer() |
1179 self.ui.pushbuffer() |
1167 t.show( |
1180 t.show( |
1168 ctx, |
1181 ctx, |
1169 changes=ctx.changeset(), |
1182 changes=ctx.changeset(), |
1170 bug=pycompat.bytestr(bugid), |
1183 bug=pycompat.bytestr(bugid), |
1171 hgweb=self.ui.config('web', 'baseurl'), |
1184 hgweb=self.ui.config(b'web', b'baseurl'), |
1172 root=self.repo.root, |
1185 root=self.repo.root, |
1173 webroot=webroot(self.repo.root), |
1186 webroot=webroot(self.repo.root), |
1174 ) |
1187 ) |
1175 data = self.ui.popbuffer() |
1188 data = self.ui.popbuffer() |
1176 self.bzdriver.updatebug( |
1189 self.bzdriver.updatebug( |