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