diff -r 57875cf423c9 -r 2372284d9457 hgext/bugzilla.py --- 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(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)' - br'\.?\s*(?:h(?:ours?)?\s*(?P\d*(?:\.\d+)?))?') +configitem( + 'bugzilla', + 'fixregexp', + default=( + br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*' + br'(?:nos?\.?|num(?:ber)?s?)?\s*' + br'(?P(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)' + br'\.?\s*(?:h(?:ours?)?\s*(?P\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(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)' - br'\.?\s*(?:h(?:ours?)?\s*(?P\d*(?:\.\d+)?))?') +configitem( + 'bugzilla', + 'regexp', + default=( + br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' + br'(?P(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)' + br'\.?\s*(?:h(?:ours?)?\s*(?P\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]