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