Platinum Partner
eclipse

Simulate Subversion Commit Email Hook on the Client Side

If you use Subversion, you probably know that you can enable a commit email hook on the server. Then you can configure svn to send emails out on every commit. You can also (I think, haven't tried this) configure it by setting hook:commit-email properties on the folder of interests.

Here at my new job, we also use Subversion for source control. However, the Subversion server is not configured to send out commit emails. I went to talk our svn admin and he told me that our svn server is too old so we are not gonna get that any time soon.

Okay... fine, that's not a problem, I figured that I can simulate the same feature on the client side with a little bit of scripting. The general idea is this, check out a project off svn onto the local drive, have the script to do a diff against the repository every 5 min and then have the script to send out email if there's a non-empty diff.

I googled around a little bit but couldn't find such scripts so I went ahead and wrote one myself with groovy. The script works as followed:

1. Check out the project you want to add the hook to, then for every 5 min, execute 'svn log -r BASE:HEAD' on that project to get a list of revisions and logs, the output of svn log looks something like (with --incremental option)
------------------------------------------------------------------------
r183 | zl25-drexel | 2008-06-18 22:44:32 -0400 (Wed, 18 Jun 2008) | 1 line

that's good enough for now
------------------------------------------------------------------------
r184 | zl25-drexel | 2008-06-18 22:57:06 -0400 (Wed, 18 Jun 2008) | 1 line

test
------------------------------------------------------------------------
r185 | zl25-drexel | 2008-06-18 22:59:11 -0400 (Wed, 18 Jun 2008) | 1 line


2. Parse these logs to get a set of revision numbers (and the author, date, etc). Then for each revision, execute 'svn diff -r $rev1:$rev2' for each adjacent revisions. For example, if the revisions were r182, r183, and r184, then the script needs to diff r182:r183 and r183:r184. For each diff, send out an email containing the diff output.

3. After all the diffs are performed, execute 'svn up -r $lastrev' to bring your local copy to the latest revision that you had checked against (note: do not update it to HEAD because there might be commits during the time when the script is sending out emails).

Voila! with the above 3 simple steps you will get exactly the same functionality as if a commit email hook is enabled on the server.

Here is the script for your viewing pleasure

#!/usr/bin/env groovy
import java.text.SimpleDateFormat
import javax.mail.internet.InternetAddress
import javax.mail.Message
import javax.mail.internet.MimeMessage
import javax.mail.Transport
import javax.mail.Session
import groovy.text.SimpleTemplateEngine

def chill = 5 //min
def emailconfig = [protocol:'smtps',
host : 'smtp.gmail.com',
port : 465, //must be int
user : 'XXX', password : 'XXX']
def projects = [
'projectName':/C:\Projects\project1/,
'projectName2':/C:\Project\project2/
]



def subject="[<%=rev%>] [SVN:<%=name%>] [Author:<%=author%>]"
def body = """<%=name%> revision <%=rev%> report
Author: <%=author%>
Date: <%=date%>

Log Message:
--------------------------------------------------
<%=log%>
--------------------------------------------------

<%=fulldiff%>
"""
def engine = new SimpleTemplateEngine()
def BODY = engine.createTemplate(body)
def SUBJECT = engine.createTemplate(subject)
def ps = new PrintStream(
new BufferedOutputStream(new FileOutputStream('svndiff.log')))
def printLog(def msg, def ps){
def df = new SimpleDateFormat()
println "[${df.format(new Date())}] $msg"
ps.println "[${df.format(new Date())}] $msg"
}

def parseLog(def log){
try{
def lines = []
new StringReader(log).eachLine{ line ->
lines << line
}
if(lines.size < 2) return;
def ret = [:]
def items = lines[1].split(/[|]/)
ret['rev'] = items[0].trim().substring(1)
ret['author'] = items[1]
ret['date'] = items[2]
ret['log'] = log
return ret
}catch (Exception e) {
e.printStackTrace()
return null
}
}

def getBASErev(def path){
def base = 'BASE'
"svn info -r BASE $path".execute().in.eachLine{
def m = it=~/Revision:\s+(\d+)/
if(m.matches()){ base = m.group(1) }
}
return base
}

while(true){
try{
projects.each{name, path ->
printLog("processing $name", ps)
def revisions =
"svn log -r BASE:HEAD --limit 30 --incremental $path".execute().text.split('-'*72)
def BASErev = getBASErev(path)
def lastrev = null
def logs = [['rev':'BASE']]
revisions.each{rev ->
def parsed = parseLog(rev)
if (parsed != null)logs << parsed
}
if(logs.size == 1){
printLog('already up to date, nothing to process', ps); return
}
for(int i =0; i< logs.size -1; i++){
def rev1 = logs[i], rev2 = logs[i+1]
if(rev2['rev'] == BASErev){ continue;} //no need to diff BASE

def diff = "svn diff -r ${rev1['rev']}:${rev2['rev']} $path".execute().text
def s = SUBJECT.make([rev: rev2['rev'], name: name, author: rev2['author'] ]).toString()
def b = BODY.make([name: name, rev: rev2['rev'],
author:rev2['author'], date: rev2['date'],
log: logs[i+1]['log'], fulldiff: diff]).toString()
printLog( "sending $s ...", ps)
sendEmail(s, b, emailconfig)
lastrev = rev2['rev']
}
// bring the local to the last rev we had diffed
if(lastrev != null){
printLog("svn up -r $lastrev $path", ps) ;
"svn up -r $lastrev $path".execute() }
}
}catch(Exception e){
e.printStackTrace()
}
printLog('\nChilling out for 5 min ...\n', ps)
//chill out for 5 min
sleep chill*60*1000
}

def sendEmail(def subject, def body, def config){
Properties props = new Properties()
props.put("mail.transport.protocol", config.protocol);
props.put("mail.smtps.host", config.host);
props.put("mail.smtps.auth", "true");

Session session = Session.getDefaultInstance(props)
Transport transport = session.getTransport()

MimeMessage message = new MimeMessage(session)
message.setSubject(subject)
message.setContent(body, 'text/plain')
message.addRecipient(Message.RecipientType.TO,
new InternetAddress('my.email@my.domain'))

transport.connect(config.host, config.port, config.user, config.password)

transport.sendMessage(message,
message.getRecipients(Message.RecipientType.TO));
transport.close();

}



A few things should be noted. First of all, it uses java mail, which is not part of the JDK. So make sure you have it
available in groovy's classpath (just throw the jars in $GROOVY_HOME/lib). I am using gmail's smtp server just for illustration
purposes. It's probably not a good idea to have your company's svn commits messages all go thru gmail, it might get you into troubles. You
should use your company's internal smtp server for internal projects.

This script works as long as you have read accesses to a repository. So if you are not a developer of projects say apache commons but still
want to track its revisions thru commit emails, you can hook this script to their repository and receive emails for every commit!

There are many features in Groovy (this language makes me so happy :-) ) which make scripting extremely easy. The above script showcased quite a
few of them. Imagine if you need to write it in Java how much codes you need to write. I don't think I will want to write this in Java.
 
