Seamless integration of pre-commit git hooks with Nix
The goal is to manage commit hooks with Nix and solve the following:
-
Trivial integration for Nix projects (wires up a few things behind the scenes)
-
Provide a low-overhead build of all the tooling available for the hooks to use (naive implementation of calling nix-shell does bring some latency when committing)
-
Common hooks for languages like Python, Haskell, Elm, etc.
-
Run hooks as part of development and on your CI
https://devenv.sh/pre-commit-hooks/
-
(optional) Use binary caches to avoid compilation:
nix-env -iA cachix -f https://cachix.org/api/v1/install cachix use pre-commit-hooks
-
Integrate hooks to be built as part of
default.nix
:let nix-pre-commit-hooks = import (builtins.fetchTarball "https://github.com/cachix/pre-commit-hooks.nix/tarball/master"); in { pre-commit-check = nix-pre-commit-hooks.run { src = ./.; # If your hooks are intrusive, avoid running on each commit with a default_states like this: # default_stages = ["manual" "push"]; hooks = { elm-format.enable = true; ormolu.enable = true; shellcheck.enable = true; }; # Some hooks offer custom settings that affect how they execute settings = { ormolu.defaultExtensions = [ "lhs" "hs" ]; }; }; }
Run
$ nix-build -A pre-commit-check
to perform the checks as a Nix derivation. -
Integrate hooks to prepare environment as part of
shell.nix
:(import <nixpkgs> {}).mkShell { shellHook = '' ${(import ./default.nix).pre-commit-check.shellHook} ''; }
Add
/.pre-commit-config.yaml
to.gitignore
.Run
$ nix-shell
to executeshellHook
which will:- build the tools and
.pre-commit-config.yaml
config file symlink which references the binaries, for speed and safe garbage collection - provide the
pre-commit
executable thatgit commit
will invoke
- build the tools and
.envrc
:
use nix
- gofmt: Runs
go fmt
- gotest: Runs
go test
- golangci-lint
- govet
- revive
- staticcheck
terraform-format
: built-in formatter- tflint
- prettier
dhall format
: built-in formatter- hadolint
- editorconfig-checker
- actionlint
- tagref
- treefmt
- topiary
- checkmake
- mkdocs-linkcheck
- headache
- crystal
You must configure which languages should be formatted by clang_format
using
clang-format.types_or
. For example to check both C and C++ files:
clang-format = {
enable = true;
types_or = [ "c" "c++" ];
};
Sometimes it is useful to add a project specific command as an extra check that is not part of the pre-defined set of hooks provided by this project.
Example configuration:
let
nix-pre-commit-hooks = import (builtins.fetchTarball "https://github.com/cachix/pre-commit-hooks.nix/tarball/master");
in {
pre-commit-check = nix-pre-commit-hooks.run {
hooks = {
# ...
# Example custom hook for a C project using Make:
unit-tests = {
enable = true;
# The name of the hook (appears on the report table):
name = "Unit tests";
# The command to execute (mandatory):
entry = "make check";
# The pattern of files to run on (default: "" (all))
# see also https://pre-commit.com/#hooks-files
files = "\\.(c|h)$";
# List of file types to run on (default: [ "file" ] (all files))
# see also https://pre-commit.com/#filtering-files-with-types
# You probably only need to specify one of `files` or `types`:
types = [ "text" "c" ];
# Exclude files that were matched by these patterns (default: [ ] (none)):
excludes = [ "irrelevant\\.c" ];
# The language of the hook - tells pre-commit
# how to install the hook (default: "system")
# see also https://pre-commit.com/#supported-languages
language = "system";
# Set this to false to not pass the changed files
# to the command (default: true):
pass_filenames = false;
};
};
};
}
Custom hooks are defined with the same schema as pre-defined hooks.
Given the following flake.nix
example:
{
description = "An example project.";
inputs.pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, pre-commit-hooks, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
{
checks = {
pre-commit-check = pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
nixpkgs-fmt.enable = true;
};
};
};
devShell = nixpkgs.legacyPackages.${system}.mkShell {
inherit (self.checks.${system}.pre-commit-check) shellHook;
};
}
);
}
Add /.pre-commit-config.yaml
to the .gitignore
.
To run the all the hooks on CI:
nix flake check
To install pre-commit hooks developers would run:
nix develop
Everyone is encouraged to add new hooks.
Have a look at the existing hooks and the options.
There's no guarantee the hook will be accepted, but the general guidelines are:
- Nix closure of the tool should be small e.g.
< 50MB
. A problematic example:
$ du -sh $(nix-build -A go)
463M /nix/store/v4ys4lrjngf62lvvrdbs7r9kbxh9nqaa-go-1.18.6
- The tool must not be very specific (e.g. language tooling is OK, but project specific tooling is not)
- The tool needs to live in a separate repository (even if a simple bash script, unless it's a oneliner)