pre-commit for Elixir Projects

In the new project I mentioned in the previous post, I’m focusing on practicing good habits, which my CI setup is a large part of. The other big part is keeping things clean and well written before they are even committed into the repository.

To accomplish this task, I set up pre-commit. Pre-commit is a really easy way to set up some useful git pre-commit hooks, as well as any custom command you want to run.

Install

Pre-commit can be installed into your python setup, or on OSX, use homebrew and run brew install pre-commit

You’ll create a .pre-commit-config.yaml in the root of the git working directory that describes the hooks you want to fire on pre-commit.

Then you can setup your actual hook to fire everything described in .pre-commit-config.yaml by running pre-commit install. You can then modify the .pre-commit-config.yaml file and adjust your hooks without another install. If you ever need to skip this stuff for a commit, just add --no-verify to the commit command (but don’t! fix the problem instead!)

Basics

I’ll start with the common hooks, defined in .pre-commit-config.yaml:

- repo: git://github.com/pre-commit/pre-commit-hooks
  sha: v1.4.0
  hooks:
  - id: trailing-whitespace
  - id: check-merge-conflict
  - id: check-yaml
  - id: end-of-file-fixer
  - id: no-commit-to-branch
    args: [-b, master, -b, production, -b, staging]

Here I’m grabbing the hooks that are public and part of the pre-commit core. I’m using a couple specific ones for things I care about:

So those are the basics, now onto my Elixir specific fun.

Elixir Hooks

Starting a new section in .pre-commit-config.yaml, we’ll define an array of custom hooks:

- repo: local
  hooks:

Tests

First, we’ll make sure the tests run, if any source or test file changes:

- id: mix-test
  name: 'elixir: mix test'
  entry: mix test
  language: system
  pass_filenames: false
  files: \.exs*$

The files pattern will be what changes this would trigger on, and we set pass_filenames as false, so that we run the full suite.

Format

Then, we’ll make sure all changed source and elixir script files are formatted correctly. Leaving the pass_filenames at its default of true, will just run the formatter on changed files.

- id: mix-format
  name: 'elixir: mix format'
  entry: mix format --check-formatted
  language: system
  files: \.exs*$

Compile

Then, we’ll make sure everything compiles without warnings.

- id: mix-compile
  name: 'elixir: mix compile'
  entry: mix compile --force --warnings-as-errors
  language: system
  pass_filenames: false
  files: \.ex$

Convention

Finally, we’ll run credo on the entire project if any elixir or elixir script file changes.

- id: mix-credo
  name: 'elixir: mix credo'
  entry: mix credo
  language: system
  pass_filenames: false
  files: \.exs*$

Putting it all together

Here is the final file. It may get to be too slow as the project gets larger, and since CI does a lot of these same checks, I’ll likely shorten it up, or move a few things to taking passed files again, rather than running on the full project even if only one piece changes.

.pre-commit-config.yaml

- repo: local
  hooks:

  - id: mix-test
    name: 'elixir: mix test'
    entry: mix test
    language: system
    pass_filenames: false
    files: \.exs*$

  - id: mix-format
    name: 'elixir: mix format'
    entry: mix format --check-formatted
    language: system
    files: \.exs*$

  - id: mix-compile
    name: 'elixir: mix compile'
    entry: mix compile --force --warnings-as-errors
    language: system
    pass_filenames: false
    files: \.ex$

  - id: mix-credo
    name: 'elixir: mix credo'
    entry: mix credo
    language: system
    pass_filenames: false
    files: \.exs*$

- repo: git://github.com/pre-commit/pre-commit-hooks
  sha: v1.4.0
  hooks:
  - id: trailing-whitespace
  - id: check-merge-conflict
  - id: check-yaml
  - id: end-of-file-fixer
  - id: no-commit-to-branch
    args: [-b, master, -b, production, -b, staging]

I hope this helps you keep your Elixir projects nice!