Over a million developers have joined DZone.

Simulate Subversion Commit Email Hook on the Client Side

DZone's Guide to

Simulate Subversion Commit Email Hook on the Client Side

· Java Zone
Free Resource

Learn how to troubleshoot and diagnose some of the most common performance issues in Java today. Brought to you in partnership with AppDynamics.

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

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 = [

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

Log Message:

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){
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) {
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

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){
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.setContent(body, 'text/plain')
new InternetAddress('my.email@my.domain'))

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



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();

Understand the needs and benefits around implementing the right monitoring solution for a growing containerized market. Brought to you in partnership with AppDynamics.


Opinions expressed by DZone contributors are their own.


Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.


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

{{ parent.tldr }}

{{ parent.urlSource.name }}