The Importance of Git Hooks
So I was working this morning and got one of those bloody awful Rails stack traces – every error was at the system layer and I could not make any progress debugging it. So I looked at git status and there were like 25 + files all tied to an overall CSV import process (2 data models, 1 controller, system level UI changes, etc) and then it hit me – I need my .git hooks setup on this project! Every bit of source code under management by git has a .git directory at the top level and under it a hooks directory. Hooks are executable files that are run when you do an atomic level git action. For example there is one called pre-commit and this is code that is executed BEFORE a commit actually happens. What you can do with a pre-commit hook is tie in a code evaluation process (think lint if you're a C guy) to find any errors before they are committed. Now I've set these up in almost every project but not here so a quick copy of a file from another project to .git/hooks gave me this:
app/controllers/csv_imports_controller.rb: 8: syntax error, unexpected ')', expecting ']' @csv_import = current_user.csv_imports.find(params[:id)) ^ 24: syntax error, unexpected $end, expecting keyword_end
And here's the offending line in question:
@csv_import = current_user.csv_imports.find(params[:id))
Yep. params[:id]) was needed (second to last char was a ) instead of a ]. I looked at that line like 4 times and didn't see it.
Having used these now for several years, its hard for me to imagine working in a dynamic language without them. Kudos to Dv for setting these up once upon a time.
##.git/hooks for the win!
And here's the code for .git/hooks/pre-commit Adjust the shebang line accordingly and make sure its an executable file.
#!/usr/bin/env ruby
#
# A hook script to verify that only syntactically valid ruby code is commited.
# Called by git-commit with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# Put this code into a file called "pre-commit" inside your .git/hooks
# directory, and make sure it is executable ("chmod +x .git/hooks/pre-commit")
puts "in .git/hooks/pre-commit"
require 'open3'
include Open3
# Set this to true if you want warnings to stop your commit
stop_on_warnings = false
stop_on_errors = true
# compiler_ruby = `which rbx`.strip
# compiler_ruby = `which ruby`.strip if compiler_ruby.length == 0
# setting compiler to ruby-1.9.3 since `which ruby` was setting it to 1.8.7
compiler_ruby = "$HOME/.rvm/rubies/ruby-1.9.3-p484/bin/ruby"
changed_ruby_files = `git diff-index --name-only --cached HEAD`.split("\n").inject([]) do |files, line|
files << line.chomp if line =~ /(.+\.(e?rb|task|rake|thor)|[Rr]akefile|[Tt]horfile)/
files
end
problematic_files = changed_ruby_files.inject([]) do |problematic_files, file|
if File.readable? file
cmd = if file =~ /\.erb\z/
# Set trim mode to "-", just as Rails does
# Replacing all <%= tags with <% for the syntax check
"sed 's/<%=/<%/g' #{file} | erb -xT - | #{compiler_ruby} -wc"
else
"#{compiler_ruby} -wc #{file}"
end
errors = nil
popen3(cmd) do |stdin, stdout, stderr|
errors = stderr.read.split("\n")
end
errors.reject!{ |line| line =~ /[0-9]+:\s+warning:/ } unless stop_on_warnings
errors.reject!{ |line| line =~ /[0-9]+:\s+syntax error/ } unless stop_on_errors
unless errors.empty?
errors.map!{ |line| line.sub(/#{file}:/, '') }
problematic_files << "#{file}:\n#{errors.join("\n")}"
end
end
problematic_files
end
if problematic_files.size > 0
$stderr.puts problematic_files.join("\n\n")
exit 1
else
puts "no syntax errors"
# All is well
exit 0
end