Subversion hooks in Ruby

Posted by Luke Francl
on Sunday, August 19

Your source control system is where the knowledge of your team is consolidated and requirements are turned into working code. That process is recorded in the change history and commit comments of the SCM. Hook scripts help you integrate that knowledge into the rest of your development process. I’ll write about Subversion because it’s what I use, but every SCM worth its salt has similar facilities.

Let’s say I’d like to integrate my commit messages with my bug tracker. Systems like CVSTrac and Trac have made this popular, and it’s really useful. At my last job, I wrote a Python script that submitted our commit messages to Bugzilla, which was what we used.

Just for fun, I decided to re-implement it in Ruby using ActiveRecord and the latest version of Bugzilla. Ruby is a nice language for writing Subversion hooks because it has a lot of useful libraries, and it’s easy to run other executables from ruby with ``. Plus, you can still read the code six months later!

Here’s how it works:

When a commit is submitted to Subversion, the post-commit hook runs svn2bugzilla.rb. This script uses svnlook to extract the commit information and searches for strings like “bug #123”, then creates a new comment in Bugzilla including the commit message, the revision, and the files changed for each bug found.

There’s two things I needed to do to get this working:

  1. First, I had to create ActiveRecord classes for the Bugzilla tables representing a bug (bugs, a comment (longdescs), and a user (profiles). These classes don’t use the ActiveRecord conventions, so I had to work around that. The longdescs class has a type column, which ActiveRecord does not like (this strikes me as a major problem for using AR with legacy databases).
  2. Second, I had to use svnlook to get the information I need.

The power of svnlook

Subversion post-commit hooks work by executing a script called hooks/post-commit (note: this script must be executable. Change the file permissions if it’s not working!). By convention, hooks/post-commit should call off to other programs to perform the work. To that end, it provides you with two variables: the repository location, and the commit number.

Using svnlook you can then extract some very useful information from the repository given the revision number. Here’s just a few of the things svnlook can tell you: author, cat (show the files changed), changed (list the files changed), date, diff, and log.

For my script, I needed to know: author, changed, and log. Using them, I can create a message like this:

Bug #1: Re-org for configuration; add comments for clarity.

Revision: 8

Changes:

U   svn2bugzilla.rb

svn2bugzilla.rb

Here’s the code. All the configurable options are at the top of the script. If your subversion user names are not the same as your bugzilla usernames, you can map them in USER_MAP. Then, configure your svnlook location and database connection information and you’re done.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/local/bin/ruby
require 'rubygems'
require 'active_record'
require 'set'

# If your Subversion usernames are not the same as your 
# Bugzilla usernames, map them here.
USER_MAP = {"luke" => "luke@slantwisedesign.com"}

# Location of svnlook binary. Change as necessary.
SVNLOOK = "/usr/local/bin/svnlook"

# Configure your AR connection here. 
# Bugzilla supports both MySQL and PostgreSQL.
AR_CONFIG = {:adapter => 'mysql', 
             :database => 'YOUR_BUGZILLA_DB', 
             :username => 'YOUR_BUGZILLA_DB_USER', 
             :password => 'YOUR_BUGZILLA_DB_PASS' }

# You should not have to change anything below this line.

if ARGV[0].nil? || ARGV[1].nil?
  puts "Usage: svn2bugzilla.rb repos_path revision"
  puts "To be used as a subversion post-commit hook."
  exit
end

REPOS_PATH = ARGV[0]
REVISION = ARGV[1]

ActiveRecord::Base.establish_connection(AR_CONFIG)

# These are the three Bugzilla tables we'll be dealing with.
# It'd probably be less code just to query the database directly, 
# bug using ActiveRecord is more fun!

class Bug < ActiveRecord::Base
  set_primary_key "bug_id"
  # longdescs has a column named 'type' which doesn't play well with AR.
  # select the columns we need manually.
  has_many :longdescs, :select => "comment_id, bug_id, who, bug_when, thetext"
end

# longdescs is the comments table.
class Longdesc < ActiveRecord::Base
  set_primary_key "comment_id"
  belongs_to :bug
  belongs_to :profile, :foreign_key => "who"
end

# profiles is the user table
class Profile < ActiveRecord::Base
  set_primary_key "userid"
end

class Commit
  def initialize(repository_path, revision_number)
    @revision_number = revision_number
    @log_message = `#{SVNLOOK} log #{repository_path} -r #{revision_number}`.strip
    @files_changed = `#{SVNLOOK} changed #{repository_path} -r #{revision_number}`
    @author = `#{SVNLOOK} author #{repository_path} -r #{revision_number}`.strip
  end
  
  def message
    <<MESSAGE
#{@log_message}


Revision: #{@revision_number}

Changes:

#{@files_changed}
MESSAGE
  end

  def author
    if USER_MAP[@author].nil? 
      return @author
    end
    
    USER_MAP[@author]
  end
  
  # return a Set of unique bug numbers in the commit message
  def bug_numbers
    bugs = Set.new
    @log_message.scan(/bug\D{1,3}(\d+)/i).each do |match|
      bugs << match[0]
    end
    
    bugs
  end
end

# Do the actual work of submitting the comment to the database

commit = Commit.new(REPOS_PATH, REVISION)
commit.bug_numbers.each do |bug|
  bug = Bug.find_by_bug_id(bug)
  
  next if bug.nil?
  
  user = Profile.find_by_login_name(commit.author)
  
  next if user.nil?
  
  bug.longdescs.create(:who => user.id, 
                       :thetext => commit.message, 
                       :bug_when => Time.now)
end

Configuring hooks in post-commit

By default, there is no hooks/post-commit file for a Subversion repository. You need to copy the template file named post-commit.tmpl to post-commit and chmod it so it’s executable.

Then, remove any examples from the post-commit script, and add svn2bugzilla.rb:

1
2
3
4
REPOS="$1"
REV="$2"

/usr/local/bin/ruby /path/to/script/svn2bugzilla.rb $1 $2

You can read more about Subversion hooks in the manual.

Testing the script

Testing glue code like this is a bit of a pain because it doesn’t exist on its own. If you install it as a hook and it’s not working, you won’t get any feedback. Since it’s a post-commit hook, the commits will succeed just fine even if the script’s not working.

To test it, you can run the script by hand with svn2bugzilla.rb /path/to/repos rev_number and see what happens.

Interesting links

Posted by Luke Francl
on Friday, August 10
  • Kristian Köhntopp writes about common MySQL performance problems with Rails. He shows some ignorance of Rails, but most of the issues he raises are important. Every database has its gotchas so at some level, database abstractions like ActiveRecord fall over. Fortunately, as long as you’re aware of the issues Kristian raises, you can work around most of them. Calling attention to Rails’ default of using large varchars and select * by default is especially important.
  • Patrick Reagan’s caches_constants plugin looks like a nice implementation of the common Java pattern of type-safe enumerations backed by the database. That means you can use constants in your code and foreign keys in your database to refer to a set of objects. With Patrick’s plug in, these objects are only queried for once when your Rails app starts up.
  • I thought the Ruby documentation for Object#instance_variable_set was pretty funny:

    Sets the instance variable names by symbol to object, thereby frustrating the efforts of the class‘s author to attempt to provide proper encapsulation. The variable did not have to exist prior to this call.

  • If you’re wondering why you can’t get Bugzilla working with Apache 2.2, the answer is that they’ve changed the default permissions. Raditha Dissanayake has information on how to fix this.
  • Finding the intersection of two date ranges is annoying, but it has a simple solution if you’re clever about it. Ryan Farley visualizes the problem, but “Dithermaster” realized it’s a lot simpler to find out if two ranges don’t overlap, and negate that. His solution is nicely usable in SQL.
  • During Ostrava on Rails I had the pleasure of meeting (and drinking a pivo or three) with Robert Cigán, developer for Czech Rails development shop Skvělý.CZ. He sent in a link to Skvělý.CZ’s latest application, sMoney.eu an easy to use personal accounting app for EU users. Check it out if you’re in the EU!