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.