hgext/bugzilla.py
changeset 43076 2372284d9457
parent 41759 aaad36b88298
child 43077 687b865b95ad
--- a/hgext/bugzilla.py	Sat Oct 05 10:29:34 2019 -0400
+++ b/hgext/bugzilla.py	Sun Oct 06 09:45:02 2019 -0400
@@ -324,72 +324,81 @@
 configtable = {}
 configitem = registrar.configitem(configtable)
 
-configitem('bugzilla', 'apikey',
-    default='',
+configitem(
+    'bugzilla', 'apikey', default='',
 )
-configitem('bugzilla', 'bzdir',
-    default='/var/www/html/bugzilla',
+configitem(
+    'bugzilla', 'bzdir', default='/var/www/html/bugzilla',
+)
+configitem(
+    'bugzilla', 'bzemail', default=None,
 )
-configitem('bugzilla', 'bzemail',
-    default=None,
+configitem(
+    'bugzilla', 'bzurl', default='http://localhost/bugzilla/',
 )
-configitem('bugzilla', 'bzurl',
-    default='http://localhost/bugzilla/',
+configitem(
+    'bugzilla', 'bzuser', default=None,
 )
-configitem('bugzilla', 'bzuser',
-    default=None,
+configitem(
+    'bugzilla', 'db', default='bugs',
 )
-configitem('bugzilla', 'db',
-    default='bugs',
-)
-configitem('bugzilla', 'fixregexp',
-    default=(br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
-             br'(?:nos?\.?|num(?:ber)?s?)?\s*'
-             br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
-             br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
+configitem(
+    'bugzilla',
+    'fixregexp',
+    default=(
+        br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
+        br'(?:nos?\.?|num(?:ber)?s?)?\s*'
+        br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
+        br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
+    ),
 )
-configitem('bugzilla', 'fixresolution',
-    default='FIXED',
+configitem(
+    'bugzilla', 'fixresolution', default='FIXED',
 )
-configitem('bugzilla', 'fixstatus',
-    default='RESOLVED',
+configitem(
+    'bugzilla', 'fixstatus', default='RESOLVED',
 )
-configitem('bugzilla', 'host',
-    default='localhost',
+configitem(
+    'bugzilla', 'host', default='localhost',
 )
-configitem('bugzilla', 'notify',
-    default=configitem.dynamicdefault,
+configitem(
+    'bugzilla', 'notify', default=configitem.dynamicdefault,
 )
-configitem('bugzilla', 'password',
-    default=None,
+configitem(
+    'bugzilla', 'password', default=None,
 )
-configitem('bugzilla', 'regexp',
-    default=(br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
-             br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
-             br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
+configitem(
+    'bugzilla',
+    'regexp',
+    default=(
+        br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
+        br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
+        br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
+    ),
 )
-configitem('bugzilla', 'strip',
-    default=0,
+configitem(
+    'bugzilla', 'strip', default=0,
 )
-configitem('bugzilla', 'style',
-    default=None,
+configitem(
+    'bugzilla', 'style', default=None,
 )
-configitem('bugzilla', 'template',
-    default=None,
+configitem(
+    'bugzilla', 'template', default=None,
 )
-configitem('bugzilla', 'timeout',
-    default=5,
+configitem(
+    'bugzilla', 'timeout', default=5,
 )
-configitem('bugzilla', 'user',
-    default='bugs',
+configitem(
+    'bugzilla', 'user', default='bugs',
 )
-configitem('bugzilla', 'usermap',
-    default=None,
+configitem(
+    'bugzilla', 'usermap', default=None,
 )
-configitem('bugzilla', 'version',
-    default=None,
+configitem(
+    'bugzilla', 'version', default=None,
 )
 
+
 class bzaccess(object):
     '''Base class for access to Bugzilla.'''
 
@@ -434,6 +443,7 @@
         emails automatically.
         '''
 
+
 # Bugzilla via direct access to MySQL database.
 class bzmysql(bzaccess):
     '''Support for direct MySQL access to Bugzilla.
@@ -454,6 +464,7 @@
     def __init__(self, ui):
         try:
             import MySQLdb as mysql
+
             bzmysql._MySQLdb = mysql
         except ImportError as err:
             raise error.Abort(_('python mysql support not available: %s') % err)
@@ -465,12 +476,13 @@
         passwd = self.ui.config('bugzilla', 'password')
         db = self.ui.config('bugzilla', 'db')
         timeout = int(self.ui.config('bugzilla', 'timeout'))
-        self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
-                     (host, db, user, '*' * len(passwd)))
-        self.conn = bzmysql._MySQLdb.connect(host=host,
-                                                   user=user, passwd=passwd,
-                                                   db=db,
-                                                   connect_timeout=timeout)
+        self.ui.note(
+            _('connecting to %s:%s as %s, password %s\n')
+            % (host, db, user, '*' * len(passwd))
+        )
+        self.conn = bzmysql._MySQLdb.connect(
+            host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
+        )
         self.cursor = self.conn.cursor()
         self.longdesc_id = self.get_longdesc_id()
         self.user_ids = {}
@@ -495,8 +507,10 @@
 
     def filter_real_bug_ids(self, bugs):
         '''filter not-existing bugs from set.'''
-        self.run('select bug_id from bugs where bug_id in %s' %
-                 bzmysql.sql_buglist(bugs.keys()))
+        self.run(
+            'select bug_id from bugs where bug_id in %s'
+            % bzmysql.sql_buglist(bugs.keys())
+        )
         existing = [id for (id,) in self.cursor.fetchall()]
         for id in bugs.keys():
             if id not in existing:
@@ -505,12 +519,16 @@
 
     def filter_cset_known_bug_ids(self, node, bugs):
         '''filter bug ids that already refer to this changeset from set.'''
-        self.run('''select bug_id from longdescs where
-                    bug_id in %s and thetext like "%%%s%%"''' %
-                 (bzmysql.sql_buglist(bugs.keys()), short(node)))
+        self.run(
+            '''select bug_id from longdescs where
+                    bug_id in %s and thetext like "%%%s%%"'''
+            % (bzmysql.sql_buglist(bugs.keys()), short(node))
+        )
         for (id,) in self.cursor.fetchall():
-            self.ui.status(_('bug %d already knows about changeset %s\n') %
-                           (id, short(node)))
+            self.ui.status(
+                _('bug %d already knows about changeset %s\n')
+                % (id, short(node))
+            )
             del bugs[id]
 
     def notify(self, bugs, committer):
@@ -534,8 +552,9 @@
             ret = fp.close()
             if ret:
                 self.ui.warn(out)
-                raise error.Abort(_('bugzilla notify command %s') %
-                                  procutil.explainexit(ret))
+                raise error.Abort(
+                    _('bugzilla notify command %s') % procutil.explainexit(ret)
+                )
         self.ui.status(_('done\n'))
 
     def get_user_id(self, user):
@@ -547,8 +566,11 @@
                 userid = int(user)
             except ValueError:
                 self.ui.note(_('looking up user %s\n') % user)
-                self.run('''select userid from profiles
-                            where login_name like %s''', user)
+                self.run(
+                    '''select userid from profiles
+                            where login_name like %s''',
+                    user,
+                )
                 all = self.cursor.fetchall()
                 if len(all) != 1:
                     raise KeyError(user)
@@ -567,13 +589,16 @@
             try:
                 defaultuser = self.ui.config('bugzilla', 'bzuser')
                 if not defaultuser:
-                    raise error.Abort(_('cannot find bugzilla user id for %s') %
-                                     user)
+                    raise error.Abort(
+                        _('cannot find bugzilla user id for %s') % user
+                    )
                 userid = self.get_user_id(defaultuser)
                 user = defaultuser
             except KeyError:
-                raise error.Abort(_('cannot find bugzilla user id for %s or %s')
-                                 % (user, defaultuser))
+                raise error.Abort(
+                    _('cannot find bugzilla user id for %s or %s')
+                    % (user, defaultuser)
+                )
         return (user, userid)
 
     def updatebug(self, bugid, newstate, text, committer):
@@ -586,22 +611,29 @@
 
         (user, userid) = self.get_bugzilla_user(committer)
         now = time.strftime(r'%Y-%m-%d %H:%M:%S')
-        self.run('''insert into longdescs
+        self.run(
+            '''insert into longdescs
                     (bug_id, who, bug_when, thetext)
                     values (%s, %s, %s, %s)''',
-                 (bugid, userid, now, text))
-        self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
+            (bugid, userid, now, text),
+        )
+        self.run(
+            '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
                     values (%s, %s, %s, %s)''',
-                 (bugid, userid, now, self.longdesc_id))
+            (bugid, userid, now, self.longdesc_id),
+        )
         self.conn.commit()
 
+
 class bzmysql_2_18(bzmysql):
     '''support for bugzilla 2.18 series.'''
 
     def __init__(self, ui):
         bzmysql.__init__(self, ui)
         self.default_notify = (
-            "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s")
+            "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
+        )
+
 
 class bzmysql_3_0(bzmysql_2_18):
     '''support for bugzilla 3.0 series.'''
@@ -617,8 +649,10 @@
             raise error.Abort(_('unknown database schema'))
         return ids[0][0]
 
+
 # Bugzilla via XMLRPC interface.
 
+
 class cookietransportrequest(object):
     """A Transport request method that retains cookies over its lifetime.
 
@@ -636,6 +670,7 @@
     # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
 
     cookies = []
+
     def send_cookies(self, connection):
         if self.cookies:
             for cookie in self.cookies:
@@ -673,8 +708,12 @@
             self.cookies.append(cookie)
 
         if response.status != 200:
-            raise xmlrpclib.ProtocolError(host + handler, response.status,
-                                          response.reason, response.msg.headers)
+            raise xmlrpclib.ProtocolError(
+                host + handler,
+                response.status,
+                response.reason,
+                response.msg.headers,
+            )
 
         payload = response.read()
         parser, unmarshaller = self.getparser()
@@ -683,6 +722,7 @@
 
         return unmarshaller.close()
 
+
 # The explicit calls to the underlying xmlrpclib __init__() methods are
 # necessary. The xmlrpclib.Transport classes are old-style classes, and
 # it turns out their __init__() doesn't get called when doing multiple
@@ -692,11 +732,13 @@
         if util.safehasattr(xmlrpclib.Transport, "__init__"):
             xmlrpclib.Transport.__init__(self, use_datetime)
 
+
 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
     def __init__(self, use_datetime=0):
         if util.safehasattr(xmlrpclib.Transport, "__init__"):
             xmlrpclib.SafeTransport.__init__(self, use_datetime)
 
+
 class bzxmlrpc(bzaccess):
     """Support for access to Bugzilla via the Bugzilla XMLRPC API.
 
@@ -719,8 +761,9 @@
         ver = self.bzproxy.Bugzilla.version()['version'].split('.')
         self.bzvermajor = int(ver[0])
         self.bzverminor = int(ver[1])
-        login = self.bzproxy.User.login({'login': user, 'password': passwd,
-                                         'restrict_login': True})
+        login = self.bzproxy.User.login(
+            {'login': user, 'password': passwd, 'restrict_login': True}
+        )
         self.bztoken = login.get('token', '')
 
     def transport(self, uri):
@@ -731,17 +774,20 @@
 
     def get_bug_comments(self, id):
         """Return a string with all comment text for a bug."""
-        c = self.bzproxy.Bug.comments({'ids': [id],
-                                       'include_fields': ['text'],
-                                       'token': self.bztoken})
+        c = self.bzproxy.Bug.comments(
+            {'ids': [id], 'include_fields': ['text'], 'token': self.bztoken}
+        )
         return ''.join([t['text'] for t in c['bugs']['%d' % id]['comments']])
 
     def filter_real_bug_ids(self, bugs):
-        probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
-                                      'include_fields': [],
-                                      'permissive': True,
-                                      'token': self.bztoken,
-                                      })
+        probe = self.bzproxy.Bug.get(
+            {
+                'ids': sorted(bugs.keys()),
+                'include_fields': [],
+                'permissive': True,
+                'token': self.bztoken,
+            }
+        )
         for badbug in probe['faults']:
             id = badbug['id']
             self.ui.status(_('bug %d does not exist\n') % id)
@@ -750,8 +796,10 @@
     def filter_cset_known_bug_ids(self, node, bugs):
         for id in sorted(bugs.keys()):
             if self.get_bug_comments(id).find(short(node)) != -1:
-                self.ui.status(_('bug %d already knows about changeset %s\n') %
-                               (id, short(node)))
+                self.ui.status(
+                    _('bug %d already knows about changeset %s\n')
+                    % (id, short(node))
+                )
                 del bugs[id]
 
     def updatebug(self, bugid, newstate, text, committer):
@@ -761,7 +809,7 @@
 
         if self.bzvermajor >= 4:
             args['ids'] = [bugid]
-            args['comment'] = {'body' : text}
+            args['comment'] = {'body': text}
             if 'fix' in newstate:
                 args['status'] = self.fixstatus
                 args['resolution'] = self.fixresolution
@@ -769,12 +817,17 @@
             self.bzproxy.Bug.update(args)
         else:
             if 'fix' in newstate:
-                self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
-                               "to mark bugs fixed\n"))
+                self.ui.warn(
+                    _(
+                        "Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
+                        "to mark bugs fixed\n"
+                    )
+                )
             args['id'] = bugid
             args['comment'] = text
             self.bzproxy.Bug.add_comment(args)
 
+
 class bzxmlrpcemail(bzxmlrpc):
     """Read data from Bugzilla via XMLRPC, send updates via email.
 
@@ -823,15 +876,18 @@
         than the subject line, and leave a blank line after it.
         '''
         user = self.map_committer(committer)
-        matches = self.bzproxy.User.get({'match': [user],
-                                         'token': self.bztoken})
+        matches = self.bzproxy.User.get(
+            {'match': [user], 'token': self.bztoken}
+        )
         if not matches['users']:
             user = self.ui.config('bugzilla', 'user')
-            matches = self.bzproxy.User.get({'match': [user],
-                                             'token': self.bztoken})
+            matches = self.bzproxy.User.get(
+                {'match': [user], 'token': self.bztoken}
+            )
             if not matches['users']:
-                raise error.Abort(_("default bugzilla user %s email not found")
-                                  % user)
+                raise error.Abort(
+                    _("default bugzilla user %s email not found") % user
+                )
         user = matches['users'][0]['email']
         commands.append(self.makecommandline("id", bugid))
 
@@ -856,13 +912,16 @@
             cmds.append(self.makecommandline("resolution", self.fixresolution))
         self.send_bug_modify_email(bugid, cmds, text, committer)
 
+
 class NotFound(LookupError):
     pass
 
+
 class bzrestapi(bzaccess):
     """Read and write bugzilla data using the REST API available since
     Bugzilla 5.0.
     """
+
     def __init__(self, ui):
         bzaccess.__init__(self, ui)
         bz = self.ui.config('bugzilla', 'bzurl')
@@ -902,14 +961,15 @@
     def _submit(self, burl, data, method='POST'):
         data = json.dumps(data)
         if method == 'PUT':
+
             class putrequest(util.urlreq.request):
                 def get_method(self):
                     return 'PUT'
+
             request_type = putrequest
         else:
             request_type = util.urlreq.request
-        req = request_type(burl, data,
-                           {'Content-Type': 'application/json'})
+        req = request_type(burl, data, {'Content-Type': 'application/json'})
         try:
             resp = url.opener(self.ui).open(req)
             return json.loads(resp.read())
@@ -941,8 +1001,9 @@
             result = self._fetch(burl)
             comments = result['bugs'][pycompat.bytestr(bugid)]['comments']
             if any(sn in c['text'] for c in comments):
-                self.ui.status(_('bug %d already knows about changeset %s\n') %
-                               (bugid, sn))
+                self.ui.status(
+                    _('bug %d already knows about changeset %s\n') % (bugid, sn)
+                )
                 del bugs[bugid]
 
     def updatebug(self, bugid, newstate, text, committer):
@@ -969,11 +1030,10 @@
             self.ui.debug('updated bug %s\n' % bugid)
         else:
             burl = self.apiurl(('bug', bugid, 'comment'))
-            self._submit(burl, {
-                'comment': text,
-                'is_private': False,
-                'is_markdown': False,
-            })
+            self._submit(
+                burl,
+                {'comment': text, 'is_private': False, 'is_markdown': False,},
+            )
             self.ui.debug('added comment to bug %s\n' % bugid)
 
     def notify(self, bugs, committer):
@@ -984,17 +1044,18 @@
         '''
         pass
 
+
 class bugzilla(object):
     # supported versions of bugzilla. different versions have
     # different schemas.
     _versions = {
         '2.16': bzmysql,
         '2.18': bzmysql_2_18,
-        '3.0':  bzmysql_3_0,
+        '3.0': bzmysql_3_0,
         'xmlrpc': bzxmlrpc,
         'xmlrpc+email': bzxmlrpcemail,
         'restapi': bzrestapi,
-        }
+    }
 
     def __init__(self, ui, repo):
         self.ui = ui
@@ -1004,14 +1065,17 @@
         try:
             bzclass = bugzilla._versions[bzversion]
         except KeyError:
-            raise error.Abort(_('bugzilla version %s not supported') %
-                             bzversion)
+            raise error.Abort(
+                _('bugzilla version %s not supported') % bzversion
+            )
         self.bzdriver = bzclass(self.ui)
 
         self.bug_re = re.compile(
-            self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
+            self.ui.config('bugzilla', 'regexp'), re.IGNORECASE
+        )
         self.fix_re = re.compile(
-            self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
+            self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE
+        )
         self.split_re = re.compile(br'\D+')
 
     def find_bugs(self, ctx):
@@ -1084,7 +1148,7 @@
                 c = root.find('/')
                 if c == -1:
                     break
-                root = root[c + 1:]
+                root = root[c + 1 :]
                 count -= 1
             return root
 
@@ -1093,31 +1157,39 @@
         if not tmpl:
             mapfile = self.ui.config('bugzilla', 'style')
         if not mapfile and not tmpl:
-            tmpl = _('changeset {node|short} in repo {root} refers '
-                     'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
+            tmpl = _(
+                'changeset {node|short} in repo {root} refers '
+                'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
+            )
         spec = logcmdutil.templatespec(tmpl, mapfile)
         t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
         self.ui.pushbuffer()
-        t.show(ctx, changes=ctx.changeset(),
-               bug=pycompat.bytestr(bugid),
-               hgweb=self.ui.config('web', 'baseurl'),
-               root=self.repo.root,
-               webroot=webroot(self.repo.root))
+        t.show(
+            ctx,
+            changes=ctx.changeset(),
+            bug=pycompat.bytestr(bugid),
+            hgweb=self.ui.config('web', 'baseurl'),
+            root=self.repo.root,
+            webroot=webroot(self.repo.root),
+        )
         data = self.ui.popbuffer()
-        self.bzdriver.updatebug(bugid, newstate, data,
-                                stringutil.email(ctx.user()))
+        self.bzdriver.updatebug(
+            bugid, newstate, data, stringutil.email(ctx.user())
+        )
 
     def notify(self, bugs, committer):
         '''ensure Bugzilla users are notified of bug change.'''
         self.bzdriver.notify(bugs, committer)
 
+
 def hook(ui, repo, hooktype, node=None, **kwargs):
     '''add comment to bugzilla for each changeset that refers to a
     bugzilla bug id. only add a comment once per bug, so same change
     seen multiple times does not fill bug with duplicate data.'''
     if node is None:
-        raise error.Abort(_('hook type %s does not pass a changeset id') %
-                         hooktype)
+        raise error.Abort(
+            _('hook type %s does not pass a changeset id') % hooktype
+        )
     try:
         bz = bugzilla(ui, repo)
         ctx = repo[node]