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