
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:
- 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. Thelongdescsclass has atypecolumn, which ActiveRecord does not like (this strikes me as a major problem for using AR with legacy databases). - Second, I had to use
svnlookto 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.
