##// END OF EJS Templates
bugzilla: extract optional hours from commit message and update bug time...
Jim Hague -
r16222:d7b7b453 default
parent child Browse files
Show More
@@ -1,787 +1,823
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-2 Jim Hague <jim.hague@acm.org>
4 # Copyright 2011-2 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 hook does not change bug status.
15 The bug references can optionally include an update for Bugzilla of the
16 hours spent working on the bug.
16
17
17 Three basic modes of access to Bugzilla are provided:
18 Three basic modes of access to Bugzilla are provided:
18
19
19 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
20
21
21 2. Check data via the Bugzilla XMLRPC interface and submit bug change
22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
22 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
23
24
24 3. Writing directly to the Bugzilla database. Only Bugzilla installations
25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
25 using MySQL are supported. Requires Python MySQLdb.
26 using MySQL are supported. Requires Python MySQLdb.
26
27
27 Writing directly to the database is susceptible to schema changes, and
28 Writing directly to the database is susceptible to schema changes, and
28 relies on a Bugzilla contrib script to send out bug change
29 relies on a Bugzilla contrib script to send out bug change
29 notification emails. This script runs as the user running Mercurial,
30 notification emails. This script runs as the user running Mercurial,
30 must be run on the host with the Bugzilla install, and requires
31 must be run on the host with the Bugzilla install, and requires
31 permission to read Bugzilla configuration details and the necessary
32 permission to read Bugzilla configuration details and the necessary
32 MySQL user and password to have full access rights to the Bugzilla
33 MySQL user and password to have full access rights to the Bugzilla
33 database. For these reasons this access mode is now considered
34 database. For these reasons this access mode is now considered
34 deprecated, and will not be updated for new Bugzilla versions going
35 deprecated, and will not be updated for new Bugzilla versions going
35 forward.
36 forward. Only adding comments is supported in this access mode.
36
37
37 Access via XMLRPC needs a Bugzilla username and password to be specified
38 Access via XMLRPC needs a Bugzilla username and password to be specified
38 in the configuration. Comments are added under that username. Since the
39 in the configuration. Comments are added under that username. Since the
39 configuration must be readable by all Mercurial users, it is recommended
40 configuration must be readable by all Mercurial users, it is recommended
40 that the rights of that user are restricted in Bugzilla to the minimum
41 that the rights of that user are restricted in Bugzilla to the minimum
41 necessary to add comments.
42 necessary to add comments.
42
43
43 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
44 email to the Bugzilla email interface to submit comments to bugs.
45 email to the Bugzilla email interface to submit comments to bugs.
45 The From: address in the email is set to the email address of the Mercurial
46 The From: address in the email is set to the email address of the Mercurial
46 user, so the comment appears to come from the Mercurial user. In the event
47 user, so the comment appears to come from the Mercurial user. In the event
47 that the Mercurial user email is not recognised by Bugzilla as a Bugzilla
48 that the Mercurial user email is not recognised by Bugzilla as a Bugzilla
48 user, the email associated with the Bugzilla username used to log into
49 user, the email associated with the Bugzilla username used to log into
49 Bugzilla is used instead as the source of the comment.
50 Bugzilla is used instead as the source of the comment.
50
51
51 Configuration items common to all access modes:
52 Configuration items common to all access modes:
52
53
53 bugzilla.version
54 bugzilla.version
54 This access type to use. Values recognised are:
55 This access type to use. Values recognised are:
55
56
56 :``xmlrpc``: Bugzilla XMLRPC interface.
57 :``xmlrpc``: Bugzilla XMLRPC interface.
57 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
58 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
58 :``3.0``: MySQL access, Bugzilla 3.0 and later.
59 :``3.0``: MySQL access, Bugzilla 3.0 and later.
59 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
60 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
60 including 3.0.
61 including 3.0.
61 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
62 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
62 including 2.18.
63 including 2.18.
63
64
64 bugzilla.regexp
65 bugzilla.regexp
65 Regular expression to match bug IDs in changeset commit message.
66 Regular expression to match bug IDs in changeset commit message.
66 Must contain one "()" group. The default expression matches ``Bug
67 It must contain one "()" named group ``<ids>`` containing the bug
67 1234``, ``Bug no. 1234``, ``Bug number 1234``, ``Bugs 1234,5678``,
68 IDs separated by non-digit characters. It may also contain
68 ``Bug 1234 and 5678`` and variations thereof. Matching is case
69 a named group ``<hours>`` with a floating-point number giving the
69 insensitive.
70 hours worked on the bug. If no named groups are present, the first
71 "()" group is assumed to contain the bug IDs, and work time is not
72 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
73 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
74 variations thereof, followed by an hours number prefixed by ``h`` or
75 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
70
76
71 bugzilla.style
77 bugzilla.style
72 The style file to use when formatting comments.
78 The style file to use when formatting comments.
73
79
74 bugzilla.template
80 bugzilla.template
75 Template to use when formatting comments. Overrides style if
81 Template to use when formatting comments. Overrides style if
76 specified. In addition to the usual Mercurial keywords, the
82 specified. In addition to the usual Mercurial keywords, the
77 extension specifies:
83 extension specifies:
78
84
79 :``{bug}``: The Bugzilla bug ID.
85 :``{bug}``: The Bugzilla bug ID.
80 :``{root}``: The full pathname of the Mercurial repository.
86 :``{root}``: The full pathname of the Mercurial repository.
81 :``{webroot}``: Stripped pathname of the Mercurial repository.
87 :``{webroot}``: Stripped pathname of the Mercurial repository.
82 :``{hgweb}``: Base URL for browsing Mercurial repositories.
88 :``{hgweb}``: Base URL for browsing Mercurial repositories.
83
89
84 Default ``changeset {node|short} in repo {root} refers to bug
90 Default ``changeset {node|short} in repo {root} refers to bug
85 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
91 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
86
92
87 bugzilla.strip
93 bugzilla.strip
88 The number of path separator characters to strip from the front of
94 The number of path separator characters to strip from the front of
89 the Mercurial repository path (``{root}`` in templates) to produce
95 the Mercurial repository path (``{root}`` in templates) to produce
90 ``{webroot}``. For example, a repository with ``{root}``
96 ``{webroot}``. For example, a repository with ``{root}``
91 ``/var/local/my-project`` with a strip of 2 gives a value for
97 ``/var/local/my-project`` with a strip of 2 gives a value for
92 ``{webroot}`` of ``my-project``. Default 0.
98 ``{webroot}`` of ``my-project``. Default 0.
93
99
94 web.baseurl
100 web.baseurl
95 Base URL for browsing Mercurial repositories. Referenced from
101 Base URL for browsing Mercurial repositories. Referenced from
96 templates as ``{hgweb}``.
102 templates as ``{hgweb}``.
97
103
98 Configuration items common to XMLRPC+email and MySQL access modes:
104 Configuration items common to XMLRPC+email and MySQL access modes:
99
105
100 bugzilla.usermap
106 bugzilla.usermap
101 Path of file containing Mercurial committer email to Bugzilla user email
107 Path of file containing Mercurial committer email to Bugzilla user email
102 mappings. If specified, the file should contain one mapping per
108 mappings. If specified, the file should contain one mapping per
103 line::
109 line::
104
110
105 committer = Bugzilla user
111 committer = Bugzilla user
106
112
107 See also the ``[usermap]`` section.
113 See also the ``[usermap]`` section.
108
114
109 The ``[usermap]`` section is used to specify mappings of Mercurial
115 The ``[usermap]`` section is used to specify mappings of Mercurial
110 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
116 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
111 Contains entries of the form ``committer = Bugzilla user``.
117 Contains entries of the form ``committer = Bugzilla user``.
112
118
113 XMLRPC access mode configuration:
119 XMLRPC access mode configuration:
114
120
115 bugzilla.bzurl
121 bugzilla.bzurl
116 The base URL for the Bugzilla installation.
122 The base URL for the Bugzilla installation.
117 Default ``http://localhost/bugzilla``.
123 Default ``http://localhost/bugzilla``.
118
124
119 bugzilla.user
125 bugzilla.user
120 The username to use to log into Bugzilla via XMLRPC. Default
126 The username to use to log into Bugzilla via XMLRPC. Default
121 ``bugs``.
127 ``bugs``.
122
128
123 bugzilla.password
129 bugzilla.password
124 The password for Bugzilla login.
130 The password for Bugzilla login.
125
131
126 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
132 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
127 and also:
133 and also:
128
134
129 bugzilla.bzemail
135 bugzilla.bzemail
130 The Bugzilla email address.
136 The Bugzilla email address.
131
137
132 In addition, the Mercurial email settings must be configured. See the
138 In addition, the Mercurial email settings must be configured. See the
133 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
139 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
134
140
135 MySQL access mode configuration:
141 MySQL access mode configuration:
136
142
137 bugzilla.host
143 bugzilla.host
138 Hostname of the MySQL server holding the Bugzilla database.
144 Hostname of the MySQL server holding the Bugzilla database.
139 Default ``localhost``.
145 Default ``localhost``.
140
146
141 bugzilla.db
147 bugzilla.db
142 Name of the Bugzilla database in MySQL. Default ``bugs``.
148 Name of the Bugzilla database in MySQL. Default ``bugs``.
143
149
144 bugzilla.user
150 bugzilla.user
145 Username to use to access MySQL server. Default ``bugs``.
151 Username to use to access MySQL server. Default ``bugs``.
146
152
147 bugzilla.password
153 bugzilla.password
148 Password to use to access MySQL server.
154 Password to use to access MySQL server.
149
155
150 bugzilla.timeout
156 bugzilla.timeout
151 Database connection timeout (seconds). Default 5.
157 Database connection timeout (seconds). Default 5.
152
158
153 bugzilla.bzuser
159 bugzilla.bzuser
154 Fallback Bugzilla user name to record comments with, if changeset
160 Fallback Bugzilla user name to record comments with, if changeset
155 committer cannot be found as a Bugzilla user.
161 committer cannot be found as a Bugzilla user.
156
162
157 bugzilla.bzdir
163 bugzilla.bzdir
158 Bugzilla install directory. Used by default notify. Default
164 Bugzilla install directory. Used by default notify. Default
159 ``/var/www/html/bugzilla``.
165 ``/var/www/html/bugzilla``.
160
166
161 bugzilla.notify
167 bugzilla.notify
162 The command to run to get Bugzilla to send bug change notification
168 The command to run to get Bugzilla to send bug change notification
163 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
169 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
164 id) and ``user`` (committer bugzilla email). Default depends on
170 id) and ``user`` (committer bugzilla email). Default depends on
165 version; from 2.18 it is "cd %(bzdir)s && perl -T
171 version; from 2.18 it is "cd %(bzdir)s && perl -T
166 contrib/sendbugmail.pl %(id)s %(user)s".
172 contrib/sendbugmail.pl %(id)s %(user)s".
167
173
168 Activating the extension::
174 Activating the extension::
169
175
170 [extensions]
176 [extensions]
171 bugzilla =
177 bugzilla =
172
178
173 [hooks]
179 [hooks]
174 # run bugzilla hook on every change pulled or pushed in here
180 # run bugzilla hook on every change pulled or pushed in here
175 incoming.bugzilla = python:hgext.bugzilla.hook
181 incoming.bugzilla = python:hgext.bugzilla.hook
176
182
177 Example configurations:
183 Example configurations:
178
184
179 XMLRPC example configuration. This uses the Bugzilla at
185 XMLRPC example configuration. This uses the Bugzilla at
180 ``http://my-project.org/bugzilla``, logging in as user
186 ``http://my-project.org/bugzilla``, logging in as user
181 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
187 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
182 collection of Mercurial repositories in ``/var/local/hg/repos/``,
188 collection of Mercurial repositories in ``/var/local/hg/repos/``,
183 with a web interface at ``http://my-project.org/hg``. ::
189 with a web interface at ``http://my-project.org/hg``. ::
184
190
185 [bugzilla]
191 [bugzilla]
186 bzurl=http://my-project.org/bugzilla
192 bzurl=http://my-project.org/bugzilla
187 user=bugmail@my-project.org
193 user=bugmail@my-project.org
188 password=plugh
194 password=plugh
189 version=xmlrpc
195 version=xmlrpc
190 template=Changeset {node|short} in {root|basename}.
196 template=Changeset {node|short} in {root|basename}.
191 {hgweb}/{webroot}/rev/{node|short}\\n
197 {hgweb}/{webroot}/rev/{node|short}\\n
192 {desc}\\n
198 {desc}\\n
193 strip=5
199 strip=5
194
200
195 [web]
201 [web]
196 baseurl=http://my-project.org/hg
202 baseurl=http://my-project.org/hg
197
203
198 XMLRPC+email example configuration. This uses the Bugzilla at
204 XMLRPC+email example configuration. This uses the Bugzilla at
199 ``http://my-project.org/bugzilla``, logging in as user
205 ``http://my-project.org/bugzilla``, logging in as user
200 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
201 collection of Mercurial repositories in ``/var/local/hg/repos/``,
207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
202 with a web interface at ``http://my-project.org/hg``. Bug comments
208 with a web interface at ``http://my-project.org/hg``. Bug comments
203 are sent to the Bugzilla email address
209 are sent to the Bugzilla email address
204 ``bugzilla@my-project.org``. ::
210 ``bugzilla@my-project.org``. ::
205
211
206 [bugzilla]
212 [bugzilla]
207 bzurl=http://my-project.org/bugzilla
213 bzurl=http://my-project.org/bugzilla
208 user=bugmail@my-project.org
214 user=bugmail@my-project.org
209 password=plugh
215 password=plugh
210 version=xmlrpc
216 version=xmlrpc
211 bzemail=bugzilla@my-project.org
217 bzemail=bugzilla@my-project.org
212 template=Changeset {node|short} in {root|basename}.
218 template=Changeset {node|short} in {root|basename}.
213 {hgweb}/{webroot}/rev/{node|short}\\n
219 {hgweb}/{webroot}/rev/{node|short}\\n
214 {desc}\\n
220 {desc}\\n
215 strip=5
221 strip=5
216
222
217 [web]
223 [web]
218 baseurl=http://my-project.org/hg
224 baseurl=http://my-project.org/hg
219
225
220 [usermap]
226 [usermap]
221 user@emaildomain.com=user.name@bugzilladomain.com
227 user@emaildomain.com=user.name@bugzilladomain.com
222
228
223 MySQL example configuration. This has a local Bugzilla 3.2 installation
229 MySQL example configuration. This has a local Bugzilla 3.2 installation
224 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
230 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
225 the Bugzilla database name is ``bugs`` and MySQL is
231 the Bugzilla database name is ``bugs`` and MySQL is
226 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
232 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
227 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
233 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
228 with a web interface at ``http://my-project.org/hg``. ::
234 with a web interface at ``http://my-project.org/hg``. ::
229
235
230 [bugzilla]
236 [bugzilla]
231 host=localhost
237 host=localhost
232 password=XYZZY
238 password=XYZZY
233 version=3.0
239 version=3.0
234 bzuser=unknown@domain.com
240 bzuser=unknown@domain.com
235 bzdir=/opt/bugzilla-3.2
241 bzdir=/opt/bugzilla-3.2
236 template=Changeset {node|short} in {root|basename}.
242 template=Changeset {node|short} in {root|basename}.
237 {hgweb}/{webroot}/rev/{node|short}\\n
243 {hgweb}/{webroot}/rev/{node|short}\\n
238 {desc}\\n
244 {desc}\\n
239 strip=5
245 strip=5
240
246
241 [web]
247 [web]
242 baseurl=http://my-project.org/hg
248 baseurl=http://my-project.org/hg
243
249
244 [usermap]
250 [usermap]
245 user@emaildomain.com=user.name@bugzilladomain.com
251 user@emaildomain.com=user.name@bugzilladomain.com
246
252
247 All the above add a comment to the Bugzilla bug record of the form::
253 All the above add a comment to the Bugzilla bug record of the form::
248
254
249 Changeset 3b16791d6642 in repository-name.
255 Changeset 3b16791d6642 in repository-name.
250 http://my-project.org/hg/repository-name/rev/3b16791d6642
256 http://my-project.org/hg/repository-name/rev/3b16791d6642
251
257
252 Changeset commit comment. Bug 1234.
258 Changeset commit comment. Bug 1234.
253 '''
259 '''
254
260
255 from mercurial.i18n import _
261 from mercurial.i18n import _
256 from mercurial.node import short
262 from mercurial.node import short
257 from mercurial import cmdutil, mail, templater, util
263 from mercurial import cmdutil, mail, templater, util
258 import re, time, urlparse, xmlrpclib
264 import re, time, urlparse, xmlrpclib
259
265
260 class bzaccess(object):
266 class bzaccess(object):
261 '''Base class for access to Bugzilla.'''
267 '''Base class for access to Bugzilla.'''
262
268
263 def __init__(self, ui):
269 def __init__(self, ui):
264 self.ui = ui
270 self.ui = ui
265 usermap = self.ui.config('bugzilla', 'usermap')
271 usermap = self.ui.config('bugzilla', 'usermap')
266 if usermap:
272 if usermap:
267 self.ui.readconfig(usermap, sections=['usermap'])
273 self.ui.readconfig(usermap, sections=['usermap'])
268
274
269 def map_committer(self, user):
275 def map_committer(self, user):
270 '''map name of committer to Bugzilla user name.'''
276 '''map name of committer to Bugzilla user name.'''
271 for committer, bzuser in self.ui.configitems('usermap'):
277 for committer, bzuser in self.ui.configitems('usermap'):
272 if committer.lower() == user.lower():
278 if committer.lower() == user.lower():
273 return bzuser
279 return bzuser
274 return user
280 return user
275
281
276 # Methods to be implemented by access classes.
282 # Methods to be implemented by access classes.
277 #
283 #
278 # 'bugs' is a dict keyed on bug id, where values are a dict holding
284 # 'bugs' is a dict keyed on bug id, where values are a dict holding
279 # updates to bug state. Currently no states are recognised, but this
285 # updates to bug state. Recognised dict keys are:
280 # will change soon.
286 #
287 # 'hours': Value, float containing work hours to be updated.
288
281 def filter_real_bug_ids(self, bugs):
289 def filter_real_bug_ids(self, bugs):
282 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
290 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
283 pass
291 pass
284
292
285 def filter_cset_known_bug_ids(self, node, bugs):
293 def filter_cset_known_bug_ids(self, node, bugs):
286 '''remove bug IDs where node occurs in comment text from bugs.'''
294 '''remove bug IDs where node occurs in comment text from bugs.'''
287 pass
295 pass
288
296
289 def updatebug(self, bugid, newstate, text, committer):
297 def updatebug(self, bugid, newstate, text, committer):
290 '''update the specified bug. Add comment text and set new states.
298 '''update the specified bug. Add comment text and set new states.
291
299
292 If possible add the comment as being from the committer of
300 If possible add the comment as being from the committer of
293 the changeset. Otherwise use the default Bugzilla user.
301 the changeset. Otherwise use the default Bugzilla user.
294 '''
302 '''
295 pass
303 pass
296
304
297 def notify(self, bugs, committer):
305 def notify(self, bugs, committer):
298 '''Force sending of Bugzilla notification emails.
306 '''Force sending of Bugzilla notification emails.
299
307
300 Only required if the access method does not trigger notification
308 Only required if the access method does not trigger notification
301 emails automatically.
309 emails automatically.
302 '''
310 '''
303 pass
311 pass
304
312
305 # Bugzilla via direct access to MySQL database.
313 # Bugzilla via direct access to MySQL database.
306 class bzmysql(bzaccess):
314 class bzmysql(bzaccess):
307 '''Support for direct MySQL access to Bugzilla.
315 '''Support for direct MySQL access to Bugzilla.
308
316
309 The earliest Bugzilla version this is tested with is version 2.16.
317 The earliest Bugzilla version this is tested with is version 2.16.
310
318
311 If your Bugzilla is version 3.2 or above, you are strongly
319 If your Bugzilla is version 3.2 or above, you are strongly
312 recommended to use the XMLRPC access method instead.
320 recommended to use the XMLRPC access method instead.
313 '''
321 '''
314
322
315 @staticmethod
323 @staticmethod
316 def sql_buglist(ids):
324 def sql_buglist(ids):
317 '''return SQL-friendly list of bug ids'''
325 '''return SQL-friendly list of bug ids'''
318 return '(' + ','.join(map(str, ids)) + ')'
326 return '(' + ','.join(map(str, ids)) + ')'
319
327
320 _MySQLdb = None
328 _MySQLdb = None
321
329
322 def __init__(self, ui):
330 def __init__(self, ui):
323 try:
331 try:
324 import MySQLdb as mysql
332 import MySQLdb as mysql
325 bzmysql._MySQLdb = mysql
333 bzmysql._MySQLdb = mysql
326 except ImportError, err:
334 except ImportError, err:
327 raise util.Abort(_('python mysql support not available: %s') % err)
335 raise util.Abort(_('python mysql support not available: %s') % err)
328
336
329 bzaccess.__init__(self, ui)
337 bzaccess.__init__(self, ui)
330
338
331 host = self.ui.config('bugzilla', 'host', 'localhost')
339 host = self.ui.config('bugzilla', 'host', 'localhost')
332 user = self.ui.config('bugzilla', 'user', 'bugs')
340 user = self.ui.config('bugzilla', 'user', 'bugs')
333 passwd = self.ui.config('bugzilla', 'password')
341 passwd = self.ui.config('bugzilla', 'password')
334 db = self.ui.config('bugzilla', 'db', 'bugs')
342 db = self.ui.config('bugzilla', 'db', 'bugs')
335 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
343 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
336 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
344 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
337 (host, db, user, '*' * len(passwd)))
345 (host, db, user, '*' * len(passwd)))
338 self.conn = bzmysql._MySQLdb.connect(host=host,
346 self.conn = bzmysql._MySQLdb.connect(host=host,
339 user=user, passwd=passwd,
347 user=user, passwd=passwd,
340 db=db,
348 db=db,
341 connect_timeout=timeout)
349 connect_timeout=timeout)
342 self.cursor = self.conn.cursor()
350 self.cursor = self.conn.cursor()
343 self.longdesc_id = self.get_longdesc_id()
351 self.longdesc_id = self.get_longdesc_id()
344 self.user_ids = {}
352 self.user_ids = {}
345 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
353 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
346
354
347 def run(self, *args, **kwargs):
355 def run(self, *args, **kwargs):
348 '''run a query.'''
356 '''run a query.'''
349 self.ui.note(_('query: %s %s\n') % (args, kwargs))
357 self.ui.note(_('query: %s %s\n') % (args, kwargs))
350 try:
358 try:
351 self.cursor.execute(*args, **kwargs)
359 self.cursor.execute(*args, **kwargs)
352 except bzmysql._MySQLdb.MySQLError:
360 except bzmysql._MySQLdb.MySQLError:
353 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
361 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
354 raise
362 raise
355
363
356 def get_longdesc_id(self):
364 def get_longdesc_id(self):
357 '''get identity of longdesc field'''
365 '''get identity of longdesc field'''
358 self.run('select fieldid from fielddefs where name = "longdesc"')
366 self.run('select fieldid from fielddefs where name = "longdesc"')
359 ids = self.cursor.fetchall()
367 ids = self.cursor.fetchall()
360 if len(ids) != 1:
368 if len(ids) != 1:
361 raise util.Abort(_('unknown database schema'))
369 raise util.Abort(_('unknown database schema'))
362 return ids[0][0]
370 return ids[0][0]
363
371
364 def filter_real_bug_ids(self, bugs):
372 def filter_real_bug_ids(self, bugs):
365 '''filter not-existing bugs from set.'''
373 '''filter not-existing bugs from set.'''
366 self.run('select bug_id from bugs where bug_id in %s' %
374 self.run('select bug_id from bugs where bug_id in %s' %
367 bzmysql.sql_buglist(bugs.keys()))
375 bzmysql.sql_buglist(bugs.keys()))
368 existing = [id for (id,) in self.cursor.fetchall()]
376 existing = [id for (id,) in self.cursor.fetchall()]
369 for id in bugs.keys():
377 for id in bugs.keys():
370 if id not in existing:
378 if id not in existing:
371 self.ui.status(_('bug %d does not exist\n') % id)
379 self.ui.status(_('bug %d does not exist\n') % id)
372 del bugs[id]
380 del bugs[id]
373
381
374 def filter_cset_known_bug_ids(self, node, bugs):
382 def filter_cset_known_bug_ids(self, node, bugs):
375 '''filter bug ids that already refer to this changeset from set.'''
383 '''filter bug ids that already refer to this changeset from set.'''
376 self.run('''select bug_id from longdescs where
384 self.run('''select bug_id from longdescs where
377 bug_id in %s and thetext like "%%%s%%"''' %
385 bug_id in %s and thetext like "%%%s%%"''' %
378 (bzmysql.sql_buglist(bugs.keys()), short(node)))
386 (bzmysql.sql_buglist(bugs.keys()), short(node)))
379 for (id,) in self.cursor.fetchall():
387 for (id,) in self.cursor.fetchall():
380 self.ui.status(_('bug %d already knows about changeset %s\n') %
388 self.ui.status(_('bug %d already knows about changeset %s\n') %
381 (id, short(node)))
389 (id, short(node)))
382 del bugs[id]
390 del bugs[id]
383
391
384 def notify(self, bugs, committer):
392 def notify(self, bugs, committer):
385 '''tell bugzilla to send mail.'''
393 '''tell bugzilla to send mail.'''
386 self.ui.status(_('telling bugzilla to send mail:\n'))
394 self.ui.status(_('telling bugzilla to send mail:\n'))
387 (user, userid) = self.get_bugzilla_user(committer)
395 (user, userid) = self.get_bugzilla_user(committer)
388 for id in bugs.keys():
396 for id in bugs.keys():
389 self.ui.status(_(' bug %s\n') % id)
397 self.ui.status(_(' bug %s\n') % id)
390 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
398 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
391 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
399 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
392 try:
400 try:
393 # Backwards-compatible with old notify string, which
401 # Backwards-compatible with old notify string, which
394 # took one string. This will throw with a new format
402 # took one string. This will throw with a new format
395 # string.
403 # string.
396 cmd = cmdfmt % id
404 cmd = cmdfmt % id
397 except TypeError:
405 except TypeError:
398 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
406 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
399 self.ui.note(_('running notify command %s\n') % cmd)
407 self.ui.note(_('running notify command %s\n') % cmd)
400 fp = util.popen('(%s) 2>&1' % cmd)
408 fp = util.popen('(%s) 2>&1' % cmd)
401 out = fp.read()
409 out = fp.read()
402 ret = fp.close()
410 ret = fp.close()
403 if ret:
411 if ret:
404 self.ui.warn(out)
412 self.ui.warn(out)
405 raise util.Abort(_('bugzilla notify command %s') %
413 raise util.Abort(_('bugzilla notify command %s') %
406 util.explainexit(ret)[0])
414 util.explainexit(ret)[0])
407 self.ui.status(_('done\n'))
415 self.ui.status(_('done\n'))
408
416
409 def get_user_id(self, user):
417 def get_user_id(self, user):
410 '''look up numeric bugzilla user id.'''
418 '''look up numeric bugzilla user id.'''
411 try:
419 try:
412 return self.user_ids[user]
420 return self.user_ids[user]
413 except KeyError:
421 except KeyError:
414 try:
422 try:
415 userid = int(user)
423 userid = int(user)
416 except ValueError:
424 except ValueError:
417 self.ui.note(_('looking up user %s\n') % user)
425 self.ui.note(_('looking up user %s\n') % user)
418 self.run('''select userid from profiles
426 self.run('''select userid from profiles
419 where login_name like %s''', user)
427 where login_name like %s''', user)
420 all = self.cursor.fetchall()
428 all = self.cursor.fetchall()
421 if len(all) != 1:
429 if len(all) != 1:
422 raise KeyError(user)
430 raise KeyError(user)
423 userid = int(all[0][0])
431 userid = int(all[0][0])
424 self.user_ids[user] = userid
432 self.user_ids[user] = userid
425 return userid
433 return userid
426
434
427 def get_bugzilla_user(self, committer):
435 def get_bugzilla_user(self, committer):
428 '''See if committer is a registered bugzilla user. Return
436 '''See if committer is a registered bugzilla user. Return
429 bugzilla username and userid if so. If not, return default
437 bugzilla username and userid if so. If not, return default
430 bugzilla username and userid.'''
438 bugzilla username and userid.'''
431 user = self.map_committer(committer)
439 user = self.map_committer(committer)
432 try:
440 try:
433 userid = self.get_user_id(user)
441 userid = self.get_user_id(user)
434 except KeyError:
442 except KeyError:
435 try:
443 try:
436 defaultuser = self.ui.config('bugzilla', 'bzuser')
444 defaultuser = self.ui.config('bugzilla', 'bzuser')
437 if not defaultuser:
445 if not defaultuser:
438 raise util.Abort(_('cannot find bugzilla user id for %s') %
446 raise util.Abort(_('cannot find bugzilla user id for %s') %
439 user)
447 user)
440 userid = self.get_user_id(defaultuser)
448 userid = self.get_user_id(defaultuser)
441 user = defaultuser
449 user = defaultuser
442 except KeyError:
450 except KeyError:
443 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
451 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
444 (user, defaultuser))
452 (user, defaultuser))
445 return (user, userid)
453 return (user, userid)
446
454
447 def updatebug(self, bugid, newstate, text, committer):
455 def updatebug(self, bugid, newstate, text, committer):
448 '''update bug state with comment text.
456 '''update bug state with comment text.
449
457
450 Try adding comment as committer of changeset, otherwise as
458 Try adding comment as committer of changeset, otherwise as
451 default bugzilla user.'''
459 default bugzilla user.'''
460 if len(newstate) > 0:
461 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
462
452 (user, userid) = self.get_bugzilla_user(committer)
463 (user, userid) = self.get_bugzilla_user(committer)
453 now = time.strftime('%Y-%m-%d %H:%M:%S')
464 now = time.strftime('%Y-%m-%d %H:%M:%S')
454 self.run('''insert into longdescs
465 self.run('''insert into longdescs
455 (bug_id, who, bug_when, thetext)
466 (bug_id, who, bug_when, thetext)
456 values (%s, %s, %s, %s)''',
467 values (%s, %s, %s, %s)''',
457 (bugid, userid, now, text))
468 (bugid, userid, now, text))
458 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
469 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
459 values (%s, %s, %s, %s)''',
470 values (%s, %s, %s, %s)''',
460 (bugid, userid, now, self.longdesc_id))
471 (bugid, userid, now, self.longdesc_id))
461 self.conn.commit()
472 self.conn.commit()
462
473
463 class bzmysql_2_18(bzmysql):
474 class bzmysql_2_18(bzmysql):
464 '''support for bugzilla 2.18 series.'''
475 '''support for bugzilla 2.18 series.'''
465
476
466 def __init__(self, ui):
477 def __init__(self, ui):
467 bzmysql.__init__(self, ui)
478 bzmysql.__init__(self, ui)
468 self.default_notify = \
479 self.default_notify = \
469 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
480 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
470
481
471 class bzmysql_3_0(bzmysql_2_18):
482 class bzmysql_3_0(bzmysql_2_18):
472 '''support for bugzilla 3.0 series.'''
483 '''support for bugzilla 3.0 series.'''
473
484
474 def __init__(self, ui):
485 def __init__(self, ui):
475 bzmysql_2_18.__init__(self, ui)
486 bzmysql_2_18.__init__(self, ui)
476
487
477 def get_longdesc_id(self):
488 def get_longdesc_id(self):
478 '''get identity of longdesc field'''
489 '''get identity of longdesc field'''
479 self.run('select id from fielddefs where name = "longdesc"')
490 self.run('select id from fielddefs where name = "longdesc"')
480 ids = self.cursor.fetchall()
491 ids = self.cursor.fetchall()
481 if len(ids) != 1:
492 if len(ids) != 1:
482 raise util.Abort(_('unknown database schema'))
493 raise util.Abort(_('unknown database schema'))
483 return ids[0][0]
494 return ids[0][0]
484
495
485 # Buzgilla via XMLRPC interface.
496 # Buzgilla via XMLRPC interface.
486
497
487 class cookietransportrequest(object):
498 class cookietransportrequest(object):
488 """A Transport request method that retains cookies over its lifetime.
499 """A Transport request method that retains cookies over its lifetime.
489
500
490 The regular xmlrpclib transports ignore cookies. Which causes
501 The regular xmlrpclib transports ignore cookies. Which causes
491 a bit of a problem when you need a cookie-based login, as with
502 a bit of a problem when you need a cookie-based login, as with
492 the Bugzilla XMLRPC interface.
503 the Bugzilla XMLRPC interface.
493
504
494 So this is a helper for defining a Transport which looks for
505 So this is a helper for defining a Transport which looks for
495 cookies being set in responses and saves them to add to all future
506 cookies being set in responses and saves them to add to all future
496 requests.
507 requests.
497 """
508 """
498
509
499 # Inspiration drawn from
510 # Inspiration drawn from
500 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
511 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
501 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
512 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
502
513
503 cookies = []
514 cookies = []
504 def send_cookies(self, connection):
515 def send_cookies(self, connection):
505 if self.cookies:
516 if self.cookies:
506 for cookie in self.cookies:
517 for cookie in self.cookies:
507 connection.putheader("Cookie", cookie)
518 connection.putheader("Cookie", cookie)
508
519
509 def request(self, host, handler, request_body, verbose=0):
520 def request(self, host, handler, request_body, verbose=0):
510 self.verbose = verbose
521 self.verbose = verbose
511 self.accept_gzip_encoding = False
522 self.accept_gzip_encoding = False
512
523
513 # issue XML-RPC request
524 # issue XML-RPC request
514 h = self.make_connection(host)
525 h = self.make_connection(host)
515 if verbose:
526 if verbose:
516 h.set_debuglevel(1)
527 h.set_debuglevel(1)
517
528
518 self.send_request(h, handler, request_body)
529 self.send_request(h, handler, request_body)
519 self.send_host(h, host)
530 self.send_host(h, host)
520 self.send_cookies(h)
531 self.send_cookies(h)
521 self.send_user_agent(h)
532 self.send_user_agent(h)
522 self.send_content(h, request_body)
533 self.send_content(h, request_body)
523
534
524 # Deal with differences between Python 2.4-2.6 and 2.7.
535 # Deal with differences between Python 2.4-2.6 and 2.7.
525 # In the former h is a HTTP(S). In the latter it's a
536 # In the former h is a HTTP(S). In the latter it's a
526 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
537 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
527 # HTTP(S) has an underlying HTTP(S)Connection, so extract
538 # HTTP(S) has an underlying HTTP(S)Connection, so extract
528 # that and use it.
539 # that and use it.
529 try:
540 try:
530 response = h.getresponse()
541 response = h.getresponse()
531 except AttributeError:
542 except AttributeError:
532 response = h._conn.getresponse()
543 response = h._conn.getresponse()
533
544
534 # Add any cookie definitions to our list.
545 # Add any cookie definitions to our list.
535 for header in response.msg.getallmatchingheaders("Set-Cookie"):
546 for header in response.msg.getallmatchingheaders("Set-Cookie"):
536 val = header.split(": ", 1)[1]
547 val = header.split(": ", 1)[1]
537 cookie = val.split(";", 1)[0]
548 cookie = val.split(";", 1)[0]
538 self.cookies.append(cookie)
549 self.cookies.append(cookie)
539
550
540 if response.status != 200:
551 if response.status != 200:
541 raise xmlrpclib.ProtocolError(host + handler, response.status,
552 raise xmlrpclib.ProtocolError(host + handler, response.status,
542 response.reason, response.msg.headers)
553 response.reason, response.msg.headers)
543
554
544 payload = response.read()
555 payload = response.read()
545 parser, unmarshaller = self.getparser()
556 parser, unmarshaller = self.getparser()
546 parser.feed(payload)
557 parser.feed(payload)
547 parser.close()
558 parser.close()
548
559
549 return unmarshaller.close()
560 return unmarshaller.close()
550
561
551 # The explicit calls to the underlying xmlrpclib __init__() methods are
562 # The explicit calls to the underlying xmlrpclib __init__() methods are
552 # necessary. The xmlrpclib.Transport classes are old-style classes, and
563 # necessary. The xmlrpclib.Transport classes are old-style classes, and
553 # it turns out their __init__() doesn't get called when doing multiple
564 # it turns out their __init__() doesn't get called when doing multiple
554 # inheritance with a new-style class.
565 # inheritance with a new-style class.
555 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
566 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
556 def __init__(self, use_datetime=0):
567 def __init__(self, use_datetime=0):
557 xmlrpclib.Transport.__init__(self, use_datetime)
568 xmlrpclib.Transport.__init__(self, use_datetime)
558
569
559 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
570 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
560 def __init__(self, use_datetime=0):
571 def __init__(self, use_datetime=0):
561 xmlrpclib.SafeTransport.__init__(self, use_datetime)
572 xmlrpclib.SafeTransport.__init__(self, use_datetime)
562
573
563 class bzxmlrpc(bzaccess):
574 class bzxmlrpc(bzaccess):
564 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
575 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
565
576
566 Requires a minimum Bugzilla version 3.4.
577 Requires a minimum Bugzilla version 3.4.
567 """
578 """
568
579
569 def __init__(self, ui):
580 def __init__(self, ui):
570 bzaccess.__init__(self, ui)
581 bzaccess.__init__(self, ui)
571
582
572 bzweb = self.ui.config('bugzilla', 'bzurl',
583 bzweb = self.ui.config('bugzilla', 'bzurl',
573 'http://localhost/bugzilla/')
584 'http://localhost/bugzilla/')
574 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
585 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
575
586
576 user = self.ui.config('bugzilla', 'user', 'bugs')
587 user = self.ui.config('bugzilla', 'user', 'bugs')
577 passwd = self.ui.config('bugzilla', 'password')
588 passwd = self.ui.config('bugzilla', 'password')
578
589
579 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
590 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
580 self.bzproxy.User.login(dict(login=user, password=passwd))
591 self.bzproxy.User.login(dict(login=user, password=passwd))
581
592
582 def transport(self, uri):
593 def transport(self, uri):
583 if urlparse.urlparse(uri, "http")[0] == "https":
594 if urlparse.urlparse(uri, "http")[0] == "https":
584 return cookiesafetransport()
595 return cookiesafetransport()
585 else:
596 else:
586 return cookietransport()
597 return cookietransport()
587
598
588 def get_bug_comments(self, id):
599 def get_bug_comments(self, id):
589 """Return a string with all comment text for a bug."""
600 """Return a string with all comment text for a bug."""
590 c = self.bzproxy.Bug.comments(dict(ids=[id], include_fields=['text']))
601 c = self.bzproxy.Bug.comments(dict(ids=[id], include_fields=['text']))
591 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
602 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
592
603
593 def filter_real_bug_ids(self, bugs):
604 def filter_real_bug_ids(self, bugs):
594 probe = self.bzproxy.Bug.get(dict(ids=sorted(bugs.keys()),
605 probe = self.bzproxy.Bug.get(dict(ids=sorted(bugs.keys()),
595 include_fields=[],
606 include_fields=[],
596 permissive=True))
607 permissive=True))
597 for badbug in probe['faults']:
608 for badbug in probe['faults']:
598 id = badbug['id']
609 id = badbug['id']
599 self.ui.status(_('bug %d does not exist\n') % id)
610 self.ui.status(_('bug %d does not exist\n') % id)
600 del bugs[id]
611 del bugs[id]
601
612
602 def filter_cset_known_bug_ids(self, node, bugs):
613 def filter_cset_known_bug_ids(self, node, bugs):
603 for id in sorted(bugs.keys()):
614 for id in sorted(bugs.keys()):
604 if self.get_bug_comments(id).find(short(node)) != -1:
615 if self.get_bug_comments(id).find(short(node)) != -1:
605 self.ui.status(_('bug %d already knows about changeset %s\n') %
616 self.ui.status(_('bug %d already knows about changeset %s\n') %
606 (id, short(node)))
617 (id, short(node)))
607 del bugs[id]
618 del bugs[id]
608
619
609 def updatebug(self, bugid, newstate, text, committer):
620 def updatebug(self, bugid, newstate, text, committer):
610 args = dict(id=bugid, comment=text)
621 args = dict(id=bugid, comment=text)
622 if 'hours' in newstate:
623 args['work_time'] = newstate['hours']
611 self.bzproxy.Bug.add_comment(args)
624 self.bzproxy.Bug.add_comment(args)
612
625
613 class bzxmlrpcemail(bzxmlrpc):
626 class bzxmlrpcemail(bzxmlrpc):
614 """Read data from Bugzilla via XMLRPC, send updates via email.
627 """Read data from Bugzilla via XMLRPC, send updates via email.
615
628
616 Advantages of sending updates via email:
629 Advantages of sending updates via email:
617 1. Comments can be added as any user, not just logged in user.
630 1. Comments can be added as any user, not just logged in user.
618 2. Bug statuses and other fields not accessible via XMLRPC can
631 2. Bug statuses or other fields not accessible via XMLRPC can
619 be updated. This is not currently used.
632 potentially be updated.
633
634 Currently all status updates recognised can be done via XMLRPC, so
635 item 1 is the only actual advantage.
620 """
636 """
621
637
622 def __init__(self, ui):
638 def __init__(self, ui):
623 bzxmlrpc.__init__(self, ui)
639 bzxmlrpc.__init__(self, ui)
624
640
625 self.bzemail = self.ui.config('bugzilla', 'bzemail')
641 self.bzemail = self.ui.config('bugzilla', 'bzemail')
626 if not self.bzemail:
642 if not self.bzemail:
627 raise util.Abort(_("configuration 'bzemail' missing"))
643 raise util.Abort(_("configuration 'bzemail' missing"))
628 mail.validateconfig(self.ui)
644 mail.validateconfig(self.ui)
629
645
630 def send_bug_modify_email(self, bugid, commands, comment, committer):
646 def send_bug_modify_email(self, bugid, commands, comment, committer):
631 '''send modification message to Bugzilla bug via email.
647 '''send modification message to Bugzilla bug via email.
632
648
633 The message format is documented in the Bugzilla email_in.pl
649 The message format is documented in the Bugzilla email_in.pl
634 specification. commands is a list of command lines, comment is the
650 specification. commands is a list of command lines, comment is the
635 comment text.
651 comment text.
636
652
637 To stop users from crafting commit comments with
653 To stop users from crafting commit comments with
638 Bugzilla commands, specify the bug ID via the message body, rather
654 Bugzilla commands, specify the bug ID via the message body, rather
639 than the subject line, and leave a blank line after it.
655 than the subject line, and leave a blank line after it.
640 '''
656 '''
641 user = self.map_committer(committer)
657 user = self.map_committer(committer)
642 matches = self.bzproxy.User.get(dict(match=[user]))
658 matches = self.bzproxy.User.get(dict(match=[user]))
643 if not matches['users']:
659 if not matches['users']:
644 user = self.ui.config('bugzilla', 'user', 'bugs')
660 user = self.ui.config('bugzilla', 'user', 'bugs')
645 matches = self.bzproxy.User.get(dict(match=[user]))
661 matches = self.bzproxy.User.get(dict(match=[user]))
646 if not matches['users']:
662 if not matches['users']:
647 raise util.Abort(_("default bugzilla user %s email not found") %
663 raise util.Abort(_("default bugzilla user %s email not found") %
648 user)
664 user)
649 user = matches['users'][0]['email']
665 user = matches['users'][0]['email']
650
666
651 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
667 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
652
668
653 _charsets = mail._charsets(self.ui)
669 _charsets = mail._charsets(self.ui)
654 user = mail.addressencode(self.ui, user, _charsets)
670 user = mail.addressencode(self.ui, user, _charsets)
655 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
671 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
656 msg = mail.mimeencode(self.ui, text, _charsets)
672 msg = mail.mimeencode(self.ui, text, _charsets)
657 msg['From'] = user
673 msg['From'] = user
658 msg['To'] = bzemail
674 msg['To'] = bzemail
659 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
675 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
660 sendmail = mail.connect(self.ui)
676 sendmail = mail.connect(self.ui)
661 sendmail(user, bzemail, msg.as_string())
677 sendmail(user, bzemail, msg.as_string())
662
678
663 def updatebug(self, bugid, newstate, text, committer):
679 def updatebug(self, bugid, newstate, text, committer):
664 self.send_bug_modify_email(bugid, [], text, committer)
680 cmds = []
681 if 'hours' in newstate:
682 cmds.append(self.makecommandline("work_time", newstate['hours']))
683 self.send_bug_modify_email(bugid, cmds, text, committer)
665
684
666 class bugzilla(object):
685 class bugzilla(object):
667 # supported versions of bugzilla. different versions have
686 # supported versions of bugzilla. different versions have
668 # different schemas.
687 # different schemas.
669 _versions = {
688 _versions = {
670 '2.16': bzmysql,
689 '2.16': bzmysql,
671 '2.18': bzmysql_2_18,
690 '2.18': bzmysql_2_18,
672 '3.0': bzmysql_3_0,
691 '3.0': bzmysql_3_0,
673 'xmlrpc': bzxmlrpc,
692 'xmlrpc': bzxmlrpc,
674 'xmlrpc+email': bzxmlrpcemail
693 'xmlrpc+email': bzxmlrpcemail
675 }
694 }
676
695
677 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
696 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
678 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
697 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
698 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
679
699
680 _bz = None
700 _bz = None
681
701
682 def __init__(self, ui, repo):
702 def __init__(self, ui, repo):
683 self.ui = ui
703 self.ui = ui
684 self.repo = repo
704 self.repo = repo
685
705
686 def bz(self):
706 def bz(self):
687 '''return object that knows how to talk to bugzilla version in
707 '''return object that knows how to talk to bugzilla version in
688 use.'''
708 use.'''
689
709
690 if bugzilla._bz is None:
710 if bugzilla._bz is None:
691 bzversion = self.ui.config('bugzilla', 'version')
711 bzversion = self.ui.config('bugzilla', 'version')
692 try:
712 try:
693 bzclass = bugzilla._versions[bzversion]
713 bzclass = bugzilla._versions[bzversion]
694 except KeyError:
714 except KeyError:
695 raise util.Abort(_('bugzilla version %s not supported') %
715 raise util.Abort(_('bugzilla version %s not supported') %
696 bzversion)
716 bzversion)
697 bugzilla._bz = bzclass(self.ui)
717 bugzilla._bz = bzclass(self.ui)
698 return bugzilla._bz
718 return bugzilla._bz
699
719
700 def __getattr__(self, key):
720 def __getattr__(self, key):
701 return getattr(self.bz(), key)
721 return getattr(self.bz(), key)
702
722
703 _bug_re = None
723 _bug_re = None
704 _split_re = None
724 _split_re = None
705
725
706 def find_bugs(self, ctx):
726 def find_bugs(self, ctx):
707 '''return bugs dictionary created from commit comment.
727 '''return bugs dictionary created from commit comment.
708
728
709 Extract bug info from changeset comments. Filter out any that are
729 Extract bug info from changeset comments. Filter out any that are
710 not known to Bugzilla, and any that already have a reference to
730 not known to Bugzilla, and any that already have a reference to
711 the given changeset in their comments.
731 the given changeset in their comments.
712 '''
732 '''
713 if bugzilla._bug_re is None:
733 if bugzilla._bug_re is None:
714 bugzilla._bug_re = re.compile(
734 bugzilla._bug_re = re.compile(
715 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
735 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
716 re.IGNORECASE)
736 re.IGNORECASE)
717 bugzilla._split_re = re.compile(r'\D+')
737 bugzilla._split_re = re.compile(r'\D+')
718 start = 0
738 start = 0
739 hours = 0.0
719 bugs = {}
740 bugs = {}
720 while True:
741 while True:
742 bugattribs = {}
721 m = bugzilla._bug_re.search(ctx.description(), start)
743 m = bugzilla._bug_re.search(ctx.description(), start)
722 if not m:
744 if not m:
723 break
745 break
724 start = m.end()
746 start = m.end()
725 for id in bugzilla._split_re.split(m.group(1)):
747 try:
748 ids = m.group('ids')
749 except IndexError:
750 ids = m.group(1)
751 try:
752 hours = float(m.group('hours'))
753 bugattribs['hours'] = hours
754 except IndexError:
755 pass
756 except TypeError:
757 pass
758 except ValueError:
759 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
760
761 for id in bugzilla._split_re.split(ids):
726 if not id:
762 if not id:
727 continue
763 continue
728 bugs[int(id)] = {}
764 bugs[int(id)] = bugattribs
729 if bugs:
765 if bugs:
730 self.filter_real_bug_ids(bugs)
766 self.filter_real_bug_ids(bugs)
731 if bugs:
767 if bugs:
732 self.filter_cset_known_bug_ids(ctx.node(), bugs)
768 self.filter_cset_known_bug_ids(ctx.node(), bugs)
733 return bugs
769 return bugs
734
770
735 def update(self, bugid, newstate, ctx):
771 def update(self, bugid, newstate, ctx):
736 '''update bugzilla bug with reference to changeset.'''
772 '''update bugzilla bug with reference to changeset.'''
737
773
738 def webroot(root):
774 def webroot(root):
739 '''strip leading prefix of repo root and turn into
775 '''strip leading prefix of repo root and turn into
740 url-safe path.'''
776 url-safe path.'''
741 count = int(self.ui.config('bugzilla', 'strip', 0))
777 count = int(self.ui.config('bugzilla', 'strip', 0))
742 root = util.pconvert(root)
778 root = util.pconvert(root)
743 while count > 0:
779 while count > 0:
744 c = root.find('/')
780 c = root.find('/')
745 if c == -1:
781 if c == -1:
746 break
782 break
747 root = root[c + 1:]
783 root = root[c + 1:]
748 count -= 1
784 count -= 1
749 return root
785 return root
750
786
751 mapfile = self.ui.config('bugzilla', 'style')
787 mapfile = self.ui.config('bugzilla', 'style')
752 tmpl = self.ui.config('bugzilla', 'template')
788 tmpl = self.ui.config('bugzilla', 'template')
753 t = cmdutil.changeset_templater(self.ui, self.repo,
789 t = cmdutil.changeset_templater(self.ui, self.repo,
754 False, None, mapfile, False)
790 False, None, mapfile, False)
755 if not mapfile and not tmpl:
791 if not mapfile and not tmpl:
756 tmpl = _('changeset {node|short} in repo {root} refers '
792 tmpl = _('changeset {node|short} in repo {root} refers '
757 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
793 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
758 if tmpl:
794 if tmpl:
759 tmpl = templater.parsestring(tmpl, quoted=False)
795 tmpl = templater.parsestring(tmpl, quoted=False)
760 t.use_template(tmpl)
796 t.use_template(tmpl)
761 self.ui.pushbuffer()
797 self.ui.pushbuffer()
762 t.show(ctx, changes=ctx.changeset(),
798 t.show(ctx, changes=ctx.changeset(),
763 bug=str(bugid),
799 bug=str(bugid),
764 hgweb=self.ui.config('web', 'baseurl'),
800 hgweb=self.ui.config('web', 'baseurl'),
765 root=self.repo.root,
801 root=self.repo.root,
766 webroot=webroot(self.repo.root))
802 webroot=webroot(self.repo.root))
767 data = self.ui.popbuffer()
803 data = self.ui.popbuffer()
768 self.updatebug(bugid, newstate, data, util.email(ctx.user()))
804 self.updatebug(bugid, newstate, data, util.email(ctx.user()))
769
805
770 def hook(ui, repo, hooktype, node=None, **kwargs):
806 def hook(ui, repo, hooktype, node=None, **kwargs):
771 '''add comment to bugzilla for each changeset that refers to a
807 '''add comment to bugzilla for each changeset that refers to a
772 bugzilla bug id. only add a comment once per bug, so same change
808 bugzilla bug id. only add a comment once per bug, so same change
773 seen multiple times does not fill bug with duplicate data.'''
809 seen multiple times does not fill bug with duplicate data.'''
774 if node is None:
810 if node is None:
775 raise util.Abort(_('hook type %s does not pass a changeset id') %
811 raise util.Abort(_('hook type %s does not pass a changeset id') %
776 hooktype)
812 hooktype)
777 try:
813 try:
778 bz = bugzilla(ui, repo)
814 bz = bugzilla(ui, repo)
779 ctx = repo[node]
815 ctx = repo[node]
780 bugs = bz.find_bugs(ctx)
816 bugs = bz.find_bugs(ctx)
781 if bugs:
817 if bugs:
782 for bug in bugs:
818 for bug in bugs:
783 bz.update(bug, bugs[bug], ctx)
819 bz.update(bug, bugs[bug], ctx)
784 bz.notify(bugs, util.email(ctx.user()))
820 bz.notify(bugs, util.email(ctx.user()))
785 except Exception, e:
821 except Exception, e:
786 raise util.Abort(_('Bugzilla error: %s') % e)
822 raise util.Abort(_('Bugzilla error: %s') % e)
787
823
General Comments 0
You need to be logged in to leave comments. Login now