Subversion post commit email hook, in Ruby

I’m a big believer in tools. Personally, I believe that better engineers tend to use better tools, but that’s a subject to explore in another post.

Here’s a handy Ruby script that sends a descriptive email after each subversion checkin. The script is based on the one written by Elliott Hughes. Somehow I seem to rewrite this script every time I take a new job, so I’m pleased to release this one into the public domain.

The script lists all files that were added, removed, or modified. Here’s a screenshot:

If you’re going to use this with a sizable engineering team, I recommend changing the HTML email to include a photo of the person who committed the change. That’ll help everyone get acquainted.

Also, it’s nice to have the “modified” lines link to the diff. You can view diffs with ViewCVS. If you find that ViewCVS is too heavyweight (or ugly), I wrote a diff viewing Ruby CGI that gets the job done. I’ll post that shortly. Update: see my subsequent blob post, Subversion diff viewer CGI, in Ruby.

Please excuse my amateurish Ruby. I’m still learning.

To install:

  1. Copy the script to hooks/post-commit in your subversion repository.
  2. Make the script executable.
  3. Modify the ADDRESS constant at the top of the file. Modify other constants if necessary.
  4. (optional) Adjust my beautiful HTML to suit your needs.

You can download the script or copy and paste it from below:

#!/usr/bin/ruby -w

# svn-email.rb
#
# Send svn checkin email, based on a script by Elliott Hughes. To
# install, copy this file into your repository’s hooks/ directory as
# "post-commit". Don’t forget to chmod a+x post-commit
#
# Author:  Elliott Hughes, Adam Doppelt
# Version: 0.2

require ‘cgi’

# constants
ADDRESS = "MODIFY_THIS@MODIFY_THIS.com"
SENDMAIL = "/usr/sbin/sendmail"
#SENDMAIL = "/usr/bin/tee" # for debugging
SVNLOOK = "/usr/bin/svnlook"

# convert a list of files to HTML
def filesToHtml(title, list, revision = nil)
  return "" if list.length == 0

  # truncate if too big
  list[200..-1] = "…" if list.length > 200

  result = ""
  result << "\\n<h3>#{title}</h3>\\n"
  result << "<div class=\"files\">\\n"
  list.each do |file|
    result << "  "
    result << CGI.escapeHTML(file)
    result << "<br/>\\n"
  end
  result << "</div>\\n"
  result
end

# Subversion’s commit-email.pl suggests that svnlook might create files.
Dir.chdir("/tmp")

# process ARGV
repo = ARGV.shift
revision = ARGV.shift
raise "bad args" if !repo || !revision

#
# Get the overview information.
#

info = `#{SVNLOOK} info #{repo} -r #{revision}`.split("\\n")
author = info.shift
date = info.shift
size = info.shift
subject = info[0]
comment = info.join("\\n")

#
# iterate changed files
#

added = []
modified = []
removed = []
props_modified = []

`#{SVNLOOK} changed #{repo} -r #{revision}`.split("\\n").each do |line|
  op = line[0,1]
  props = line[1,1]
  file = line[4..-1]

  # escape the filename
  file = CGI.escapeHTML(file)

  case op
  when ‘A’ then added.push(file)
  when ‘U’ then modified.push(file)
  when ‘D’ then removed.push(file)
  end

  props_modified.push(file) if props == ‘U’
end

#
# build the message body
#

body = <<EOF
<html>
    <head>
        <style type="text/css">
            .main {
              font : 10pt verdana;
              background: white;
              width: 95%
            }
            .main h3 {
              margin: 15px 0px 5px 0px;
            }
            .comment {
              border: 1px solid #dddddd;
              padding: 5px;
            }
            .files {
              border: 1px solid #dddddd;
              padding: 5px;
              background: #eeeeff;
            }
        </style>
    </head>
    <body>
        <div class="main">
            <h3>Revision #{revision} by #{CGI.escapeHTML(author)}</h3>
            <div class="comment">
                #{CGI.escapeHTML(comment).split("\\n").join("<br/>")}
            </div>
            #{filesToHtml("Added Paths", added)}
            #{filesToHtml("Modified Paths", modified, revision)}
            #{filesToHtml("Removed Paths", removed)}
            #{filesToHtml("Property Changed", props_modified)}
        </div>
    </body>
</html>
EOF

#
# Write the mail headers
#

header = ""
header << "To: #{ADDRESS}\\n"
header << "From: #{ADDRESS}\\n"
header << "Subject: [svn] [#{revision}] #{subject}\\n"
header << "MIME-Version: 1.0\\n"
header << "Content-Type: text/html; charset=UTF-8\\n"
header << "Content-Transfer-Encoding: 8bit\\n"
header << "\\n"

#
# Send the mail.
#

begin
    fd = open("|#{SENDMAIL} #{ADDRESS}", "w")
    fd.print(header)
    fd.print(body)
rescue
    exit 1
end
fd.close

3 comments to “Subversion post commit email hook, in Ruby”

  1. Comment by Perry:

    Nice … the scruffy default emails are history!

  2. Comment by Derrick Petzold:

    This is great. If your ever in Honolulu I’ll buy you a beer.

  3. Comment by David:

    This is great. I’m having a problem executing the script though (I’m a ruby/unix newbie trying to get this checkin emails going for our svn repository).

    To narrow the problem down, I’m running the script directly - ruby post-commit
    where both both are valid.

    The script seems to run fine but I’m not seing any emails sent to my email address that I changed the script to.

    Any ideas?