Here's a sample email that was produced by the above script:
myproject revision 173 report

Author:  zl25-drexel

Date:  2008-06-17 20:26:38 -0400 (Tue, 17 Jun 2008)



Log Message:

------------------------------
r173 | zl25-drexel | 2008-06-17 20:26:38 -0400 (Tue, 17 Jun 2008) | 1 line

test test test
--------------------------------------------------

Index: C:/cygwin/home/jliang/testdiff/jgeocoder/jgeocoder/difftest.txt
===================================================================
--- C:/cygwin/home/jliang/testdiff/jgeocoder/jgeocoder/difftest.txt     (revision 172)
+++ C:/cygwin/home/jliang/testdiff/jgeocoder/jgeocoder/difftest.txt     (revision 173)
@@ -2,4 +2,4 @@

 one more line

-one more line
\ No newline at end of file
+one more test
\ No newline at end of file
Index: C:/cygwin/home/jliang/testdiff/jgeocoder/jgeocoder/src/main/java/net/sourceforge/jgeocoder/test/JaysTest.java
===================================================================
--- C:/cygwin/home/jliang/testdiff/jgeocoder/jgeocoder/src/main/java/net/sourceforge/jgeocoder/test/JaysTest.java       (revision 172)
+++ C:/cygwin/home/jliang/testdiff/jgeocoder/jgeocoder/src/main/java/net/sourceforge/jgeocoder/test/JaysTest.java       (revision 173)
@@ -10,7 +10,11 @@
 import net.sourceforge.jgeocoder.tiger.H2DbDataSourceFactory;
 import net.sourceforge.jgeocoder.tiger.JGeocoder;
 import net.sourceforge.jgeocoder.tiger.JGeocoderConfig;
-
+/**
+ * blah blah
+ * @author jliang
+ *
+ */
 public class JaysTest{
  public static void main(String[] args) {
    JGeocoderConfig config = new JGeocoderConfig();
 
{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}