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