##// END OF EJS Templates
bugzilla: correct comment typo
Jim Hague -
r16226:674ecd23 stable
parent child Browse files
Show More
@@ -1,774 +1,774
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, urlparse, 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.4 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 cookietransportrequest(object):
476 class cookietransportrequest(object):
477 """A Transport request method 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 helper for defining a Transport which looks for
483 So this is a helper for defining a Transport which looks for
484 cookies being set in responses and saves them to add to all future
484 cookies being set in responses and saves them to add to all future
485 requests.
485 requests.
486 """
486 """
487
487
488 # Inspiration drawn from
488 # Inspiration drawn from
489 # 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
490 # 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/
491
491
492 cookies = []
492 cookies = []
493 def send_cookies(self, connection):
493 def send_cookies(self, connection):
494 if self.cookies:
494 if self.cookies:
495 for cookie in self.cookies:
495 for cookie in self.cookies:
496 connection.putheader("Cookie", cookie)
496 connection.putheader("Cookie", cookie)
497
497
498 def request(self, host, handler, request_body, verbose=0):
498 def request(self, host, handler, request_body, verbose=0):
499 self.verbose = verbose
499 self.verbose = verbose
500 self.accept_gzip_encoding = False
500 self.accept_gzip_encoding = False
501
501
502 # issue XML-RPC request
502 # issue XML-RPC request
503 h = self.make_connection(host)
503 h = self.make_connection(host)
504 if verbose:
504 if verbose:
505 h.set_debuglevel(1)
505 h.set_debuglevel(1)
506
506
507 self.send_request(h, handler, request_body)
507 self.send_request(h, handler, request_body)
508 self.send_host(h, host)
508 self.send_host(h, host)
509 self.send_cookies(h)
509 self.send_cookies(h)
510 self.send_user_agent(h)
510 self.send_user_agent(h)
511 self.send_content(h, request_body)
511 self.send_content(h, request_body)
512
512
513 # Deal with differences between Python 2.4-2.6 and 2.7.
513 # 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
514 # 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
515 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
516 # HTTP(S) has an underlying HTTP(S)Connection, so extract
516 # HTTP(S) has an underlying HTTP(S)Connection, so extract
517 # that and use it.
517 # that and use it.
518 try:
518 try:
519 response = h.getresponse()
519 response = h.getresponse()
520 except AttributeError:
520 except AttributeError:
521 response = h._conn.getresponse()
521 response = h._conn.getresponse()
522
522
523 # Add any cookie definitions to our list.
523 # Add any cookie definitions to our list.
524 for header in response.msg.getallmatchingheaders("Set-Cookie"):
524 for header in response.msg.getallmatchingheaders("Set-Cookie"):
525 val = header.split(": ", 1)[1]
525 val = header.split(": ", 1)[1]
526 cookie = val.split(";", 1)[0]
526 cookie = val.split(";", 1)[0]
527 self.cookies.append(cookie)
527 self.cookies.append(cookie)
528
528
529 if response.status != 200:
529 if response.status != 200:
530 raise xmlrpclib.ProtocolError(host + handler, response.status,
530 raise xmlrpclib.ProtocolError(host + handler, response.status,
531 response.reason, response.msg.headers)
531 response.reason, response.msg.headers)
532
532
533 payload = response.read()
533 payload = response.read()
534 parser, unmarshaller = self.getparser()
534 parser, unmarshaller = self.getparser()
535 parser.feed(payload)
535 parser.feed(payload)
536 parser.close()
536 parser.close()
537
537
538 return unmarshaller.close()
538 return unmarshaller.close()
539
539
540 # The explicit calls to the underlying xmlrpclib __init__() methods are
540 # The explicit calls to the underlying xmlrpclib __init__() methods are
541 # necessary. The xmlrpclib.Transport classes are old-style classes, and
541 # necessary. The xmlrpclib.Transport classes are old-style classes, and
542 # it turns out their __init__() doesn't get called when doing multiple
542 # it turns out their __init__() doesn't get called when doing multiple
543 # inheritance with a new-style class.
543 # inheritance with a new-style class.
544 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
544 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
545 def __init__(self, use_datetime=0):
545 def __init__(self, use_datetime=0):
546 xmlrpclib.Transport.__init__(self, use_datetime)
546 xmlrpclib.Transport.__init__(self, use_datetime)
547
547
548 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
548 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
549 def __init__(self, use_datetime=0):
549 def __init__(self, use_datetime=0):
550 xmlrpclib.SafeTransport.__init__(self, use_datetime)
550 xmlrpclib.SafeTransport.__init__(self, use_datetime)
551
551
552 class bzxmlrpc(bzaccess):
552 class bzxmlrpc(bzaccess):
553 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
553 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
554
554
555 Requires a minimum Bugzilla version 3.4.
555 Requires a minimum Bugzilla version 3.4.
556 """
556 """
557
557
558 def __init__(self, ui):
558 def __init__(self, ui):
559 bzaccess.__init__(self, ui)
559 bzaccess.__init__(self, ui)
560
560
561 bzweb = self.ui.config('bugzilla', 'bzurl',
561 bzweb = self.ui.config('bugzilla', 'bzurl',
562 'http://localhost/bugzilla/')
562 'http://localhost/bugzilla/')
563 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
563 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
564
564
565 user = self.ui.config('bugzilla', 'user', 'bugs')
565 user = self.ui.config('bugzilla', 'user', 'bugs')
566 passwd = self.ui.config('bugzilla', 'password')
566 passwd = self.ui.config('bugzilla', 'password')
567
567
568 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
568 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
569 self.bzproxy.User.login(dict(login=user, password=passwd))
569 self.bzproxy.User.login(dict(login=user, password=passwd))
570
570
571 def transport(self, uri):
571 def transport(self, uri):
572 if urlparse.urlparse(uri, "http")[0] == "https":
572 if urlparse.urlparse(uri, "http")[0] == "https":
573 return cookiesafetransport()
573 return cookiesafetransport()
574 else:
574 else:
575 return cookietransport()
575 return cookietransport()
576
576
577 def get_bug_comments(self, id):
577 def get_bug_comments(self, id):
578 """Return a string with all comment text for a bug."""
578 """Return a string with all comment text for a bug."""
579 c = self.bzproxy.Bug.comments(dict(ids=[id]))
579 c = self.bzproxy.Bug.comments(dict(ids=[id]))
580 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
580 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
581
581
582 def filter_real_bug_ids(self, ids):
582 def filter_real_bug_ids(self, ids):
583 res = set()
583 res = set()
584 bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True))
584 bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True))
585 for bug in bugs['bugs']:
585 for bug in bugs['bugs']:
586 res.add(bug['id'])
586 res.add(bug['id'])
587 return res
587 return res
588
588
589 def filter_cset_known_bug_ids(self, node, ids):
589 def filter_cset_known_bug_ids(self, node, ids):
590 for id in sorted(ids):
590 for id in sorted(ids):
591 if self.get_bug_comments(id).find(short(node)) != -1:
591 if self.get_bug_comments(id).find(short(node)) != -1:
592 self.ui.status(_('bug %d already knows about changeset %s\n') %
592 self.ui.status(_('bug %d already knows about changeset %s\n') %
593 (id, short(node)))
593 (id, short(node)))
594 ids.discard(id)
594 ids.discard(id)
595 return ids
595 return ids
596
596
597 def add_comment(self, bugid, text, committer):
597 def add_comment(self, bugid, text, committer):
598 self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text))
598 self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text))
599
599
600 class bzxmlrpcemail(bzxmlrpc):
600 class bzxmlrpcemail(bzxmlrpc):
601 """Read data from Bugzilla via XMLRPC, send updates via email.
601 """Read data from Bugzilla via XMLRPC, send updates via email.
602
602
603 Advantages of sending updates via email:
603 Advantages of sending updates via email:
604 1. Comments can be added as any user, not just logged in user.
604 1. Comments can be added as any user, not just logged in user.
605 2. Bug statuses and other fields not accessible via XMLRPC can
605 2. Bug statuses and other fields not accessible via XMLRPC can
606 be updated. This is not currently used.
606 be updated. This is not currently used.
607 """
607 """
608
608
609 def __init__(self, ui):
609 def __init__(self, ui):
610 bzxmlrpc.__init__(self, ui)
610 bzxmlrpc.__init__(self, ui)
611
611
612 self.bzemail = self.ui.config('bugzilla', 'bzemail')
612 self.bzemail = self.ui.config('bugzilla', 'bzemail')
613 if not self.bzemail:
613 if not self.bzemail:
614 raise util.Abort(_("configuration 'bzemail' missing"))
614 raise util.Abort(_("configuration 'bzemail' missing"))
615 mail.validateconfig(self.ui)
615 mail.validateconfig(self.ui)
616
616
617 def send_bug_modify_email(self, bugid, commands, comment, committer):
617 def send_bug_modify_email(self, bugid, commands, comment, committer):
618 '''send modification message to Bugzilla bug via email.
618 '''send modification message to Bugzilla bug via email.
619
619
620 The message format is documented in the Bugzilla email_in.pl
620 The message format is documented in the Bugzilla email_in.pl
621 specification. commands is a list of command lines, comment is the
621 specification. commands is a list of command lines, comment is the
622 comment text.
622 comment text.
623
623
624 To stop users from crafting commit comments with
624 To stop users from crafting commit comments with
625 Bugzilla commands, specify the bug ID via the message body, rather
625 Bugzilla commands, specify the bug ID via the message body, rather
626 than the subject line, and leave a blank line after it.
626 than the subject line, and leave a blank line after it.
627 '''
627 '''
628 user = self.map_committer(committer)
628 user = self.map_committer(committer)
629 matches = self.bzproxy.User.get(dict(match=[user]))
629 matches = self.bzproxy.User.get(dict(match=[user]))
630 if not matches['users']:
630 if not matches['users']:
631 user = self.ui.config('bugzilla', 'user', 'bugs')
631 user = self.ui.config('bugzilla', 'user', 'bugs')
632 matches = self.bzproxy.User.get(dict(match=[user]))
632 matches = self.bzproxy.User.get(dict(match=[user]))
633 if not matches['users']:
633 if not matches['users']:
634 raise util.Abort(_("default bugzilla user %s email not found") %
634 raise util.Abort(_("default bugzilla user %s email not found") %
635 user)
635 user)
636 user = matches['users'][0]['email']
636 user = matches['users'][0]['email']
637
637
638 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
638 text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment
639
639
640 _charsets = mail._charsets(self.ui)
640 _charsets = mail._charsets(self.ui)
641 user = mail.addressencode(self.ui, user, _charsets)
641 user = mail.addressencode(self.ui, user, _charsets)
642 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
642 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
643 msg = mail.mimeencode(self.ui, text, _charsets)
643 msg = mail.mimeencode(self.ui, text, _charsets)
644 msg['From'] = user
644 msg['From'] = user
645 msg['To'] = bzemail
645 msg['To'] = bzemail
646 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
646 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
647 sendmail = mail.connect(self.ui)
647 sendmail = mail.connect(self.ui)
648 sendmail(user, bzemail, msg.as_string())
648 sendmail(user, bzemail, msg.as_string())
649
649
650 def add_comment(self, bugid, text, committer):
650 def add_comment(self, bugid, text, committer):
651 self.send_bug_modify_email(bugid, [], text, committer)
651 self.send_bug_modify_email(bugid, [], text, committer)
652
652
653 class bugzilla(object):
653 class bugzilla(object):
654 # supported versions of bugzilla. different versions have
654 # supported versions of bugzilla. different versions have
655 # different schemas.
655 # different schemas.
656 _versions = {
656 _versions = {
657 '2.16': bzmysql,
657 '2.16': bzmysql,
658 '2.18': bzmysql_2_18,
658 '2.18': bzmysql_2_18,
659 '3.0': bzmysql_3_0,
659 '3.0': bzmysql_3_0,
660 'xmlrpc': bzxmlrpc,
660 'xmlrpc': bzxmlrpc,
661 'xmlrpc+email': bzxmlrpcemail
661 'xmlrpc+email': bzxmlrpcemail
662 }
662 }
663
663
664 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
664 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
665 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
665 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
666
666
667 _bz = None
667 _bz = None
668
668
669 def __init__(self, ui, repo):
669 def __init__(self, ui, repo):
670 self.ui = ui
670 self.ui = ui
671 self.repo = repo
671 self.repo = repo
672
672
673 def bz(self):
673 def bz(self):
674 '''return object that knows how to talk to bugzilla version in
674 '''return object that knows how to talk to bugzilla version in
675 use.'''
675 use.'''
676
676
677 if bugzilla._bz is None:
677 if bugzilla._bz is None:
678 bzversion = self.ui.config('bugzilla', 'version')
678 bzversion = self.ui.config('bugzilla', 'version')
679 try:
679 try:
680 bzclass = bugzilla._versions[bzversion]
680 bzclass = bugzilla._versions[bzversion]
681 except KeyError:
681 except KeyError:
682 raise util.Abort(_('bugzilla version %s not supported') %
682 raise util.Abort(_('bugzilla version %s not supported') %
683 bzversion)
683 bzversion)
684 bugzilla._bz = bzclass(self.ui)
684 bugzilla._bz = bzclass(self.ui)
685 return bugzilla._bz
685 return bugzilla._bz
686
686
687 def __getattr__(self, key):
687 def __getattr__(self, key):
688 return getattr(self.bz(), key)
688 return getattr(self.bz(), key)
689
689
690 _bug_re = None
690 _bug_re = None
691 _split_re = None
691 _split_re = None
692
692
693 def find_bug_ids(self, ctx):
693 def find_bug_ids(self, ctx):
694 '''return set of integer bug IDs from commit comment.
694 '''return set of integer bug IDs from commit comment.
695
695
696 Extract bug IDs from changeset comments. Filter out any that are
696 Extract bug IDs from changeset comments. Filter out any that are
697 not known to Bugzilla, and any that already have a reference to
697 not known to Bugzilla, and any that already have a reference to
698 the given changeset in their comments.
698 the given changeset in their comments.
699 '''
699 '''
700 if bugzilla._bug_re is None:
700 if bugzilla._bug_re is None:
701 bugzilla._bug_re = re.compile(
701 bugzilla._bug_re = re.compile(
702 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
702 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
703 re.IGNORECASE)
703 re.IGNORECASE)
704 bugzilla._split_re = re.compile(r'\D+')
704 bugzilla._split_re = re.compile(r'\D+')
705 start = 0
705 start = 0
706 ids = set()
706 ids = set()
707 while True:
707 while True:
708 m = bugzilla._bug_re.search(ctx.description(), start)
708 m = bugzilla._bug_re.search(ctx.description(), start)
709 if not m:
709 if not m:
710 break
710 break
711 start = m.end()
711 start = m.end()
712 for id in bugzilla._split_re.split(m.group(1)):
712 for id in bugzilla._split_re.split(m.group(1)):
713 if not id:
713 if not id:
714 continue
714 continue
715 ids.add(int(id))
715 ids.add(int(id))
716 if ids:
716 if ids:
717 ids = self.filter_real_bug_ids(ids)
717 ids = self.filter_real_bug_ids(ids)
718 if ids:
718 if ids:
719 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
719 ids = self.filter_cset_known_bug_ids(ctx.node(), ids)
720 return ids
720 return ids
721
721
722 def update(self, bugid, ctx):
722 def update(self, bugid, ctx):
723 '''update bugzilla bug with reference to changeset.'''
723 '''update bugzilla bug with reference to changeset.'''
724
724
725 def webroot(root):
725 def webroot(root):
726 '''strip leading prefix of repo root and turn into
726 '''strip leading prefix of repo root and turn into
727 url-safe path.'''
727 url-safe path.'''
728 count = int(self.ui.config('bugzilla', 'strip', 0))
728 count = int(self.ui.config('bugzilla', 'strip', 0))
729 root = util.pconvert(root)
729 root = util.pconvert(root)
730 while count > 0:
730 while count > 0:
731 c = root.find('/')
731 c = root.find('/')
732 if c == -1:
732 if c == -1:
733 break
733 break
734 root = root[c + 1:]
734 root = root[c + 1:]
735 count -= 1
735 count -= 1
736 return root
736 return root
737
737
738 mapfile = self.ui.config('bugzilla', 'style')
738 mapfile = self.ui.config('bugzilla', 'style')
739 tmpl = self.ui.config('bugzilla', 'template')
739 tmpl = self.ui.config('bugzilla', 'template')
740 t = cmdutil.changeset_templater(self.ui, self.repo,
740 t = cmdutil.changeset_templater(self.ui, self.repo,
741 False, None, mapfile, False)
741 False, None, mapfile, False)
742 if not mapfile and not tmpl:
742 if not mapfile and not tmpl:
743 tmpl = _('changeset {node|short} in repo {root} refers '
743 tmpl = _('changeset {node|short} in repo {root} refers '
744 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
744 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
745 if tmpl:
745 if tmpl:
746 tmpl = templater.parsestring(tmpl, quoted=False)
746 tmpl = templater.parsestring(tmpl, quoted=False)
747 t.use_template(tmpl)
747 t.use_template(tmpl)
748 self.ui.pushbuffer()
748 self.ui.pushbuffer()
749 t.show(ctx, changes=ctx.changeset(),
749 t.show(ctx, changes=ctx.changeset(),
750 bug=str(bugid),
750 bug=str(bugid),
751 hgweb=self.ui.config('web', 'baseurl'),
751 hgweb=self.ui.config('web', 'baseurl'),
752 root=self.repo.root,
752 root=self.repo.root,
753 webroot=webroot(self.repo.root))
753 webroot=webroot(self.repo.root))
754 data = self.ui.popbuffer()
754 data = self.ui.popbuffer()
755 self.add_comment(bugid, data, util.email(ctx.user()))
755 self.add_comment(bugid, data, util.email(ctx.user()))
756
756
757 def hook(ui, repo, hooktype, node=None, **kwargs):
757 def hook(ui, repo, hooktype, node=None, **kwargs):
758 '''add comment to bugzilla for each changeset that refers to a
758 '''add comment to bugzilla for each changeset that refers to a
759 bugzilla bug id. only add a comment once per bug, so same change
759 bugzilla bug id. only add a comment once per bug, so same change
760 seen multiple times does not fill bug with duplicate data.'''
760 seen multiple times does not fill bug with duplicate data.'''
761 if node is None:
761 if node is None:
762 raise util.Abort(_('hook type %s does not pass a changeset id') %
762 raise util.Abort(_('hook type %s does not pass a changeset id') %
763 hooktype)
763 hooktype)
764 try:
764 try:
765 bz = bugzilla(ui, repo)
765 bz = bugzilla(ui, repo)
766 ctx = repo[node]
766 ctx = repo[node]
767 ids = bz.find_bug_ids(ctx)
767 ids = bz.find_bug_ids(ctx)
768 if ids:
768 if ids:
769 for id in ids:
769 for id in ids:
770 bz.update(id, ctx)
770 bz.update(id, ctx)
771 bz.notify(ids, util.email(ctx.user()))
771 bz.notify(ids, util.email(ctx.user()))
772 except Exception, e:
772 except Exception, e:
773 raise util.Abort(_('Bugzilla error: %s') % e)
773 raise util.Abort(_('Bugzilla error: %s') % e)
774
774
General Comments 0
You need to be logged in to leave comments. Login now