Package svnmailer :: Package notifier :: Module cia_xmlrpc

[hide private]

# Source Code for Module svnmailer.notifier.cia_xmlrpc

      1  # -*- coding: utf-8 -*-
      2  #
      3  # Copyright 2005-2006 André Malo or his licensors, as applicable
      4  #
      5  # Licensed under the Apache License, Version 2.0 (the "License");
      6  # you may not use this file except in compliance with the License.
      7  # You may obtain a copy of the License at
      8  #
      9  #     http://www.apache.org/licenses/LICENSE-2.0
     10  #
     11  # Unless required by applicable law or agreed to in writing, software
     12  # distributed under the License is distributed on an "AS IS" BASIS,
     13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  # See the License for the specific language governing permissions and
     15  # limitations under the License.
     16  """
     17  CIA XML-RPC Notifier
     18  ====================
     19
     20  This notifier delivers a notification message in XML format
     21  to a U{CIA server <http://cia.navi.cx/>}. The data delivered
     22  contains in particular:
     23      - The timestamp of the revision
     24      - Information about the generator (svnmailer)
     25      - Information about the project (C{cia_project_name},
     26        C{cia_project_module}, C{cia_project_branch},
     27        C{cia_project_submodule}).
     28      - Revision metadata:
     29          - author
     30          - revision number
     31          - log entry
     32          - summary URL (if C{browser_base_url} if configured)
     33          - number of modified lines
     34      - Information about the files modified (C{cia_project_path} is
     35        stripped). If C{browser_base_url} is supplied, it is used to
     36        generate an URI for each file.
     37
     38  The notifier runs only in the post-commit hook. For activation you need
     39  to supply the C{cia_rpc_server} option in C{[general]} and at least a
     40  C{cia_project_name} in the group that should be tracked by CIA.
     41  """
     42  __author__    = "André Malo"
     43  __docformat__ = "epytext en"
     44  __all__       = ['getNotifier']
     45
     46  # global imports
     47  from svnmailer.notifier import _base
     48
     49
     50 -def getNotifier(config, groupset):
     51      """ Returns an initialized notifier or nothing
     52
     53          @param config: The svnmailer config
     54          @type config: C{svnmailer.settings.Settings}
     55
     56          @param groupset: The groupset to process
     57          @type groupset: C{list}
     58
     59          @return: The list of notifiers (containing 0 or 1 member)
     60          @rtype: C{list}
     61      """
     62      if [group for group in groupset.groups
     63              if group.cia_rpc_server and group.cia_project_name]:
     64          return [CIAXMLRPCNotifier(config, groupset)]
     65
     66      return []
     67
     68
     69 -class CIAXMLRPCNotifier(_base.BaseNotifier):
     70      """ The CIA XML-RPC Notifier class
     71
     72          @ivar config: The current group configuration
     73          @type config: C{svnmailer.settings.GroupSettingsContainer}
     74
     75          @ivar changeset: The changeset to process
     76          @type changeset: C{list}
     77
     78          @ivar differ: The differ object
     79          @type differ: C{svnmailer.differ.*}
     80      """
     81      __implements__ = [_base.BaseNotifier]
     82
     83
     84 -    def __init__(self, config, groupset):
     85          """ Initialization """
     86          super(CIAXMLRPCNotifier, self).__init__(config, groupset)
     87          self.differ = self.getDiffer() # internal differ
     88          self.changeset = None
     89          self.config = None
     90
     91
     92 -    def run(self):
     93          """ Submits notification via XMLRPC to a CIA server """
     94          import sys
     95          from svnmailer import settings
     96
     97          # only work on normal commits
     98          if not self._settings.runtime.mode == settings.modes.commit:
     99              return
    100
    101          groups, changeset = (self._groupset.groups, self._groupset.changes)
    102          self.changeset = changeset[:]
    103          xset = self._groupset.xchanges
    104          if xset:
    105              self.changeset.extend(xset)
    106          for group in [group for group in groups
    107                  if group.cia_rpc_server and group.cia_project_name]:
    108              self.config = group
    109              doc = self.composeCIAXMLMessage()
    110
    111              if self._settings.runtime.debug:
    112                  sys.stdout.write(doc.toprettyxml(' ' * 4, encoding = 'utf-8'))
    113              else:
    114                  self.deliverRPCMessage(group.cia_rpc_server, doc)
    115
    116
    117 -    def deliverRPCMessage(self, rpc_server, doc):
    118          """ Delivers the supplied message via XML-RPC
    119
    120              @param rpc_server: The server to deliver to
    121              @type rpc_server: C{unicode}
    122
    123              @param doc: The message document
    124              @type doc: DOM object
    125          """
    126          import xmlrpclib
    127
    128          server = xmlrpclib.ServerProxy(rpc_server)
    129          server.hub.deliver(doc.toxml(encoding = 'utf-8'))
    130
    131
    132 -    def composeCIAXMLMessage(self):
    133          """ Composes the XML message to send
    134
    135              (a commit message according to
    136              U{http://navi.cx/svn/misc/trunk/cia/xml/schema.xsd})
    137
    138              @return: The message
    139              @rtype: DOM object
    140          """
    141          from xml import dom
    142          message = dom.getDOMImplementation().createDocument(
    143              None, 'message', None
    144          )
    145
    146          self._addTimeStamp(message)
    147          self._addGenerator(message)
    148          self._addSource(message)
    149          self._addBody(message)
    150
    151          return message
    152
    153
    154 -    def _addTimeStamp(self, doc):
    155          """ Adds revision timestamp to the message
    156
    157              @param doc: The message document
    158              @type doc: DOM object
    159          """
    160          stamp = self.getTime()
    161
    162          if self._settings.runtime.debug:
    163              import time
    164              comment = doc.createComment(u" %s " % time.ctime(stamp))
    165              doc.documentElement.appendChild(comment)
    166
    167          timestamp = self._getTextElement(doc, u'timestamp', unicode(stamp))
    168          doc.documentElement.appendChild(timestamp)
    169
    170
    171 -    def _addGenerator(self, doc):
    172          """ Adds the generator info to the message
    173
    174              @param doc: The message document
    175              @type doc: DOM object
    176          """
    177          from svnmailer import version
    178
    179          generator = doc.createElement('generator')
    180          self._addTextElements(generator,
    181              (u'name', u'svnmailer (cia_xmlrpc notifier)'),
    182              (u'version', version.decode('utf-8')),
    183              (u'url', u'http://opensource.perlig.de/svnmailer/'),
    184          )
    185
    186          doc.documentElement.appendChild(generator)
    187
    188
    189 -    def _addSource(self, doc):
    190          """ Adds the source info to the message
    191
    192              @param doc: The message document
    193              @type doc: DOM object
    194          """
    195          source = doc.createElement('source')
    196
    197          self._addTextElements(source,
    198              (u'project',   self.config.cia_project_name),
    199              (u'module',    self.config.cia_project_module),
    200              (u'branch',    self.config.cia_project_branch),
    201              (u'submodule', self.config.cia_project_submodule),
    202          )
    203
    204          doc.documentElement.appendChild(source)
    205
    206
    207 -    def _addBody(self, doc):
    208          """ Adds the actual commit info to the message
    209
    210              @param doc: The message document
    211              @type doc: DOM object
    212          """
    213          body = doc.createElement(u'body')
    214          commit = doc.createElement(u'commit')
    215          body.appendChild(commit)
    216
    217          self._addTextElements(commit,
    218              (u'author', self._getAuthorName()),
    219              (u'revision', unicode(self._settings.runtime.revision)),
    220              (u'log', self.getLog().decode('utf-8', 'replace')),
    221              (u'url', self.getUrl(self.config)),
    222              (u'diffLines', self._getDiffLineCount())
    223          )
    224
    225          files = doc.createElement(u'files')
    226          commit.appendChild(files)
    227          self._addTextElements(files, *[(
    228              u'file',
    229              "%s%s" % (
    230                  self._stripPath(change.path),
    231                  ["", "/"][change.isDirectory()]
    232              ),
    233              {
    234                  u'uri'   : self._getFileUri(change),
    235                  u'action': self._getFileAction(change),
    236                  u'type'  : self._getFileType(change),
    237              },
    238          ) for change in self.changeset])
    239
    240          doc.documentElement.appendChild(body)
    241
    242
    243 -    def _getDiffLineCount(self):
    244          """ Returns the number of changed lines
    245
    246              It counts the number of minus lines and the number of plus lines
    247              and returns the greater value.
    248
    249              @return: The diff line count or C{None}
    250              @rtype: C{unicode}
    251          """
    252          count = 0
    253          for change in self.changeset:
    254              # content
    255              if not (change.isDirectory() or change.isBinary()):
    256                  file1, file2 = self.dumpContent(change)[:2]
    257                  diff_out = self.differ.getFileDiff(
    258                      file1.name, file2.name, '1', '2',
    259                  )
    260                  count += self._getMinusPlusCount(diff_out)
    261                  del file1, file2, diff_out
    262
    263              # properties
    264              if change.hasPropertyChanges():
    265                  propdict = change.getModifiedProperties()
    266                  for name, values in propdict.items():
    267                      if not self.isBinaryProperty(values):
    268                          diff_out = self.differ.getStringDiff(
    269                              values[0], values[1], name, name
    270                          )
    271                          count += self._getMinusPlusCount(diff_out)
    272                          del diff_out
    273
    274          return count and unicode(count) or None
    275
    276
    277 -    def _getMinusPlusCount(self, diff_out):
    278          """ Returns the number of changed lines for a diff
    279
    280              @param diff_out: The diff output (one diff line per item)
    281              @type diff_out: iterable
    282
    283              @return: Number of changed lines
    284              @rtype: C{int}
    285          """
    286          minus = 0
    287          plus = 0
    288          for line in diff_out:
    289              if line.startswith('-'):
    290                  if not(line.startswith('---') and minus == 0):
    291                      minus += 1
    292              elif line.startswith('+'):
    293                  if not(line.startswith('+++') and plus == 0):
    294                      plus += 1
    295
    296          return max(minus, plus)
    297
    298
    299 -    def _getFileUri(self, change):
    300          """ Returns an URL associated with the changed file
    301
    302              @TODO: add the ability to use raw subversion urls?
    303
    304              @param change: The change to process
    305              @type change: C{svnmailer.subversion.VersionedPathDescriptor}
    306
    307              @return: The URI or C{None}
    308              @rtype: C{unicode}
    309          """
    310          return self.getContentDiffUrl(self.config, change)
    311
    312
    313 -    def _getFileAction(self, change):
    314          """ Returns the action applied to the changed file
    315
    316              @TODO: file renaming?
    317
    318              @param change: The change to process
    319              @type change: C{svnmailer.subversion.VersionedPathDescriptor}
    320
    321              @return: The action
    322              @rtype: C{unicode}
    323          """
    324          if change.wasDeleted():
    325              return u"remove"
    326          elif change.wasCopied() or change.wasAdded():
    327              return u"add"
    328
    329          return u"modify"
    330
    331
    332 -    def _getFileType(self, change):
    333          """ Returns the type of the modified file
    334
    335              @note: currently it only marks directories as x-directory/normal
    336              @note: This is currently not implemented yet on server side
    337              @note: What do we do with different types (changed in the
    338                  revision)?
    339
    340              @param change: The change to process
    341              @type change: C{svnmailer.subversion.VersionedPathDescriptor}
    342
    343              @return: The type or C{None}
    344              @rtype: C{unicode}
    345          """
    346          if change.isDirectory():
    347              return u"x-directory/normal"
    348
    349          return None
    350
    351
    352 -    def _stripPath(self, path):
    353          """ Returns the stripped path of a change
    354
    355              @param path: The path to strip
    356              @type path: C{str}
    357
    358              @return: The stripped path
    359              @rtype: C{unicode}
    360          """
    361          path = path.decode('utf-8', 'replace')
    362          tostrip = self.config.cia_project_path
    363          if tostrip:
    364              # normalize
    365              while tostrip.startswith('/'):
    366                  tostrip = tostrip[1:]
    367              while tostrip.endswith('//'):
    368                  tostrip = tostrip[:-1]
    369              if not tostrip.endswith('/'):
    370                  tostrip = "%s/" % tostrip
    371              # strip it
    372              if path.startswith(tostrip):
    373                  path = path.replace(tostrip, "", 1)
    374
    375          return path
    376
    377
    378 -    def _getAuthorName(self):
    379          """ Returns the name of the author
    380
    381              @return: The name
    382              @rtype: C{unicode}
    383          """
    384          if self.config.extract_x509_author:
    385              from svnmailer import util
    386              author = util.extractX509User(self.getAuthor())
    387              if author and author[0]:
    388                  return author[0]
    389
    390          return (self.getAuthor() or "(no author)").decode('utf-8', 'replace')
    391
    392
    393 -    def _addTextElements(self, parent, *elems):
    394          """ Add multiple text elements
    395
    396              @param parent: The parent element
    397              @type parent: Element Node
    398
    399              @param elems: The elements to add (name, value)
    400              @type elems: C{list} of C{tuple}
    401          """
    402          for elem in [elem for elem in elems if elem[1]]:
    403              name, value = elem[:2]
    404              attr = elem[2:] and elem[2] or {}
    405              elem = self._getTextElement(parent.ownerDocument,
    406                  name, value, attr
    407              )
    408              parent.appendChild(elem)
    409
    410
    411 -    def _getTextElement(self, doc, name, value, attr = None):
    412          """ Returns a new element containing text to the message
    413
    414              @param doc: The message document
    415              @type doc: DOM object
    416
    417              @param name: The name of the element
    418              @type name: C{unicode}
    419
    420              @param value: The content of the element
    421              @type value: C{unicode}
    422
    423              @param attr: Attributes
    424              @type attr: C{dict}
    425          """
    426          from svnmailer import util
    427
    428          elem = doc.createElement(name)
    429          cont = doc.createTextNode(util.filterForXml(value))
    430          elem.appendChild(cont)
    431
    432          if attr:
    433              for key, value in [item for item in attr.items() if item[1]]:
    434                  elem.setAttribute(key, util.filterForXml(value))
    435
    436          return elem
    437