##// END OF EJS Templates
bugzilla: word-wrap help texts at 70 characters
Martin Geisler -
r7985:0edca606 default
parent child Browse files
Show More
@@ -1,410 +1,417
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 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 '''Bugzilla integration
8 '''Bugzilla integration
9
9
10 This hook extension adds comments on bugs in Bugzilla when changesets
10 This hook extension adds comments on bugs in Bugzilla when changesets
11 that refer to bugs by Bugzilla ID are seen. The hook does not change bug
11 that refer to bugs by Bugzilla ID are seen. The hook does not change
12 status.
12 bug status.
13
13
14 The hook updates the Bugzilla database directly. Only Bugzilla installations
14 The hook updates the Bugzilla database directly. Only Bugzilla
15 using MySQL are supported.
15 installations using MySQL are supported.
16
16
17 The hook relies on a Bugzilla script to send bug change notification emails.
17 The hook relies on a Bugzilla script to send bug change notification
18 That script changes between Bugzilla versions; the 'processmail' script used
18 emails. That script changes between Bugzilla versions; the
19 prior to 2.18 is replaced in 2.18 and subsequent versions by
19 'processmail' script used prior to 2.18 is replaced in 2.18 and
20 'config/sendbugmail.pl'. Note that these will be run by Mercurial as the user
20 subsequent versions by 'config/sendbugmail.pl'. Note that these will
21 pushing the change; you will need to ensure the Bugzilla install file
21 be run by Mercurial as the user pushing the change; you will need to
22 permissions are set appropriately.
22 ensure the Bugzilla install file permissions are set appropriately.
23
23
24 Configuring the extension:
24 Configuring the extension:
25
25
26 [bugzilla]
26 [bugzilla]
27 host Hostname of the MySQL server holding the Bugzilla database.
27
28 host Hostname of the MySQL server holding the Bugzilla
29 database.
28 db Name of the Bugzilla database in MySQL. Default 'bugs'.
30 db Name of the Bugzilla database in MySQL. Default 'bugs'.
29 user Username to use to access MySQL server. Default 'bugs'.
31 user Username to use to access MySQL server. Default 'bugs'.
30 password Password to use to access MySQL server.
32 password Password to use to access MySQL server.
31 timeout Database connection timeout (seconds). Default 5.
33 timeout Database connection timeout (seconds). Default 5.
32 version Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and
34 version Bugzilla version. Specify '3.0' for Bugzilla versions
33 later, '2.18' for Bugzilla versions from 2.18 and '2.16' for
35 3.0 and later, '2.18' for Bugzilla versions from 2.18
34 versions prior to 2.18.
36 and '2.16' for versions prior to 2.18.
35 bzuser Fallback Bugzilla user name to record comments with, if
37 bzuser Fallback Bugzilla user name to record comments with, if
36 changeset committer cannot be found as a Bugzilla user.
38 changeset committer cannot be found as a Bugzilla user.
37 bzdir Bugzilla install directory. Used by default notify.
39 bzdir Bugzilla install directory. Used by default notify.
38 Default '/var/www/html/bugzilla'.
40 Default '/var/www/html/bugzilla'.
39 notify The command to run to get Bugzilla to send bug change
41 notify The command to run to get Bugzilla to send bug change
40 notification emails. Substitutes from a map with 3 keys,
42 notification emails. Substitutes from a map with 3
41 'bzdir', 'id' (bug id) and 'user' (committer bugzilla email).
43 keys, 'bzdir', 'id' (bug id) and 'user' (committer
42 Default depends on version; from 2.18 it is
44 bugzilla email). Default depends on version; from 2.18
43 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s".
45 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
44 regexp Regular expression to match bug IDs in changeset commit message.
46 %(id)s %(user)s".
45 Must contain one "()" group. The default expression matches
47 regexp Regular expression to match bug IDs in changeset commit
46 'Bug 1234', 'Bug no. 1234', 'Bug number 1234',
48 message. Must contain one "()" group. The default
47 'Bugs 1234,5678', 'Bug 1234 and 5678' and variations thereof.
49 expression matches 'Bug 1234', 'Bug no. 1234', 'Bug
48 Matching is case insensitive.
50 number 1234', 'Bugs 1234,5678', 'Bug 1234 and 5678' and
51 variations thereof. Matching is case insensitive.
49 style The style file to use when formatting comments.
52 style The style file to use when formatting comments.
50 template Template to use when formatting comments. Overrides
53 template Template to use when formatting comments. Overrides
51 style if specified. In addition to the usual Mercurial
54 style if specified. In addition to the usual Mercurial
52 keywords, the extension specifies:
55 keywords, the extension specifies:
53 {bug} The Bugzilla bug ID.
56 {bug} The Bugzilla bug ID.
54 {root} The full pathname of the Mercurial repository.
57 {root} The full pathname of the Mercurial
55 {webroot} Stripped pathname of the Mercurial repository.
58 repository.
56 {hgweb} Base URL for browsing Mercurial repositories.
59 {webroot} Stripped pathname of the Mercurial
60 repository.
61 {hgweb} Base URL for browsing Mercurial
62 repositories.
57 Default 'changeset {node|short} in repo {root} refers '
63 Default 'changeset {node|short} in repo {root} refers '
58 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
64 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
59 strip The number of slashes to strip from the front of {root}
65 strip The number of slashes to strip from the front of {root}
60 to produce {webroot}. Default 0.
66 to produce {webroot}. Default 0.
61 usermap Path of file containing Mercurial committer ID to Bugzilla user
67 usermap Path of file containing Mercurial committer ID to
62 ID mappings. If specified, the file should contain one mapping
68 Bugzilla user ID mappings. If specified, the file
63 per line, "committer"="Bugzilla user". See also the
69 should contain one mapping per line,
64 [usermap] section.
70 "committer"="Bugzilla user". See also the [usermap]
71 section.
65
72
66 [usermap]
73 [usermap]
67 Any entries in this section specify mappings of Mercurial committer ID
74 Any entries in this section specify mappings of Mercurial
68 to Bugzilla user ID. See also [bugzilla].usermap.
75 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
69 "committer"="Bugzilla user"
76 "committer"="Bugzilla user"
70
77
71 [web]
78 [web]
72 baseurl Base URL for browsing Mercurial repositories. Reference from
79 baseurl Base URL for browsing Mercurial repositories. Reference
73 templates as {hgweb}.
80 from templates as {hgweb}.
74
81
75 Activating the extension:
82 Activating the extension:
76
83
77 [extensions]
84 [extensions]
78 hgext.bugzilla =
85 hgext.bugzilla =
79
86
80 [hooks]
87 [hooks]
81 # run bugzilla hook on every change pulled or pushed in here
88 # run bugzilla hook on every change pulled or pushed in here
82 incoming.bugzilla = python:hgext.bugzilla.hook
89 incoming.bugzilla = python:hgext.bugzilla.hook
83
90
84 Example configuration:
91 Example configuration:
85
92
86 This example configuration is for a collection of Mercurial repositories
93 This example configuration is for a collection of Mercurial
87 in /var/local/hg/repos/ used with a local Bugzilla 3.2 installation in
94 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
88 /opt/bugzilla-3.2.
95 installation in /opt/bugzilla-3.2.
89
96
90 [bugzilla]
97 [bugzilla]
91 host=localhost
98 host=localhost
92 password=XYZZY
99 password=XYZZY
93 version=3.0
100 version=3.0
94 bzuser=unknown@domain.com
101 bzuser=unknown@domain.com
95 bzdir=/opt/bugzilla-3.2
102 bzdir=/opt/bugzilla-3.2
96 template=Changeset {node|short} in {root|basename}.\\n{hgweb}/{webroot}/rev/{node|short}\\n\\n{desc}\\n
103 template=Changeset {node|short} in {root|basename}.\\n{hgweb}/{webroot}/rev/{node|short}\\n\\n{desc}\\n
97 strip=5
104 strip=5
98
105
99 [web]
106 [web]
100 baseurl=http://dev.domain.com/hg
107 baseurl=http://dev.domain.com/hg
101
108
102 [usermap]
109 [usermap]
103 user@emaildomain.com=user.name@bugzilladomain.com
110 user@emaildomain.com=user.name@bugzilladomain.com
104
111
105 Commits add a comment to the Bugzilla bug record of the form:
112 Commits add a comment to the Bugzilla bug record of the form:
106
113
107 Changeset 3b16791d6642 in repository-name.
114 Changeset 3b16791d6642 in repository-name.
108 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
115 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
109
116
110 Changeset commit comment. Bug 1234.
117 Changeset commit comment. Bug 1234.
111 '''
118 '''
112
119
113 from mercurial.i18n import _
120 from mercurial.i18n import _
114 from mercurial.node import short
121 from mercurial.node import short
115 from mercurial import cmdutil, templater, util
122 from mercurial import cmdutil, templater, util
116 import re, time
123 import re, time
117
124
118 MySQLdb = None
125 MySQLdb = None
119
126
120 def buglist(ids):
127 def buglist(ids):
121 return '(' + ','.join(map(str, ids)) + ')'
128 return '(' + ','.join(map(str, ids)) + ')'
122
129
123 class bugzilla_2_16(object):
130 class bugzilla_2_16(object):
124 '''support for bugzilla version 2.16.'''
131 '''support for bugzilla version 2.16.'''
125
132
126 def __init__(self, ui):
133 def __init__(self, ui):
127 self.ui = ui
134 self.ui = ui
128 host = self.ui.config('bugzilla', 'host', 'localhost')
135 host = self.ui.config('bugzilla', 'host', 'localhost')
129 user = self.ui.config('bugzilla', 'user', 'bugs')
136 user = self.ui.config('bugzilla', 'user', 'bugs')
130 passwd = self.ui.config('bugzilla', 'password')
137 passwd = self.ui.config('bugzilla', 'password')
131 db = self.ui.config('bugzilla', 'db', 'bugs')
138 db = self.ui.config('bugzilla', 'db', 'bugs')
132 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
139 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
133 usermap = self.ui.config('bugzilla', 'usermap')
140 usermap = self.ui.config('bugzilla', 'usermap')
134 if usermap:
141 if usermap:
135 self.ui.readsections(usermap, 'usermap')
142 self.ui.readsections(usermap, 'usermap')
136 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
143 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
137 (host, db, user, '*' * len(passwd)))
144 (host, db, user, '*' * len(passwd)))
138 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
145 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
139 db=db, connect_timeout=timeout)
146 db=db, connect_timeout=timeout)
140 self.cursor = self.conn.cursor()
147 self.cursor = self.conn.cursor()
141 self.longdesc_id = self.get_longdesc_id()
148 self.longdesc_id = self.get_longdesc_id()
142 self.user_ids = {}
149 self.user_ids = {}
143 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
150 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
144
151
145 def run(self, *args, **kwargs):
152 def run(self, *args, **kwargs):
146 '''run a query.'''
153 '''run a query.'''
147 self.ui.note(_('query: %s %s\n') % (args, kwargs))
154 self.ui.note(_('query: %s %s\n') % (args, kwargs))
148 try:
155 try:
149 self.cursor.execute(*args, **kwargs)
156 self.cursor.execute(*args, **kwargs)
150 except MySQLdb.MySQLError:
157 except MySQLdb.MySQLError:
151 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
158 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
152 raise
159 raise
153
160
154 def get_longdesc_id(self):
161 def get_longdesc_id(self):
155 '''get identity of longdesc field'''
162 '''get identity of longdesc field'''
156 self.run('select fieldid from fielddefs where name = "longdesc"')
163 self.run('select fieldid from fielddefs where name = "longdesc"')
157 ids = self.cursor.fetchall()
164 ids = self.cursor.fetchall()
158 if len(ids) != 1:
165 if len(ids) != 1:
159 raise util.Abort(_('unknown database schema'))
166 raise util.Abort(_('unknown database schema'))
160 return ids[0][0]
167 return ids[0][0]
161
168
162 def filter_real_bug_ids(self, ids):
169 def filter_real_bug_ids(self, ids):
163 '''filter not-existing bug ids from list.'''
170 '''filter not-existing bug ids from list.'''
164 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
171 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
165 return util.sort([c[0] for c in self.cursor.fetchall()])
172 return util.sort([c[0] for c in self.cursor.fetchall()])
166
173
167 def filter_unknown_bug_ids(self, node, ids):
174 def filter_unknown_bug_ids(self, node, ids):
168 '''filter bug ids from list that already refer to this changeset.'''
175 '''filter bug ids from list that already refer to this changeset.'''
169
176
170 self.run('''select bug_id from longdescs where
177 self.run('''select bug_id from longdescs where
171 bug_id in %s and thetext like "%%%s%%"''' %
178 bug_id in %s and thetext like "%%%s%%"''' %
172 (buglist(ids), short(node)))
179 (buglist(ids), short(node)))
173 unknown = dict.fromkeys(ids)
180 unknown = dict.fromkeys(ids)
174 for (id,) in self.cursor.fetchall():
181 for (id,) in self.cursor.fetchall():
175 self.ui.status(_('bug %d already knows about changeset %s\n') %
182 self.ui.status(_('bug %d already knows about changeset %s\n') %
176 (id, short(node)))
183 (id, short(node)))
177 unknown.pop(id, None)
184 unknown.pop(id, None)
178 return util.sort(unknown.keys())
185 return util.sort(unknown.keys())
179
186
180 def notify(self, ids, committer):
187 def notify(self, ids, committer):
181 '''tell bugzilla to send mail.'''
188 '''tell bugzilla to send mail.'''
182
189
183 self.ui.status(_('telling bugzilla to send mail:\n'))
190 self.ui.status(_('telling bugzilla to send mail:\n'))
184 (user, userid) = self.get_bugzilla_user(committer)
191 (user, userid) = self.get_bugzilla_user(committer)
185 for id in ids:
192 for id in ids:
186 self.ui.status(_(' bug %s\n') % id)
193 self.ui.status(_(' bug %s\n') % id)
187 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
194 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
188 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
195 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
189 try:
196 try:
190 # Backwards-compatible with old notify string, which
197 # Backwards-compatible with old notify string, which
191 # took one string. This will throw with a new format
198 # took one string. This will throw with a new format
192 # string.
199 # string.
193 cmd = cmdfmt % id
200 cmd = cmdfmt % id
194 except TypeError:
201 except TypeError:
195 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
202 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
196 self.ui.note(_('running notify command %s\n') % cmd)
203 self.ui.note(_('running notify command %s\n') % cmd)
197 fp = util.popen('(%s) 2>&1' % cmd)
204 fp = util.popen('(%s) 2>&1' % cmd)
198 out = fp.read()
205 out = fp.read()
199 ret = fp.close()
206 ret = fp.close()
200 if ret:
207 if ret:
201 self.ui.warn(out)
208 self.ui.warn(out)
202 raise util.Abort(_('bugzilla notify command %s') %
209 raise util.Abort(_('bugzilla notify command %s') %
203 util.explain_exit(ret)[0])
210 util.explain_exit(ret)[0])
204 self.ui.status(_('done\n'))
211 self.ui.status(_('done\n'))
205
212
206 def get_user_id(self, user):
213 def get_user_id(self, user):
207 '''look up numeric bugzilla user id.'''
214 '''look up numeric bugzilla user id.'''
208 try:
215 try:
209 return self.user_ids[user]
216 return self.user_ids[user]
210 except KeyError:
217 except KeyError:
211 try:
218 try:
212 userid = int(user)
219 userid = int(user)
213 except ValueError:
220 except ValueError:
214 self.ui.note(_('looking up user %s\n') % user)
221 self.ui.note(_('looking up user %s\n') % user)
215 self.run('''select userid from profiles
222 self.run('''select userid from profiles
216 where login_name like %s''', user)
223 where login_name like %s''', user)
217 all = self.cursor.fetchall()
224 all = self.cursor.fetchall()
218 if len(all) != 1:
225 if len(all) != 1:
219 raise KeyError(user)
226 raise KeyError(user)
220 userid = int(all[0][0])
227 userid = int(all[0][0])
221 self.user_ids[user] = userid
228 self.user_ids[user] = userid
222 return userid
229 return userid
223
230
224 def map_committer(self, user):
231 def map_committer(self, user):
225 '''map name of committer to bugzilla user name.'''
232 '''map name of committer to bugzilla user name.'''
226 for committer, bzuser in self.ui.configitems('usermap'):
233 for committer, bzuser in self.ui.configitems('usermap'):
227 if committer.lower() == user.lower():
234 if committer.lower() == user.lower():
228 return bzuser
235 return bzuser
229 return user
236 return user
230
237
231 def get_bugzilla_user(self, committer):
238 def get_bugzilla_user(self, committer):
232 '''see if committer is a registered bugzilla user. Return
239 '''see if committer is a registered bugzilla user. Return
233 bugzilla username and userid if so. If not, return default
240 bugzilla username and userid if so. If not, return default
234 bugzilla username and userid.'''
241 bugzilla username and userid.'''
235 user = self.map_committer(committer)
242 user = self.map_committer(committer)
236 try:
243 try:
237 userid = self.get_user_id(user)
244 userid = self.get_user_id(user)
238 except KeyError:
245 except KeyError:
239 try:
246 try:
240 defaultuser = self.ui.config('bugzilla', 'bzuser')
247 defaultuser = self.ui.config('bugzilla', 'bzuser')
241 if not defaultuser:
248 if not defaultuser:
242 raise util.Abort(_('cannot find bugzilla user id for %s') %
249 raise util.Abort(_('cannot find bugzilla user id for %s') %
243 user)
250 user)
244 userid = self.get_user_id(defaultuser)
251 userid = self.get_user_id(defaultuser)
245 user = defaultuser
252 user = defaultuser
246 except KeyError:
253 except KeyError:
247 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
254 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
248 (user, defaultuser))
255 (user, defaultuser))
249 return (user, userid)
256 return (user, userid)
250
257
251 def add_comment(self, bugid, text, committer):
258 def add_comment(self, bugid, text, committer):
252 '''add comment to bug. try adding comment as committer of
259 '''add comment to bug. try adding comment as committer of
253 changeset, otherwise as default bugzilla user.'''
260 changeset, otherwise as default bugzilla user.'''
254 (user, userid) = self.get_bugzilla_user(committer)
261 (user, userid) = self.get_bugzilla_user(committer)
255 now = time.strftime('%Y-%m-%d %H:%M:%S')
262 now = time.strftime('%Y-%m-%d %H:%M:%S')
256 self.run('''insert into longdescs
263 self.run('''insert into longdescs
257 (bug_id, who, bug_when, thetext)
264 (bug_id, who, bug_when, thetext)
258 values (%s, %s, %s, %s)''',
265 values (%s, %s, %s, %s)''',
259 (bugid, userid, now, text))
266 (bugid, userid, now, text))
260 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
267 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
261 values (%s, %s, %s, %s)''',
268 values (%s, %s, %s, %s)''',
262 (bugid, userid, now, self.longdesc_id))
269 (bugid, userid, now, self.longdesc_id))
263 self.conn.commit()
270 self.conn.commit()
264
271
265 class bugzilla_2_18(bugzilla_2_16):
272 class bugzilla_2_18(bugzilla_2_16):
266 '''support for bugzilla 2.18 series.'''
273 '''support for bugzilla 2.18 series.'''
267
274
268 def __init__(self, ui):
275 def __init__(self, ui):
269 bugzilla_2_16.__init__(self, ui)
276 bugzilla_2_16.__init__(self, ui)
270 self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
277 self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
271
278
272 class bugzilla_3_0(bugzilla_2_18):
279 class bugzilla_3_0(bugzilla_2_18):
273 '''support for bugzilla 3.0 series.'''
280 '''support for bugzilla 3.0 series.'''
274
281
275 def __init__(self, ui):
282 def __init__(self, ui):
276 bugzilla_2_18.__init__(self, ui)
283 bugzilla_2_18.__init__(self, ui)
277
284
278 def get_longdesc_id(self):
285 def get_longdesc_id(self):
279 '''get identity of longdesc field'''
286 '''get identity of longdesc field'''
280 self.run('select id from fielddefs where name = "longdesc"')
287 self.run('select id from fielddefs where name = "longdesc"')
281 ids = self.cursor.fetchall()
288 ids = self.cursor.fetchall()
282 if len(ids) != 1:
289 if len(ids) != 1:
283 raise util.Abort(_('unknown database schema'))
290 raise util.Abort(_('unknown database schema'))
284 return ids[0][0]
291 return ids[0][0]
285
292
286 class bugzilla(object):
293 class bugzilla(object):
287 # supported versions of bugzilla. different versions have
294 # supported versions of bugzilla. different versions have
288 # different schemas.
295 # different schemas.
289 _versions = {
296 _versions = {
290 '2.16': bugzilla_2_16,
297 '2.16': bugzilla_2_16,
291 '2.18': bugzilla_2_18,
298 '2.18': bugzilla_2_18,
292 '3.0': bugzilla_3_0
299 '3.0': bugzilla_3_0
293 }
300 }
294
301
295 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
302 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
296 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
303 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
297
304
298 _bz = None
305 _bz = None
299
306
300 def __init__(self, ui, repo):
307 def __init__(self, ui, repo):
301 self.ui = ui
308 self.ui = ui
302 self.repo = repo
309 self.repo = repo
303
310
304 def bz(self):
311 def bz(self):
305 '''return object that knows how to talk to bugzilla version in
312 '''return object that knows how to talk to bugzilla version in
306 use.'''
313 use.'''
307
314
308 if bugzilla._bz is None:
315 if bugzilla._bz is None:
309 bzversion = self.ui.config('bugzilla', 'version')
316 bzversion = self.ui.config('bugzilla', 'version')
310 try:
317 try:
311 bzclass = bugzilla._versions[bzversion]
318 bzclass = bugzilla._versions[bzversion]
312 except KeyError:
319 except KeyError:
313 raise util.Abort(_('bugzilla version %s not supported') %
320 raise util.Abort(_('bugzilla version %s not supported') %
314 bzversion)
321 bzversion)
315 bugzilla._bz = bzclass(self.ui)
322 bugzilla._bz = bzclass(self.ui)
316 return bugzilla._bz
323 return bugzilla._bz
317
324
318 def __getattr__(self, key):
325 def __getattr__(self, key):
319 return getattr(self.bz(), key)
326 return getattr(self.bz(), key)
320
327
321 _bug_re = None
328 _bug_re = None
322 _split_re = None
329 _split_re = None
323
330
324 def find_bug_ids(self, ctx):
331 def find_bug_ids(self, ctx):
325 '''find valid bug ids that are referred to in changeset
332 '''find valid bug ids that are referred to in changeset
326 comments and that do not already have references to this
333 comments and that do not already have references to this
327 changeset.'''
334 changeset.'''
328
335
329 if bugzilla._bug_re is None:
336 if bugzilla._bug_re is None:
330 bugzilla._bug_re = re.compile(
337 bugzilla._bug_re = re.compile(
331 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
338 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
332 re.IGNORECASE)
339 re.IGNORECASE)
333 bugzilla._split_re = re.compile(r'\D+')
340 bugzilla._split_re = re.compile(r'\D+')
334 start = 0
341 start = 0
335 ids = {}
342 ids = {}
336 while True:
343 while True:
337 m = bugzilla._bug_re.search(ctx.description(), start)
344 m = bugzilla._bug_re.search(ctx.description(), start)
338 if not m:
345 if not m:
339 break
346 break
340 start = m.end()
347 start = m.end()
341 for id in bugzilla._split_re.split(m.group(1)):
348 for id in bugzilla._split_re.split(m.group(1)):
342 if not id: continue
349 if not id: continue
343 ids[int(id)] = 1
350 ids[int(id)] = 1
344 ids = ids.keys()
351 ids = ids.keys()
345 if ids:
352 if ids:
346 ids = self.filter_real_bug_ids(ids)
353 ids = self.filter_real_bug_ids(ids)
347 if ids:
354 if ids:
348 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
355 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
349 return ids
356 return ids
350
357
351 def update(self, bugid, ctx):
358 def update(self, bugid, ctx):
352 '''update bugzilla bug with reference to changeset.'''
359 '''update bugzilla bug with reference to changeset.'''
353
360
354 def webroot(root):
361 def webroot(root):
355 '''strip leading prefix of repo root and turn into
362 '''strip leading prefix of repo root and turn into
356 url-safe path.'''
363 url-safe path.'''
357 count = int(self.ui.config('bugzilla', 'strip', 0))
364 count = int(self.ui.config('bugzilla', 'strip', 0))
358 root = util.pconvert(root)
365 root = util.pconvert(root)
359 while count > 0:
366 while count > 0:
360 c = root.find('/')
367 c = root.find('/')
361 if c == -1:
368 if c == -1:
362 break
369 break
363 root = root[c+1:]
370 root = root[c+1:]
364 count -= 1
371 count -= 1
365 return root
372 return root
366
373
367 mapfile = self.ui.config('bugzilla', 'style')
374 mapfile = self.ui.config('bugzilla', 'style')
368 tmpl = self.ui.config('bugzilla', 'template')
375 tmpl = self.ui.config('bugzilla', 'template')
369 t = cmdutil.changeset_templater(self.ui, self.repo,
376 t = cmdutil.changeset_templater(self.ui, self.repo,
370 False, None, mapfile, False)
377 False, None, mapfile, False)
371 if not mapfile and not tmpl:
378 if not mapfile and not tmpl:
372 tmpl = _('changeset {node|short} in repo {root} refers '
379 tmpl = _('changeset {node|short} in repo {root} refers '
373 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
380 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
374 if tmpl:
381 if tmpl:
375 tmpl = templater.parsestring(tmpl, quoted=False)
382 tmpl = templater.parsestring(tmpl, quoted=False)
376 t.use_template(tmpl)
383 t.use_template(tmpl)
377 self.ui.pushbuffer()
384 self.ui.pushbuffer()
378 t.show(ctx, changes=ctx.changeset(),
385 t.show(ctx, changes=ctx.changeset(),
379 bug=str(bugid),
386 bug=str(bugid),
380 hgweb=self.ui.config('web', 'baseurl'),
387 hgweb=self.ui.config('web', 'baseurl'),
381 root=self.repo.root,
388 root=self.repo.root,
382 webroot=webroot(self.repo.root))
389 webroot=webroot(self.repo.root))
383 data = self.ui.popbuffer()
390 data = self.ui.popbuffer()
384 self.add_comment(bugid, data, util.email(ctx.user()))
391 self.add_comment(bugid, data, util.email(ctx.user()))
385
392
386 def hook(ui, repo, hooktype, node=None, **kwargs):
393 def hook(ui, repo, hooktype, node=None, **kwargs):
387 '''add comment to bugzilla for each changeset that refers to a
394 '''add comment to bugzilla for each changeset that refers to a
388 bugzilla bug id. only add a comment once per bug, so same change
395 bugzilla bug id. only add a comment once per bug, so same change
389 seen multiple times does not fill bug with duplicate data.'''
396 seen multiple times does not fill bug with duplicate data.'''
390 try:
397 try:
391 import MySQLdb as mysql
398 import MySQLdb as mysql
392 global MySQLdb
399 global MySQLdb
393 MySQLdb = mysql
400 MySQLdb = mysql
394 except ImportError, err:
401 except ImportError, err:
395 raise util.Abort(_('python mysql support not available: %s') % err)
402 raise util.Abort(_('python mysql support not available: %s') % err)
396
403
397 if node is None:
404 if node is None:
398 raise util.Abort(_('hook type %s does not pass a changeset id') %
405 raise util.Abort(_('hook type %s does not pass a changeset id') %
399 hooktype)
406 hooktype)
400 try:
407 try:
401 bz = bugzilla(ui, repo)
408 bz = bugzilla(ui, repo)
402 ctx = repo[node]
409 ctx = repo[node]
403 ids = bz.find_bug_ids(ctx)
410 ids = bz.find_bug_ids(ctx)
404 if ids:
411 if ids:
405 for id in ids:
412 for id in ids:
406 bz.update(id, ctx)
413 bz.update(id, ctx)
407 bz.notify(ids, util.email(ctx.user()))
414 bz.notify(ids, util.email(ctx.user()))
408 except MySQLdb.MySQLError, err:
415 except MySQLdb.MySQLError, err:
409 raise util.Abort(_('database error: %s') % err[1])
416 raise util.Abort(_('database error: %s') % err[1])
410
417
General Comments 0
You need to be logged in to leave comments. Login now