1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
47 from svnmailer.notifier import _base
48
49
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
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
90
91
115
116
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
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
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
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
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
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
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
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
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
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
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
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
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
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
372 if path.startswith(tostrip):
373 path = path.replace(tostrip, "", 1)
374
375 return path
376
377
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