Mercurial > p > roundup > code
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 # |
