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