hgext/bugzilla.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43115 4aa72cdf616f
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
   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
   629     '''support for bugzilla 2.18 series.'''
   631     '''support for bugzilla 2.18 series.'''
   630 
   632 
   631     def __init__(self, ui):
   633     def __init__(self, ui):
   632         bzmysql.__init__(self, ui)
   634         bzmysql.__init__(self, ui)
   633         self.default_notify = (
   635         self.default_notify = (
   634             "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
   636             b"cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
   635         )
   637         )
   636 
   638 
   637 
   639 
   638 class bzmysql_3_0(bzmysql_2_18):
   640 class bzmysql_3_0(bzmysql_2_18):
   639     '''support for bugzilla 3.0 series.'''
   641     '''support for bugzilla 3.0 series.'''
   641     def __init__(self, ui):
   643     def __init__(self, ui):
   642         bzmysql_2_18.__init__(self, ui)
   644         bzmysql_2_18.__init__(self, ui)
   643 
   645 
   644     def get_longdesc_id(self):
   646     def get_longdesc_id(self):
   645         '''get identity of longdesc field'''
   647         '''get identity of longdesc field'''
   646         self.run('select id from fielddefs where name = "longdesc"')
   648         self.run(b'select id from fielddefs where name = "longdesc"')
   647         ids = self.cursor.fetchall()
   649         ids = self.cursor.fetchall()
   648         if len(ids) != 1:
   650         if len(ids) != 1:
   649             raise error.Abort(_('unknown database schema'))
   651             raise error.Abort(_(b'unknown database schema'))
   650         return ids[0][0]
   652         return ids[0][0]
   651 
   653 
   652 
   654 
   653 # Bugzilla via XMLRPC interface.
   655 # Bugzilla via XMLRPC interface.
   654 
   656 
   672     cookies = []
   674     cookies = []
   673 
   675 
   674     def send_cookies(self, connection):
   676     def send_cookies(self, connection):
   675         if self.cookies:
   677         if self.cookies:
   676             for cookie in self.cookies:
   678             for cookie in self.cookies:
   677                 connection.putheader("Cookie", cookie)
   679                 connection.putheader(b"Cookie", cookie)
   678 
   680 
   679     def request(self, host, handler, request_body, verbose=0):
   681     def request(self, host, handler, request_body, verbose=0):
   680         self.verbose = verbose
   682         self.verbose = verbose
   681         self.accept_gzip_encoding = False
   683         self.accept_gzip_encoding = False
   682 
   684 
   700             response = h.getresponse()
   702             response = h.getresponse()
   701         except AttributeError:
   703         except AttributeError:
   702             response = h._conn.getresponse()
   704             response = h._conn.getresponse()
   703 
   705 
   704         # Add any cookie definitions to our list.
   706         # Add any cookie definitions to our list.
   705         for header in response.msg.getallmatchingheaders("Set-Cookie"):
   707         for header in response.msg.getallmatchingheaders(b"Set-Cookie"):
   706             val = header.split(": ", 1)[1]
   708             val = header.split(b": ", 1)[1]
   707             cookie = val.split(";", 1)[0]
   709             cookie = val.split(b";", 1)[0]
   708             self.cookies.append(cookie)
   710             self.cookies.append(cookie)
   709 
   711 
   710         if response.status != 200:
   712         if response.status != 200:
   711             raise xmlrpclib.ProtocolError(
   713             raise xmlrpclib.ProtocolError(
   712                 host + handler,
   714                 host + handler,
   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.
  1104                 else:
  1117                 else:
  1105                     m = fixmatch
  1118                     m = fixmatch
  1106             start = m.end()
  1119             start = m.end()
  1107             if m is bugmatch:
  1120             if m is bugmatch:
  1108                 bugmatch = self.bug_re.search(ctx.description(), start)
  1121                 bugmatch = self.bug_re.search(ctx.description(), start)
  1109                 if 'fix' in bugattribs:
  1122                 if b'fix' in bugattribs:
  1110                     del bugattribs['fix']
  1123                     del bugattribs[b'fix']
  1111             else:
  1124             else:
  1112                 fixmatch = self.fix_re.search(ctx.description(), start)
  1125                 fixmatch = self.fix_re.search(ctx.description(), start)
  1113                 bugattribs['fix'] = None
  1126                 bugattribs[b'fix'] = None
  1114 
  1127 
  1115             try:
  1128             try:
  1116                 ids = m.group('ids')
  1129                 ids = m.group(b'ids')
  1117             except IndexError:
  1130             except IndexError:
  1118                 ids = m.group(1)
  1131                 ids = m.group(1)
  1119             try:
  1132             try:
  1120                 hours = float(m.group('hours'))
  1133                 hours = float(m.group(b'hours'))
  1121                 bugattribs['hours'] = hours
  1134                 bugattribs[b'hours'] = hours
  1122             except IndexError:
  1135             except IndexError:
  1123                 pass
  1136                 pass
  1124             except TypeError:
  1137             except TypeError:
  1125                 pass
  1138                 pass
  1126             except ValueError:
  1139             except ValueError:
  1127                 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
  1140                 self.ui.status(_(b"%s: invalid hours\n") % m.group(b'hours'))
  1128 
  1141 
  1129             for id in self.split_re.split(ids):
  1142             for id in self.split_re.split(ids):
  1130                 if not id:
  1143                 if not id:
  1131                     continue
  1144                     continue
  1132                 bugs[int(id)] = bugattribs
  1145                 bugs[int(id)] = bugattribs
  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(
  1186     '''add comment to bugzilla for each changeset that refers to a
  1199     '''add comment to bugzilla for each changeset that refers to a
  1187     bugzilla bug id. only add a comment once per bug, so same change
  1200     bugzilla bug id. only add a comment once per bug, so same change
  1188     seen multiple times does not fill bug with duplicate data.'''
  1201     seen multiple times does not fill bug with duplicate data.'''
  1189     if node is None:
  1202     if node is None:
  1190         raise error.Abort(
  1203         raise error.Abort(
  1191             _('hook type %s does not pass a changeset id') % hooktype
  1204             _(b'hook type %s does not pass a changeset id') % hooktype
  1192         )
  1205         )
  1193     try:
  1206     try:
  1194         bz = bugzilla(ui, repo)
  1207         bz = bugzilla(ui, repo)
  1195         ctx = repo[node]
  1208         ctx = repo[node]
  1196         bugs = bz.find_bugs(ctx)
  1209         bugs = bz.find_bugs(ctx)
  1197         if bugs:
  1210         if bugs:
  1198             for bug in bugs:
  1211             for bug in bugs:
  1199                 bz.update(bug, bugs[bug], ctx)
  1212                 bz.update(bug, bugs[bug], ctx)
  1200             bz.notify(bugs, stringutil.email(ctx.user()))
  1213             bz.notify(bugs, stringutil.email(ctx.user()))
  1201     except Exception as e:
  1214     except Exception as e:
  1202         raise error.Abort(_('Bugzilla error: %s') % e)
  1215         raise error.Abort(_(b'Bugzilla error: %s') % e)