This article was something I wrote for our team in an internal wiki a few years ago, the code is not pretty but it does the job.

Linting is good, right. Keeps the code consistent and catches trivial mistakes early. The Ruby go-to linter is called RuboCop. It’s used in many OSS project as in in-house projects. Some like it and some don’t, on whatever side you are on the usage will often result in commit histories like this:

1fd8d091bdf27f5c73c12117e8d751a0445da4c1 fix rubocop issue
b621033f47e9dd515dcdfb472feca28934b73370 Oh c'mon rubocop
37203fa96eb96a3c07e3726c4590ae9d8470f77c Fix rubocop issue
a1b631ba5c0c99c01912705a0b16d069032537a1 And some rubocop fixes

It’s not only cluttering the commit history but many minutes are spent weekly fixing failed CI pipelines because the trivial things RuboCop is whining about. In the worst case running a rubocop -A will not fix the issue but you need to start changing the implementation (or disabling some RuboCop rules) and when you’re done with the fixes you probably forgot what you actually were doing.

A cure to all this context switching is to commit often and run RuboCop before committing. This article will present a way to automate the linting to every commit you do.

Global git commit hook executing RuboCop (or any other linter out there)

Start by creating a folder for your local pre-commit hooks.

mkdir -p ~/dot_files/git-hooks

In there create two files.

~/dot_files/git-hooks/pre-commit The file that git calls every time a commit is about to happen

#!/bin/bash

for hook in $(dirname "$0")/pre-commit.*
do
  if [[ -x "$hook" ]]
  then
    $hook "$@"
    status=$?
    exit $status
  fi
done

exit 0

~/dot_files/git-hooks/pre-commit.rubocop The file that executes RuboCop on all the staged files

#!/usr/bin/env ruby

begin
  require 'rubocop'
rescue
  puts 'RuboCop not found, skipping pre-commit hook'
  exit 0
end

system("bundle list | grep 'rubocop' > /dev/null")

unless $CHILD_STATUS.to_s[-1].to_i == 0
  puts 'RuboCop not included in project, skipping pre-commit hook'
  exit 0
end

EXTENSIONS = ['.rb', '.rake', '.gemspec'].freeze
FILES = ['Gemfile'].freeze
ADDED_OR_MODIFIED = /A|AM|^M/

puts "Running RuboCop on staged '#{(EXTENSIONS+FILES).join(' ')}' files"

# rubocop:disable Style/MultilineBlockChain
changed_files = `git status --porcelain`.split(/\n/).
    select { |file_name_with_status|
      file_name_with_status =~ ADDED_OR_MODIFIED
    }.
    map { |file_name_with_status|
      file_name_with_status.split(' ')[1]
    }.
    select { |file_name|
      EXTENSIONS.include?(File.extname(file_name)) || FILES.include?(File.basename(file_name))
    }.join(' ')
# rubocop:enable Style/MultilineBlockChain

system("bundle exec rubocop --force-exclusion #{changed_files}") unless changed_files.empty?

exit $CHILD_STATUS.to_s[-1].to_i

To allow the hooks to get executed we need to make the files executable

chmod 0755 ~/dot_files/git-hooks/*

The last step of the game is to configure a global hooks folder for git

git config --global core.hooksPath ~/dot_files/git-hooks

Now RuboCop will be executed on the files you changed on each commit. If for some reason you want to commit something that is violating the rules the pre-commit hooks can be skipped with the --no-verify parameter to the commit command.