Jamie's Blog

Lessons from a life of startups, coding, countryside, and kids

Preventing the stupid mistakes, like committing focus:true

I frequently use focus: true in my specs when I’m working in a particular area of the code. I also frequently forget to remove this tag and subsequently commit the spec to Git, resulting in most of the test suite not running. Duh! “Removing focus:true” is my most frequent commit message :-/

The solution is to write a pre-commit hook for Git. Or, in my case, find someone who has done it and tweak their solution until it actually works (grep -P isn’t supported on a Mac, for example).

So, here’s my pre-commit hook. Simply copy this script into .git/hooks/pre-commit and make sure it’s executable.

#!/usr/bin/env ruby
spec_hits = []

# Find the names of all the filenames that have been (A)dded (C)opied or (M)odified
filenames = `git diff --cached --name-only --diff-filter=ACM`.split("\n")

filenames.each do |filename|
  # Perform special checks for _spec filenames (rspec tests)
  if filename.match /_spec\.rb$/
    # Filter all the additions to this file, find if they contain `focus: true` and store these lines without the initial `+` and spaces
    results = `git diff --cached  #{filename} | grep "^\+[^+]" | grep "focus:[:space:]*true"`.split("\n").map { |r| r.sub(/^\+[\s\t]*/, '') }

    if $? == 0
      # Add the relevant change with line number to the spec_hits array
      results.each{ |r| spec_hits.push "#{filename}:" + `grep -n '#{r}' #{filename}`.sub(/:\s+/, ' ').chomp }
    end
  end
end

if spec_hits.any?
  puts "\e[33m>>> Please remove your `focus: true` from the following tests before committing\e[0m"
  puts spec_hits.join("\n")
end

exit 1 if spec_hits.any?

You might need to edit the regex if you use the hashrocket syntax

Update: I’ve modified the script to prevent debugger, binding.pry and make it easily configurable:

#!/usr/bin/env          

ruby spec_hits = []

checks = {
            '_spec\.rb$' => ['focus:[:space:]*true'],
            '\.rb$' => ['binding\.pry', 'debugger']
         }

# Find the names of all the filenames that have been (A)dded (C)opied or (M)odified
filenames = `git diff --cached --name-only --diff-filter=ACM`.split("\n")

filenames.each do |filename|
  # Perform special checks for _spec filenames (rspec tests)
  checks.each do |filename_pattern, patterns|
    if filename.match filename_pattern
      patterns.each do |contents_pattern|
        results = `git diff --cached  #{filename} | grep "^\+[^+]" | grep "#{contents_pattern}"`.split("\n").map { |r| r.sub(/^\+[\s\t]*/, '') }
        if $? == 0
          # Add the relevant change with line number to the spec_hits array
          results.each{ |r|
            spec_hits.push "#{filename}:" + `grep -n '#{r}' #{filename}`.sub(/:\s+/, ' ').chomp
          }
        end
      end
    end
  end
end

if spec_hits.any?
  puts "\e[33m>>> Please remove the following problems from these files before committing\e[0m"
  puts spec_hits.join("\n")
end

exit 1 if spec_hits.any?

Download the Gist