##// END OF EJS Templates
bugzilla: fix reporting of exceptions with py3
Mads Kiilerich -
r46694:1d252e73 stable
parent child Browse files
Show More
@@ -1,1214 +1,1214 b''
1 # bugzilla.py - bugzilla integration for mercurial
1 # bugzilla.py - bugzilla integration for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''hooks for integrating with the Bugzilla bug tracker
9 '''hooks for integrating with the Bugzilla bug tracker
10
10
11 This hook extension adds comments on bugs in Bugzilla when changesets
11 This hook extension adds comments on bugs in Bugzilla when changesets
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
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 Four 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 REST-API. Requires bugzilla 5.0 or later.
20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21
21
22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23
23
24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 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.
26
26
27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 using MySQL are supported. Requires Python MySQLdb.
28 using MySQL are supported. Requires Python MySQLdb.
29
29
30 Writing directly to the database is susceptible to schema changes, and
30 Writing directly to the database is susceptible to schema changes, and
31 relies on a Bugzilla contrib script to send out bug change
31 relies on a Bugzilla contrib script to send out bug change
32 notification emails. This script runs as the user running Mercurial,
32 notification emails. This script runs as the user running Mercurial,
33 must be run on the host with the Bugzilla install, and requires
33 must be run on the host with the Bugzilla install, and requires
34 permission to read Bugzilla configuration details and the necessary
34 permission to read Bugzilla configuration details and the necessary
35 MySQL user and password to have full access rights to the Bugzilla
35 MySQL user and password to have full access rights to the Bugzilla
36 database. For these reasons this access mode is now considered
36 database. For these reasons this access mode is now considered
37 deprecated, and will not be updated for new Bugzilla versions going
37 deprecated, and will not be updated for new Bugzilla versions going
38 forward. Only adding comments is supported in this access mode.
38 forward. Only adding comments is supported in this access mode.
39
39
40 Access via XMLRPC needs a Bugzilla username and password to be specified
40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 in the configuration. Comments are added under that username. Since the
41 in the configuration. Comments are added under that username. Since the
42 configuration must be readable by all Mercurial users, it is recommended
42 configuration must be readable by all Mercurial users, it is recommended
43 that the rights of that user are restricted in Bugzilla to the minimum
43 that the rights of that user are restricted in Bugzilla to the minimum
44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45
45
46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 email to the Bugzilla email interface to submit comments to bugs.
47 email to the Bugzilla email interface to submit comments to bugs.
48 The From: address in the email is set to the email address of the Mercurial
48 The From: address in the email is set to the email address of the Mercurial
49 user, so the comment appears to come from the Mercurial user. In the event
49 user, so the comment appears to come from the Mercurial user. In the event
50 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
51 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
52 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
53 works on all supported Bugzilla versions.
53 works on all supported Bugzilla versions.
54
54
55 Access via the REST-API needs either a Bugzilla username and password
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
56 or an apikey specified in the configuration. Comments are made under
57 the given username or the user associated with the apikey in Bugzilla.
57 the given username or the user associated with the apikey in Bugzilla.
58
58
59 Configuration items common to all access modes:
59 Configuration items common to all access modes:
60
60
61 bugzilla.version
61 bugzilla.version
62 The access type to use. Values recognized are:
62 The access type to use. Values recognized are:
63
63
64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 :``xmlrpc``: Bugzilla XMLRPC interface.
65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 :``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
69 including 3.0.
69 including 3.0.
70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 including 2.18.
71 including 2.18.
72
72
73 bugzilla.regexp
73 bugzilla.regexp
74 Regular expression to match bug IDs for update in changeset commit message.
74 Regular expression to match bug IDs for update in changeset commit message.
75 It must contain one "()" named group ``<ids>`` containing the bug
75 It must contain one "()" named group ``<ids>`` containing the bug
76 IDs separated by non-digit characters. It may also contain
76 IDs separated by non-digit characters. It may also contain
77 a named group ``<hours>`` with a floating-point number giving the
77 a named group ``<hours>`` with a floating-point number giving the
78 hours worked on the bug. If no named groups are present, the first
78 hours worked on the bug. If no named groups are present, the first
79 "()" group is assumed to contain the bug IDs, and work time is not
79 "()" group is assumed to contain the bug IDs, and work time is not
80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 variations thereof, followed by an hours number prefixed by ``h`` or
82 variations thereof, followed by an hours number prefixed by ``h`` or
83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84
84
85 bugzilla.fixregexp
85 bugzilla.fixregexp
86 Regular expression to match bug IDs for marking fixed in changeset
86 Regular expression to match bug IDs for marking fixed in changeset
87 commit message. This must contain a "()" named group ``<ids>` containing
87 commit message. This must contain a "()" named group ``<ids>` containing
88 the bug IDs separated by non-digit characters. It may also contain
88 the bug IDs separated by non-digit characters. It may also contain
89 a named group ``<hours>`` with a floating-point number giving the
89 a named group ``<hours>`` with a floating-point number giving the
90 hours worked on the bug. If no named groups are present, the first
90 hours worked on the bug. If no named groups are present, the first
91 "()" group is assumed to contain the bug IDs, and work time is not
91 "()" group is assumed to contain the bug IDs, and work time is not
92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 variations thereof, followed by an hours number prefixed by ``h`` or
94 variations thereof, followed by an hours number prefixed by ``h`` or
95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96
96
97 bugzilla.fixstatus
97 bugzilla.fixstatus
98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99
99
100 bugzilla.fixresolution
100 bugzilla.fixresolution
101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102
102
103 bugzilla.style
103 bugzilla.style
104 The style file to use when formatting comments.
104 The style file to use when formatting comments.
105
105
106 bugzilla.template
106 bugzilla.template
107 Template to use when formatting comments. Overrides style if
107 Template to use when formatting comments. Overrides style if
108 specified. In addition to the usual Mercurial keywords, the
108 specified. In addition to the usual Mercurial keywords, the
109 extension specifies:
109 extension specifies:
110
110
111 :``{bug}``: The Bugzilla bug ID.
111 :``{bug}``: The Bugzilla bug ID.
112 :``{root}``: The full pathname of the Mercurial repository.
112 :``{root}``: The full pathname of the Mercurial repository.
113 :``{webroot}``: Stripped pathname of the Mercurial repository.
113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115
115
116 Default ``changeset {node|short} in repo {root} refers to bug
116 Default ``changeset {node|short} in repo {root} refers to bug
117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118
118
119 bugzilla.strip
119 bugzilla.strip
120 The number of path separator characters to strip from the front of
120 The number of path separator characters to strip from the front of
121 the Mercurial repository path (``{root}`` in templates) to produce
121 the Mercurial repository path (``{root}`` in templates) to produce
122 ``{webroot}``. For example, a repository with ``{root}``
122 ``{webroot}``. For example, a repository with ``{root}``
123 ``/var/local/my-project`` with a strip of 2 gives a value for
123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 ``{webroot}`` of ``my-project``. Default 0.
124 ``{webroot}`` of ``my-project``. Default 0.
125
125
126 web.baseurl
126 web.baseurl
127 Base URL for browsing Mercurial repositories. Referenced from
127 Base URL for browsing Mercurial repositories. Referenced from
128 templates as ``{hgweb}``.
128 templates as ``{hgweb}``.
129
129
130 Configuration items common to XMLRPC+email and MySQL access modes:
130 Configuration items common to XMLRPC+email and MySQL access modes:
131
131
132 bugzilla.usermap
132 bugzilla.usermap
133 Path of file containing Mercurial committer email to Bugzilla user email
133 Path of file containing Mercurial committer email to Bugzilla user email
134 mappings. If specified, the file should contain one mapping per
134 mappings. If specified, the file should contain one mapping per
135 line::
135 line::
136
136
137 committer = Bugzilla user
137 committer = Bugzilla user
138
138
139 See also the ``[usermap]`` section.
139 See also the ``[usermap]`` section.
140
140
141 The ``[usermap]`` section is used to specify mappings of Mercurial
141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 Contains entries of the form ``committer = Bugzilla user``.
143 Contains entries of the form ``committer = Bugzilla user``.
144
144
145 XMLRPC and REST-API access mode configuration:
145 XMLRPC and REST-API access mode configuration:
146
146
147 bugzilla.bzurl
147 bugzilla.bzurl
148 The base URL for the Bugzilla installation.
148 The base URL for the Bugzilla installation.
149 Default ``http://localhost/bugzilla``.
149 Default ``http://localhost/bugzilla``.
150
150
151 bugzilla.user
151 bugzilla.user
152 The username to use to log into Bugzilla via XMLRPC. Default
152 The username to use to log into Bugzilla via XMLRPC. Default
153 ``bugs``.
153 ``bugs``.
154
154
155 bugzilla.password
155 bugzilla.password
156 The password for Bugzilla login.
156 The password for Bugzilla login.
157
157
158 REST-API access mode uses the options listed above as well as:
158 REST-API access mode uses the options listed above as well as:
159
159
160 bugzilla.apikey
160 bugzilla.apikey
161 An apikey generated on the Bugzilla instance for api access.
161 An apikey generated on the Bugzilla instance for api access.
162 Using an apikey removes the need to store the user and password
162 Using an apikey removes the need to store the user and password
163 options.
163 options.
164
164
165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 and also:
166 and also:
167
167
168 bugzilla.bzemail
168 bugzilla.bzemail
169 The Bugzilla email address.
169 The Bugzilla email address.
170
170
171 In addition, the Mercurial email settings must be configured. See the
171 In addition, the Mercurial email settings must be configured. See the
172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173
173
174 MySQL access mode configuration:
174 MySQL access mode configuration:
175
175
176 bugzilla.host
176 bugzilla.host
177 Hostname of the MySQL server holding the Bugzilla database.
177 Hostname of the MySQL server holding the Bugzilla database.
178 Default ``localhost``.
178 Default ``localhost``.
179
179
180 bugzilla.db
180 bugzilla.db
181 Name of the Bugzilla database in MySQL. Default ``bugs``.
181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182
182
183 bugzilla.user
183 bugzilla.user
184 Username to use to access MySQL server. Default ``bugs``.
184 Username to use to access MySQL server. Default ``bugs``.
185
185
186 bugzilla.password
186 bugzilla.password
187 Password to use to access MySQL server.
187 Password to use to access MySQL server.
188
188
189 bugzilla.timeout
189 bugzilla.timeout
190 Database connection timeout (seconds). Default 5.
190 Database connection timeout (seconds). Default 5.
191
191
192 bugzilla.bzuser
192 bugzilla.bzuser
193 Fallback Bugzilla user name to record comments with, if changeset
193 Fallback Bugzilla user name to record comments with, if changeset
194 committer cannot be found as a Bugzilla user.
194 committer cannot be found as a Bugzilla user.
195
195
196 bugzilla.bzdir
196 bugzilla.bzdir
197 Bugzilla install directory. Used by default notify. Default
197 Bugzilla install directory. Used by default notify. Default
198 ``/var/www/html/bugzilla``.
198 ``/var/www/html/bugzilla``.
199
199
200 bugzilla.notify
200 bugzilla.notify
201 The command to run to get Bugzilla to send bug change notification
201 The command to run to get Bugzilla to send bug change notification
202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 id) and ``user`` (committer bugzilla email). Default depends on
203 id) and ``user`` (committer bugzilla email). Default depends on
204 version; from 2.18 it is "cd %(bzdir)s && perl -T
204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 contrib/sendbugmail.pl %(id)s %(user)s".
205 contrib/sendbugmail.pl %(id)s %(user)s".
206
206
207 Activating the extension::
207 Activating the extension::
208
208
209 [extensions]
209 [extensions]
210 bugzilla =
210 bugzilla =
211
211
212 [hooks]
212 [hooks]
213 # run bugzilla hook on every change pulled or pushed in here
213 # run bugzilla hook on every change pulled or pushed in here
214 incoming.bugzilla = python:hgext.bugzilla.hook
214 incoming.bugzilla = python:hgext.bugzilla.hook
215
215
216 Example configurations:
216 Example configurations:
217
217
218 XMLRPC example configuration. This uses the Bugzilla at
218 XMLRPC example configuration. This uses the Bugzilla at
219 ``http://my-project.org/bugzilla``, logging in as user
219 ``http://my-project.org/bugzilla``, logging in as user
220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 with a web interface at ``http://my-project.org/hg``. ::
222 with a web interface at ``http://my-project.org/hg``. ::
223
223
224 [bugzilla]
224 [bugzilla]
225 bzurl=http://my-project.org/bugzilla
225 bzurl=http://my-project.org/bugzilla
226 user=bugmail@my-project.org
226 user=bugmail@my-project.org
227 password=plugh
227 password=plugh
228 version=xmlrpc
228 version=xmlrpc
229 template=Changeset {node|short} in {root|basename}.
229 template=Changeset {node|short} in {root|basename}.
230 {hgweb}/{webroot}/rev/{node|short}\\n
230 {hgweb}/{webroot}/rev/{node|short}\\n
231 {desc}\\n
231 {desc}\\n
232 strip=5
232 strip=5
233
233
234 [web]
234 [web]
235 baseurl=http://my-project.org/hg
235 baseurl=http://my-project.org/hg
236
236
237 XMLRPC+email example configuration. This uses the Bugzilla at
237 XMLRPC+email example configuration. This uses the Bugzilla at
238 ``http://my-project.org/bugzilla``, logging in as user
238 ``http://my-project.org/bugzilla``, logging in as user
239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 with a web interface at ``http://my-project.org/hg``. Bug comments
241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 are sent to the Bugzilla email address
242 are sent to the Bugzilla email address
243 ``bugzilla@my-project.org``. ::
243 ``bugzilla@my-project.org``. ::
244
244
245 [bugzilla]
245 [bugzilla]
246 bzurl=http://my-project.org/bugzilla
246 bzurl=http://my-project.org/bugzilla
247 user=bugmail@my-project.org
247 user=bugmail@my-project.org
248 password=plugh
248 password=plugh
249 version=xmlrpc+email
249 version=xmlrpc+email
250 bzemail=bugzilla@my-project.org
250 bzemail=bugzilla@my-project.org
251 template=Changeset {node|short} in {root|basename}.
251 template=Changeset {node|short} in {root|basename}.
252 {hgweb}/{webroot}/rev/{node|short}\\n
252 {hgweb}/{webroot}/rev/{node|short}\\n
253 {desc}\\n
253 {desc}\\n
254 strip=5
254 strip=5
255
255
256 [web]
256 [web]
257 baseurl=http://my-project.org/hg
257 baseurl=http://my-project.org/hg
258
258
259 [usermap]
259 [usermap]
260 user@emaildomain.com=user.name@bugzilladomain.com
260 user@emaildomain.com=user.name@bugzilladomain.com
261
261
262 MySQL example configuration. This has a local Bugzilla 3.2 installation
262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 the Bugzilla database name is ``bugs`` and MySQL is
264 the Bugzilla database name is ``bugs`` and MySQL is
265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 with a web interface at ``http://my-project.org/hg``. ::
267 with a web interface at ``http://my-project.org/hg``. ::
268
268
269 [bugzilla]
269 [bugzilla]
270 host=localhost
270 host=localhost
271 password=XYZZY
271 password=XYZZY
272 version=3.0
272 version=3.0
273 bzuser=unknown@domain.com
273 bzuser=unknown@domain.com
274 bzdir=/opt/bugzilla-3.2
274 bzdir=/opt/bugzilla-3.2
275 template=Changeset {node|short} in {root|basename}.
275 template=Changeset {node|short} in {root|basename}.
276 {hgweb}/{webroot}/rev/{node|short}\\n
276 {hgweb}/{webroot}/rev/{node|short}\\n
277 {desc}\\n
277 {desc}\\n
278 strip=5
278 strip=5
279
279
280 [web]
280 [web]
281 baseurl=http://my-project.org/hg
281 baseurl=http://my-project.org/hg
282
282
283 [usermap]
283 [usermap]
284 user@emaildomain.com=user.name@bugzilladomain.com
284 user@emaildomain.com=user.name@bugzilladomain.com
285
285
286 All the above add a comment to the Bugzilla bug record of the form::
286 All the above add a comment to the Bugzilla bug record of the form::
287
287
288 Changeset 3b16791d6642 in repository-name.
288 Changeset 3b16791d6642 in repository-name.
289 http://my-project.org/hg/repository-name/rev/3b16791d6642
289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290
290
291 Changeset commit comment. Bug 1234.
291 Changeset commit comment. Bug 1234.
292 '''
292 '''
293
293
294 from __future__ import absolute_import
294 from __future__ import absolute_import
295
295
296 import json
296 import json
297 import re
297 import re
298 import time
298 import time
299
299
300 from mercurial.i18n import _
300 from mercurial.i18n import _
301 from mercurial.node import short
301 from mercurial.node import short
302 from mercurial import (
302 from mercurial import (
303 error,
303 error,
304 logcmdutil,
304 logcmdutil,
305 mail,
305 mail,
306 pycompat,
306 pycompat,
307 registrar,
307 registrar,
308 url,
308 url,
309 util,
309 util,
310 )
310 )
311 from mercurial.utils import (
311 from mercurial.utils import (
312 procutil,
312 procutil,
313 stringutil,
313 stringutil,
314 )
314 )
315
315
316 xmlrpclib = util.xmlrpclib
316 xmlrpclib = util.xmlrpclib
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 = b'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 b'bugzilla', b'apikey', default=b'',
328 b'bugzilla', b'apikey', default=b'',
329 )
329 )
330 configitem(
330 configitem(
331 b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla',
331 b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla',
332 )
332 )
333 configitem(
333 configitem(
334 b'bugzilla', b'bzemail', default=None,
334 b'bugzilla', b'bzemail', default=None,
335 )
335 )
336 configitem(
336 configitem(
337 b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/',
337 b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/',
338 )
338 )
339 configitem(
339 configitem(
340 b'bugzilla', b'bzuser', default=None,
340 b'bugzilla', b'bzuser', default=None,
341 )
341 )
342 configitem(
342 configitem(
343 b'bugzilla', b'db', default=b'bugs',
343 b'bugzilla', b'db', default=b'bugs',
344 )
344 )
345 configitem(
345 configitem(
346 b'bugzilla',
346 b'bugzilla',
347 b'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 b'bugzilla', b'fixresolution', default=b'FIXED',
356 b'bugzilla', b'fixresolution', default=b'FIXED',
357 )
357 )
358 configitem(
358 configitem(
359 b'bugzilla', b'fixstatus', default=b'RESOLVED',
359 b'bugzilla', b'fixstatus', default=b'RESOLVED',
360 )
360 )
361 configitem(
361 configitem(
362 b'bugzilla', b'host', default=b'localhost',
362 b'bugzilla', b'host', default=b'localhost',
363 )
363 )
364 configitem(
364 configitem(
365 b'bugzilla', b'notify', default=configitem.dynamicdefault,
365 b'bugzilla', b'notify', default=configitem.dynamicdefault,
366 )
366 )
367 configitem(
367 configitem(
368 b'bugzilla', b'password', default=None,
368 b'bugzilla', b'password', default=None,
369 )
369 )
370 configitem(
370 configitem(
371 b'bugzilla',
371 b'bugzilla',
372 b'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 b'bugzilla', b'strip', default=0,
380 b'bugzilla', b'strip', default=0,
381 )
381 )
382 configitem(
382 configitem(
383 b'bugzilla', b'style', default=None,
383 b'bugzilla', b'style', default=None,
384 )
384 )
385 configitem(
385 configitem(
386 b'bugzilla', b'template', default=None,
386 b'bugzilla', b'template', default=None,
387 )
387 )
388 configitem(
388 configitem(
389 b'bugzilla', b'timeout', default=5,
389 b'bugzilla', b'timeout', default=5,
390 )
390 )
391 configitem(
391 configitem(
392 b'bugzilla', b'user', default=b'bugs',
392 b'bugzilla', b'user', default=b'bugs',
393 )
393 )
394 configitem(
394 configitem(
395 b'bugzilla', b'usermap', default=None,
395 b'bugzilla', b'usermap', default=None,
396 )
396 )
397 configitem(
397 configitem(
398 b'bugzilla', b'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(b'bugzilla', b'usermap')
407 usermap = self.ui.config(b'bugzilla', b'usermap')
408 if usermap:
408 if usermap:
409 self.ui.readconfig(usermap, sections=[b'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(b'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.
419 #
419 #
420 # 'bugs' is a dict keyed on bug id, where values are a dict holding
420 # 'bugs' is a dict keyed on bug id, where values are a dict holding
421 # updates to bug state. Recognized dict keys are:
421 # updates to bug state. Recognized dict keys are:
422 #
422 #
423 # 'hours': Value, float containing work hours to be updated.
423 # 'hours': Value, float containing work hours to be updated.
424 # 'fix': If key present, bug is to be marked fixed. Value ignored.
424 # 'fix': If key present, bug is to be marked fixed. Value ignored.
425
425
426 def filter_real_bug_ids(self, bugs):
426 def filter_real_bug_ids(self, bugs):
427 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
427 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
428
428
429 def filter_cset_known_bug_ids(self, node, bugs):
429 def filter_cset_known_bug_ids(self, node, bugs):
430 '''remove bug IDs where node occurs in comment text from bugs.'''
430 '''remove bug IDs where node occurs in comment text from bugs.'''
431
431
432 def updatebug(self, bugid, newstate, text, committer):
432 def updatebug(self, bugid, newstate, text, committer):
433 '''update the specified bug. Add comment text and set new states.
433 '''update the specified bug. Add comment text and set new states.
434
434
435 If possible add the comment as being from the committer of
435 If possible add the comment as being from the committer of
436 the changeset. Otherwise use the default Bugzilla user.
436 the changeset. Otherwise use the default Bugzilla user.
437 '''
437 '''
438
438
439 def notify(self, bugs, committer):
439 def notify(self, bugs, committer):
440 '''Force sending of Bugzilla notification emails.
440 '''Force sending of Bugzilla notification emails.
441
441
442 Only required if the access method does not trigger notification
442 Only required if the access method does not trigger notification
443 emails automatically.
443 emails automatically.
444 '''
444 '''
445
445
446
446
447 # Bugzilla via direct access to MySQL database.
447 # Bugzilla via direct access to MySQL database.
448 class bzmysql(bzaccess):
448 class bzmysql(bzaccess):
449 '''Support for direct MySQL access to Bugzilla.
449 '''Support for direct MySQL access to Bugzilla.
450
450
451 The earliest Bugzilla version this is tested with is version 2.16.
451 The earliest Bugzilla version this is tested with is version 2.16.
452
452
453 If your Bugzilla is version 3.4 or above, you are strongly
453 If your Bugzilla is version 3.4 or above, you are strongly
454 recommended to use the XMLRPC access method instead.
454 recommended to use the XMLRPC access method instead.
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 b'(' + b','.join(map(str, ids)) + b')'
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(
470 raise error.Abort(
471 _(b'python mysql support not available: %s') % err
471 _(b'python mysql support not available: %s') % err
472 )
472 )
473
473
474 bzaccess.__init__(self, ui)
474 bzaccess.__init__(self, ui)
475
475
476 host = self.ui.config(b'bugzilla', b'host')
476 host = self.ui.config(b'bugzilla', b'host')
477 user = self.ui.config(b'bugzilla', b'user')
477 user = self.ui.config(b'bugzilla', b'user')
478 passwd = self.ui.config(b'bugzilla', b'password')
478 passwd = self.ui.config(b'bugzilla', b'password')
479 db = self.ui.config(b'bugzilla', b'db')
479 db = self.ui.config(b'bugzilla', b'db')
480 timeout = int(self.ui.config(b'bugzilla', b'timeout'))
480 timeout = int(self.ui.config(b'bugzilla', b'timeout'))
481 self.ui.note(
481 self.ui.note(
482 _(b'connecting to %s:%s as %s, password %s\n')
482 _(b'connecting to %s:%s as %s, password %s\n')
483 % (host, db, user, b'*' * len(passwd))
483 % (host, db, user, b'*' * len(passwd))
484 )
484 )
485 self.conn = bzmysql._MySQLdb.connect(
485 self.conn = bzmysql._MySQLdb.connect(
486 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
486 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
487 )
487 )
488 self.cursor = self.conn.cursor()
488 self.cursor = self.conn.cursor()
489 self.longdesc_id = self.get_longdesc_id()
489 self.longdesc_id = self.get_longdesc_id()
490 self.user_ids = {}
490 self.user_ids = {}
491 self.default_notify = b"cd %(bzdir)s && ./processmail %(id)s %(user)s"
491 self.default_notify = b"cd %(bzdir)s && ./processmail %(id)s %(user)s"
492
492
493 def run(self, *args, **kwargs):
493 def run(self, *args, **kwargs):
494 '''run a query.'''
494 '''run a query.'''
495 self.ui.note(_(b'query: %s %s\n') % (args, kwargs))
495 self.ui.note(_(b'query: %s %s\n') % (args, kwargs))
496 try:
496 try:
497 self.cursor.execute(*args, **kwargs)
497 self.cursor.execute(*args, **kwargs)
498 except bzmysql._MySQLdb.MySQLError:
498 except bzmysql._MySQLdb.MySQLError:
499 self.ui.note(_(b'failed query: %s %s\n') % (args, kwargs))
499 self.ui.note(_(b'failed query: %s %s\n') % (args, kwargs))
500 raise
500 raise
501
501
502 def get_longdesc_id(self):
502 def get_longdesc_id(self):
503 '''get identity of longdesc field'''
503 '''get identity of longdesc field'''
504 self.run(b'select fieldid from fielddefs where name = "longdesc"')
504 self.run(b'select fieldid from fielddefs where name = "longdesc"')
505 ids = self.cursor.fetchall()
505 ids = self.cursor.fetchall()
506 if len(ids) != 1:
506 if len(ids) != 1:
507 raise error.Abort(_(b'unknown database schema'))
507 raise error.Abort(_(b'unknown database schema'))
508 return ids[0][0]
508 return ids[0][0]
509
509
510 def filter_real_bug_ids(self, bugs):
510 def filter_real_bug_ids(self, bugs):
511 '''filter not-existing bugs from set.'''
511 '''filter not-existing bugs from set.'''
512 self.run(
512 self.run(
513 b'select bug_id from bugs where bug_id in %s'
513 b'select bug_id from bugs where bug_id in %s'
514 % bzmysql.sql_buglist(bugs.keys())
514 % bzmysql.sql_buglist(bugs.keys())
515 )
515 )
516 existing = [id for (id,) in self.cursor.fetchall()]
516 existing = [id for (id,) in self.cursor.fetchall()]
517 for id in bugs.keys():
517 for id in bugs.keys():
518 if id not in existing:
518 if id not in existing:
519 self.ui.status(_(b'bug %d does not exist\n') % id)
519 self.ui.status(_(b'bug %d does not exist\n') % id)
520 del bugs[id]
520 del bugs[id]
521
521
522 def filter_cset_known_bug_ids(self, node, bugs):
522 def filter_cset_known_bug_ids(self, node, bugs):
523 '''filter bug ids that already refer to this changeset from set.'''
523 '''filter bug ids that already refer to this changeset from set.'''
524 self.run(
524 self.run(
525 '''select bug_id from longdescs where
525 '''select bug_id from longdescs where
526 bug_id in %s and thetext like "%%%s%%"'''
526 bug_id in %s and thetext like "%%%s%%"'''
527 % (bzmysql.sql_buglist(bugs.keys()), short(node))
527 % (bzmysql.sql_buglist(bugs.keys()), short(node))
528 )
528 )
529 for (id,) in self.cursor.fetchall():
529 for (id,) in self.cursor.fetchall():
530 self.ui.status(
530 self.ui.status(
531 _(b'bug %d already knows about changeset %s\n')
531 _(b'bug %d already knows about changeset %s\n')
532 % (id, short(node))
532 % (id, short(node))
533 )
533 )
534 del bugs[id]
534 del bugs[id]
535
535
536 def notify(self, bugs, committer):
536 def notify(self, bugs, committer):
537 '''tell bugzilla to send mail.'''
537 '''tell bugzilla to send mail.'''
538 self.ui.status(_(b'telling bugzilla to send mail:\n'))
538 self.ui.status(_(b'telling bugzilla to send mail:\n'))
539 (user, userid) = self.get_bugzilla_user(committer)
539 (user, userid) = self.get_bugzilla_user(committer)
540 for id in bugs.keys():
540 for id in bugs.keys():
541 self.ui.status(_(b' bug %s\n') % id)
541 self.ui.status(_(b' bug %s\n') % id)
542 cmdfmt = self.ui.config(b'bugzilla', b'notify', self.default_notify)
542 cmdfmt = self.ui.config(b'bugzilla', b'notify', self.default_notify)
543 bzdir = self.ui.config(b'bugzilla', b'bzdir')
543 bzdir = self.ui.config(b'bugzilla', b'bzdir')
544 try:
544 try:
545 # Backwards-compatible with old notify string, which
545 # Backwards-compatible with old notify string, which
546 # took one string. This will throw with a new format
546 # took one string. This will throw with a new format
547 # string.
547 # string.
548 cmd = cmdfmt % id
548 cmd = cmdfmt % id
549 except TypeError:
549 except TypeError:
550 cmd = cmdfmt % {b'bzdir': bzdir, b'id': id, b'user': user}
550 cmd = cmdfmt % {b'bzdir': bzdir, b'id': id, b'user': user}
551 self.ui.note(_(b'running notify command %s\n') % cmd)
551 self.ui.note(_(b'running notify command %s\n') % cmd)
552 fp = procutil.popen(b'(%s) 2>&1' % cmd, b'rb')
552 fp = procutil.popen(b'(%s) 2>&1' % cmd, b'rb')
553 out = util.fromnativeeol(fp.read())
553 out = util.fromnativeeol(fp.read())
554 ret = fp.close()
554 ret = fp.close()
555 if ret:
555 if ret:
556 self.ui.warn(out)
556 self.ui.warn(out)
557 raise error.Abort(
557 raise error.Abort(
558 _(b'bugzilla notify command %s') % procutil.explainexit(ret)
558 _(b'bugzilla notify command %s') % procutil.explainexit(ret)
559 )
559 )
560 self.ui.status(_(b'done\n'))
560 self.ui.status(_(b'done\n'))
561
561
562 def get_user_id(self, user):
562 def get_user_id(self, user):
563 '''look up numeric bugzilla user id.'''
563 '''look up numeric bugzilla user id.'''
564 try:
564 try:
565 return self.user_ids[user]
565 return self.user_ids[user]
566 except KeyError:
566 except KeyError:
567 try:
567 try:
568 userid = int(user)
568 userid = int(user)
569 except ValueError:
569 except ValueError:
570 self.ui.note(_(b'looking up user %s\n') % user)
570 self.ui.note(_(b'looking up user %s\n') % user)
571 self.run(
571 self.run(
572 '''select userid from profiles
572 '''select userid from profiles
573 where login_name like %s''',
573 where login_name like %s''',
574 user,
574 user,
575 )
575 )
576 all = self.cursor.fetchall()
576 all = self.cursor.fetchall()
577 if len(all) != 1:
577 if len(all) != 1:
578 raise KeyError(user)
578 raise KeyError(user)
579 userid = int(all[0][0])
579 userid = int(all[0][0])
580 self.user_ids[user] = userid
580 self.user_ids[user] = userid
581 return userid
581 return userid
582
582
583 def get_bugzilla_user(self, committer):
583 def get_bugzilla_user(self, committer):
584 '''See if committer is a registered bugzilla user. Return
584 '''See if committer is a registered bugzilla user. Return
585 bugzilla username and userid if so. If not, return default
585 bugzilla username and userid if so. If not, return default
586 bugzilla username and userid.'''
586 bugzilla username and userid.'''
587 user = self.map_committer(committer)
587 user = self.map_committer(committer)
588 try:
588 try:
589 userid = self.get_user_id(user)
589 userid = self.get_user_id(user)
590 except KeyError:
590 except KeyError:
591 try:
591 try:
592 defaultuser = self.ui.config(b'bugzilla', b'bzuser')
592 defaultuser = self.ui.config(b'bugzilla', b'bzuser')
593 if not defaultuser:
593 if not defaultuser:
594 raise error.Abort(
594 raise error.Abort(
595 _(b'cannot find bugzilla user id for %s') % user
595 _(b'cannot find bugzilla user id for %s') % user
596 )
596 )
597 userid = self.get_user_id(defaultuser)
597 userid = self.get_user_id(defaultuser)
598 user = defaultuser
598 user = defaultuser
599 except KeyError:
599 except KeyError:
600 raise error.Abort(
600 raise error.Abort(
601 _(b'cannot find bugzilla user id for %s or %s')
601 _(b'cannot find bugzilla user id for %s or %s')
602 % (user, defaultuser)
602 % (user, defaultuser)
603 )
603 )
604 return (user, userid)
604 return (user, userid)
605
605
606 def updatebug(self, bugid, newstate, text, committer):
606 def updatebug(self, bugid, newstate, text, committer):
607 '''update bug state with comment text.
607 '''update bug state with comment text.
608
608
609 Try adding comment as committer of changeset, otherwise as
609 Try adding comment as committer of changeset, otherwise as
610 default bugzilla user.'''
610 default bugzilla user.'''
611 if len(newstate) > 0:
611 if len(newstate) > 0:
612 self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n"))
612 self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n"))
613
613
614 (user, userid) = self.get_bugzilla_user(committer)
614 (user, userid) = self.get_bugzilla_user(committer)
615 now = time.strftime('%Y-%m-%d %H:%M:%S')
615 now = time.strftime('%Y-%m-%d %H:%M:%S')
616 self.run(
616 self.run(
617 '''insert into longdescs
617 '''insert into longdescs
618 (bug_id, who, bug_when, thetext)
618 (bug_id, who, bug_when, thetext)
619 values (%s, %s, %s, %s)''',
619 values (%s, %s, %s, %s)''',
620 (bugid, userid, now, text),
620 (bugid, userid, now, text),
621 )
621 )
622 self.run(
622 self.run(
623 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
623 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
624 values (%s, %s, %s, %s)''',
624 values (%s, %s, %s, %s)''',
625 (bugid, userid, now, self.longdesc_id),
625 (bugid, userid, now, self.longdesc_id),
626 )
626 )
627 self.conn.commit()
627 self.conn.commit()
628
628
629
629
630 class bzmysql_2_18(bzmysql):
630 class bzmysql_2_18(bzmysql):
631 '''support for bugzilla 2.18 series.'''
631 '''support for bugzilla 2.18 series.'''
632
632
633 def __init__(self, ui):
633 def __init__(self, ui):
634 bzmysql.__init__(self, ui)
634 bzmysql.__init__(self, ui)
635 self.default_notify = (
635 self.default_notify = (
636 b"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"
637 )
637 )
638
638
639
639
640 class bzmysql_3_0(bzmysql_2_18):
640 class bzmysql_3_0(bzmysql_2_18):
641 '''support for bugzilla 3.0 series.'''
641 '''support for bugzilla 3.0 series.'''
642
642
643 def __init__(self, ui):
643 def __init__(self, ui):
644 bzmysql_2_18.__init__(self, ui)
644 bzmysql_2_18.__init__(self, ui)
645
645
646 def get_longdesc_id(self):
646 def get_longdesc_id(self):
647 '''get identity of longdesc field'''
647 '''get identity of longdesc field'''
648 self.run(b'select id from fielddefs where name = "longdesc"')
648 self.run(b'select id from fielddefs where name = "longdesc"')
649 ids = self.cursor.fetchall()
649 ids = self.cursor.fetchall()
650 if len(ids) != 1:
650 if len(ids) != 1:
651 raise error.Abort(_(b'unknown database schema'))
651 raise error.Abort(_(b'unknown database schema'))
652 return ids[0][0]
652 return ids[0][0]
653
653
654
654
655 # Bugzilla via XMLRPC interface.
655 # Bugzilla via XMLRPC interface.
656
656
657
657
658 class cookietransportrequest(object):
658 class cookietransportrequest(object):
659 """A Transport request method that retains cookies over its lifetime.
659 """A Transport request method that retains cookies over its lifetime.
660
660
661 The regular xmlrpclib transports ignore cookies. Which causes
661 The regular xmlrpclib transports ignore cookies. Which causes
662 a bit of a problem when you need a cookie-based login, as with
662 a bit of a problem when you need a cookie-based login, as with
663 the Bugzilla XMLRPC interface prior to 4.4.3.
663 the Bugzilla XMLRPC interface prior to 4.4.3.
664
664
665 So this is a helper for defining a Transport which looks for
665 So this is a helper for defining a Transport which looks for
666 cookies being set in responses and saves them to add to all future
666 cookies being set in responses and saves them to add to all future
667 requests.
667 requests.
668 """
668 """
669
669
670 # Inspiration drawn from
670 # Inspiration drawn from
671 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
671 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
672 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
672 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
673
673
674 cookies = []
674 cookies = []
675
675
676 def send_cookies(self, connection):
676 def send_cookies(self, connection):
677 if self.cookies:
677 if self.cookies:
678 for cookie in self.cookies:
678 for cookie in self.cookies:
679 connection.putheader(b"Cookie", cookie)
679 connection.putheader(b"Cookie", cookie)
680
680
681 def request(self, host, handler, request_body, verbose=0):
681 def request(self, host, handler, request_body, verbose=0):
682 self.verbose = verbose
682 self.verbose = verbose
683 self.accept_gzip_encoding = False
683 self.accept_gzip_encoding = False
684
684
685 # issue XML-RPC request
685 # issue XML-RPC request
686 h = self.make_connection(host)
686 h = self.make_connection(host)
687 if verbose:
687 if verbose:
688 h.set_debuglevel(1)
688 h.set_debuglevel(1)
689
689
690 self.send_request(h, handler, request_body)
690 self.send_request(h, handler, request_body)
691 self.send_host(h, host)
691 self.send_host(h, host)
692 self.send_cookies(h)
692 self.send_cookies(h)
693 self.send_user_agent(h)
693 self.send_user_agent(h)
694 self.send_content(h, request_body)
694 self.send_content(h, request_body)
695
695
696 # Deal with differences between Python 2.6 and 2.7.
696 # Deal with differences between Python 2.6 and 2.7.
697 # In the former h is a HTTP(S). In the latter it's a
697 # In the former h is a HTTP(S). In the latter it's a
698 # HTTP(S)Connection. Luckily, the 2.6 implementation of
698 # HTTP(S)Connection. Luckily, the 2.6 implementation of
699 # HTTP(S) has an underlying HTTP(S)Connection, so extract
699 # HTTP(S) has an underlying HTTP(S)Connection, so extract
700 # that and use it.
700 # that and use it.
701 try:
701 try:
702 response = h.getresponse()
702 response = h.getresponse()
703 except AttributeError:
703 except AttributeError:
704 response = h._conn.getresponse()
704 response = h._conn.getresponse()
705
705
706 # Add any cookie definitions to our list.
706 # Add any cookie definitions to our list.
707 for header in response.msg.getallmatchingheaders(b"Set-Cookie"):
707 for header in response.msg.getallmatchingheaders(b"Set-Cookie"):
708 val = header.split(b": ", 1)[1]
708 val = header.split(b": ", 1)[1]
709 cookie = val.split(b";", 1)[0]
709 cookie = val.split(b";", 1)[0]
710 self.cookies.append(cookie)
710 self.cookies.append(cookie)
711
711
712 if response.status != 200:
712 if response.status != 200:
713 raise xmlrpclib.ProtocolError(
713 raise xmlrpclib.ProtocolError(
714 host + handler,
714 host + handler,
715 response.status,
715 response.status,
716 response.reason,
716 response.reason,
717 response.msg.headers,
717 response.msg.headers,
718 )
718 )
719
719
720 payload = response.read()
720 payload = response.read()
721 parser, unmarshaller = self.getparser()
721 parser, unmarshaller = self.getparser()
722 parser.feed(payload)
722 parser.feed(payload)
723 parser.close()
723 parser.close()
724
724
725 return unmarshaller.close()
725 return unmarshaller.close()
726
726
727
727
728 # The explicit calls to the underlying xmlrpclib __init__() methods are
728 # The explicit calls to the underlying xmlrpclib __init__() methods are
729 # necessary. The xmlrpclib.Transport classes are old-style classes, and
729 # necessary. The xmlrpclib.Transport classes are old-style classes, and
730 # 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
731 # inheritance with a new-style class.
731 # inheritance with a new-style class.
732 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
732 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
733 def __init__(self, use_datetime=0):
733 def __init__(self, use_datetime=0):
734 if util.safehasattr(xmlrpclib.Transport, "__init__"):
734 if util.safehasattr(xmlrpclib.Transport, "__init__"):
735 xmlrpclib.Transport.__init__(self, use_datetime)
735 xmlrpclib.Transport.__init__(self, use_datetime)
736
736
737
737
738 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
738 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
739 def __init__(self, use_datetime=0):
739 def __init__(self, use_datetime=0):
740 if util.safehasattr(xmlrpclib.Transport, "__init__"):
740 if util.safehasattr(xmlrpclib.Transport, "__init__"):
741 xmlrpclib.SafeTransport.__init__(self, use_datetime)
741 xmlrpclib.SafeTransport.__init__(self, use_datetime)
742
742
743
743
744 class bzxmlrpc(bzaccess):
744 class bzxmlrpc(bzaccess):
745 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
745 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
746
746
747 Requires a minimum Bugzilla version 3.4.
747 Requires a minimum Bugzilla version 3.4.
748 """
748 """
749
749
750 def __init__(self, ui):
750 def __init__(self, ui):
751 bzaccess.__init__(self, ui)
751 bzaccess.__init__(self, ui)
752
752
753 bzweb = self.ui.config(b'bugzilla', b'bzurl')
753 bzweb = self.ui.config(b'bugzilla', b'bzurl')
754 bzweb = bzweb.rstrip(b"/") + b"/xmlrpc.cgi"
754 bzweb = bzweb.rstrip(b"/") + b"/xmlrpc.cgi"
755
755
756 user = self.ui.config(b'bugzilla', b'user')
756 user = self.ui.config(b'bugzilla', b'user')
757 passwd = self.ui.config(b'bugzilla', b'password')
757 passwd = self.ui.config(b'bugzilla', b'password')
758
758
759 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
759 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
760 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
760 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
761
761
762 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
762 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
763 ver = self.bzproxy.Bugzilla.version()[b'version'].split(b'.')
763 ver = self.bzproxy.Bugzilla.version()[b'version'].split(b'.')
764 self.bzvermajor = int(ver[0])
764 self.bzvermajor = int(ver[0])
765 self.bzverminor = int(ver[1])
765 self.bzverminor = int(ver[1])
766 login = self.bzproxy.User.login(
766 login = self.bzproxy.User.login(
767 {b'login': user, b'password': passwd, b'restrict_login': True}
767 {b'login': user, b'password': passwd, b'restrict_login': True}
768 )
768 )
769 self.bztoken = login.get(b'token', b'')
769 self.bztoken = login.get(b'token', b'')
770
770
771 def transport(self, uri):
771 def transport(self, uri):
772 if util.urlreq.urlparse(uri, b"http")[0] == b"https":
772 if util.urlreq.urlparse(uri, b"http")[0] == b"https":
773 return cookiesafetransport()
773 return cookiesafetransport()
774 else:
774 else:
775 return cookietransport()
775 return cookietransport()
776
776
777 def get_bug_comments(self, id):
777 def get_bug_comments(self, id):
778 """Return a string with all comment text for a bug."""
778 """Return a string with all comment text for a bug."""
779 c = self.bzproxy.Bug.comments(
779 c = self.bzproxy.Bug.comments(
780 {b'ids': [id], b'include_fields': [b'text'], b'token': self.bztoken}
780 {b'ids': [id], b'include_fields': [b'text'], b'token': self.bztoken}
781 )
781 )
782 return b''.join(
782 return b''.join(
783 [t[b'text'] for t in c[b'bugs'][b'%d' % id][b'comments']]
783 [t[b'text'] for t in c[b'bugs'][b'%d' % id][b'comments']]
784 )
784 )
785
785
786 def filter_real_bug_ids(self, bugs):
786 def filter_real_bug_ids(self, bugs):
787 probe = self.bzproxy.Bug.get(
787 probe = self.bzproxy.Bug.get(
788 {
788 {
789 b'ids': sorted(bugs.keys()),
789 b'ids': sorted(bugs.keys()),
790 b'include_fields': [],
790 b'include_fields': [],
791 b'permissive': True,
791 b'permissive': True,
792 b'token': self.bztoken,
792 b'token': self.bztoken,
793 }
793 }
794 )
794 )
795 for badbug in probe[b'faults']:
795 for badbug in probe[b'faults']:
796 id = badbug[b'id']
796 id = badbug[b'id']
797 self.ui.status(_(b'bug %d does not exist\n') % id)
797 self.ui.status(_(b'bug %d does not exist\n') % id)
798 del bugs[id]
798 del bugs[id]
799
799
800 def filter_cset_known_bug_ids(self, node, bugs):
800 def filter_cset_known_bug_ids(self, node, bugs):
801 for id in sorted(bugs.keys()):
801 for id in sorted(bugs.keys()):
802 if self.get_bug_comments(id).find(short(node)) != -1:
802 if self.get_bug_comments(id).find(short(node)) != -1:
803 self.ui.status(
803 self.ui.status(
804 _(b'bug %d already knows about changeset %s\n')
804 _(b'bug %d already knows about changeset %s\n')
805 % (id, short(node))
805 % (id, short(node))
806 )
806 )
807 del bugs[id]
807 del bugs[id]
808
808
809 def updatebug(self, bugid, newstate, text, committer):
809 def updatebug(self, bugid, newstate, text, committer):
810 args = {}
810 args = {}
811 if b'hours' in newstate:
811 if b'hours' in newstate:
812 args[b'work_time'] = newstate[b'hours']
812 args[b'work_time'] = newstate[b'hours']
813
813
814 if self.bzvermajor >= 4:
814 if self.bzvermajor >= 4:
815 args[b'ids'] = [bugid]
815 args[b'ids'] = [bugid]
816 args[b'comment'] = {b'body': text}
816 args[b'comment'] = {b'body': text}
817 if b'fix' in newstate:
817 if b'fix' in newstate:
818 args[b'status'] = self.fixstatus
818 args[b'status'] = self.fixstatus
819 args[b'resolution'] = self.fixresolution
819 args[b'resolution'] = self.fixresolution
820 args[b'token'] = self.bztoken
820 args[b'token'] = self.bztoken
821 self.bzproxy.Bug.update(args)
821 self.bzproxy.Bug.update(args)
822 else:
822 else:
823 if b'fix' in newstate:
823 if b'fix' in newstate:
824 self.ui.warn(
824 self.ui.warn(
825 _(
825 _(
826 b"Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
826 b"Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
827 b"to mark bugs fixed\n"
827 b"to mark bugs fixed\n"
828 )
828 )
829 )
829 )
830 args[b'id'] = bugid
830 args[b'id'] = bugid
831 args[b'comment'] = text
831 args[b'comment'] = text
832 self.bzproxy.Bug.add_comment(args)
832 self.bzproxy.Bug.add_comment(args)
833
833
834
834
835 class bzxmlrpcemail(bzxmlrpc):
835 class bzxmlrpcemail(bzxmlrpc):
836 """Read data from Bugzilla via XMLRPC, send updates via email.
836 """Read data from Bugzilla via XMLRPC, send updates via email.
837
837
838 Advantages of sending updates via email:
838 Advantages of sending updates via email:
839 1. Comments can be added as any user, not just logged in user.
839 1. Comments can be added as any user, not just logged in user.
840 2. Bug statuses or other fields not accessible via XMLRPC can
840 2. Bug statuses or other fields not accessible via XMLRPC can
841 potentially be updated.
841 potentially be updated.
842
842
843 There is no XMLRPC function to change bug status before Bugzilla
843 There is no XMLRPC function to change bug status before Bugzilla
844 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
844 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
845 But bugs can be marked fixed via email from 3.4 onwards.
845 But bugs can be marked fixed via email from 3.4 onwards.
846 """
846 """
847
847
848 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
848 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
849 # in-email fields are specified as '@<fieldname> = <value>'. In
849 # in-email fields are specified as '@<fieldname> = <value>'. In
850 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
850 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
851 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
851 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
852 # compatibility, but rather than rely on this use the new format for
852 # compatibility, but rather than rely on this use the new format for
853 # 4.0 onwards.
853 # 4.0 onwards.
854
854
855 def __init__(self, ui):
855 def __init__(self, ui):
856 bzxmlrpc.__init__(self, ui)
856 bzxmlrpc.__init__(self, ui)
857
857
858 self.bzemail = self.ui.config(b'bugzilla', b'bzemail')
858 self.bzemail = self.ui.config(b'bugzilla', b'bzemail')
859 if not self.bzemail:
859 if not self.bzemail:
860 raise error.Abort(_(b"configuration 'bzemail' missing"))
860 raise error.Abort(_(b"configuration 'bzemail' missing"))
861 mail.validateconfig(self.ui)
861 mail.validateconfig(self.ui)
862
862
863 def makecommandline(self, fieldname, value):
863 def makecommandline(self, fieldname, value):
864 if self.bzvermajor >= 4:
864 if self.bzvermajor >= 4:
865 return b"@%s %s" % (fieldname, pycompat.bytestr(value))
865 return b"@%s %s" % (fieldname, pycompat.bytestr(value))
866 else:
866 else:
867 if fieldname == b"id":
867 if fieldname == b"id":
868 fieldname = b"bug_id"
868 fieldname = b"bug_id"
869 return b"@%s = %s" % (fieldname, pycompat.bytestr(value))
869 return b"@%s = %s" % (fieldname, pycompat.bytestr(value))
870
870
871 def send_bug_modify_email(self, bugid, commands, comment, committer):
871 def send_bug_modify_email(self, bugid, commands, comment, committer):
872 '''send modification message to Bugzilla bug via email.
872 '''send modification message to Bugzilla bug via email.
873
873
874 The message format is documented in the Bugzilla email_in.pl
874 The message format is documented in the Bugzilla email_in.pl
875 specification. commands is a list of command lines, comment is the
875 specification. commands is a list of command lines, comment is the
876 comment text.
876 comment text.
877
877
878 To stop users from crafting commit comments with
878 To stop users from crafting commit comments with
879 Bugzilla commands, specify the bug ID via the message body, rather
879 Bugzilla commands, specify the bug ID via the message body, rather
880 than the subject line, and leave a blank line after it.
880 than the subject line, and leave a blank line after it.
881 '''
881 '''
882 user = self.map_committer(committer)
882 user = self.map_committer(committer)
883 matches = self.bzproxy.User.get(
883 matches = self.bzproxy.User.get(
884 {b'match': [user], b'token': self.bztoken}
884 {b'match': [user], b'token': self.bztoken}
885 )
885 )
886 if not matches[b'users']:
886 if not matches[b'users']:
887 user = self.ui.config(b'bugzilla', b'user')
887 user = self.ui.config(b'bugzilla', b'user')
888 matches = self.bzproxy.User.get(
888 matches = self.bzproxy.User.get(
889 {b'match': [user], b'token': self.bztoken}
889 {b'match': [user], b'token': self.bztoken}
890 )
890 )
891 if not matches[b'users']:
891 if not matches[b'users']:
892 raise error.Abort(
892 raise error.Abort(
893 _(b"default bugzilla user %s email not found") % user
893 _(b"default bugzilla user %s email not found") % user
894 )
894 )
895 user = matches[b'users'][0][b'email']
895 user = matches[b'users'][0][b'email']
896 commands.append(self.makecommandline(b"id", bugid))
896 commands.append(self.makecommandline(b"id", bugid))
897
897
898 text = b"\n".join(commands) + b"\n\n" + comment
898 text = b"\n".join(commands) + b"\n\n" + comment
899
899
900 _charsets = mail._charsets(self.ui)
900 _charsets = mail._charsets(self.ui)
901 user = mail.addressencode(self.ui, user, _charsets)
901 user = mail.addressencode(self.ui, user, _charsets)
902 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
902 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
903 msg = mail.mimeencode(self.ui, text, _charsets)
903 msg = mail.mimeencode(self.ui, text, _charsets)
904 msg[b'From'] = user
904 msg[b'From'] = user
905 msg[b'To'] = bzemail
905 msg[b'To'] = bzemail
906 msg[b'Subject'] = mail.headencode(
906 msg[b'Subject'] = mail.headencode(
907 self.ui, b"Bug modification", _charsets
907 self.ui, b"Bug modification", _charsets
908 )
908 )
909 sendmail = mail.connect(self.ui)
909 sendmail = mail.connect(self.ui)
910 sendmail(user, bzemail, msg.as_string())
910 sendmail(user, bzemail, msg.as_string())
911
911
912 def updatebug(self, bugid, newstate, text, committer):
912 def updatebug(self, bugid, newstate, text, committer):
913 cmds = []
913 cmds = []
914 if b'hours' in newstate:
914 if b'hours' in newstate:
915 cmds.append(self.makecommandline(b"work_time", newstate[b'hours']))
915 cmds.append(self.makecommandline(b"work_time", newstate[b'hours']))
916 if b'fix' in newstate:
916 if b'fix' in newstate:
917 cmds.append(self.makecommandline(b"bug_status", self.fixstatus))
917 cmds.append(self.makecommandline(b"bug_status", self.fixstatus))
918 cmds.append(self.makecommandline(b"resolution", self.fixresolution))
918 cmds.append(self.makecommandline(b"resolution", self.fixresolution))
919 self.send_bug_modify_email(bugid, cmds, text, committer)
919 self.send_bug_modify_email(bugid, cmds, text, committer)
920
920
921
921
922 class NotFound(LookupError):
922 class NotFound(LookupError):
923 pass
923 pass
924
924
925
925
926 class bzrestapi(bzaccess):
926 class bzrestapi(bzaccess):
927 """Read and write bugzilla data using the REST API available since
927 """Read and write bugzilla data using the REST API available since
928 Bugzilla 5.0.
928 Bugzilla 5.0.
929 """
929 """
930
930
931 def __init__(self, ui):
931 def __init__(self, ui):
932 bzaccess.__init__(self, ui)
932 bzaccess.__init__(self, ui)
933 bz = self.ui.config(b'bugzilla', b'bzurl')
933 bz = self.ui.config(b'bugzilla', b'bzurl')
934 self.bzroot = b'/'.join([bz, b'rest'])
934 self.bzroot = b'/'.join([bz, b'rest'])
935 self.apikey = self.ui.config(b'bugzilla', b'apikey')
935 self.apikey = self.ui.config(b'bugzilla', b'apikey')
936 self.user = self.ui.config(b'bugzilla', b'user')
936 self.user = self.ui.config(b'bugzilla', b'user')
937 self.passwd = self.ui.config(b'bugzilla', b'password')
937 self.passwd = self.ui.config(b'bugzilla', b'password')
938 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
938 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
939 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
939 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
940
940
941 def apiurl(self, targets, include_fields=None):
941 def apiurl(self, targets, include_fields=None):
942 url = b'/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets])
942 url = b'/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets])
943 qv = {}
943 qv = {}
944 if self.apikey:
944 if self.apikey:
945 qv[b'api_key'] = self.apikey
945 qv[b'api_key'] = self.apikey
946 elif self.user and self.passwd:
946 elif self.user and self.passwd:
947 qv[b'login'] = self.user
947 qv[b'login'] = self.user
948 qv[b'password'] = self.passwd
948 qv[b'password'] = self.passwd
949 if include_fields:
949 if include_fields:
950 qv[b'include_fields'] = include_fields
950 qv[b'include_fields'] = include_fields
951 if qv:
951 if qv:
952 url = b'%s?%s' % (url, util.urlreq.urlencode(qv))
952 url = b'%s?%s' % (url, util.urlreq.urlencode(qv))
953 return url
953 return url
954
954
955 def _fetch(self, burl):
955 def _fetch(self, burl):
956 try:
956 try:
957 resp = url.open(self.ui, burl)
957 resp = url.open(self.ui, burl)
958 return pycompat.json_loads(resp.read())
958 return pycompat.json_loads(resp.read())
959 except util.urlerr.httperror as inst:
959 except util.urlerr.httperror as inst:
960 if inst.code == 401:
960 if inst.code == 401:
961 raise error.Abort(_(b'authorization failed'))
961 raise error.Abort(_(b'authorization failed'))
962 if inst.code == 404:
962 if inst.code == 404:
963 raise NotFound()
963 raise NotFound()
964 else:
964 else:
965 raise
965 raise
966
966
967 def _submit(self, burl, data, method=b'POST'):
967 def _submit(self, burl, data, method=b'POST'):
968 data = json.dumps(data)
968 data = json.dumps(data)
969 if method == b'PUT':
969 if method == b'PUT':
970
970
971 class putrequest(util.urlreq.request):
971 class putrequest(util.urlreq.request):
972 def get_method(self):
972 def get_method(self):
973 return b'PUT'
973 return b'PUT'
974
974
975 request_type = putrequest
975 request_type = putrequest
976 else:
976 else:
977 request_type = util.urlreq.request
977 request_type = util.urlreq.request
978 req = request_type(burl, data, {b'Content-Type': b'application/json'})
978 req = request_type(burl, data, {b'Content-Type': b'application/json'})
979 try:
979 try:
980 resp = url.opener(self.ui).open(req)
980 resp = url.opener(self.ui).open(req)
981 return pycompat.json_loads(resp.read())
981 return pycompat.json_loads(resp.read())
982 except util.urlerr.httperror as inst:
982 except util.urlerr.httperror as inst:
983 if inst.code == 401:
983 if inst.code == 401:
984 raise error.Abort(_(b'authorization failed'))
984 raise error.Abort(_(b'authorization failed'))
985 if inst.code == 404:
985 if inst.code == 404:
986 raise NotFound()
986 raise NotFound()
987 else:
987 else:
988 raise
988 raise
989
989
990 def filter_real_bug_ids(self, bugs):
990 def filter_real_bug_ids(self, bugs):
991 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
991 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
992 badbugs = set()
992 badbugs = set()
993 for bugid in bugs:
993 for bugid in bugs:
994 burl = self.apiurl((b'bug', bugid), include_fields=b'status')
994 burl = self.apiurl((b'bug', bugid), include_fields=b'status')
995 try:
995 try:
996 self._fetch(burl)
996 self._fetch(burl)
997 except NotFound:
997 except NotFound:
998 badbugs.add(bugid)
998 badbugs.add(bugid)
999 for bugid in badbugs:
999 for bugid in badbugs:
1000 del bugs[bugid]
1000 del bugs[bugid]
1001
1001
1002 def filter_cset_known_bug_ids(self, node, bugs):
1002 def filter_cset_known_bug_ids(self, node, bugs):
1003 '''remove bug IDs where node occurs in comment text from bugs.'''
1003 '''remove bug IDs where node occurs in comment text from bugs.'''
1004 sn = short(node)
1004 sn = short(node)
1005 for bugid in bugs.keys():
1005 for bugid in bugs.keys():
1006 burl = self.apiurl(
1006 burl = self.apiurl(
1007 (b'bug', bugid, b'comment'), include_fields=b'text'
1007 (b'bug', bugid, b'comment'), include_fields=b'text'
1008 )
1008 )
1009 result = self._fetch(burl)
1009 result = self._fetch(burl)
1010 comments = result[b'bugs'][pycompat.bytestr(bugid)][b'comments']
1010 comments = result[b'bugs'][pycompat.bytestr(bugid)][b'comments']
1011 if any(sn in c[b'text'] for c in comments):
1011 if any(sn in c[b'text'] for c in comments):
1012 self.ui.status(
1012 self.ui.status(
1013 _(b'bug %d already knows about changeset %s\n')
1013 _(b'bug %d already knows about changeset %s\n')
1014 % (bugid, sn)
1014 % (bugid, sn)
1015 )
1015 )
1016 del bugs[bugid]
1016 del bugs[bugid]
1017
1017
1018 def updatebug(self, bugid, newstate, text, committer):
1018 def updatebug(self, bugid, newstate, text, committer):
1019 '''update the specified bug. Add comment text and set new states.
1019 '''update the specified bug. Add comment text and set new states.
1020
1020
1021 If possible add the comment as being from the committer of
1021 If possible add the comment as being from the committer of
1022 the changeset. Otherwise use the default Bugzilla user.
1022 the changeset. Otherwise use the default Bugzilla user.
1023 '''
1023 '''
1024 bugmod = {}
1024 bugmod = {}
1025 if b'hours' in newstate:
1025 if b'hours' in newstate:
1026 bugmod[b'work_time'] = newstate[b'hours']
1026 bugmod[b'work_time'] = newstate[b'hours']
1027 if b'fix' in newstate:
1027 if b'fix' in newstate:
1028 bugmod[b'status'] = self.fixstatus
1028 bugmod[b'status'] = self.fixstatus
1029 bugmod[b'resolution'] = self.fixresolution
1029 bugmod[b'resolution'] = self.fixresolution
1030 if bugmod:
1030 if bugmod:
1031 # if we have to change the bugs state do it here
1031 # if we have to change the bugs state do it here
1032 bugmod[b'comment'] = {
1032 bugmod[b'comment'] = {
1033 b'comment': text,
1033 b'comment': text,
1034 b'is_private': False,
1034 b'is_private': False,
1035 b'is_markdown': False,
1035 b'is_markdown': False,
1036 }
1036 }
1037 burl = self.apiurl((b'bug', bugid))
1037 burl = self.apiurl((b'bug', bugid))
1038 self._submit(burl, bugmod, method=b'PUT')
1038 self._submit(burl, bugmod, method=b'PUT')
1039 self.ui.debug(b'updated bug %s\n' % bugid)
1039 self.ui.debug(b'updated bug %s\n' % bugid)
1040 else:
1040 else:
1041 burl = self.apiurl((b'bug', bugid, b'comment'))
1041 burl = self.apiurl((b'bug', bugid, b'comment'))
1042 self._submit(
1042 self._submit(
1043 burl,
1043 burl,
1044 {
1044 {
1045 b'comment': text,
1045 b'comment': text,
1046 b'is_private': False,
1046 b'is_private': False,
1047 b'is_markdown': False,
1047 b'is_markdown': False,
1048 },
1048 },
1049 )
1049 )
1050 self.ui.debug(b'added comment to bug %s\n' % bugid)
1050 self.ui.debug(b'added comment to bug %s\n' % bugid)
1051
1051
1052 def notify(self, bugs, committer):
1052 def notify(self, bugs, committer):
1053 '''Force sending of Bugzilla notification emails.
1053 '''Force sending of Bugzilla notification emails.
1054
1054
1055 Only required if the access method does not trigger notification
1055 Only required if the access method does not trigger notification
1056 emails automatically.
1056 emails automatically.
1057 '''
1057 '''
1058 pass
1058 pass
1059
1059
1060
1060
1061 class bugzilla(object):
1061 class bugzilla(object):
1062 # supported versions of bugzilla. different versions have
1062 # supported versions of bugzilla. different versions have
1063 # different schemas.
1063 # different schemas.
1064 _versions = {
1064 _versions = {
1065 b'2.16': bzmysql,
1065 b'2.16': bzmysql,
1066 b'2.18': bzmysql_2_18,
1066 b'2.18': bzmysql_2_18,
1067 b'3.0': bzmysql_3_0,
1067 b'3.0': bzmysql_3_0,
1068 b'xmlrpc': bzxmlrpc,
1068 b'xmlrpc': bzxmlrpc,
1069 b'xmlrpc+email': bzxmlrpcemail,
1069 b'xmlrpc+email': bzxmlrpcemail,
1070 b'restapi': bzrestapi,
1070 b'restapi': bzrestapi,
1071 }
1071 }
1072
1072
1073 def __init__(self, ui, repo):
1073 def __init__(self, ui, repo):
1074 self.ui = ui
1074 self.ui = ui
1075 self.repo = repo
1075 self.repo = repo
1076
1076
1077 bzversion = self.ui.config(b'bugzilla', b'version')
1077 bzversion = self.ui.config(b'bugzilla', b'version')
1078 try:
1078 try:
1079 bzclass = bugzilla._versions[bzversion]
1079 bzclass = bugzilla._versions[bzversion]
1080 except KeyError:
1080 except KeyError:
1081 raise error.Abort(
1081 raise error.Abort(
1082 _(b'bugzilla version %s not supported') % bzversion
1082 _(b'bugzilla version %s not supported') % bzversion
1083 )
1083 )
1084 self.bzdriver = bzclass(self.ui)
1084 self.bzdriver = bzclass(self.ui)
1085
1085
1086 self.bug_re = re.compile(
1086 self.bug_re = re.compile(
1087 self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE
1087 self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE
1088 )
1088 )
1089 self.fix_re = re.compile(
1089 self.fix_re = re.compile(
1090 self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE
1090 self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE
1091 )
1091 )
1092 self.split_re = re.compile(br'\D+')
1092 self.split_re = re.compile(br'\D+')
1093
1093
1094 def find_bugs(self, ctx):
1094 def find_bugs(self, ctx):
1095 '''return bugs dictionary created from commit comment.
1095 '''return bugs dictionary created from commit comment.
1096
1096
1097 Extract bug info from changeset comments. Filter out any that are
1097 Extract bug info from changeset comments. Filter out any that are
1098 not known to Bugzilla, and any that already have a reference to
1098 not known to Bugzilla, and any that already have a reference to
1099 the given changeset in their comments.
1099 the given changeset in their comments.
1100 '''
1100 '''
1101 start = 0
1101 start = 0
1102 bugs = {}
1102 bugs = {}
1103 bugmatch = self.bug_re.search(ctx.description(), start)
1103 bugmatch = self.bug_re.search(ctx.description(), start)
1104 fixmatch = self.fix_re.search(ctx.description(), start)
1104 fixmatch = self.fix_re.search(ctx.description(), start)
1105 while True:
1105 while True:
1106 bugattribs = {}
1106 bugattribs = {}
1107 if not bugmatch and not fixmatch:
1107 if not bugmatch and not fixmatch:
1108 break
1108 break
1109 if not bugmatch:
1109 if not bugmatch:
1110 m = fixmatch
1110 m = fixmatch
1111 elif not fixmatch:
1111 elif not fixmatch:
1112 m = bugmatch
1112 m = bugmatch
1113 else:
1113 else:
1114 if bugmatch.start() < fixmatch.start():
1114 if bugmatch.start() < fixmatch.start():
1115 m = bugmatch
1115 m = bugmatch
1116 else:
1116 else:
1117 m = fixmatch
1117 m = fixmatch
1118 start = m.end()
1118 start = m.end()
1119 if m is bugmatch:
1119 if m is bugmatch:
1120 bugmatch = self.bug_re.search(ctx.description(), start)
1120 bugmatch = self.bug_re.search(ctx.description(), start)
1121 if b'fix' in bugattribs:
1121 if b'fix' in bugattribs:
1122 del bugattribs[b'fix']
1122 del bugattribs[b'fix']
1123 else:
1123 else:
1124 fixmatch = self.fix_re.search(ctx.description(), start)
1124 fixmatch = self.fix_re.search(ctx.description(), start)
1125 bugattribs[b'fix'] = None
1125 bugattribs[b'fix'] = None
1126
1126
1127 try:
1127 try:
1128 ids = m.group(b'ids')
1128 ids = m.group(b'ids')
1129 except IndexError:
1129 except IndexError:
1130 ids = m.group(1)
1130 ids = m.group(1)
1131 try:
1131 try:
1132 hours = float(m.group(b'hours'))
1132 hours = float(m.group(b'hours'))
1133 bugattribs[b'hours'] = hours
1133 bugattribs[b'hours'] = hours
1134 except IndexError:
1134 except IndexError:
1135 pass
1135 pass
1136 except TypeError:
1136 except TypeError:
1137 pass
1137 pass
1138 except ValueError:
1138 except ValueError:
1139 self.ui.status(_(b"%s: invalid hours\n") % m.group(b'hours'))
1139 self.ui.status(_(b"%s: invalid hours\n") % m.group(b'hours'))
1140
1140
1141 for id in self.split_re.split(ids):
1141 for id in self.split_re.split(ids):
1142 if not id:
1142 if not id:
1143 continue
1143 continue
1144 bugs[int(id)] = bugattribs
1144 bugs[int(id)] = bugattribs
1145 if bugs:
1145 if bugs:
1146 self.bzdriver.filter_real_bug_ids(bugs)
1146 self.bzdriver.filter_real_bug_ids(bugs)
1147 if bugs:
1147 if bugs:
1148 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1148 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1149 return bugs
1149 return bugs
1150
1150
1151 def update(self, bugid, newstate, ctx):
1151 def update(self, bugid, newstate, ctx):
1152 '''update bugzilla bug with reference to changeset.'''
1152 '''update bugzilla bug with reference to changeset.'''
1153
1153
1154 def webroot(root):
1154 def webroot(root):
1155 '''strip leading prefix of repo root and turn into
1155 '''strip leading prefix of repo root and turn into
1156 url-safe path.'''
1156 url-safe path.'''
1157 count = int(self.ui.config(b'bugzilla', b'strip'))
1157 count = int(self.ui.config(b'bugzilla', b'strip'))
1158 root = util.pconvert(root)
1158 root = util.pconvert(root)
1159 while count > 0:
1159 while count > 0:
1160 c = root.find(b'/')
1160 c = root.find(b'/')
1161 if c == -1:
1161 if c == -1:
1162 break
1162 break
1163 root = root[c + 1 :]
1163 root = root[c + 1 :]
1164 count -= 1
1164 count -= 1
1165 return root
1165 return root
1166
1166
1167 mapfile = None
1167 mapfile = None
1168 tmpl = self.ui.config(b'bugzilla', b'template')
1168 tmpl = self.ui.config(b'bugzilla', b'template')
1169 if not tmpl:
1169 if not tmpl:
1170 mapfile = self.ui.config(b'bugzilla', b'style')
1170 mapfile = self.ui.config(b'bugzilla', b'style')
1171 if not mapfile and not tmpl:
1171 if not mapfile and not tmpl:
1172 tmpl = _(
1172 tmpl = _(
1173 b'changeset {node|short} in repo {root} refers '
1173 b'changeset {node|short} in repo {root} refers '
1174 b'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1174 b'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1175 )
1175 )
1176 spec = logcmdutil.templatespec(tmpl, mapfile)
1176 spec = logcmdutil.templatespec(tmpl, mapfile)
1177 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1177 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1178 self.ui.pushbuffer()
1178 self.ui.pushbuffer()
1179 t.show(
1179 t.show(
1180 ctx,
1180 ctx,
1181 changes=ctx.changeset(),
1181 changes=ctx.changeset(),
1182 bug=pycompat.bytestr(bugid),
1182 bug=pycompat.bytestr(bugid),
1183 hgweb=self.ui.config(b'web', b'baseurl'),
1183 hgweb=self.ui.config(b'web', b'baseurl'),
1184 root=self.repo.root,
1184 root=self.repo.root,
1185 webroot=webroot(self.repo.root),
1185 webroot=webroot(self.repo.root),
1186 )
1186 )
1187 data = self.ui.popbuffer()
1187 data = self.ui.popbuffer()
1188 self.bzdriver.updatebug(
1188 self.bzdriver.updatebug(
1189 bugid, newstate, data, stringutil.email(ctx.user())
1189 bugid, newstate, data, stringutil.email(ctx.user())
1190 )
1190 )
1191
1191
1192 def notify(self, bugs, committer):
1192 def notify(self, bugs, committer):
1193 '''ensure Bugzilla users are notified of bug change.'''
1193 '''ensure Bugzilla users are notified of bug change.'''
1194 self.bzdriver.notify(bugs, committer)
1194 self.bzdriver.notify(bugs, committer)
1195
1195
1196
1196
1197 def hook(ui, repo, hooktype, node=None, **kwargs):
1197 def hook(ui, repo, hooktype, node=None, **kwargs):
1198 '''add comment to bugzilla for each changeset that refers to a
1198 '''add comment to bugzilla for each changeset that refers to a
1199 bugzilla bug id. only add a comment once per bug, so same change
1199 bugzilla bug id. only add a comment once per bug, so same change
1200 seen multiple times does not fill bug with duplicate data.'''
1200 seen multiple times does not fill bug with duplicate data.'''
1201 if node is None:
1201 if node is None:
1202 raise error.Abort(
1202 raise error.Abort(
1203 _(b'hook type %s does not pass a changeset id') % hooktype
1203 _(b'hook type %s does not pass a changeset id') % hooktype
1204 )
1204 )
1205 try:
1205 try:
1206 bz = bugzilla(ui, repo)
1206 bz = bugzilla(ui, repo)
1207 ctx = repo[node]
1207 ctx = repo[node]
1208 bugs = bz.find_bugs(ctx)
1208 bugs = bz.find_bugs(ctx)
1209 if bugs:
1209 if bugs:
1210 for bug in bugs:
1210 for bug in bugs:
1211 bz.update(bug, bugs[bug], ctx)
1211 bz.update(bug, bugs[bug], ctx)
1212 bz.notify(bugs, stringutil.email(ctx.user()))
1212 bz.notify(bugs, stringutil.email(ctx.user()))
1213 except Exception as e:
1213 except Exception as e:
1214 raise error.Abort(_(b'Bugzilla error: %s') % e)
1214 raise error.Abort(_(b'Bugzilla error: %s') % stringutil.forcebytestr(e))
General Comments 0
You need to be logged in to leave comments. Login now