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