Git hooks with Ruby and Growl

One thing I’ve been guilty of in the past is committing something in git, then running the tests and finding there’s a typo, then having to go back and fix the typo, the commit and run the whole process over again. It’s frustrating, and it’s a time waster, and if you do it to a shared branch it will pass on that frustration to others.

So, I wrote this pre-commit hook for Git. It runs any ruby file through ruby -c to check the syntax is ok, and if it’s not it stops the commit. This saves a lot of time. It also runs the ruby code through reekhttps://github.com/kevinrutherford/reek/wiki, a code smell detector for Ruby. It informs me what standard it thinks the code I’ve done is. I don’t always agree with it, so it doesn’t stop a commit, but it is useful to know.

I’m on a mac, so I’ve hooked this into Growlhttp://growl.info/. You’ll need to have installed growlnotify, the commandline Growl tool to get this script to work.

A couple of other notes:

  • I added an image of a green tick and a red cross to make it easy to see what had passed/failed. You’ll have to get these yourself, or remove the lines in the middle that refer to the images. See: “TickImage” “CrossImage”
  • Don’t forget to replace your username in the path to the images, or the whole path if you do decide to use images. See: “YOUR_ACCOUNT_NAME”
  • This needs growlnotify from the Growl Extras. Look on their site for it.
  • It uses the network to send Growl notifications to localhost, because it’s a bit more robust. Set a password in your Growl preferences. The port is the default 9887, but you can change this if you need to.
  • Don’t forget to change the password in the script. See: “PUT_YOUR_GROWL_PASSWORD_HERE”
  • Failures are made sticky. Sometimes Growl decides to make all notifications sticky if just one is set. This will get annoying if you commit a large number of files and they fail a reek but pass ruby -c, as you’ll have to manually clear them all. It’s up to you how you deal with this, smaller commits and trying to write better code are the best way ;)

    #!/usr/bin/env ruby -wKU
    class String
      def expand_path
        File.expand_path self
      def parent_dir
        File.dirname self.expand_path
    require "open3"
    files = nil 
    Open3.popen3("git diff --staged --name-only HEAD") do |stdin, stdout, stderr|
      files = stdout.read.split
    puts "Files: #{files}" 
    project_dir = __FILE__.parent_dir.parent_dir.parent_dir
    networked = false
    cmd = "growlnotify -n Git"
    if networked
      " -u -A SHA256 -H #{host} -P #{blah_pah} --port 9887 "
    Passed_test = Struct.new( :rubyc, :reek ) do
      def tests
        self.public_methods( false ).map {|m| m.to_s }.select{|m| m.end_with? "="}.uniq.map{|m| meth = m[0..-2];}
    #Images for Growl
    TickImage = "--image /Users/YOUR_ACCOUNT_NAME/Pictures/git-logo-tick.png"
    CrossImage = "--image /Users/YOUR_ACCOUNT_NAME/Pictures/git-logo-cross.png"
    exit_status = true #set this to false if any critical tests fail, like ruby -c
    notifications = files.each_with_object([]) do |file, mem|
      next unless File.extname( file ) == ".rb"
      next unless File.exists? file
      fileabs = File.expand_path(file)
      passes = Passed_test.new( false, false ) #set to false as standard
      Open3.popen3( "ruby -c #{fileabs}"  )do |stdin, stdout, stderr, wait_thr|
        passes.rubyc = stdout.read.include? "Syntax OK" #true if pass
        exit_status = false unless passes.rubyc #exit_status can only be set to false, else isn't changed at all
      Open3.popen3( "reek #{fileabs}"  )do |stdin, stdout, stderr, wait_thr|
        passes.reek = stdout.read.include? "0 warnings" #true if pass
      passes.tests.each_with_object([cmd]) do |test, local_cmd|
        message = passes.public_send(test) ? "#{test} pass" : "#{test} failed"
        local_cmd << %Q/ -m "#{message}"/
        local_cmd << "-s" unless passes.public_send(test) 
        local_cmd << "-t #{File.basename file}"
        # remove next line if you don't want images
        local_cmd << (passes.public_send(test) ? TickImage : CrossImage )
        mem << local_cmd.join( " " )
    notifications.each do |note|
      system note
    exit exit_status

Put this in WHEREVER_YOUR_GIT_IS_INSTALLED/share/git-core/templates/hooks/pre-commit to have it added to any new Git repo, or just .git/hooks/pre-commit for a single project.

Added on:
Last updated:
Info: - Ruby 1.9.2 - Git 1.7.1 - Growl 1.2.1 - Reek 1.2.8