Use `credo` Linter in Git `pre-commit` Hook

· 4 min read

tricks elixir hacking

credo

is great .

Every single Elixir project all around should use it. Besides fancy mix task to report project styling issues, it has bindings to most of modern editors, like neovim , emacs through alchemist.el and even Atom .

The greatest thing about credo is it provides the meaningful shell return codes , making it quite easy to plug it as a linter into git hooks toolchain. Let’s do it:

Include credo dependency into mix.exs project file

  defp deps do
    [
      ...
      {:credo, "~> 0.7", only: [:dev, :test]},
      ...
    ]
  end

if you are reading this article from the future, immediately run

$ mix deps.update credo

to get the up-to-date version. It’s safe, since we’d use it in non-prod environments only. Let’s try it in action.

Running credo for the first time

$ mix credo

Checking 36 source files ...

  Software Design
┃
┃ [D] β†— Found a FIXME tag in a comment: # FIXME FIXME FIXME
┃       lib/my_app/file1.ex:194 (MyApp.File1.fun1)
┃ [D] β†’ Found a TODO tag in a comment: # Todo: should round here?
┃       lib/my_app/file2.ex:53 (MyApp.File2.fun2)

  Code Readability                                                              
┃
┃ [R] β†’ Modules should have a @moduledoc tag.
┃       lib/my_app/file1.ex:4:13 (MyApp.File1)
┃ [R] β†’ Modules should have a @moduledoc tag.
┃       lib/my_app/file3.ex:3:11 (MyApp.File3)

  Refactoring opportunities                                                     
┃
┃ [F] β†’ Function body is nested too deep (max depth is 2, was 4).
┃       lib/my_app/file1.ex:179:21 (MyApp.File1.fun1)

  Warnings - please take a look                                                 
┃
┃ [W] β†— Prefer lazy Logger calls.
┃       lib/my_app/file1.ex:91 (MyApp.File1.fun3)
┃ [W] β†— Prefer lazy Logger calls.
┃       lib/my_app/file1.ex:82 (MyApp.File1.fun2)
┃ [W] β†— Prefer lazy Logger calls.
┃       lib/my_app/file1.ex:169 (MyApp.File1.fun3)

Please report incorrect results: https://github.com/rrrene/credo/issues

Analysis took 1.1 seconds (0.02s to load, 1.1s running checks)
220 mods/funs, found 3 warnings,
                     1 refactoring opportunity,
                     2 code readability issues,
                     2 software design suggestions.

Here we go. First run report is always depressive and driving me bonkers. I was writing this code today morning, it’s, you know, kinda perfect. I hope you’ll get zero warnings and at most one TODO tag found, but I never get there. First run turns me into a couple of hours of refactoring. The good thing is from now on it’s fairly easy to keep the project in clean state, that satisfies even such a captious judge as credo .

Configure credo

The very straightforward way to make everything tuned would be to:

cd config
# wget https://raw.githubusercontent.com/rrrene/credo/master/.credo.exs
mix credo gen.config
vim .credo.exs
cd -

The file is self-documented. Just go tweak it.

Making it happen recurrently

Well, I knew one guy who had put running linters into crontab at 2AM. He started every morning with few cups of coffee and several hours of refactoring. I am not as brave. I use git hooks to lint my code (save for editor plugins.)

Let’s configure our .git/config in the first place. Just add the following section to it:

[credo]
  terminate = 16

The above will terminate the commit when there are warnings. Check how credo calculates an exit status for details.

OK, now we are to create a hook itself. You might just put this in your .git/hooks/pre-commit local hook (create this file if it’s not existing yet):

#!/bin/sh
#
# Linter Elixir files using Credo.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# Config
# ------
# credo.terminate
#   The credo exit status level to be considered as β€œnot passed”—to prevent
#   git commit until fixed.

# Config
terminate_on=$(git config --int credo.terminate)
if [[ -z "$terminate_on" ]]; then terminate_on=16; fi

# test it :: run tests before commit (silently)
mix test 2>&1 >/dev/null
TEST_RES=$?
if [ $TEST_RES -ne 0 ]
then
  echo ""
  echo "β˜† ==================================== β˜†"
	echo "β˜†  Some tests are failed.              β˜†" >&2
	echo "β˜†  Please fix them before committing.  β˜†" >&2
  echo "β˜† ==================================== β˜†"
  echo ""
  exit $TEST_RES
fi
echo ""
echo "β˜… ============================== β˜…"
echo "β˜…   Tests passed successfully.   β˜…"
echo "β˜… ============================== β˜…"
echo ""

# lint it :: credo checks before commit
mix credo
CREDO_RES=$?
if [ $CREDO_RES -ge $(terminate_on) ]; then
  echo ""
  echo "β˜† ============================================= β˜†"
	echo "β˜† Credo found critical problems with your code  β˜†" >&2
	echo "β˜†   and commit can not proceed. Please examine  β˜†" >&2
	echo "β˜†   log above and fix issues before committing. β˜†" >&2
  echo "β˜† ============================================= β˜†"
  echo ""
  exit $CREDO_RES
fi
if [ $CREDO_RES -le 9 ]; then CREDO_RES=" $CREDO_RES"; fi
echo ""
echo "β˜… ============================== β˜…"
echo "β˜…   Credo passed successfully.   β˜…"
echo "β˜…   Exit value is: (total: $CREDO_RES).  β˜…"
echo "β˜… ============================== β˜…"
echo ""

# Finished
exit 0

Before credo we also run tests. If you are like me and want to commit code even while all the tests are failing (hopefully providing the comment like β€œ:bomb: [BROKEN] Evening commit! Do not use at home or school!”,) just supply --no-verify option to git commit .

And yes, we are done. Try to git commit and you’ll see as your code will be tested and credo’ed for you automagically.