1#!/usr/bin/env python 2# Copyright (c) 2010 Eric S. Raymond <esr@thyrsus.com> 3# Distributed under BSD terms. 4# 5# This script contains porcelain and porcelain byproducts. 6# It's Python because the Python standard libraries avoid portability/security 7# issues raised by callouts in the ancestral Perl and sh scripts. It should 8# be compatible back to Python 2.1.5 9# 10# usage: ciabot.py [-V] [-n] [-p projectname] [refname [commits...]] 11# 12# This script is meant to be run either in a post-commit hook or in an 13# update hook. If there's nothing unusual about your hosting setup, 14# you can specify the project name and repo with config variables and 15# avoid having to modify this script. Try it with -n to see the 16# notification mail dumped to stdout and verify that it looks 17# sane. With -V it dumps its version and exits. 18# 19# In post-commit, run it without arguments. It will query for 20# current HEAD and the latest commit ID to get the information it 21# needs. 22# 23# In update, call it with a refname followed by a list of commits: 24# You want to reverse the order git rev-list emits because it lists 25# from most recent to oldest. 26# 27# /path/to/ciabot.py ${refname} $(git rev-list ${oldhead}..${newhead} | tac) 28# 29# Configuration variables affecting this script: 30# ciabot.project = name of the project (required) 31# ciabot.repo = name of the project repo for gitweb/cgit purposes 32# ciabot.xmlrpc = if true (default), ship notifications via XML-RPC 33# ciabot.revformat = format in which the revision is shown 34# 35# The ciabot.repo value defaults to ciabot.project lowercased. 36# 37# The revformat variable may have the following values 38# raw -> full hex ID of commit 39# short -> first 12 chars of hex ID 40# describe = -> describe relative to last tag, falling back to short 41# The default is 'describe'. 42# 43# Note: the CIA project now says only XML-RPC is reliable, so 44# we default to that. 45# 46 47import os, sys, commands, socket, urllib 48from xml.sax.saxutils import escape 49 50# Changeset URL prefix for your repo: when the commit ID is appended 51# to this, it should point at a CGI that will display the commit 52# through gitweb or something similar. The defaults will probably 53# work if you have a typical gitweb/cgit setup. 54# 55#urlprefix="http://%(host)s/cgi-bin/gitweb.cgi?p=%(repo)s;a=commit;h=" 56urlprefix="http://%(host)s/cgi-bin/cgit.cgi/%(repo)s/commit/?id=" 57 58# The service used to turn your gitwebbish URL into a tinyurl so it 59# will take up less space on the IRC notification line. 60tinyifier ="http://tinyurl.com/api-create.php?url=" 61 62# The template used to generate the XML messages to CIA. You can make 63# visible changes to the IRC-bot notification lines by hacking this. 64# The default will produce a notfication line that looks like this: 65# 66# ${project}: ${author} ${repo}:${branch} * ${rev} ${files}: ${logmsg} ${url} 67# 68# By omitting $files you can collapse the files part to a single slash. 69xml ='''\ 70<message> 71 <generator> 72 <name>CIA Python client for Git</name> 73 <version>%(version)s</version> 74 <url>%(generator)s</url> 75 </generator> 76 <source> 77 <project>%(project)s</project> 78 <branch>%(repo)s:%(branch)s</branch> 79 </source> 80 <timestamp>%(ts)s</timestamp> 81 <body> 82 <commit> 83 <author>%(author)s</author> 84 <revision>%(rev)s</revision> 85 <files> 86%(files)s 87 </files> 88 <log>%(logmsg)s %(url)s</log> 89 <url>%(url)s</url> 90 </commit> 91 </body> 92</message> 93''' 94 95# 96# No user-serviceable parts below this line: 97# 98 99# Where to ship e-mail notifications. 100toaddr ="cia@cia.vc" 101 102# Identify the generator script. 103# Should only change when the script itself gets a new home and maintainer. 104generator ="http://www.catb.org/~esr/ciabot.py" 105version ="3.5" 106 107defdo(command): 108return commands.getstatusoutput(command)[1] 109 110defreport(refname, merged, xmlrpc=True): 111"Generate a commit notification to be reported to CIA" 112 113# Try to tinyfy a reference to a web view for this commit. 114try: 115 url =open(urllib.urlretrieve(tinyifier + urlprefix + merged)[0]).read() 116except: 117 url = urlprefix + merged 118 119 branch = os.path.basename(refname) 120 121# Compute a description for the revision 122if revformat =='raw': 123 rev = merged 124elif revformat =='short': 125 rev ='' 126else:# revformat == 'describe' 127 rev =do("git describe%s2>/dev/null"% merged) 128if not rev: 129 rev = merged[:12] 130 131# Extract the meta-information for the commit 132 files=do("git diff-tree -r --name-only '"+ merged +"' | sed -e '1d' -e 's-.*-<file>&</file>-'") 133 metainfo =do("git log -1 '--pretty=format:%an <%ae>%n%at%n%s' "+ merged) 134(author, ts, logmsg) = metainfo.split("\n") 135 logmsg =escape(logmsg) 136 137# This discards the part of the author's address after @. 138# Might be be nice to ship the full email address, if not 139# for spammers' address harvesters - getting this wrong 140# would make the freenode #commits channel into harvester heaven. 141 author =escape(author.replace("<","").split("@")[0].split()[-1]) 142 143# This ignores the timezone. Not clear what to do with it... 144 ts = ts.strip().split()[0] 145 146 context =locals() 147 context.update(globals()) 148 149 out = xml % context 150 mail ='''\ 151Message-ID: <%(merged)s.%(author)s@%(project)s> 152From:%(fromaddr)s 153To:%(toaddr)s 154Content-type: text/xml 155Subject: DeliverXML 156 157%(out)s'''%locals() 158 159if xmlrpc: 160return out 161else: 162return mail 163 164if __name__ =="__main__": 165import getopt 166 167# Get all config variables 168 revformat =do("git config --get ciabot.revformat") 169 project =do("git config --get ciabot.project") 170 repo =do("git config --get ciabot.repo") 171 xmlrpc =do("git config --get ciabot.xmlrpc") 172 xmlrpc =not(xmlrpc and xmlrpc =="false") 173 174 host = socket.getfqdn() 175 fromaddr ="CIABOT-NOREPLY@"+ host 176 177try: 178(options, arguments) = getopt.getopt(sys.argv[1:],"np:xV") 179except getopt.GetoptError, msg: 180print"ciabot.py: "+str(msg) 181raiseSystemExit,1 182 183 notify =True 184for(switch, val)in options: 185if switch =='-p': 186 project = val 187elif switch =='-n': 188 notify =False 189elif switch =='-x': 190 xmlrpc =True 191elif switch =='-V': 192print"ciabot.py: version", version 193 sys.exit(0) 194 195# Cough and die if user has not specified a project 196if not project: 197 sys.stderr.write("ciabot.py: no project specified, bailing out.\n") 198 sys.exit(1) 199 200if not repo: 201 repo = project.lower() 202 203 urlprefix = urlprefix %globals() 204 205# The script wants a reference to head followed by the list of 206# commit ID to report about. 207iflen(arguments) ==0: 208 refname =do("git symbolic-ref HEAD 2>/dev/null") 209 merges = [do("git rev-parse HEAD")] 210else: 211 refname = arguments[0] 212 merges = arguments[1:] 213 214if notify: 215if xmlrpc: 216import xmlrpclib 217 server = xmlrpclib.Server('http://cia.vc/RPC2'); 218else: 219import smtplib 220 server = smtplib.SMTP('localhost') 221 222for merged in merges: 223 message =report(refname, merged, xmlrpc) 224if not notify: 225print message 226elif xmlrpc: 227try: 228# RPC server is flaky, this can fail due to timeout. 229 server.hub.deliver(message) 230except socket.error, e: 231 sys.stderr.write("%s\n"% e) 232else: 233 server.sendmail(fromaddr, [toaddr], message) 234 235if notify: 236if not xmlrpc: 237 server.quit() 238 239#End