Package svnmailer :: Module main
[hide private]

Source Code for Module svnmailer.main

  1  # -*- coding: utf-8 -*- 
  2  # pylint: disable-msg = W0704 
  3  # 
  4  # Copyright 2004-2006 André Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  Main Logic of the svnmailer 
 19  =========================== 
 20   
 21  This module is the central core of the svnmailer. It dispatches all work to be 
 22  done. It contains just one class (L{Main}), which reads the config file while 
 23  it is initialized. When the C{Main.run()} method is called, it selects the 
 24  groups to be notified, the notifiers to be run and runs all notifiers for 
 25  each group. 
 26   
 27  The Main class may raise several exceptions (which all inherit from L{Error}): 
 28      - L{ConfigError} occurs, if the configuration contains errors (like type 
 29        or value errors, unicode errors etc). The L{ConfigError} exception is 
 30        initialized with a string describing what kind of error occured. 
 31   
 32      - L{NotifierError} occurs, if one or more of the notifiers throw an 
 33        exception. The L{Main} class catches these exceptions (except 
 34        C{KeyboardInterrupt} and C{SystemExit}) and will initialize the 
 35        L{NotifierError} with the list of traceback strings, one for each 
 36        exception occured. (See the format_exception docs at 
 37        U{http://docs.python.org/lib/module-traceback.html}). 
 38   
 39      - L{svnmailer.subversion.RepositoryError} occurs, if something failed 
 40        while accessing the subversion repository. It contains some attributes 
 41        for identifying the error: C{svn_err_code}, C{svn_err_name} and 
 42        C{svn_err_str} 
 43  """ 
 44  __author__    = "André Malo" 
 45  __docformat__ = "epytext en" 
 46  __all__       = ['Main', 'Error', 'ConfigError', 'NotifierError'] 
 47   
 48  # Exceptions 
49 -class Error(Exception):
50 """ Base exception for this module """ 51 pass
52
53 -class ConfigError(Error):
54 """ Configuration error occurred """ 55 pass
56
57 -class NotifierError(Error):
58 """ An Notifier error occured """ 59 pass
60 61
62 -class Main(object):
63 """ main svnmailer logic 64 65 @ivar _settings: The settings to use 66 @type _settings: C{svnmailer.settings.Settings} 67 """ 68
69 - def __init__(self, options):
70 """ Initialization 71 72 @param options: Command line options 73 @type options: C{optparse.OptionParser} 74 75 @exception ConfigError: Configuration error 76 """ 77 self._settings = self._getSettings(options)
78 79
80 - def run(self):
81 """ Dispatches the work to be done 82 83 @exception svnmailer.subversion.RepositoryError: Error while 84 accessing the subversion repository 85 @exception NotifierError: One or more notifiers went crazy 86 """ 87 from svnmailer import subversion 88 89 try: 90 try: 91 self._openRepository() 92 93 notifier_errors = [] 94 throwables = (KeyboardInterrupt, SystemExit, subversion.Error) 95 selector = self._getNotifierSelector() 96 97 for groupset in self._getGroupSets(): 98 notifiers = selector.selectNotifiers(groupset) 99 for notifier in notifiers: 100 try: 101 notifier.run() 102 except throwables: 103 raise 104 except: 105 import sys, traceback 106 info = sys.exc_info() 107 backtrace = traceback.format_exception( 108 info[0], info[1], info[2] 109 ) 110 del info 111 backtrace[0] = "Notifier: %s.%s\nRevision: %s\n" \ 112 "Groups: %r\n%s" % ( 113 notifier.__module__, 114 notifier.__class__.__name__, 115 self._settings.runtime.revision, 116 [group._name for group in groupset.groups], 117 backtrace[0], 118 ) 119 notifier_errors.append(''.join(backtrace)) 120 if notifier_errors: 121 raise NotifierError(*notifier_errors) 122 123 except subversion.Error, exc: 124 import sys 125 raise subversion.RepositoryError, exc, sys.exc_info()[2] 126 127 finally: 128 # IMPORTANT! otherwise the locks are kept and 129 # we run into bdb "out of memory" errors some time 130 self._closeRepository()
131 132
133 - def _getNotifierSelector(self):
134 """ Returns the notifier selector 135 136 @return: The selector 137 @rtype: C{svnmailer.notifier.selector.Selector} 138 """ 139 from svnmailer.notifier import selector 140 return selector.Selector(self._settings)
141 142
143 - def _getChanges(self):
144 """ Returns the list of changes for the requested revision 145 146 @return: The list of changes (C{[Descriptor, ...]}) 147 @rtype: C{list} 148 149 @exception svnmailer.subversion.Error: Error while accessing the 150 subversion repository 151 """ 152 from svnmailer import settings, subversion 153 154 modes = settings.modes 155 runtime = self._settings.runtime 156 157 if runtime.mode in (modes.commit, modes.propchange): 158 changes = runtime._repos.getChangesList(runtime.revision) 159 elif runtime.mode in (modes.lock, modes.unlock): 160 is_locked = bool(runtime.mode == modes.lock) 161 changes = [ 162 subversion.LockedPathDescriptor(runtime._repos, path, is_locked) 163 for path in runtime.stdin.splitlines() if path 164 ] 165 changes.sort() 166 else: 167 raise AssertionError("Unknown runtime.mode %r" % (runtime.mode,)) 168 169 return changes
170 171
172 - def _getGroupSets(self):
173 """ Returns the list of groupsets (grouped groups...) to notify 174 175 @return: The list (maybe empty). (C{[GroupSet, ...]}) 176 @rtype: C{list} 177 """ 178 # collect changes and group by group [ ;-) ] 179 group_changes = {} 180 group_cache = {} 181 changes = self._getChanges() 182 for change in changes: 183 for group in self._getGroupsByChange(change): 184 groupid = id(group) 185 try: 186 group_changes[groupid].append(change) 187 except KeyError: 188 group_cache[groupid] = group 189 group_changes[groupid] = [change] 190 191 # Build the groupset 192 # TODO: make group compression configurable? 193 group_sets = [] 194 for groupid, changelist in group_changes.items(): 195 group = group_cache[groupid] 196 for stored in group_sets: 197 # We don't need to compare the group with *all* 198 # groups of this set. If the group is considered 199 # equal to the first stored group, all other stored 200 # groups are considered equal as well. (Otherwise 201 # they wouldn't been there ...) 202 if stored.changes == changelist and \ 203 group._compare(stored.groups[0]): 204 stored.groups.append(group) 205 group = None 206 break 207 208 if group is not None: 209 group_sets.append(GroupSet([group], changelist, changes)) 210 211 return group_sets
212 213
214 - def _getGroupsByChange(self, change):
215 """ Returns the matching groups for a particular change 216 217 @param change: The change to select 218 @type change: C{svnmailer.subversion.VersionedPathDescriptor} 219 220 @return: The group list 221 @rtype: C{list} 222 """ 223 selected_groups = [] 224 ignored_groups = [] 225 226 # the repos path is always *without* slash (see 227 # subversion.Respository.__init__) 228 repos_path = change.repos.path.decode("utf-8", "strict") 229 230 # we guarantee, that directories end with a slash 231 path = "%s%s" % (change.path, ["", "/"][change.isDirectory()]) 232 path = path.decode("utf-8", "strict") 233 234 for group in self._settings.groups: 235 subst = self._getDefaultSubst(group, repos_path, path) 236 237 # if for_repos is set and does not match -> ignore 238 if group.for_repos: 239 match = group.for_repos.match(repos_path) 240 if match: 241 subst.update(match.groupdict()) 242 else: 243 continue 244 245 # if exclude_paths is set and does match -> ignore 246 if group.exclude_paths and group.exclude_paths.match(path): 247 continue 248 249 # if for_paths is set and does not match -> ignore 250 if group.for_paths: 251 match = group.for_paths.match(path) 252 if match: 253 subst.update(match.groupdict()) 254 else: 255 continue 256 257 # store the substdict for later use 258 for name, value in subst.items(): 259 group._sub_(name, value) 260 261 (selected_groups, ignored_groups)[ 262 bool(group.ignore_if_other_matches) 263 ].append(group) 264 265 # BRAINER: theoretically there could be more than one group 266 # in the ignore list, which would have to be ignored at all then. 267 # (ignore_if_OTHER_MATCHES, think about it) 268 # Instead we select them ALL, so the output isn't lost 269 return selected_groups and selected_groups or ignored_groups
270 271
272 - def _getDefaultSubst(self, group, repos_path, path):
273 """ Returns the default substitution dict 274 275 @param group: The group to consider 276 @type group: C{svnmailer.settings.GroupSettingsContainer} 277 278 @param repos_path: The repository path 279 @type repos_path: C{unicode} 280 281 @param path: The change path 282 @type path: C{unicode} 283 284 @return: The initialized dictionary 285 @rtype: C{dict} 286 287 @exception svnmailer.subversion.Error: An error occured while 288 accessing the subversion repository 289 """ 290 from svnmailer.settings import modes 291 292 runtime = self._settings.runtime 293 author = runtime.author 294 if not author and runtime.mode in (modes.commit, modes.propchange): 295 author = runtime._repos.getRevisionAuthor(runtime.revision) 296 297 subst = { 298 'author' : author or 'no_author', 299 'group' : group._name, 300 'property': runtime.propname, 301 'revision': runtime.revision and u"%d" % runtime.revision, 302 } 303 304 if group.extract_x509_author: 305 from svnmailer import util 306 307 x509 = util.extractX509User(author) 308 if x509: 309 from email import Header 310 311 realname, mail = x509 312 subst.update({ 313 'x509_address': realname and "%s <%s>" % ( 314 Header.Header(realname).encode(), mail) or mail, 315 'x509_CN': realname, 316 'x509_emailAddress': mail, 317 }) 318 319 if group._def_for_repos: 320 match = group._def_for_repos.match(repos_path) 321 if match: 322 subst.update(match.groupdict()) 323 324 if group._def_for_paths: 325 match = group._def_for_paths.match(path) 326 if match: 327 subst.update(match.groupdict()) 328 329 return subst
330 331
332 - def _getSettings(self, options):
333 """ Returns the settings object 334 335 @param options: Command line options 336 @type options: C{svnmailer.cli.SvnmailerOptionParser} 337 338 @return: The settings object 339 @rtype: C{svnmailer.config.ConfigFileSettings} 340 341 @exception ConfigError: configuration error 342 """ 343 from svnmailer import config 344 345 try: 346 return config.ConfigFileSettings(options) 347 except config.Error, exc: 348 import sys 349 raise ConfigError, str(exc), sys.exc_info()[2]
350 351
352 - def _openRepository(self):
353 """ Opens the repository 354 355 @exception svnmailer.subversion.Error: Error while accessing the 356 subversion repository 357 """ 358 from svnmailer import subversion, util 359 360 config = self._settings.runtime 361 repos_path = util.filename.fromLocale( 362 config.repository, config.path_encoding 363 ) 364 if isinstance(repos_path, str): 365 # !!! HACK ALERT !!! 366 # 367 # --path-encoding=none 368 # subversion needs unicode as path and translates it 369 # back to the locale, we try our best by translating 370 # literally to unicode... 371 repos_path = repos_path.decode("iso-8859-1", "strict") 372 373 config._repos = subversion.Repository(repos_path)
374 375
376 - def _closeRepository(self):
377 """ Closes the repository """ 378 try: 379 self._settings.runtime._repos.close() 380 except AttributeError: 381 # That's ok 382 pass
383 384
385 -class GroupSet(object):
386 """ Container object for a single groupset 387 388 @ivar groups: The groups to process 389 @type groups: C{list} 390 391 @ivar changes: The changes that belong to the group 392 @type changes: C{list} 393 394 @ivar xchanges: The changes that don't belong to the 395 group (only filled if show_nonmatching_paths = yes) 396 @type xchanges: C{list} 397 """ 398
399 - def __init__(self, groups, changes, allchanges):
400 """ Initialization 401 402 @param groups: The groups to process 403 @type groups: C{list} 404 405 @param changes: The changes that belong to the group 406 @type changes: C{list} 407 408 @param allchanges: All changes 409 @type allchanges: C{list} 410 """ 411 from svnmailer.settings import xpath 412 413 self.groups = groups 414 self.changes = changes 415 416 nongroups = groups[0].show_nonmatching_paths 417 if nongroups == xpath.ignore: 418 self.xchanges = None 419 elif nongroups == xpath.yes: 420 self.xchanges = [ 421 change for change in allchanges 422 if change not in changes 423 ] 424 else: 425 # no is default 426 self.xchanges = []
427