##// END OF EJS Templates
Add support for Bugzilla 3.0 series to bugzilla hook....
Jim Hague -
r7019:6b1ece89 default
parent child Browse files
Show More
@@ -1,307 +1,326
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # hook extension to update comments of bugzilla bugs when changesets
9 9 # that refer to bugs by id are seen. this hook does not change bug
10 10 # status, only comments.
11 11 #
12 12 # to configure, add items to '[bugzilla]' section of hgrc.
13 13 #
14 14 # to use, configure bugzilla extension and enable like this:
15 15 #
16 16 # [extensions]
17 17 # hgext.bugzilla =
18 18 #
19 19 # [hooks]
20 20 # # run bugzilla hook on every change pulled or pushed in here
21 21 # incoming.bugzilla = python:hgext.bugzilla.hook
22 22 #
23 23 # config items:
24 24 #
25 25 # section name is 'bugzilla'.
26 26 # [bugzilla]
27 27 #
28 28 # REQUIRED:
29 29 # host = bugzilla # mysql server where bugzilla database lives
30 30 # password = ** # user's password
31 31 # version = 2.16 # version of bugzilla installed
32 32 #
33 33 # OPTIONAL:
34 34 # bzuser = ... # fallback bugzilla user name to record comments with
35 35 # db = bugs # database to connect to
36 36 # notify = ... # command to run to get bugzilla to send mail
37 37 # regexp = ... # regexp to match bug ids (must contain one "()" group)
38 38 # strip = 0 # number of slashes to strip for url paths
39 39 # style = ... # style file to use when formatting comments
40 40 # template = ... # template to use when formatting comments
41 41 # timeout = 5 # database connection timeout (seconds)
42 42 # user = bugs # user to connect to database as
43 43 # [web]
44 44 # baseurl = http://hgserver/... # root of hg web site for browsing commits
45 45 #
46 46 # if hg committer names are not same as bugzilla user names, use
47 47 # "usermap" feature to map from committer email to bugzilla user name.
48 48 # usermap can be in hgrc or separate config file.
49 49 #
50 50 # [bugzilla]
51 51 # usermap = filename # cfg file with "committer"="bugzilla user" info
52 52 # [usermap]
53 53 # committer_email = bugzilla_user_name
54 54
55 55 from mercurial.i18n import _
56 56 from mercurial.node import short
57 57 from mercurial import cmdutil, templater, util
58 58 import re, time
59 59
60 60 MySQLdb = None
61 61
62 62 def buglist(ids):
63 63 return '(' + ','.join(map(str, ids)) + ')'
64 64
65 65 class bugzilla_2_16(object):
66 66 '''support for bugzilla version 2.16.'''
67 67
68 68 def __init__(self, ui):
69 69 self.ui = ui
70 70 host = self.ui.config('bugzilla', 'host', 'localhost')
71 71 user = self.ui.config('bugzilla', 'user', 'bugs')
72 72 passwd = self.ui.config('bugzilla', 'password')
73 73 db = self.ui.config('bugzilla', 'db', 'bugs')
74 74 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
75 75 usermap = self.ui.config('bugzilla', 'usermap')
76 76 if usermap:
77 77 self.ui.readsections(usermap, 'usermap')
78 78 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
79 79 (host, db, user, '*' * len(passwd)))
80 80 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
81 81 db=db, connect_timeout=timeout)
82 82 self.cursor = self.conn.cursor()
83 self.run('select fieldid from fielddefs where name = "longdesc"')
84 ids = self.cursor.fetchall()
85 if len(ids) != 1:
86 raise util.Abort(_('unknown database schema'))
87 self.longdesc_id = ids[0][0]
83 self.longdesc_id = self.get_longdesc_id()
88 84 self.user_ids = {}
89 85
90 86 def run(self, *args, **kwargs):
91 87 '''run a query.'''
92 88 self.ui.note(_('query: %s %s\n') % (args, kwargs))
93 89 try:
94 90 self.cursor.execute(*args, **kwargs)
95 91 except MySQLdb.MySQLError, err:
96 92 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
97 93 raise
98 94
95 def get_longdesc_id(self):
96 '''get identity of longdesc field'''
97 self.run('select fieldid from fielddefs where name = "longdesc"')
98 ids = self.cursor.fetchall()
99 if len(ids) != 1:
100 raise util.Abort(_('unknown database schema'))
101 return ids[0][0]
102
99 103 def filter_real_bug_ids(self, ids):
100 104 '''filter not-existing bug ids from list.'''
101 105 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
102 106 return util.sort([c[0] for c in self.cursor.fetchall()])
103 107
104 108 def filter_unknown_bug_ids(self, node, ids):
105 109 '''filter bug ids from list that already refer to this changeset.'''
106 110
107 111 self.run('''select bug_id from longdescs where
108 112 bug_id in %s and thetext like "%%%s%%"''' %
109 113 (buglist(ids), short(node)))
110 114 unknown = dict.fromkeys(ids)
111 115 for (id,) in self.cursor.fetchall():
112 116 self.ui.status(_('bug %d already knows about changeset %s\n') %
113 117 (id, short(node)))
114 118 unknown.pop(id, None)
115 119 return util.sort(unknown.keys())
116 120
117 121 def notify(self, ids):
118 122 '''tell bugzilla to send mail.'''
119 123
120 124 self.ui.status(_('telling bugzilla to send mail:\n'))
121 125 for id in ids:
122 126 self.ui.status(_(' bug %s\n') % id)
123 127 cmd = self.ui.config('bugzilla', 'notify',
124 128 'cd /var/www/html/bugzilla && '
125 129 './processmail %s nobody@nowhere.com') % id
126 130 fp = util.popen('(%s) 2>&1' % cmd)
127 131 out = fp.read()
128 132 ret = fp.close()
129 133 if ret:
130 134 self.ui.warn(out)
131 135 raise util.Abort(_('bugzilla notify command %s') %
132 136 util.explain_exit(ret)[0])
133 137 self.ui.status(_('done\n'))
134 138
135 139 def get_user_id(self, user):
136 140 '''look up numeric bugzilla user id.'''
137 141 try:
138 142 return self.user_ids[user]
139 143 except KeyError:
140 144 try:
141 145 userid = int(user)
142 146 except ValueError:
143 147 self.ui.note(_('looking up user %s\n') % user)
144 148 self.run('''select userid from profiles
145 149 where login_name like %s''', user)
146 150 all = self.cursor.fetchall()
147 151 if len(all) != 1:
148 152 raise KeyError(user)
149 153 userid = int(all[0][0])
150 154 self.user_ids[user] = userid
151 155 return userid
152 156
153 157 def map_committer(self, user):
154 158 '''map name of committer to bugzilla user name.'''
155 159 for committer, bzuser in self.ui.configitems('usermap'):
156 160 if committer.lower() == user.lower():
157 161 return bzuser
158 162 return user
159 163
160 164 def add_comment(self, bugid, text, committer):
161 165 '''add comment to bug. try adding comment as committer of
162 166 changeset, otherwise as default bugzilla user.'''
163 167 user = self.map_committer(committer)
164 168 try:
165 169 userid = self.get_user_id(user)
166 170 except KeyError:
167 171 try:
168 172 defaultuser = self.ui.config('bugzilla', 'bzuser')
169 173 if not defaultuser:
170 174 raise util.Abort(_('cannot find bugzilla user id for %s') %
171 175 user)
172 176 userid = self.get_user_id(defaultuser)
173 177 except KeyError:
174 178 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
175 179 (user, defaultuser))
176 180 now = time.strftime('%Y-%m-%d %H:%M:%S')
177 181 self.run('''insert into longdescs
178 182 (bug_id, who, bug_when, thetext)
179 183 values (%s, %s, %s, %s)''',
180 184 (bugid, userid, now, text))
181 185 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
182 186 values (%s, %s, %s, %s)''',
183 187 (bugid, userid, now, self.longdesc_id))
184 188
189 class bugzilla_3_0(bugzilla_2_16):
190 '''support for bugzilla 3.0 series.'''
191
192 def __init__(self, ui):
193 bugzilla_2_16.__init__(self, ui)
194
195 def get_longdesc_id(self):
196 '''get identity of longdesc field'''
197 self.run('select id from fielddefs where name = "longdesc"')
198 ids = self.cursor.fetchall()
199 if len(ids) != 1:
200 raise util.Abort(_('unknown database schema'))
201 return ids[0][0]
202
185 203 class bugzilla(object):
186 204 # supported versions of bugzilla. different versions have
187 205 # different schemas.
188 206 _versions = {
189 207 '2.16': bugzilla_2_16,
208 '3.0': bugzilla_3_0
190 209 }
191 210
192 211 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
193 212 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
194 213
195 214 _bz = None
196 215
197 216 def __init__(self, ui, repo):
198 217 self.ui = ui
199 218 self.repo = repo
200 219
201 220 def bz(self):
202 221 '''return object that knows how to talk to bugzilla version in
203 222 use.'''
204 223
205 224 if bugzilla._bz is None:
206 225 bzversion = self.ui.config('bugzilla', 'version')
207 226 try:
208 227 bzclass = bugzilla._versions[bzversion]
209 228 except KeyError:
210 229 raise util.Abort(_('bugzilla version %s not supported') %
211 230 bzversion)
212 231 bugzilla._bz = bzclass(self.ui)
213 232 return bugzilla._bz
214 233
215 234 def __getattr__(self, key):
216 235 return getattr(self.bz(), key)
217 236
218 237 _bug_re = None
219 238 _split_re = None
220 239
221 240 def find_bug_ids(self, ctx):
222 241 '''find valid bug ids that are referred to in changeset
223 242 comments and that do not already have references to this
224 243 changeset.'''
225 244
226 245 if bugzilla._bug_re is None:
227 246 bugzilla._bug_re = re.compile(
228 247 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
229 248 re.IGNORECASE)
230 249 bugzilla._split_re = re.compile(r'\D+')
231 250 start = 0
232 251 ids = {}
233 252 while True:
234 253 m = bugzilla._bug_re.search(ctx.description(), start)
235 254 if not m:
236 255 break
237 256 start = m.end()
238 257 for id in bugzilla._split_re.split(m.group(1)):
239 258 if not id: continue
240 259 ids[int(id)] = 1
241 260 ids = ids.keys()
242 261 if ids:
243 262 ids = self.filter_real_bug_ids(ids)
244 263 if ids:
245 264 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
246 265 return ids
247 266
248 267 def update(self, bugid, ctx):
249 268 '''update bugzilla bug with reference to changeset.'''
250 269
251 270 def webroot(root):
252 271 '''strip leading prefix of repo root and turn into
253 272 url-safe path.'''
254 273 count = int(self.ui.config('bugzilla', 'strip', 0))
255 274 root = util.pconvert(root)
256 275 while count > 0:
257 276 c = root.find('/')
258 277 if c == -1:
259 278 break
260 279 root = root[c+1:]
261 280 count -= 1
262 281 return root
263 282
264 283 mapfile = self.ui.config('bugzilla', 'style')
265 284 tmpl = self.ui.config('bugzilla', 'template')
266 285 t = cmdutil.changeset_templater(self.ui, self.repo,
267 286 False, mapfile, False)
268 287 if not mapfile and not tmpl:
269 288 tmpl = _('changeset {node|short} in repo {root} refers '
270 289 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
271 290 if tmpl:
272 291 tmpl = templater.parsestring(tmpl, quoted=False)
273 292 t.use_template(tmpl)
274 293 self.ui.pushbuffer()
275 294 t.show(changenode=ctx.node(), changes=ctx.changeset(),
276 295 bug=str(bugid),
277 296 hgweb=self.ui.config('web', 'baseurl'),
278 297 root=self.repo.root,
279 298 webroot=webroot(self.repo.root))
280 299 data = self.ui.popbuffer()
281 300 self.add_comment(bugid, data, util.email(ctx.user()))
282 301
283 302 def hook(ui, repo, hooktype, node=None, **kwargs):
284 303 '''add comment to bugzilla for each changeset that refers to a
285 304 bugzilla bug id. only add a comment once per bug, so same change
286 305 seen multiple times does not fill bug with duplicate data.'''
287 306 try:
288 307 import MySQLdb as mysql
289 308 global MySQLdb
290 309 MySQLdb = mysql
291 310 except ImportError, err:
292 311 raise util.Abort(_('python mysql support not available: %s') % err)
293 312
294 313 if node is None:
295 314 raise util.Abort(_('hook type %s does not pass a changeset id') %
296 315 hooktype)
297 316 try:
298 317 bz = bugzilla(ui, repo)
299 318 ctx = repo[node]
300 319 ids = bz.find_bug_ids(ctx)
301 320 if ids:
302 321 for id in ids:
303 322 bz.update(id, ctx)
304 323 bz.notify(ids)
305 324 except MySQLdb.MySQLError, err:
306 325 raise util.Abort(_('database error: %s') % err[1])
307 326
General Comments 0
You need to be logged in to leave comments. Login now