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