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