comparison scripts/beta-notify/notify-roundup.py @ 4162:5d8246ac6e89 gsoc-2009

Added beta notify development dir
author Pygi <pygi@users.sourceforge.net>
date Thu, 02 Jul 2009 17:45:36 +0000
parents
children 6750b2dae53e
comparison
equal deleted inserted replaced
4161:8b381ee4e15e 4162:5d8246ac6e89
1 #!/usr/bin/python
2 #
3 # notify-roundup.py: call into a roundup tracker to notify it of commits
4 #
5 # USAGE: notify-roundup.py TRACKER-HOME REPOS-DIR REVISION
6 # notify-roundup.py TRACKER-HOME REPOS-DIR REVISION AUTHOR PROPNAME
7 #
8 # TRACKER-HOME is the tracker to notify
9 #
10 # See end of file for change history
11
12 import sys, os, time, cStringIO, re, logging, smtplib, ConfigParser, socket
13
14
15 # configure logging
16 logger = logging.getLogger('notify-roundup')
17 hdlr = logging.FileHandler('/tmp/log')
18 formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
19 hdlr.setFormatter(formatter)
20 logger.addHandler(hdlr)
21 logger.propogate = False
22 logger.setLevel(logging.DEBUG)
23
24 #print sys.argv
25 # now try to import stuff that might not work
26 try:
27 import roundup.instance, roundup.date
28
29 import svn.fs
30 import svn.delta
31 import svn.repos
32 import svn.core
33 except:
34 logger.exception('Exception while importing Roundup and SVN')
35 sys.exit(1)
36
37 class Failed(Exception):
38 pass
39 class Unauthorised(Failed):
40 pass
41
42 def main(pool):
43 '''Handle the commit revision.
44 '''
45 # command-line args
46 cfg = ConfigParser.ConfigParser()
47 cfg.read(sys.argv[1])
48 repos_dir = sys.argv[2]
49 revision = int(sys.argv[3])
50
51 # get a handle on the revision in the repository
52 repos = Repository(repos_dir, revision, pool)
53
54 repos.klass = cfg.get('main', 'item-class')
55 if not repos.extract_info():
56 return
57
58 if cfg.has_option('main', 'host'):
59 repos.host = cfg.get('main', 'host')
60 else:
61 repos.host = socket.gethostname()
62
63 mode = cfg.get('main', 'mode')
64 if mode == 'local':
65 notify_local(cfg.get('local', 'tracker-home'), repos)
66 elif mode == 'email':
67 tracker_address = cfg.get('email', 'tracker-address')
68 domain = cfg.get('email', 'default-domain')
69 smtp_host = cfg.get('email', 'smtp-host')
70 if cfg.has_option('address mappings', repos.author):
71 mapped_email = cfg.get('address mappings', repos.author)
72 elif cfg.has_option('address mappings', '*'):
73 mapped_email = cfg.get('address mappings', '*')
74 else:
75 mapped_email = repos.author
76 if '@' not in mapped_email:
77 mapped_email += domain
78 notify_email(tracker_address, mapped_email, smtp_host, repos)
79 else:
80 logging.error('invalid mode %s in config file'%mode)
81
82
83 def notify_email(tracker_address, from_address, smtp_host, repos):
84 subject = '[%s%s] SVN commit message'%(repos.klass, repos.itemid)
85 if repos.status:
86 subject += ' [status=%s]'%repos.status
87 date = time.strftime('%Y-%m-%d %H:%M:%S', repos.date)
88 message = '''From: %s
89 To: %s
90 Subject: %s
91
92 revision=%s
93 host=%s
94 repos=%s
95 date=%s
96 summary=%s
97
98 %s'''%(from_address, tracker_address, subject, repos.rev, repos.host,
99 repos.repos_dir, date, repos.summary, repos.message)
100
101 logger.debug('MESSAGE TO SEND\n%s'%message)
102
103 smtp = smtplib.SMTP(smtp_host)
104 try:
105 smtp.sendmail(from_address, [tracker_address], message)
106 except:
107 logging.exception('mail to %r from %r via %r'%(tracker_address,
108 from_address, smtp_host))
109
110 def notify_local(tracker_home, repos):
111 # get a handle on the tracker db
112 tracker = roundup.instance.open(tracker_home)
113 db = tracker.open('admin')
114 try:
115 notify_local_inner(db, tracker_home, repos)
116 except:
117 db.rollback()
118 db.close()
119 raise
120
121 def notify_local_inner(db, tracker_home, repos):
122 # sanity check
123 try:
124 db.getclass(repos.klass)
125 except KeyError:
126 logger.error('no such tracker class %s'%repos.klass)
127 raise Failed
128 if not db.getclass(repos.klass).hasnode(repos.itemid):
129 logger.error('no such %s item %s'%(repos.klass, repos.itemid))
130 raise Failed
131 if repos.status:
132 try:
133 status_id = db.status.lookup(repos.status)
134 except KeyError:
135 logger.error('no such status %s'%repos.status)
136 raise Failed
137
138 print repos.host, repos.repos_dir
139 # get the svn repo information from the tracker
140 try:
141 svn_repo_id = db.svn_repo.stringFind(host=repos.host,
142 path=repos.repos_dir)[0]
143 except IndexError:
144 logger.error('no repository %s in tracker'%repos.repos_dir)
145 raise Failed
146
147 # log in as the appropriate user
148 try:
149 matches = db.user.stringFind(svn_name=repos.author)
150 except KeyError:
151 # the user class has no property "svn_name"
152 matches = []
153 if matches:
154 userid = matches[0]
155 else:
156 try:
157 userid = db.user.lookup(repos.author)
158 except KeyError:
159 raise Failed, 'no Roundup user matching %s'%repos.author
160 username = db.user.get(userid, 'username')
161 db.close()
162
163 # tell Roundup
164 tracker = roundup.instance.open(tracker_home)
165 db = tracker.open(username)
166
167 # check perms
168 if not db.security.hasPermission('Create', userid, 'svn_rev'):
169 raise Unauthorised, "Can't create items of class 'svn_rev'"
170 if not db.security.hasPermission('Create', userid, 'msg'):
171 raise Unauthorised, "Can't create items of class 'msg'"
172 if not db.security.hasPermission('Edit', userid, repos.klass,
173 'messages', repos.itemid):
174 raise Unauthorised, "Can't edit items of class '%s'"%repos.klass
175 if repos.status and not db.security.hasPermission('Edit', userid,
176 repos.klass, 'status', repos.itemid):
177 raise Unauthorised, "Can't edit items of class '%s'"%repos.klass
178
179 # create the revision
180 svn_rev_id = db.svn_rev.create(repository=svn_repo_id, revision=repos.rev)
181
182 # add the message to the spool
183 date = roundup.date.Date(repos.date)
184 msgid = db.msg.create(content=repos.message, summary=repos.summary,
185 author=userid, date=date, revision=svn_rev_id)
186 klass = db.getclass(repos.klass)
187 messages = klass.get(repos.itemid, 'messages')
188 messages.append(msgid)
189 klass.set(repos.itemid, messages=messages)
190
191 # and set the status
192 if repos.status:
193 klass.set(repos.itemid, status=status_id)
194
195 db.commit()
196 logger.debug('Roundup modification complete')
197 db.close()
198
199
200 def _select_adds(change):
201 return change.added
202 def _select_deletes(change):
203 return change.path is None
204 def _select_modifies(change):
205 return not change.added and change.path is not None
206
207
208 def generate_list(output, header, changelist, selection):
209 items = [ ]
210 for path, change in changelist:
211 if selection(change):
212 items.append((path, change))
213 if not items:
214 return
215
216 output.write('%s:\n' % header)
217 for fname, change in items:
218 if change.item_kind == svn.core.svn_node_dir:
219 is_dir = '/'
220 else:
221 is_dir = ''
222 if change.prop_changes:
223 if change.text_changed:
224 props = ' (contents, props changed)'
225 else:
226 props = ' (props changed)'
227 else:
228 props = ''
229 output.write(' %s%s%s\n' % (fname, is_dir, props))
230 if change.added and change.base_path:
231 if is_dir:
232 text = ''
233 elif change.text_changed:
234 text = ', changed'
235 else:
236 text = ' unchanged'
237 output.write(' - copied%s from r%d, %s%s\n'
238 % (text, change.base_rev, change.base_path[1:], is_dir))
239
240 class Repository:
241 '''Hold roots and other information about the repository. From mailer.py
242 '''
243 def __init__(self, repos_dir, rev, pool):
244 self.repos_dir = repos_dir
245 self.rev = rev
246 self.pool = pool
247
248 self.repos_ptr = svn.repos.svn_repos_open(repos_dir, pool)
249 self.fs_ptr = svn.repos.svn_repos_fs(self.repos_ptr)
250
251 self.roots = {}
252
253 self.root_this = self.roots[rev] = svn.fs.revision_root(self.fs_ptr,
254 rev, self.pool)
255
256 self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR)
257
258 def get_rev_prop(self, propname):
259 return svn.fs.revision_prop(self.fs_ptr, self.rev, propname, self.pool)
260
261 def extract_info(self):
262 issue_re = re.compile('^\s*(%s)\s*(\d+)(\s+(\S+))?\s*$'%self.klass,
263 re.I)
264
265 # parse for Roundup item information
266 log = self.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or ''
267 for line in log.splitlines():
268 m = issue_re.match(line)
269 if m:
270 break
271 else:
272 # nothing to do
273 return
274
275 # parse out the issue information
276 klass = m.group(1)
277 self.itemid = m.group(2)
278
279 issue = klass + self.itemid
280 self.status = m.group(4)
281
282 logger.debug('Roundup info item=%r, status=%r'%(issue, self.status))
283
284 # get all the changes and sort by path
285 editor = svn.repos.RevisionChangeCollector(self.fs_ptr, self.rev,
286 self.pool)
287 e_ptr, e_baton = svn.delta.make_editor(editor, self.pool)
288 svn.repos.svn_repos_replay(self.root_this, e_ptr, e_baton, self.pool)
289
290 changelist = editor.changes.items()
291 changelist.sort()
292
293 # figure out the changed directories
294 dirs = { }
295 for path, change in changelist:
296 if change.item_kind == svn.core.svn_node_dir:
297 dirs[path] = None
298 else:
299 idx = path.rfind('/')
300 if idx == -1:
301 dirs[''] = None
302 else:
303 dirs[path[:idx]] = None
304
305 dirlist = dirs.keys()
306
307 # figure out the common portion of all the dirs. note that there is
308 # no "common" if only a single dir was changed, or the root was changed.
309 if len(dirs) == 1 or dirs.has_key(''):
310 commondir = ''
311 else:
312 common = dirlist.pop().split('/')
313 for d in dirlist:
314 parts = d.split('/')
315 for i in range(len(common)):
316 if i == len(parts) or common[i] != parts[i]:
317 del common[i:]
318 break
319 commondir = '/'.join(common)
320 if commondir:
321 # strip the common portion from each directory
322 l = len(commondir) + 1
323 dirlist = [ ]
324 for d in dirs.keys():
325 if d == commondir:
326 dirlist.append('.')
327 else:
328 dirlist.append(d[l:])
329 else:
330 # nothing in common, so reset the list of directories
331 dirlist = dirs.keys()
332
333 # compose the basic subject line. later, we can prefix it.
334 dirlist.sort()
335 dirlist = ' '.join(dirlist)
336
337 if commondir:
338 self.summary = 'r%d - in %s: %s' % (self.rev, commondir, dirlist)
339 else:
340 self.summary = 'r%d - %s' % (self.rev, dirlist)
341
342 # Generate email for the various groups and option-params.
343 output = cStringIO.StringIO()
344
345 # print summary sections
346 generate_list(output, 'Added', changelist, _select_adds)
347 generate_list(output, 'Removed', changelist, _select_deletes)
348 generate_list(output, 'Modified', changelist, _select_modifies)
349
350 output.write('Log:\n%s\n'%log)
351
352 self.message = output.getvalue()
353
354 svndate = self.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE)
355 self.date = time.localtime(svn.core.secs_from_timestr(svndate,
356 self.pool))
357
358 return True
359
360 if __name__ == '__main__':
361 try:
362 svn.core.run_app(main)
363 except Failed, message:
364 logger.error(message)
365 sys.exit(1)
366 except:
367 logger.exception('top level')
368 sys.exit(1)
369
370 #
371 # 2005-05-16 - 1.2
372 #
373 # - Status wasn't being set by ID in local mode
374 # - Wasn't catching errors in local changes, hence not cleaning up db
375 # correctly
376 # - svnauditor.py wasn't handling the fifth argument from notify-roundup.py
377 # - viewcvs_url formatting wasn't quite right
378 #
379 # 2005-05-04 - 1.1
380 # - Several fixes from Ron Alford
381 # - Don't change issue titles to "SVN commit message..."
382 #
383 # 2005-04-26 - 1.0
384 # - Initial version released
385 #

Roundup Issue Tracker: http://roundup-tracker.org/