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