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