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