hgext/bugzilla.py
changeset 30923 78de43ab585f
parent 30478 f7d66746ec18
child 31570 29fcfb981324
equal deleted inserted replaced
30922:1beeb5185930 30923:78de43ab585f
    13 the Mercurial template mechanism.
    13 the Mercurial template mechanism.
    14 
    14 
    15 The bug references can optionally include an update for Bugzilla of the
    15 The bug references can optionally include an update for Bugzilla of the
    16 hours spent working on the bug. Bugs can also be marked fixed.
    16 hours spent working on the bug. Bugs can also be marked fixed.
    17 
    17 
    18 Three basic modes of access to Bugzilla are provided:
    18 Four basic modes of access to Bugzilla are provided:
    19 
    19 
    20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
    20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
    21 
    21 
    22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
    22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
       
    23 
       
    24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
    23    via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
    25    via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
    24 
    26 
    25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
    27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
    26    using MySQL are supported. Requires Python MySQLdb.
    28    using MySQL are supported. Requires Python MySQLdb.
    27 
    29 
    28 Writing directly to the database is susceptible to schema changes, and
    30 Writing directly to the database is susceptible to schema changes, and
    29 relies on a Bugzilla contrib script to send out bug change
    31 relies on a Bugzilla contrib script to send out bug change
    30 notification emails. This script runs as the user running Mercurial,
    32 notification emails. This script runs as the user running Mercurial,
    48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
    50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
    49 user, the email associated with the Bugzilla username used to log into
    51 user, the email associated with the Bugzilla username used to log into
    50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
    52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
    51 works on all supported Bugzilla versions.
    53 works on all supported Bugzilla versions.
    52 
    54 
       
    55 Access via the REST-API needs either a Bugzilla username and password
       
    56 or an apikey specified in the configuration. Comments are made under
       
    57 the given username or the user assoicated with the apikey in Bugzilla.
       
    58 
    53 Configuration items common to all access modes:
    59 Configuration items common to all access modes:
    54 
    60 
    55 bugzilla.version
    61 bugzilla.version
    56   The access type to use. Values recognized are:
    62   The access type to use. Values recognized are:
    57 
    63 
       
    64   :``restapi``:      Bugzilla REST-API, Bugzilla 5.0 and later.
    58   :``xmlrpc``:       Bugzilla XMLRPC interface.
    65   :``xmlrpc``:       Bugzilla XMLRPC interface.
    59   :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
    66   :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
    60   :``3.0``:          MySQL access, Bugzilla 3.0 and later.
    67   :``3.0``:          MySQL access, Bugzilla 3.0 and later.
    61   :``2.18``:         MySQL access, Bugzilla 2.18 and up to but not
    68   :``2.18``:         MySQL access, Bugzilla 2.18 and up to but not
    62                      including 3.0.
    69                      including 3.0.
   133 
   140 
   134 The ``[usermap]`` section is used to specify mappings of Mercurial
   141 The ``[usermap]`` section is used to specify mappings of Mercurial
   135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
   142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
   136 Contains entries of the form ``committer = Bugzilla user``.
   143 Contains entries of the form ``committer = Bugzilla user``.
   137 
   144 
   138 XMLRPC access mode configuration:
   145 XMLRPC and REST-API access mode configuration:
   139 
   146 
   140 bugzilla.bzurl
   147 bugzilla.bzurl
   141   The base URL for the Bugzilla installation.
   148   The base URL for the Bugzilla installation.
   142   Default ``http://localhost/bugzilla``.
   149   Default ``http://localhost/bugzilla``.
   143 
   150 
   145   The username to use to log into Bugzilla via XMLRPC. Default
   152   The username to use to log into Bugzilla via XMLRPC. Default
   146   ``bugs``.
   153   ``bugs``.
   147 
   154 
   148 bugzilla.password
   155 bugzilla.password
   149   The password for Bugzilla login.
   156   The password for Bugzilla login.
       
   157 
       
   158 REST-API access mode uses the options listed above as well as:
       
   159 
       
   160 bugzilla.apikey
       
   161   An apikey generated on the Bugzilla instance for api access.
       
   162   Using an apikey removes the need to store the user and password
       
   163   options.
   150 
   164 
   151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
   165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
   152 and also:
   166 and also:
   153 
   167 
   154 bugzilla.bzemail
   168 bugzilla.bzemail
   277     Changeset commit comment. Bug 1234.
   291     Changeset commit comment. Bug 1234.
   278 '''
   292 '''
   279 
   293 
   280 from __future__ import absolute_import
   294 from __future__ import absolute_import
   281 
   295 
       
   296 import json
   282 import re
   297 import re
   283 import time
   298 import time
   284 
   299 
   285 from mercurial.i18n import _
   300 from mercurial.i18n import _
   286 from mercurial.node import short
   301 from mercurial.node import short
   287 from mercurial import (
   302 from mercurial import (
   288     cmdutil,
   303     cmdutil,
   289     error,
   304     error,
   290     mail,
   305     mail,
       
   306     url,
   291     util,
   307     util,
   292 )
   308 )
   293 
   309 
   294 urlparse = util.urlparse
   310 urlparse = util.urlparse
   295 xmlrpclib = util.xmlrpclib
   311 xmlrpclib = util.xmlrpclib
   771         if 'fix' in newstate:
   787         if 'fix' in newstate:
   772             cmds.append(self.makecommandline("bug_status", self.fixstatus))
   788             cmds.append(self.makecommandline("bug_status", self.fixstatus))
   773             cmds.append(self.makecommandline("resolution", self.fixresolution))
   789             cmds.append(self.makecommandline("resolution", self.fixresolution))
   774         self.send_bug_modify_email(bugid, cmds, text, committer)
   790         self.send_bug_modify_email(bugid, cmds, text, committer)
   775 
   791 
       
   792 class NotFound(LookupError):
       
   793     pass
       
   794 
       
   795 class bzrestapi(bzaccess):
       
   796     """Read and write bugzilla data using the REST API available since
       
   797     Bugzilla 5.0.
       
   798     """
       
   799     def __init__(self, ui):
       
   800         bzaccess.__init__(self, ui)
       
   801         bz = self.ui.config('bugzilla', 'bzurl',
       
   802                             'http://localhost/bugzilla/')
       
   803         self.bzroot = '/'.join([bz, 'rest'])
       
   804         self.apikey = self.ui.config('bugzilla', 'apikey', '')
       
   805         self.user = self.ui.config('bugzilla', 'user', 'bugs')
       
   806         self.passwd = self.ui.config('bugzilla', 'password')
       
   807         self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
       
   808         self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
       
   809                                             'FIXED')
       
   810 
       
   811     def apiurl(self, targets, include_fields=None):
       
   812         url = '/'.join([self.bzroot] + [str(t) for t in targets])
       
   813         qv = {}
       
   814         if self.apikey:
       
   815             qv['api_key'] = self.apikey
       
   816         elif self.user and self.passwd:
       
   817             qv['login'] = self.user
       
   818             qv['password'] = self.passwd
       
   819         if include_fields:
       
   820             qv['include_fields'] = include_fields
       
   821         if qv:
       
   822             url = '%s?%s' % (url, util.urlreq.urlencode(qv))
       
   823         return url
       
   824 
       
   825     def _fetch(self, burl):
       
   826         try:
       
   827             resp = url.open(self.ui, burl)
       
   828             return json.loads(resp.read())
       
   829         except util.urlerr.httperror as inst:
       
   830             if inst.code == 401:
       
   831                 raise error.Abort(_('authorization failed'))
       
   832             if inst.code == 404:
       
   833                 raise NotFound()
       
   834             else:
       
   835                 raise
       
   836 
       
   837     def _submit(self, burl, data, method='POST'):
       
   838         data = json.dumps(data)
       
   839         if method == 'PUT':
       
   840             class putrequest(util.urlreq.request):
       
   841                 def get_method(self):
       
   842                     return 'PUT'
       
   843             request_type = putrequest
       
   844         else:
       
   845             request_type = util.urlreq.request
       
   846         req = request_type(burl, data,
       
   847                            {'Content-Type': 'application/json'})
       
   848         try:
       
   849             resp = url.opener(self.ui).open(req)
       
   850             return json.loads(resp.read())
       
   851         except util.urlerr.httperror as inst:
       
   852             if inst.code == 401:
       
   853                 raise error.Abort(_('authorization failed'))
       
   854             if inst.code == 404:
       
   855                 raise NotFound()
       
   856             else:
       
   857                 raise
       
   858 
       
   859     def filter_real_bug_ids(self, bugs):
       
   860         '''remove bug IDs that do not exist in Bugzilla from bugs.'''
       
   861         badbugs = set()
       
   862         for bugid in bugs:
       
   863             burl = self.apiurl(('bug', bugid), include_fields='status')
       
   864             try:
       
   865                 self._fetch(burl)
       
   866             except NotFound:
       
   867                 badbugs.add(bugid)
       
   868         for bugid in badbugs:
       
   869             del bugs[bugid]
       
   870 
       
   871     def filter_cset_known_bug_ids(self, node, bugs):
       
   872         '''remove bug IDs where node occurs in comment text from bugs.'''
       
   873         sn = short(node)
       
   874         for bugid in bugs.keys():
       
   875             burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
       
   876             result = self._fetch(burl)
       
   877             comments = result['bugs'][str(bugid)]['comments']
       
   878             if any(sn in c['text'] for c in comments):
       
   879                 self.ui.status(_('bug %d already knows about changeset %s\n') %
       
   880                                (bugid, sn))
       
   881                 del bugs[bugid]
       
   882 
       
   883     def updatebug(self, bugid, newstate, text, committer):
       
   884         '''update the specified bug. Add comment text and set new states.
       
   885 
       
   886         If possible add the comment as being from the committer of
       
   887         the changeset. Otherwise use the default Bugzilla user.
       
   888         '''
       
   889         bugmod = {}
       
   890         if 'hours' in newstate:
       
   891             bugmod['work_time'] = newstate['hours']
       
   892         if 'fix' in newstate:
       
   893             bugmod['status'] = self.fixstatus
       
   894             bugmod['resolution'] = self.fixresolution
       
   895         if bugmod:
       
   896             # if we have to change the bugs state do it here
       
   897             bugmod['comment'] = {
       
   898                 'comment': text,
       
   899                 'is_private': False,
       
   900                 'is_markdown': False,
       
   901             }
       
   902             burl = self.apiurl(('bug', bugid))
       
   903             self._submit(burl, bugmod, method='PUT')
       
   904             self.ui.debug('updated bug %s\n' % bugid)
       
   905         else:
       
   906             burl = self.apiurl(('bug', bugid, 'comment'))
       
   907             self._submit(burl, {
       
   908                 'comment': text,
       
   909                 'is_private': False,
       
   910                 'is_markdown': False,
       
   911             })
       
   912             self.ui.debug('added comment to bug %s\n' % bugid)
       
   913 
       
   914     def notify(self, bugs, committer):
       
   915         '''Force sending of Bugzilla notification emails.
       
   916 
       
   917         Only required if the access method does not trigger notification
       
   918         emails automatically.
       
   919         '''
       
   920         pass
       
   921 
   776 class bugzilla(object):
   922 class bugzilla(object):
   777     # supported versions of bugzilla. different versions have
   923     # supported versions of bugzilla. different versions have
   778     # different schemas.
   924     # different schemas.
   779     _versions = {
   925     _versions = {
   780         '2.16': bzmysql,
   926         '2.16': bzmysql,
   781         '2.18': bzmysql_2_18,
   927         '2.18': bzmysql_2_18,
   782         '3.0':  bzmysql_3_0,
   928         '3.0':  bzmysql_3_0,
   783         'xmlrpc': bzxmlrpc,
   929         'xmlrpc': bzxmlrpc,
   784         'xmlrpc+email': bzxmlrpcemail
   930         'xmlrpc+email': bzxmlrpcemail,
       
   931         'restapi': bzrestapi,
   785         }
   932         }
   786 
   933 
   787     _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
   934     _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
   788                        r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
   935                        r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
   789                        r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
   936                        r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')