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