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