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