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. |
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+)?))?') |