##// END OF EJS Templates
configitems: register the 'bugzilla.usermap' config
Boris Feld -
r33468:76269ea9 default
parent child Browse files
Show More
@@ -1,1122 +1,1125 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 cmdutil,
303 cmdutil,
304 error,
304 error,
305 mail,
305 mail,
306 registrar,
306 registrar,
307 url,
307 url,
308 util,
308 util,
309 )
309 )
310
310
311 xmlrpclib = util.xmlrpclib
311 xmlrpclib = util.xmlrpclib
312
312
313 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
313 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
314 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
314 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
315 # be specifying the version(s) of Mercurial they are tested with, or
315 # be specifying the version(s) of Mercurial they are tested with, or
316 # leave the attribute unspecified.
316 # leave the attribute unspecified.
317 testedwith = 'ships-with-hg-core'
317 testedwith = 'ships-with-hg-core'
318
318
319 configtable = {}
319 configtable = {}
320 configitem = registrar.configitem(configtable)
320 configitem = registrar.configitem(configtable)
321
321
322 configitem('bugzilla', 'apikey',
322 configitem('bugzilla', 'apikey',
323 default='',
323 default='',
324 )
324 )
325 configitem('bugzilla', 'bzdir',
325 configitem('bugzilla', 'bzdir',
326 default='/var/www/html/bugzilla',
326 default='/var/www/html/bugzilla',
327 )
327 )
328 configitem('bugzilla', 'bzemail',
328 configitem('bugzilla', 'bzemail',
329 default=None,
329 default=None,
330 )
330 )
331 configitem('bugzilla', 'bzurl',
331 configitem('bugzilla', 'bzurl',
332 default='http://localhost/bugzilla/',
332 default='http://localhost/bugzilla/',
333 )
333 )
334 configitem('bugzilla', 'bzuser',
334 configitem('bugzilla', 'bzuser',
335 default=None,
335 default=None,
336 )
336 )
337 configitem('bugzilla', 'db',
337 configitem('bugzilla', 'db',
338 default='bugs',
338 default='bugs',
339 )
339 )
340 configitem('bugzilla', 'fixregexp',
340 configitem('bugzilla', 'fixregexp',
341 default=lambda: bugzilla._default_fix_re,
341 default=lambda: bugzilla._default_fix_re,
342 )
342 )
343 configitem('bugzilla', 'fixresolution',
343 configitem('bugzilla', 'fixresolution',
344 default='FIXED',
344 default='FIXED',
345 )
345 )
346 configitem('bugzilla', 'fixstatus',
346 configitem('bugzilla', 'fixstatus',
347 default='RESOLVED',
347 default='RESOLVED',
348 )
348 )
349 configitem('bugzilla', 'host',
349 configitem('bugzilla', 'host',
350 default='localhost',
350 default='localhost',
351 )
351 )
352 configitem('bugzilla', 'password',
352 configitem('bugzilla', 'password',
353 default=None,
353 default=None,
354 )
354 )
355 configitem('bugzilla', 'regexp',
355 configitem('bugzilla', 'regexp',
356 default=(r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
356 default=(r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
357 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
357 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
358 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
358 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
359 )
359 )
360 configitem('bugzilla', 'strip',
360 configitem('bugzilla', 'strip',
361 default=0,
361 default=0,
362 )
362 )
363 configitem('bugzilla', 'style',
363 configitem('bugzilla', 'style',
364 default=None,
364 default=None,
365 )
365 )
366 configitem('bugzilla', 'template',
366 configitem('bugzilla', 'template',
367 default=None,
367 default=None,
368 )
368 )
369 configitem('bugzilla', 'timeout',
369 configitem('bugzilla', 'timeout',
370 default=5,
370 default=5,
371 )
371 )
372 configitem('bugzilla', 'user',
372 configitem('bugzilla', 'user',
373 default='bugs',
373 default='bugs',
374 )
374 )
375 configitem('bugzilla', 'usermap',
376 default=None,
377 )
375
378
376 class bzaccess(object):
379 class bzaccess(object):
377 '''Base class for access to Bugzilla.'''
380 '''Base class for access to Bugzilla.'''
378
381
379 def __init__(self, ui):
382 def __init__(self, ui):
380 self.ui = ui
383 self.ui = ui
381 usermap = self.ui.config('bugzilla', 'usermap')
384 usermap = self.ui.config('bugzilla', 'usermap')
382 if usermap:
385 if usermap:
383 self.ui.readconfig(usermap, sections=['usermap'])
386 self.ui.readconfig(usermap, sections=['usermap'])
384
387
385 def map_committer(self, user):
388 def map_committer(self, user):
386 '''map name of committer to Bugzilla user name.'''
389 '''map name of committer to Bugzilla user name.'''
387 for committer, bzuser in self.ui.configitems('usermap'):
390 for committer, bzuser in self.ui.configitems('usermap'):
388 if committer.lower() == user.lower():
391 if committer.lower() == user.lower():
389 return bzuser
392 return bzuser
390 return user
393 return user
391
394
392 # Methods to be implemented by access classes.
395 # Methods to be implemented by access classes.
393 #
396 #
394 # 'bugs' is a dict keyed on bug id, where values are a dict holding
397 # 'bugs' is a dict keyed on bug id, where values are a dict holding
395 # updates to bug state. Recognized dict keys are:
398 # updates to bug state. Recognized dict keys are:
396 #
399 #
397 # 'hours': Value, float containing work hours to be updated.
400 # 'hours': Value, float containing work hours to be updated.
398 # 'fix': If key present, bug is to be marked fixed. Value ignored.
401 # 'fix': If key present, bug is to be marked fixed. Value ignored.
399
402
400 def filter_real_bug_ids(self, bugs):
403 def filter_real_bug_ids(self, bugs):
401 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
404 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
402 pass
405 pass
403
406
404 def filter_cset_known_bug_ids(self, node, bugs):
407 def filter_cset_known_bug_ids(self, node, bugs):
405 '''remove bug IDs where node occurs in comment text from bugs.'''
408 '''remove bug IDs where node occurs in comment text from bugs.'''
406 pass
409 pass
407
410
408 def updatebug(self, bugid, newstate, text, committer):
411 def updatebug(self, bugid, newstate, text, committer):
409 '''update the specified bug. Add comment text and set new states.
412 '''update the specified bug. Add comment text and set new states.
410
413
411 If possible add the comment as being from the committer of
414 If possible add the comment as being from the committer of
412 the changeset. Otherwise use the default Bugzilla user.
415 the changeset. Otherwise use the default Bugzilla user.
413 '''
416 '''
414 pass
417 pass
415
418
416 def notify(self, bugs, committer):
419 def notify(self, bugs, committer):
417 '''Force sending of Bugzilla notification emails.
420 '''Force sending of Bugzilla notification emails.
418
421
419 Only required if the access method does not trigger notification
422 Only required if the access method does not trigger notification
420 emails automatically.
423 emails automatically.
421 '''
424 '''
422 pass
425 pass
423
426
424 # Bugzilla via direct access to MySQL database.
427 # Bugzilla via direct access to MySQL database.
425 class bzmysql(bzaccess):
428 class bzmysql(bzaccess):
426 '''Support for direct MySQL access to Bugzilla.
429 '''Support for direct MySQL access to Bugzilla.
427
430
428 The earliest Bugzilla version this is tested with is version 2.16.
431 The earliest Bugzilla version this is tested with is version 2.16.
429
432
430 If your Bugzilla is version 3.4 or above, you are strongly
433 If your Bugzilla is version 3.4 or above, you are strongly
431 recommended to use the XMLRPC access method instead.
434 recommended to use the XMLRPC access method instead.
432 '''
435 '''
433
436
434 @staticmethod
437 @staticmethod
435 def sql_buglist(ids):
438 def sql_buglist(ids):
436 '''return SQL-friendly list of bug ids'''
439 '''return SQL-friendly list of bug ids'''
437 return '(' + ','.join(map(str, ids)) + ')'
440 return '(' + ','.join(map(str, ids)) + ')'
438
441
439 _MySQLdb = None
442 _MySQLdb = None
440
443
441 def __init__(self, ui):
444 def __init__(self, ui):
442 try:
445 try:
443 import MySQLdb as mysql
446 import MySQLdb as mysql
444 bzmysql._MySQLdb = mysql
447 bzmysql._MySQLdb = mysql
445 except ImportError as err:
448 except ImportError as err:
446 raise error.Abort(_('python mysql support not available: %s') % err)
449 raise error.Abort(_('python mysql support not available: %s') % err)
447
450
448 bzaccess.__init__(self, ui)
451 bzaccess.__init__(self, ui)
449
452
450 host = self.ui.config('bugzilla', 'host')
453 host = self.ui.config('bugzilla', 'host')
451 user = self.ui.config('bugzilla', 'user')
454 user = self.ui.config('bugzilla', 'user')
452 passwd = self.ui.config('bugzilla', 'password')
455 passwd = self.ui.config('bugzilla', 'password')
453 db = self.ui.config('bugzilla', 'db')
456 db = self.ui.config('bugzilla', 'db')
454 timeout = int(self.ui.config('bugzilla', 'timeout'))
457 timeout = int(self.ui.config('bugzilla', 'timeout'))
455 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
458 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
456 (host, db, user, '*' * len(passwd)))
459 (host, db, user, '*' * len(passwd)))
457 self.conn = bzmysql._MySQLdb.connect(host=host,
460 self.conn = bzmysql._MySQLdb.connect(host=host,
458 user=user, passwd=passwd,
461 user=user, passwd=passwd,
459 db=db,
462 db=db,
460 connect_timeout=timeout)
463 connect_timeout=timeout)
461 self.cursor = self.conn.cursor()
464 self.cursor = self.conn.cursor()
462 self.longdesc_id = self.get_longdesc_id()
465 self.longdesc_id = self.get_longdesc_id()
463 self.user_ids = {}
466 self.user_ids = {}
464 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
467 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
465
468
466 def run(self, *args, **kwargs):
469 def run(self, *args, **kwargs):
467 '''run a query.'''
470 '''run a query.'''
468 self.ui.note(_('query: %s %s\n') % (args, kwargs))
471 self.ui.note(_('query: %s %s\n') % (args, kwargs))
469 try:
472 try:
470 self.cursor.execute(*args, **kwargs)
473 self.cursor.execute(*args, **kwargs)
471 except bzmysql._MySQLdb.MySQLError:
474 except bzmysql._MySQLdb.MySQLError:
472 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
475 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
473 raise
476 raise
474
477
475 def get_longdesc_id(self):
478 def get_longdesc_id(self):
476 '''get identity of longdesc field'''
479 '''get identity of longdesc field'''
477 self.run('select fieldid from fielddefs where name = "longdesc"')
480 self.run('select fieldid from fielddefs where name = "longdesc"')
478 ids = self.cursor.fetchall()
481 ids = self.cursor.fetchall()
479 if len(ids) != 1:
482 if len(ids) != 1:
480 raise error.Abort(_('unknown database schema'))
483 raise error.Abort(_('unknown database schema'))
481 return ids[0][0]
484 return ids[0][0]
482
485
483 def filter_real_bug_ids(self, bugs):
486 def filter_real_bug_ids(self, bugs):
484 '''filter not-existing bugs from set.'''
487 '''filter not-existing bugs from set.'''
485 self.run('select bug_id from bugs where bug_id in %s' %
488 self.run('select bug_id from bugs where bug_id in %s' %
486 bzmysql.sql_buglist(bugs.keys()))
489 bzmysql.sql_buglist(bugs.keys()))
487 existing = [id for (id,) in self.cursor.fetchall()]
490 existing = [id for (id,) in self.cursor.fetchall()]
488 for id in bugs.keys():
491 for id in bugs.keys():
489 if id not in existing:
492 if id not in existing:
490 self.ui.status(_('bug %d does not exist\n') % id)
493 self.ui.status(_('bug %d does not exist\n') % id)
491 del bugs[id]
494 del bugs[id]
492
495
493 def filter_cset_known_bug_ids(self, node, bugs):
496 def filter_cset_known_bug_ids(self, node, bugs):
494 '''filter bug ids that already refer to this changeset from set.'''
497 '''filter bug ids that already refer to this changeset from set.'''
495 self.run('''select bug_id from longdescs where
498 self.run('''select bug_id from longdescs where
496 bug_id in %s and thetext like "%%%s%%"''' %
499 bug_id in %s and thetext like "%%%s%%"''' %
497 (bzmysql.sql_buglist(bugs.keys()), short(node)))
500 (bzmysql.sql_buglist(bugs.keys()), short(node)))
498 for (id,) in self.cursor.fetchall():
501 for (id,) in self.cursor.fetchall():
499 self.ui.status(_('bug %d already knows about changeset %s\n') %
502 self.ui.status(_('bug %d already knows about changeset %s\n') %
500 (id, short(node)))
503 (id, short(node)))
501 del bugs[id]
504 del bugs[id]
502
505
503 def notify(self, bugs, committer):
506 def notify(self, bugs, committer):
504 '''tell bugzilla to send mail.'''
507 '''tell bugzilla to send mail.'''
505 self.ui.status(_('telling bugzilla to send mail:\n'))
508 self.ui.status(_('telling bugzilla to send mail:\n'))
506 (user, userid) = self.get_bugzilla_user(committer)
509 (user, userid) = self.get_bugzilla_user(committer)
507 for id in bugs.keys():
510 for id in bugs.keys():
508 self.ui.status(_(' bug %s\n') % id)
511 self.ui.status(_(' bug %s\n') % id)
509 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
512 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
510 bzdir = self.ui.config('bugzilla', 'bzdir')
513 bzdir = self.ui.config('bugzilla', 'bzdir')
511 try:
514 try:
512 # Backwards-compatible with old notify string, which
515 # Backwards-compatible with old notify string, which
513 # took one string. This will throw with a new format
516 # took one string. This will throw with a new format
514 # string.
517 # string.
515 cmd = cmdfmt % id
518 cmd = cmdfmt % id
516 except TypeError:
519 except TypeError:
517 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
520 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
518 self.ui.note(_('running notify command %s\n') % cmd)
521 self.ui.note(_('running notify command %s\n') % cmd)
519 fp = util.popen('(%s) 2>&1' % cmd)
522 fp = util.popen('(%s) 2>&1' % cmd)
520 out = fp.read()
523 out = fp.read()
521 ret = fp.close()
524 ret = fp.close()
522 if ret:
525 if ret:
523 self.ui.warn(out)
526 self.ui.warn(out)
524 raise error.Abort(_('bugzilla notify command %s') %
527 raise error.Abort(_('bugzilla notify command %s') %
525 util.explainexit(ret)[0])
528 util.explainexit(ret)[0])
526 self.ui.status(_('done\n'))
529 self.ui.status(_('done\n'))
527
530
528 def get_user_id(self, user):
531 def get_user_id(self, user):
529 '''look up numeric bugzilla user id.'''
532 '''look up numeric bugzilla user id.'''
530 try:
533 try:
531 return self.user_ids[user]
534 return self.user_ids[user]
532 except KeyError:
535 except KeyError:
533 try:
536 try:
534 userid = int(user)
537 userid = int(user)
535 except ValueError:
538 except ValueError:
536 self.ui.note(_('looking up user %s\n') % user)
539 self.ui.note(_('looking up user %s\n') % user)
537 self.run('''select userid from profiles
540 self.run('''select userid from profiles
538 where login_name like %s''', user)
541 where login_name like %s''', user)
539 all = self.cursor.fetchall()
542 all = self.cursor.fetchall()
540 if len(all) != 1:
543 if len(all) != 1:
541 raise KeyError(user)
544 raise KeyError(user)
542 userid = int(all[0][0])
545 userid = int(all[0][0])
543 self.user_ids[user] = userid
546 self.user_ids[user] = userid
544 return userid
547 return userid
545
548
546 def get_bugzilla_user(self, committer):
549 def get_bugzilla_user(self, committer):
547 '''See if committer is a registered bugzilla user. Return
550 '''See if committer is a registered bugzilla user. Return
548 bugzilla username and userid if so. If not, return default
551 bugzilla username and userid if so. If not, return default
549 bugzilla username and userid.'''
552 bugzilla username and userid.'''
550 user = self.map_committer(committer)
553 user = self.map_committer(committer)
551 try:
554 try:
552 userid = self.get_user_id(user)
555 userid = self.get_user_id(user)
553 except KeyError:
556 except KeyError:
554 try:
557 try:
555 defaultuser = self.ui.config('bugzilla', 'bzuser')
558 defaultuser = self.ui.config('bugzilla', 'bzuser')
556 if not defaultuser:
559 if not defaultuser:
557 raise error.Abort(_('cannot find bugzilla user id for %s') %
560 raise error.Abort(_('cannot find bugzilla user id for %s') %
558 user)
561 user)
559 userid = self.get_user_id(defaultuser)
562 userid = self.get_user_id(defaultuser)
560 user = defaultuser
563 user = defaultuser
561 except KeyError:
564 except KeyError:
562 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
565 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
563 % (user, defaultuser))
566 % (user, defaultuser))
564 return (user, userid)
567 return (user, userid)
565
568
566 def updatebug(self, bugid, newstate, text, committer):
569 def updatebug(self, bugid, newstate, text, committer):
567 '''update bug state with comment text.
570 '''update bug state with comment text.
568
571
569 Try adding comment as committer of changeset, otherwise as
572 Try adding comment as committer of changeset, otherwise as
570 default bugzilla user.'''
573 default bugzilla user.'''
571 if len(newstate) > 0:
574 if len(newstate) > 0:
572 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
575 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
573
576
574 (user, userid) = self.get_bugzilla_user(committer)
577 (user, userid) = self.get_bugzilla_user(committer)
575 now = time.strftime('%Y-%m-%d %H:%M:%S')
578 now = time.strftime('%Y-%m-%d %H:%M:%S')
576 self.run('''insert into longdescs
579 self.run('''insert into longdescs
577 (bug_id, who, bug_when, thetext)
580 (bug_id, who, bug_when, thetext)
578 values (%s, %s, %s, %s)''',
581 values (%s, %s, %s, %s)''',
579 (bugid, userid, now, text))
582 (bugid, userid, now, text))
580 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
583 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
581 values (%s, %s, %s, %s)''',
584 values (%s, %s, %s, %s)''',
582 (bugid, userid, now, self.longdesc_id))
585 (bugid, userid, now, self.longdesc_id))
583 self.conn.commit()
586 self.conn.commit()
584
587
585 class bzmysql_2_18(bzmysql):
588 class bzmysql_2_18(bzmysql):
586 '''support for bugzilla 2.18 series.'''
589 '''support for bugzilla 2.18 series.'''
587
590
588 def __init__(self, ui):
591 def __init__(self, ui):
589 bzmysql.__init__(self, ui)
592 bzmysql.__init__(self, ui)
590 self.default_notify = \
593 self.default_notify = \
591 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
594 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
592
595
593 class bzmysql_3_0(bzmysql_2_18):
596 class bzmysql_3_0(bzmysql_2_18):
594 '''support for bugzilla 3.0 series.'''
597 '''support for bugzilla 3.0 series.'''
595
598
596 def __init__(self, ui):
599 def __init__(self, ui):
597 bzmysql_2_18.__init__(self, ui)
600 bzmysql_2_18.__init__(self, ui)
598
601
599 def get_longdesc_id(self):
602 def get_longdesc_id(self):
600 '''get identity of longdesc field'''
603 '''get identity of longdesc field'''
601 self.run('select id from fielddefs where name = "longdesc"')
604 self.run('select id from fielddefs where name = "longdesc"')
602 ids = self.cursor.fetchall()
605 ids = self.cursor.fetchall()
603 if len(ids) != 1:
606 if len(ids) != 1:
604 raise error.Abort(_('unknown database schema'))
607 raise error.Abort(_('unknown database schema'))
605 return ids[0][0]
608 return ids[0][0]
606
609
607 # Bugzilla via XMLRPC interface.
610 # Bugzilla via XMLRPC interface.
608
611
609 class cookietransportrequest(object):
612 class cookietransportrequest(object):
610 """A Transport request method that retains cookies over its lifetime.
613 """A Transport request method that retains cookies over its lifetime.
611
614
612 The regular xmlrpclib transports ignore cookies. Which causes
615 The regular xmlrpclib transports ignore cookies. Which causes
613 a bit of a problem when you need a cookie-based login, as with
616 a bit of a problem when you need a cookie-based login, as with
614 the Bugzilla XMLRPC interface prior to 4.4.3.
617 the Bugzilla XMLRPC interface prior to 4.4.3.
615
618
616 So this is a helper for defining a Transport which looks for
619 So this is a helper for defining a Transport which looks for
617 cookies being set in responses and saves them to add to all future
620 cookies being set in responses and saves them to add to all future
618 requests.
621 requests.
619 """
622 """
620
623
621 # Inspiration drawn from
624 # Inspiration drawn from
622 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
625 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
623 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
626 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
624
627
625 cookies = []
628 cookies = []
626 def send_cookies(self, connection):
629 def send_cookies(self, connection):
627 if self.cookies:
630 if self.cookies:
628 for cookie in self.cookies:
631 for cookie in self.cookies:
629 connection.putheader("Cookie", cookie)
632 connection.putheader("Cookie", cookie)
630
633
631 def request(self, host, handler, request_body, verbose=0):
634 def request(self, host, handler, request_body, verbose=0):
632 self.verbose = verbose
635 self.verbose = verbose
633 self.accept_gzip_encoding = False
636 self.accept_gzip_encoding = False
634
637
635 # issue XML-RPC request
638 # issue XML-RPC request
636 h = self.make_connection(host)
639 h = self.make_connection(host)
637 if verbose:
640 if verbose:
638 h.set_debuglevel(1)
641 h.set_debuglevel(1)
639
642
640 self.send_request(h, handler, request_body)
643 self.send_request(h, handler, request_body)
641 self.send_host(h, host)
644 self.send_host(h, host)
642 self.send_cookies(h)
645 self.send_cookies(h)
643 self.send_user_agent(h)
646 self.send_user_agent(h)
644 self.send_content(h, request_body)
647 self.send_content(h, request_body)
645
648
646 # Deal with differences between Python 2.6 and 2.7.
649 # Deal with differences between Python 2.6 and 2.7.
647 # In the former h is a HTTP(S). In the latter it's a
650 # In the former h is a HTTP(S). In the latter it's a
648 # HTTP(S)Connection. Luckily, the 2.6 implementation of
651 # HTTP(S)Connection. Luckily, the 2.6 implementation of
649 # HTTP(S) has an underlying HTTP(S)Connection, so extract
652 # HTTP(S) has an underlying HTTP(S)Connection, so extract
650 # that and use it.
653 # that and use it.
651 try:
654 try:
652 response = h.getresponse()
655 response = h.getresponse()
653 except AttributeError:
656 except AttributeError:
654 response = h._conn.getresponse()
657 response = h._conn.getresponse()
655
658
656 # Add any cookie definitions to our list.
659 # Add any cookie definitions to our list.
657 for header in response.msg.getallmatchingheaders("Set-Cookie"):
660 for header in response.msg.getallmatchingheaders("Set-Cookie"):
658 val = header.split(": ", 1)[1]
661 val = header.split(": ", 1)[1]
659 cookie = val.split(";", 1)[0]
662 cookie = val.split(";", 1)[0]
660 self.cookies.append(cookie)
663 self.cookies.append(cookie)
661
664
662 if response.status != 200:
665 if response.status != 200:
663 raise xmlrpclib.ProtocolError(host + handler, response.status,
666 raise xmlrpclib.ProtocolError(host + handler, response.status,
664 response.reason, response.msg.headers)
667 response.reason, response.msg.headers)
665
668
666 payload = response.read()
669 payload = response.read()
667 parser, unmarshaller = self.getparser()
670 parser, unmarshaller = self.getparser()
668 parser.feed(payload)
671 parser.feed(payload)
669 parser.close()
672 parser.close()
670
673
671 return unmarshaller.close()
674 return unmarshaller.close()
672
675
673 # The explicit calls to the underlying xmlrpclib __init__() methods are
676 # The explicit calls to the underlying xmlrpclib __init__() methods are
674 # necessary. The xmlrpclib.Transport classes are old-style classes, and
677 # necessary. The xmlrpclib.Transport classes are old-style classes, and
675 # it turns out their __init__() doesn't get called when doing multiple
678 # it turns out their __init__() doesn't get called when doing multiple
676 # inheritance with a new-style class.
679 # inheritance with a new-style class.
677 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
680 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
678 def __init__(self, use_datetime=0):
681 def __init__(self, use_datetime=0):
679 if util.safehasattr(xmlrpclib.Transport, "__init__"):
682 if util.safehasattr(xmlrpclib.Transport, "__init__"):
680 xmlrpclib.Transport.__init__(self, use_datetime)
683 xmlrpclib.Transport.__init__(self, use_datetime)
681
684
682 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
685 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
683 def __init__(self, use_datetime=0):
686 def __init__(self, use_datetime=0):
684 if util.safehasattr(xmlrpclib.Transport, "__init__"):
687 if util.safehasattr(xmlrpclib.Transport, "__init__"):
685 xmlrpclib.SafeTransport.__init__(self, use_datetime)
688 xmlrpclib.SafeTransport.__init__(self, use_datetime)
686
689
687 class bzxmlrpc(bzaccess):
690 class bzxmlrpc(bzaccess):
688 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
691 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
689
692
690 Requires a minimum Bugzilla version 3.4.
693 Requires a minimum Bugzilla version 3.4.
691 """
694 """
692
695
693 def __init__(self, ui):
696 def __init__(self, ui):
694 bzaccess.__init__(self, ui)
697 bzaccess.__init__(self, ui)
695
698
696 bzweb = self.ui.config('bugzilla', 'bzurl')
699 bzweb = self.ui.config('bugzilla', 'bzurl')
697 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
700 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
698
701
699 user = self.ui.config('bugzilla', 'user')
702 user = self.ui.config('bugzilla', 'user')
700 passwd = self.ui.config('bugzilla', 'password')
703 passwd = self.ui.config('bugzilla', 'password')
701
704
702 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
705 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
703 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
706 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
704
707
705 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
708 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
706 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
709 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
707 self.bzvermajor = int(ver[0])
710 self.bzvermajor = int(ver[0])
708 self.bzverminor = int(ver[1])
711 self.bzverminor = int(ver[1])
709 login = self.bzproxy.User.login({'login': user, 'password': passwd,
712 login = self.bzproxy.User.login({'login': user, 'password': passwd,
710 'restrict_login': True})
713 'restrict_login': True})
711 self.bztoken = login.get('token', '')
714 self.bztoken = login.get('token', '')
712
715
713 def transport(self, uri):
716 def transport(self, uri):
714 if util.urlreq.urlparse(uri, "http")[0] == "https":
717 if util.urlreq.urlparse(uri, "http")[0] == "https":
715 return cookiesafetransport()
718 return cookiesafetransport()
716 else:
719 else:
717 return cookietransport()
720 return cookietransport()
718
721
719 def get_bug_comments(self, id):
722 def get_bug_comments(self, id):
720 """Return a string with all comment text for a bug."""
723 """Return a string with all comment text for a bug."""
721 c = self.bzproxy.Bug.comments({'ids': [id],
724 c = self.bzproxy.Bug.comments({'ids': [id],
722 'include_fields': ['text'],
725 'include_fields': ['text'],
723 'token': self.bztoken})
726 'token': self.bztoken})
724 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
727 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
725
728
726 def filter_real_bug_ids(self, bugs):
729 def filter_real_bug_ids(self, bugs):
727 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
730 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
728 'include_fields': [],
731 'include_fields': [],
729 'permissive': True,
732 'permissive': True,
730 'token': self.bztoken,
733 'token': self.bztoken,
731 })
734 })
732 for badbug in probe['faults']:
735 for badbug in probe['faults']:
733 id = badbug['id']
736 id = badbug['id']
734 self.ui.status(_('bug %d does not exist\n') % id)
737 self.ui.status(_('bug %d does not exist\n') % id)
735 del bugs[id]
738 del bugs[id]
736
739
737 def filter_cset_known_bug_ids(self, node, bugs):
740 def filter_cset_known_bug_ids(self, node, bugs):
738 for id in sorted(bugs.keys()):
741 for id in sorted(bugs.keys()):
739 if self.get_bug_comments(id).find(short(node)) != -1:
742 if self.get_bug_comments(id).find(short(node)) != -1:
740 self.ui.status(_('bug %d already knows about changeset %s\n') %
743 self.ui.status(_('bug %d already knows about changeset %s\n') %
741 (id, short(node)))
744 (id, short(node)))
742 del bugs[id]
745 del bugs[id]
743
746
744 def updatebug(self, bugid, newstate, text, committer):
747 def updatebug(self, bugid, newstate, text, committer):
745 args = {}
748 args = {}
746 if 'hours' in newstate:
749 if 'hours' in newstate:
747 args['work_time'] = newstate['hours']
750 args['work_time'] = newstate['hours']
748
751
749 if self.bzvermajor >= 4:
752 if self.bzvermajor >= 4:
750 args['ids'] = [bugid]
753 args['ids'] = [bugid]
751 args['comment'] = {'body' : text}
754 args['comment'] = {'body' : text}
752 if 'fix' in newstate:
755 if 'fix' in newstate:
753 args['status'] = self.fixstatus
756 args['status'] = self.fixstatus
754 args['resolution'] = self.fixresolution
757 args['resolution'] = self.fixresolution
755 args['token'] = self.bztoken
758 args['token'] = self.bztoken
756 self.bzproxy.Bug.update(args)
759 self.bzproxy.Bug.update(args)
757 else:
760 else:
758 if 'fix' in newstate:
761 if 'fix' in newstate:
759 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
762 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
760 "to mark bugs fixed\n"))
763 "to mark bugs fixed\n"))
761 args['id'] = bugid
764 args['id'] = bugid
762 args['comment'] = text
765 args['comment'] = text
763 self.bzproxy.Bug.add_comment(args)
766 self.bzproxy.Bug.add_comment(args)
764
767
765 class bzxmlrpcemail(bzxmlrpc):
768 class bzxmlrpcemail(bzxmlrpc):
766 """Read data from Bugzilla via XMLRPC, send updates via email.
769 """Read data from Bugzilla via XMLRPC, send updates via email.
767
770
768 Advantages of sending updates via email:
771 Advantages of sending updates via email:
769 1. Comments can be added as any user, not just logged in user.
772 1. Comments can be added as any user, not just logged in user.
770 2. Bug statuses or other fields not accessible via XMLRPC can
773 2. Bug statuses or other fields not accessible via XMLRPC can
771 potentially be updated.
774 potentially be updated.
772
775
773 There is no XMLRPC function to change bug status before Bugzilla
776 There is no XMLRPC function to change bug status before Bugzilla
774 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
777 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
775 But bugs can be marked fixed via email from 3.4 onwards.
778 But bugs can be marked fixed via email from 3.4 onwards.
776 """
779 """
777
780
778 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
781 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
779 # in-email fields are specified as '@<fieldname> = <value>'. In
782 # in-email fields are specified as '@<fieldname> = <value>'. In
780 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
783 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
781 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
784 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
782 # compatibility, but rather than rely on this use the new format for
785 # compatibility, but rather than rely on this use the new format for
783 # 4.0 onwards.
786 # 4.0 onwards.
784
787
785 def __init__(self, ui):
788 def __init__(self, ui):
786 bzxmlrpc.__init__(self, ui)
789 bzxmlrpc.__init__(self, ui)
787
790
788 self.bzemail = self.ui.config('bugzilla', 'bzemail')
791 self.bzemail = self.ui.config('bugzilla', 'bzemail')
789 if not self.bzemail:
792 if not self.bzemail:
790 raise error.Abort(_("configuration 'bzemail' missing"))
793 raise error.Abort(_("configuration 'bzemail' missing"))
791 mail.validateconfig(self.ui)
794 mail.validateconfig(self.ui)
792
795
793 def makecommandline(self, fieldname, value):
796 def makecommandline(self, fieldname, value):
794 if self.bzvermajor >= 4:
797 if self.bzvermajor >= 4:
795 return "@%s %s" % (fieldname, str(value))
798 return "@%s %s" % (fieldname, str(value))
796 else:
799 else:
797 if fieldname == "id":
800 if fieldname == "id":
798 fieldname = "bug_id"
801 fieldname = "bug_id"
799 return "@%s = %s" % (fieldname, str(value))
802 return "@%s = %s" % (fieldname, str(value))
800
803
801 def send_bug_modify_email(self, bugid, commands, comment, committer):
804 def send_bug_modify_email(self, bugid, commands, comment, committer):
802 '''send modification message to Bugzilla bug via email.
805 '''send modification message to Bugzilla bug via email.
803
806
804 The message format is documented in the Bugzilla email_in.pl
807 The message format is documented in the Bugzilla email_in.pl
805 specification. commands is a list of command lines, comment is the
808 specification. commands is a list of command lines, comment is the
806 comment text.
809 comment text.
807
810
808 To stop users from crafting commit comments with
811 To stop users from crafting commit comments with
809 Bugzilla commands, specify the bug ID via the message body, rather
812 Bugzilla commands, specify the bug ID via the message body, rather
810 than the subject line, and leave a blank line after it.
813 than the subject line, and leave a blank line after it.
811 '''
814 '''
812 user = self.map_committer(committer)
815 user = self.map_committer(committer)
813 matches = self.bzproxy.User.get({'match': [user],
816 matches = self.bzproxy.User.get({'match': [user],
814 'token': self.bztoken})
817 'token': self.bztoken})
815 if not matches['users']:
818 if not matches['users']:
816 user = self.ui.config('bugzilla', 'user')
819 user = self.ui.config('bugzilla', 'user')
817 matches = self.bzproxy.User.get({'match': [user],
820 matches = self.bzproxy.User.get({'match': [user],
818 'token': self.bztoken})
821 'token': self.bztoken})
819 if not matches['users']:
822 if not matches['users']:
820 raise error.Abort(_("default bugzilla user %s email not found")
823 raise error.Abort(_("default bugzilla user %s email not found")
821 % user)
824 % user)
822 user = matches['users'][0]['email']
825 user = matches['users'][0]['email']
823 commands.append(self.makecommandline("id", bugid))
826 commands.append(self.makecommandline("id", bugid))
824
827
825 text = "\n".join(commands) + "\n\n" + comment
828 text = "\n".join(commands) + "\n\n" + comment
826
829
827 _charsets = mail._charsets(self.ui)
830 _charsets = mail._charsets(self.ui)
828 user = mail.addressencode(self.ui, user, _charsets)
831 user = mail.addressencode(self.ui, user, _charsets)
829 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
832 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
830 msg = mail.mimeencode(self.ui, text, _charsets)
833 msg = mail.mimeencode(self.ui, text, _charsets)
831 msg['From'] = user
834 msg['From'] = user
832 msg['To'] = bzemail
835 msg['To'] = bzemail
833 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
836 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
834 sendmail = mail.connect(self.ui)
837 sendmail = mail.connect(self.ui)
835 sendmail(user, bzemail, msg.as_string())
838 sendmail(user, bzemail, msg.as_string())
836
839
837 def updatebug(self, bugid, newstate, text, committer):
840 def updatebug(self, bugid, newstate, text, committer):
838 cmds = []
841 cmds = []
839 if 'hours' in newstate:
842 if 'hours' in newstate:
840 cmds.append(self.makecommandline("work_time", newstate['hours']))
843 cmds.append(self.makecommandline("work_time", newstate['hours']))
841 if 'fix' in newstate:
844 if 'fix' in newstate:
842 cmds.append(self.makecommandline("bug_status", self.fixstatus))
845 cmds.append(self.makecommandline("bug_status", self.fixstatus))
843 cmds.append(self.makecommandline("resolution", self.fixresolution))
846 cmds.append(self.makecommandline("resolution", self.fixresolution))
844 self.send_bug_modify_email(bugid, cmds, text, committer)
847 self.send_bug_modify_email(bugid, cmds, text, committer)
845
848
846 class NotFound(LookupError):
849 class NotFound(LookupError):
847 pass
850 pass
848
851
849 class bzrestapi(bzaccess):
852 class bzrestapi(bzaccess):
850 """Read and write bugzilla data using the REST API available since
853 """Read and write bugzilla data using the REST API available since
851 Bugzilla 5.0.
854 Bugzilla 5.0.
852 """
855 """
853 def __init__(self, ui):
856 def __init__(self, ui):
854 bzaccess.__init__(self, ui)
857 bzaccess.__init__(self, ui)
855 bz = self.ui.config('bugzilla', 'bzurl')
858 bz = self.ui.config('bugzilla', 'bzurl')
856 self.bzroot = '/'.join([bz, 'rest'])
859 self.bzroot = '/'.join([bz, 'rest'])
857 self.apikey = self.ui.config('bugzilla', 'apikey')
860 self.apikey = self.ui.config('bugzilla', 'apikey')
858 self.user = self.ui.config('bugzilla', 'user')
861 self.user = self.ui.config('bugzilla', 'user')
859 self.passwd = self.ui.config('bugzilla', 'password')
862 self.passwd = self.ui.config('bugzilla', 'password')
860 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
863 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
861 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
864 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
862
865
863 def apiurl(self, targets, include_fields=None):
866 def apiurl(self, targets, include_fields=None):
864 url = '/'.join([self.bzroot] + [str(t) for t in targets])
867 url = '/'.join([self.bzroot] + [str(t) for t in targets])
865 qv = {}
868 qv = {}
866 if self.apikey:
869 if self.apikey:
867 qv['api_key'] = self.apikey
870 qv['api_key'] = self.apikey
868 elif self.user and self.passwd:
871 elif self.user and self.passwd:
869 qv['login'] = self.user
872 qv['login'] = self.user
870 qv['password'] = self.passwd
873 qv['password'] = self.passwd
871 if include_fields:
874 if include_fields:
872 qv['include_fields'] = include_fields
875 qv['include_fields'] = include_fields
873 if qv:
876 if qv:
874 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
877 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
875 return url
878 return url
876
879
877 def _fetch(self, burl):
880 def _fetch(self, burl):
878 try:
881 try:
879 resp = url.open(self.ui, burl)
882 resp = url.open(self.ui, burl)
880 return json.loads(resp.read())
883 return json.loads(resp.read())
881 except util.urlerr.httperror as inst:
884 except util.urlerr.httperror as inst:
882 if inst.code == 401:
885 if inst.code == 401:
883 raise error.Abort(_('authorization failed'))
886 raise error.Abort(_('authorization failed'))
884 if inst.code == 404:
887 if inst.code == 404:
885 raise NotFound()
888 raise NotFound()
886 else:
889 else:
887 raise
890 raise
888
891
889 def _submit(self, burl, data, method='POST'):
892 def _submit(self, burl, data, method='POST'):
890 data = json.dumps(data)
893 data = json.dumps(data)
891 if method == 'PUT':
894 if method == 'PUT':
892 class putrequest(util.urlreq.request):
895 class putrequest(util.urlreq.request):
893 def get_method(self):
896 def get_method(self):
894 return 'PUT'
897 return 'PUT'
895 request_type = putrequest
898 request_type = putrequest
896 else:
899 else:
897 request_type = util.urlreq.request
900 request_type = util.urlreq.request
898 req = request_type(burl, data,
901 req = request_type(burl, data,
899 {'Content-Type': 'application/json'})
902 {'Content-Type': 'application/json'})
900 try:
903 try:
901 resp = url.opener(self.ui).open(req)
904 resp = url.opener(self.ui).open(req)
902 return json.loads(resp.read())
905 return json.loads(resp.read())
903 except util.urlerr.httperror as inst:
906 except util.urlerr.httperror as inst:
904 if inst.code == 401:
907 if inst.code == 401:
905 raise error.Abort(_('authorization failed'))
908 raise error.Abort(_('authorization failed'))
906 if inst.code == 404:
909 if inst.code == 404:
907 raise NotFound()
910 raise NotFound()
908 else:
911 else:
909 raise
912 raise
910
913
911 def filter_real_bug_ids(self, bugs):
914 def filter_real_bug_ids(self, bugs):
912 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
915 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
913 badbugs = set()
916 badbugs = set()
914 for bugid in bugs:
917 for bugid in bugs:
915 burl = self.apiurl(('bug', bugid), include_fields='status')
918 burl = self.apiurl(('bug', bugid), include_fields='status')
916 try:
919 try:
917 self._fetch(burl)
920 self._fetch(burl)
918 except NotFound:
921 except NotFound:
919 badbugs.add(bugid)
922 badbugs.add(bugid)
920 for bugid in badbugs:
923 for bugid in badbugs:
921 del bugs[bugid]
924 del bugs[bugid]
922
925
923 def filter_cset_known_bug_ids(self, node, bugs):
926 def filter_cset_known_bug_ids(self, node, bugs):
924 '''remove bug IDs where node occurs in comment text from bugs.'''
927 '''remove bug IDs where node occurs in comment text from bugs.'''
925 sn = short(node)
928 sn = short(node)
926 for bugid in bugs.keys():
929 for bugid in bugs.keys():
927 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
930 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
928 result = self._fetch(burl)
931 result = self._fetch(burl)
929 comments = result['bugs'][str(bugid)]['comments']
932 comments = result['bugs'][str(bugid)]['comments']
930 if any(sn in c['text'] for c in comments):
933 if any(sn in c['text'] for c in comments):
931 self.ui.status(_('bug %d already knows about changeset %s\n') %
934 self.ui.status(_('bug %d already knows about changeset %s\n') %
932 (bugid, sn))
935 (bugid, sn))
933 del bugs[bugid]
936 del bugs[bugid]
934
937
935 def updatebug(self, bugid, newstate, text, committer):
938 def updatebug(self, bugid, newstate, text, committer):
936 '''update the specified bug. Add comment text and set new states.
939 '''update the specified bug. Add comment text and set new states.
937
940
938 If possible add the comment as being from the committer of
941 If possible add the comment as being from the committer of
939 the changeset. Otherwise use the default Bugzilla user.
942 the changeset. Otherwise use the default Bugzilla user.
940 '''
943 '''
941 bugmod = {}
944 bugmod = {}
942 if 'hours' in newstate:
945 if 'hours' in newstate:
943 bugmod['work_time'] = newstate['hours']
946 bugmod['work_time'] = newstate['hours']
944 if 'fix' in newstate:
947 if 'fix' in newstate:
945 bugmod['status'] = self.fixstatus
948 bugmod['status'] = self.fixstatus
946 bugmod['resolution'] = self.fixresolution
949 bugmod['resolution'] = self.fixresolution
947 if bugmod:
950 if bugmod:
948 # if we have to change the bugs state do it here
951 # if we have to change the bugs state do it here
949 bugmod['comment'] = {
952 bugmod['comment'] = {
950 'comment': text,
953 'comment': text,
951 'is_private': False,
954 'is_private': False,
952 'is_markdown': False,
955 'is_markdown': False,
953 }
956 }
954 burl = self.apiurl(('bug', bugid))
957 burl = self.apiurl(('bug', bugid))
955 self._submit(burl, bugmod, method='PUT')
958 self._submit(burl, bugmod, method='PUT')
956 self.ui.debug('updated bug %s\n' % bugid)
959 self.ui.debug('updated bug %s\n' % bugid)
957 else:
960 else:
958 burl = self.apiurl(('bug', bugid, 'comment'))
961 burl = self.apiurl(('bug', bugid, 'comment'))
959 self._submit(burl, {
962 self._submit(burl, {
960 'comment': text,
963 'comment': text,
961 'is_private': False,
964 'is_private': False,
962 'is_markdown': False,
965 'is_markdown': False,
963 })
966 })
964 self.ui.debug('added comment to bug %s\n' % bugid)
967 self.ui.debug('added comment to bug %s\n' % bugid)
965
968
966 def notify(self, bugs, committer):
969 def notify(self, bugs, committer):
967 '''Force sending of Bugzilla notification emails.
970 '''Force sending of Bugzilla notification emails.
968
971
969 Only required if the access method does not trigger notification
972 Only required if the access method does not trigger notification
970 emails automatically.
973 emails automatically.
971 '''
974 '''
972 pass
975 pass
973
976
974 class bugzilla(object):
977 class bugzilla(object):
975 # supported versions of bugzilla. different versions have
978 # supported versions of bugzilla. different versions have
976 # different schemas.
979 # different schemas.
977 _versions = {
980 _versions = {
978 '2.16': bzmysql,
981 '2.16': bzmysql,
979 '2.18': bzmysql_2_18,
982 '2.18': bzmysql_2_18,
980 '3.0': bzmysql_3_0,
983 '3.0': bzmysql_3_0,
981 'xmlrpc': bzxmlrpc,
984 'xmlrpc': bzxmlrpc,
982 'xmlrpc+email': bzxmlrpcemail,
985 'xmlrpc+email': bzxmlrpcemail,
983 'restapi': bzrestapi,
986 'restapi': bzrestapi,
984 }
987 }
985
988
986 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
989 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
987 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
990 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
988 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
991 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
989 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
992 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
990
993
991 def __init__(self, ui, repo):
994 def __init__(self, ui, repo):
992 self.ui = ui
995 self.ui = ui
993 self.repo = repo
996 self.repo = repo
994
997
995 bzversion = self.ui.config('bugzilla', 'version')
998 bzversion = self.ui.config('bugzilla', 'version')
996 try:
999 try:
997 bzclass = bugzilla._versions[bzversion]
1000 bzclass = bugzilla._versions[bzversion]
998 except KeyError:
1001 except KeyError:
999 raise error.Abort(_('bugzilla version %s not supported') %
1002 raise error.Abort(_('bugzilla version %s not supported') %
1000 bzversion)
1003 bzversion)
1001 self.bzdriver = bzclass(self.ui)
1004 self.bzdriver = bzclass(self.ui)
1002
1005
1003 self.bug_re = re.compile(
1006 self.bug_re = re.compile(
1004 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
1007 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
1005 self.fix_re = re.compile(
1008 self.fix_re = re.compile(
1006 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
1009 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
1007 self.split_re = re.compile(r'\D+')
1010 self.split_re = re.compile(r'\D+')
1008
1011
1009 def find_bugs(self, ctx):
1012 def find_bugs(self, ctx):
1010 '''return bugs dictionary created from commit comment.
1013 '''return bugs dictionary created from commit comment.
1011
1014
1012 Extract bug info from changeset comments. Filter out any that are
1015 Extract bug info from changeset comments. Filter out any that are
1013 not known to Bugzilla, and any that already have a reference to
1016 not known to Bugzilla, and any that already have a reference to
1014 the given changeset in their comments.
1017 the given changeset in their comments.
1015 '''
1018 '''
1016 start = 0
1019 start = 0
1017 hours = 0.0
1020 hours = 0.0
1018 bugs = {}
1021 bugs = {}
1019 bugmatch = self.bug_re.search(ctx.description(), start)
1022 bugmatch = self.bug_re.search(ctx.description(), start)
1020 fixmatch = self.fix_re.search(ctx.description(), start)
1023 fixmatch = self.fix_re.search(ctx.description(), start)
1021 while True:
1024 while True:
1022 bugattribs = {}
1025 bugattribs = {}
1023 if not bugmatch and not fixmatch:
1026 if not bugmatch and not fixmatch:
1024 break
1027 break
1025 if not bugmatch:
1028 if not bugmatch:
1026 m = fixmatch
1029 m = fixmatch
1027 elif not fixmatch:
1030 elif not fixmatch:
1028 m = bugmatch
1031 m = bugmatch
1029 else:
1032 else:
1030 if bugmatch.start() < fixmatch.start():
1033 if bugmatch.start() < fixmatch.start():
1031 m = bugmatch
1034 m = bugmatch
1032 else:
1035 else:
1033 m = fixmatch
1036 m = fixmatch
1034 start = m.end()
1037 start = m.end()
1035 if m is bugmatch:
1038 if m is bugmatch:
1036 bugmatch = self.bug_re.search(ctx.description(), start)
1039 bugmatch = self.bug_re.search(ctx.description(), start)
1037 if 'fix' in bugattribs:
1040 if 'fix' in bugattribs:
1038 del bugattribs['fix']
1041 del bugattribs['fix']
1039 else:
1042 else:
1040 fixmatch = self.fix_re.search(ctx.description(), start)
1043 fixmatch = self.fix_re.search(ctx.description(), start)
1041 bugattribs['fix'] = None
1044 bugattribs['fix'] = None
1042
1045
1043 try:
1046 try:
1044 ids = m.group('ids')
1047 ids = m.group('ids')
1045 except IndexError:
1048 except IndexError:
1046 ids = m.group(1)
1049 ids = m.group(1)
1047 try:
1050 try:
1048 hours = float(m.group('hours'))
1051 hours = float(m.group('hours'))
1049 bugattribs['hours'] = hours
1052 bugattribs['hours'] = hours
1050 except IndexError:
1053 except IndexError:
1051 pass
1054 pass
1052 except TypeError:
1055 except TypeError:
1053 pass
1056 pass
1054 except ValueError:
1057 except ValueError:
1055 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1058 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1056
1059
1057 for id in self.split_re.split(ids):
1060 for id in self.split_re.split(ids):
1058 if not id:
1061 if not id:
1059 continue
1062 continue
1060 bugs[int(id)] = bugattribs
1063 bugs[int(id)] = bugattribs
1061 if bugs:
1064 if bugs:
1062 self.bzdriver.filter_real_bug_ids(bugs)
1065 self.bzdriver.filter_real_bug_ids(bugs)
1063 if bugs:
1066 if bugs:
1064 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1067 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1065 return bugs
1068 return bugs
1066
1069
1067 def update(self, bugid, newstate, ctx):
1070 def update(self, bugid, newstate, ctx):
1068 '''update bugzilla bug with reference to changeset.'''
1071 '''update bugzilla bug with reference to changeset.'''
1069
1072
1070 def webroot(root):
1073 def webroot(root):
1071 '''strip leading prefix of repo root and turn into
1074 '''strip leading prefix of repo root and turn into
1072 url-safe path.'''
1075 url-safe path.'''
1073 count = int(self.ui.config('bugzilla', 'strip'))
1076 count = int(self.ui.config('bugzilla', 'strip'))
1074 root = util.pconvert(root)
1077 root = util.pconvert(root)
1075 while count > 0:
1078 while count > 0:
1076 c = root.find('/')
1079 c = root.find('/')
1077 if c == -1:
1080 if c == -1:
1078 break
1081 break
1079 root = root[c + 1:]
1082 root = root[c + 1:]
1080 count -= 1
1083 count -= 1
1081 return root
1084 return root
1082
1085
1083 mapfile = None
1086 mapfile = None
1084 tmpl = self.ui.config('bugzilla', 'template')
1087 tmpl = self.ui.config('bugzilla', 'template')
1085 if not tmpl:
1088 if not tmpl:
1086 mapfile = self.ui.config('bugzilla', 'style')
1089 mapfile = self.ui.config('bugzilla', 'style')
1087 if not mapfile and not tmpl:
1090 if not mapfile and not tmpl:
1088 tmpl = _('changeset {node|short} in repo {root} refers '
1091 tmpl = _('changeset {node|short} in repo {root} refers '
1089 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1092 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1090 spec = cmdutil.logtemplatespec(tmpl, mapfile)
1093 spec = cmdutil.logtemplatespec(tmpl, mapfile)
1091 t = cmdutil.changeset_templater(self.ui, self.repo, spec,
1094 t = cmdutil.changeset_templater(self.ui, self.repo, spec,
1092 False, None, False)
1095 False, None, False)
1093 self.ui.pushbuffer()
1096 self.ui.pushbuffer()
1094 t.show(ctx, changes=ctx.changeset(),
1097 t.show(ctx, changes=ctx.changeset(),
1095 bug=str(bugid),
1098 bug=str(bugid),
1096 hgweb=self.ui.config('web', 'baseurl'),
1099 hgweb=self.ui.config('web', 'baseurl'),
1097 root=self.repo.root,
1100 root=self.repo.root,
1098 webroot=webroot(self.repo.root))
1101 webroot=webroot(self.repo.root))
1099 data = self.ui.popbuffer()
1102 data = self.ui.popbuffer()
1100 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
1103 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
1101
1104
1102 def notify(self, bugs, committer):
1105 def notify(self, bugs, committer):
1103 '''ensure Bugzilla users are notified of bug change.'''
1106 '''ensure Bugzilla users are notified of bug change.'''
1104 self.bzdriver.notify(bugs, committer)
1107 self.bzdriver.notify(bugs, committer)
1105
1108
1106 def hook(ui, repo, hooktype, node=None, **kwargs):
1109 def hook(ui, repo, hooktype, node=None, **kwargs):
1107 '''add comment to bugzilla for each changeset that refers to a
1110 '''add comment to bugzilla for each changeset that refers to a
1108 bugzilla bug id. only add a comment once per bug, so same change
1111 bugzilla bug id. only add a comment once per bug, so same change
1109 seen multiple times does not fill bug with duplicate data.'''
1112 seen multiple times does not fill bug with duplicate data.'''
1110 if node is None:
1113 if node is None:
1111 raise error.Abort(_('hook type %s does not pass a changeset id') %
1114 raise error.Abort(_('hook type %s does not pass a changeset id') %
1112 hooktype)
1115 hooktype)
1113 try:
1116 try:
1114 bz = bugzilla(ui, repo)
1117 bz = bugzilla(ui, repo)
1115 ctx = repo[node]
1118 ctx = repo[node]
1116 bugs = bz.find_bugs(ctx)
1119 bugs = bz.find_bugs(ctx)
1117 if bugs:
1120 if bugs:
1118 for bug in bugs:
1121 for bug in bugs:
1119 bz.update(bug, bugs[bug], ctx)
1122 bz.update(bug, bugs[bug], ctx)
1120 bz.notify(bugs, util.email(ctx.user()))
1123 bz.notify(bugs, util.email(ctx.user()))
1121 except Exception as e:
1124 except Exception as e:
1122 raise error.Abort(_('Bugzilla error: %s') % e)
1125 raise error.Abort(_('Bugzilla error: %s') % e)
General Comments 0
You need to be logged in to leave comments. Login now