/[adm]/puppet/modules/subversion/templates/ciabot_svn.py
ViewVC logotype

Contents of /puppet/modules/subversion/templates/ciabot_svn.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 704 - (show annotations) (download) (as text)
Fri Jan 7 17:43:33 2011 UTC (13 years, 3 months ago) by boklm
File MIME type: text/x-python
File size: 14990 byte(s)
add cia bot svn hook
1 #!/usr/bin/env python
2 #
3 # This is a CIA client script for Subversion repositories, written in python.
4 # It generates commit messages using CIA's XML format, and can deliver them
5 # using either XML-RPC or email. See below for usage and cuztomization
6 # information.
7 #
8 # --------------------------------------------------------------------------
9 #
10 # Copyright (c) 2004-2007, Micah Dowty
11 # All rights reserved.
12 #
13 # Redistribution and use in source and binary forms, with or without
14 # modification, are permitted provided that the following conditions are met:
15 #
16 # * Redistributions of source code must retain the above copyright notice,
17 # this list of conditions and the following disclaimer.
18 # * Redistributions in binary form must reproduce the above copyright
19 # notice, this list of conditions and the following disclaimer in the
20 # documentation and/or other materials provided with the distribution.
21 # * The name of the author may not be used to endorse or promote products
22 # derived from this software without specific prior written permission.
23 #
24 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
28 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 # POSSIBILITY OF SUCH DAMAGE.
35 #
36 # --------------------------------------------------------------------------
37 #
38 # This script is cleaner and much more featureful than the shell
39 # script version, but won't work on systems without Python.
40 #
41 # To use the CIA bot in your Subversion repository...
42 #
43 # 1. Customize the parameters below
44 #
45 # 2. This script should be called from your repository's post-commit
46 # hook with the repository and revision as arguments. For example,
47 # you could copy this script into your repository's "hooks" directory
48 # and add something like the following to the "post-commit" script,
49 # also in the repository's "hooks" directory:
50 #
51 # REPOS="$1"
52 # REV="$2"
53 # $REPOS/hooks/ciabot_svn.py "$REPOS" "$REV" &
54 #
55 # Or, if you have multiple project hosted, you can add each
56 # project's name to the commandline in that project's post-commit
57 # hook:
58 #
59 # $REPOS/hooks/ciabot_svn.py "$REPOS" "$REV" "ProjectName" &
60 #
61 ############# There are some parameters for this script that you can customize:
62
63 class config:
64 # Replace this with your project's name, or always provide a project
65 # name on the commandline.
66 #
67 # NOTE: This shouldn't be a long description of your project. Ideally
68 # it is a short identifier with no spaces, punctuation, or
69 # unnecessary capitalization. This will be used in URLs related
70 # to your project, as an internal identifier, and in IRC messages.
71 # If you want a longer name shown for your project on the web
72 # interface, please use the "title" metadata key rather than
73 # putting that here.
74 #
75 project = "Mageia"
76
77 # Subversion's normal directory hierarchy is powerful enough that
78 # it doesn't have special methods of specifying modules, tags, or
79 # branches like CVS does. Most projects do use a naming
80 # convention though that works similarly to CVS's modules, tags,
81 # and branches.
82 #
83 # This is a list of regular expressions that are tested against
84 # paths in the order specified. If a regex matches, the 'branch'
85 # and 'module' groups are stored and the matching section of the
86 # path is removed.
87 #
88 # Several common directory structure styles are below as defaults.
89 # Uncomment the ones you're using, or add your own regexes.
90 # Whitespace in the each regex are ignored.
91
92 pathRegexes = [
93 # r"^ trunk/ (?P<module>[^/]+)/ ",
94 # r"^ (branches|tags)/ (?P<branch>[^/]+)/ ",
95 # r"^ (branches|tags)/ (?P<module>[^/]+)/ (?P<branch>[^/]+)/ ",
96 ]
97
98 # If your repository is accessible over the web, put its base URL here
99 # and 'uri' attributes will be given to all <file> elements. This means
100 # that in CIA's online message viewer, each file in the tree will link
101 # directly to the file in your repository.
102 repositoryURI = None
103
104 # If your repository is accessible over the web via a tool like ViewVC
105 # that allows viewing information about a full revision, put a format string
106 # for its URL here. You can specify various substitution keys in the Python
107 # syntax: "%(project)s" is replaced by the project name, and likewise
108 # "%(revision)s" and "%(author)s" are replaced by the revision / author.
109 # The resulting URI is added to the data sent to CIA. After this, in CIA's
110 # online message viewer, the commit will link directly to the corresponding
111 # revision page.
112 revisionURI = None
113 # Example (works for ViewVC as used by SourceForge.net):
114 #revisionURI = "https://svn.sourceforge.net/viewcvs.cgi/%(project)s?view=rev&rev=%(revision)s"
115
116 # This can be the http:// URI of the CIA server to deliver commits over
117 # XML-RPC, or it can be an email address to deliver using SMTP. The
118 # default here should work for most people. If you need to use e-mail
119 # instead, you can replace this with "cia@cia.navi.cx"
120 server = "http://cia.navi.cx"
121
122 # The SMTP server to use, only used if the CIA server above is an
123 # email address.
124 smtpServer = "localhost"
125
126 # The 'from' address to use. If you're delivering commits via email, set
127 # this to the address you would normally send email from on this host.
128 fromAddress = "cia-user@localhost"
129
130 # When nonzero, print the message to stdout instead of delivering it to CIA.
131 debug = 0
132
133
134 ############# Normally the rest of this won't need modification
135
136 import sys, os, re, urllib, getopt
137
138 class File:
139 """A file in a Subversion repository. According to our current
140 configuration, this may have a module, branch, and URI in addition
141 to a path."""
142
143 # Map svn's status letters to our action names
144 actionMap = {
145 'U': 'modify',
146 'A': 'add',
147 'D': 'remove',
148 }
149
150 def __init__(self, fullPath, status=None):
151 self.fullPath = fullPath
152 self.path = fullPath
153 self.action = self.actionMap.get(status)
154
155 def getURI(self, repo):
156 """Get the URI of this file, given the repository's URI. This
157 encodes the full path and joins it to the given URI."""
158 quotedPath = urllib.quote(self.fullPath)
159 if quotedPath[0] == '/':
160 quotedPath = quotedPath[1:]
161 if repo[-1] != '/':
162 repo = repo + '/'
163 return repo + quotedPath
164
165 def makeTag(self, config):
166 """Return an XML tag for this file, using the given config"""
167 attrs = {}
168
169 if config.repositoryURI is not None:
170 attrs['uri'] = self.getURI(config.repositoryURI)
171
172 if self.action:
173 attrs['action'] = self.action
174
175 attrString = ''.join([' %s="%s"' % (key, escapeToXml(value,1))
176 for key, value in attrs.items()])
177 return "<file%s>%s</file>" % (attrString, escapeToXml(self.path))
178
179
180 class SvnClient:
181 """A CIA client for Subversion repositories. Uses svnlook to
182 gather information"""
183 name = 'Python Subversion client for CIA'
184 version = '1.20'
185
186 def __init__(self, repository, revision, config):
187 self.repository = repository
188 self.revision = revision
189 self.config = config
190
191 def deliver(self, message):
192 if config.debug:
193 print message
194 else:
195 server = self.config.server
196 if server.startswith('http:') or server.startswith('https:'):
197 # Deliver over XML-RPC
198 import xmlrpclib
199 xmlrpclib.ServerProxy(server).hub.deliver(message)
200 else:
201 # Deliver over email
202 import smtplib
203 smtp = smtplib.SMTP(self.config.smtpServer)
204 smtp.sendmail(self.config.fromAddress, server,
205 "From: %s\r\nTo: %s\r\n"
206 "Subject: DeliverXML\r\n\r\n%s" %
207 (self.config.fromAddress, server, message))
208
209 def main(self):
210 self.collectData()
211 self.deliver("<message>" +
212 self.makeGeneratorTag() +
213 self.makeSourceTag() +
214 self.makeBodyTag() +
215 "</message>")
216
217 def makeAttrTags(self, *names):
218 """Given zero or more attribute names, generate XML elements for
219 those attributes only if they exist and are non-None.
220 """
221 s = ''
222 for name in names:
223 if hasattr(self, name):
224 v = getattr(self, name)
225 if v is not None:
226 # Recent Pythons don't need this, but Python 2.1
227 # at least can't convert other types directly
228 # to Unicode. We have to take an intermediate step.
229 if type(v) not in (type(''), type(u'')):
230 v = str(v)
231
232 s += "<%s>%s</%s>" % (name, escapeToXml(v), name)
233 return s
234
235 def makeGeneratorTag(self):
236 return "<generator>%s</generator>" % self.makeAttrTags(
237 'name',
238 'version',
239 )
240
241 def makeSourceTag(self):
242 return "<source>%s</source>" % self.makeAttrTags(
243 'project',
244 'module',
245 'branch',
246 )
247
248 def makeBodyTag(self):
249 return "<body><commit>%s%s</commit></body>" % (
250 self.makeAttrTags(
251 'revision',
252 'author',
253 'log',
254 'diffLines',
255 'url',
256 ),
257 self.makeFileTags(),
258 )
259
260 def makeFileTags(self):
261 """Return XML tags for our file list"""
262 return "<files>%s</files>" % ''.join([file.makeTag(self.config)
263 for file in self.files])
264
265 def svnlook(self, command):
266 """Run the given svnlook command on our current repository and
267 revision, returning all output"""
268 # We have to set LC_ALL to force svnlook to give us UTF-8 output,
269 # then we explicitly slurp that into a unicode object.
270 return unicode(os.popen(
271 'LC_ALL="en_US.UTF-8" svnlook %s -r "%s" "%s"' %
272 (command, self.revision, self.repository)).read(),
273 'utf-8', 'replace')
274
275 def collectData(self):
276 self.author = self.svnlook('author').strip()
277 self.project = self.config.project
278 self.log = self.svnlook('log')
279 self.diffLines = len(self.svnlook('diff').split('\n'))
280 self.files = self.collectFiles()
281 if self.config.revisionURI is not None:
282 self.url = self.config.revisionURI % self.__dict__
283 else:
284 self.url = None
285
286 def collectFiles(self):
287 # Extract all the files from the output of 'svnlook changed'
288 files = []
289 for line in self.svnlook('changed').split('\n'):
290 path = line[2:].strip()
291 if path:
292 status = line[0]
293 files.append(File(path, status))
294
295 # Try each of our several regexes. To be applied, the same
296 # regex must mach every file under consideration and they must
297 # all return the same results. If we find one matching regex,
298 # or we try all regexes without a match, we're done.
299 matchDict = None
300 for regex in self.config.pathRegexes:
301 matchDict = matchAgainstFiles(regex, files)
302 if matchDict is not None:
303 self.__dict__.update(matchDict)
304 break
305
306 return files
307
308
309 def matchAgainstFiles(regex, files):
310 """Try matching a regex against all File objects in the provided list.
311 If the regex returns the same matches for every file, the matches
312 are returned in a dict and the matched portions are filtered out.
313 If not, returns None.
314 """
315 prevMatchDict = None
316 compiled = re.compile(regex, re.VERBOSE)
317 for f in files:
318
319 match = compiled.match(f.fullPath)
320 if not match:
321 # Give up, it must match every file
322 return None
323
324 matchDict = match.groupdict()
325 if prevMatchDict is not None and prevMatchDict != matchDict:
326 # Give up, we got conflicting matches
327 return None
328
329 prevMatchDict = matchDict
330
331 # If we got this far, the regex matched every file with
332 # the same results. Now filter the matched portion out of
333 # each file and store the matches we found.
334 for f in files:
335 f.path = compiled.sub('', f.fullPath)
336 return prevMatchDict
337
338
339 def escapeToXml(text, isAttrib=0):
340 text = unicode(text)
341 text = text.replace("&", "&amp;")
342 text = text.replace("<", "&lt;")
343 text = text.replace(">", "&gt;")
344 if isAttrib == 1:
345 text = text.replace("'", "&apos;")
346 text = text.replace("\"", "&quot;")
347 return text
348
349
350 def usage():
351 """Print a short usage description of this script and exit"""
352 sys.stderr.write("Usage: %s [OPTIONS] REPOS-PATH REVISION [PROJECTNAME]\n" %
353 sys.argv[0])
354
355
356 def version():
357 """Print out the version of this script"""
358 sys.stderr.write("%s %s\n" % (sys.argv[0], SvnClient.version))
359
360
361 def main():
362 try:
363 options = [ "version" ]
364 for key in config.__dict__:
365 if not key.startswith("_"):
366 options.append(key + "=");
367 opts, args = getopt.getopt(sys.argv[1:], "", options)
368 except getopt.GetoptError:
369 usage()
370 sys.exit(2)
371
372 for o, a in opts:
373 if o == "--version":
374 version()
375 sys.exit()
376 else:
377 # Everything else maps straight to a config key. Just have
378 # to remove the "--" prefix from the option name.
379 config.__dict__[o[2:]] = a
380
381 # Print a usage message when not enough parameters are provided.
382 if not len(args) in (2,3):
383 sys.stderr.write("%s: incorrect number of arguments\n" % sys.argv[0])
384 usage();
385 sys.exit(2);
386
387 # If a project name was provided, override the default project name.
388 if len(args) == 3:
389 config.project = args[2]
390
391 # Go do the real work.
392 SvnClient(args[0], args[1], config).main()
393
394
395 if __name__ == "__main__":
396 main()
397
398 ### The End ###

  ViewVC Help
Powered by ViewVC 1.1.30