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