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