diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 21bba7ec..93eb75a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,18 @@ -repos: - - repo: .pre-commit-hooks/ - rev: master - hooks: - - id: shellcheck - - id: canonix +# DO NOT MODIFY +# This file was generated by nix-pre-commit-hooks +{ + "repos": [ + { + "hooks": [ + { + "id": "canonix" + }, + { + "id": "shellcheck" + } + ], + "repo": ".pre-commit-hooks/", + "rev": "master" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..67d291ed --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and is a rolling release. + +## 2019-10-02 + +### Added + +- The `run` derivation now uses [gitignore](https://github.com/hercules-ci/gitignore#readme) + +- Custom hooks can now be added + +### Changed + +- Hooks configuration is now module-based (using the module system, like NixOS). + `.pre-commit-config.yaml` is now obsolete with `nix-pre-commit-hooks`. Translate it to the `hooks` argument. For example: + + ``` + pre-commit-check = nix-pre-commit-hooks.run { + src = ./.; + hooks = { + elm-format.enable = true; + ormolu.enable = true; + shellcheck.enable = true; + }; + }; + ``` + +### Fixed + +- Some small improvements to the installation script (`shellHook`) diff --git a/README.md b/README.md index 32e8c003..321ecb82 100644 --- a/README.md +++ b/README.md @@ -17,39 +17,32 @@ The goal is to manage these hooks with Nix and solve the following: # Installation & Usage -1. Create `.pre-commit-config.yaml` with hooks you want to run in your git repository: - ```yaml - repos: - - repo: .pre-commit-hooks/ - rev: master - hooks: - - id: ormolu - - id: shellcheck - - id: elm-format - ``` - -2. (optional) Use binary caches to avoid compilation: +1. (optional) Use binary caches to avoid compilation: ```bash $ nix-env -iA cachix -f https://cachix.org/api/v1/install $ cachix use hercules-ci ``` -3. Integrate hooks to be built as part of `default.nix`: +2. Integrate hooks to be built as part of `default.nix`: ```nix let - inherit (import (builtins.fetchTarball "https://github.com/hercules-ci/gitignore/tarball/master" {})) gitignoreSource; nix-pre-commit-hooks = import (builtins.fetchTarball "https://github.com/hercules-ci/nix-pre-commit-hooks/tarball/master"); in { pre-commit-check = nix-pre-commit-hooks.run { - src = gitignoreSource ./.; + src = ./.; + hooks = { + elm-format.enable = true; + ormolu.enable = true; + shellcheck.enable = true; + }; }; } ``` Run `$ nix-build -A pre-commit-check` to perform the checks as a Nix derivation. -2. Integrate hooks to prepare environment as part of `shell.nix`: +3. Integrate hooks to prepare environment as part of `shell.nix`: ```nix (import {}).mkShell { inherit ((import ./. {}).pre-commit-check) shellHook; @@ -109,6 +102,9 @@ eval "$shellHook" Everyone is encouraged to add new hooks. + +Have a look at the [existing hooks](modules/hooks.nix) and the [options](modules/pre-commit.nix). + 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` diff --git a/modules/all-modules.nix b/modules/all-modules.nix new file mode 100644 index 00000000..9a72a78c --- /dev/null +++ b/modules/all-modules.nix @@ -0,0 +1,7 @@ +{ + imports = + [ + ./pre-commit.nix + ./hooks.nix + ]; +} diff --git a/modules/hooks.nix b/modules/hooks.nix new file mode 100644 index 00000000..f90786bb --- /dev/null +++ b/modules/hooks.nix @@ -0,0 +1,83 @@ +{ config, lib, pkgs, ... }: +let + inherit (config.pre-commit) tools; +in { + config.pre-commit.hooks = + { + hlint = + { + name = "hlint"; + description = + "HLint gives suggestions on how to improve your source code."; + entry = "${tools.hlint}/bin/hlint"; + files = "\\.l?hs$"; + }; + ormolu = + { + name = "ormolu"; + description = "Haskell code prettifier."; + entry = "${tools.ormolu}/bin/ormolu --mode inplace"; + files = "\\.l?hs$"; + }; + hindent = + { + name = "hindent"; + description = "Haskell code prettifier."; + entry = "${tools.hindent}/bin/hindent"; + files = "\\.l?hs$"; + }; + cabal-fmt = + { + name = "cabal-fmt"; + description = "Format Cabal files"; + entry = "${tools.cabal-fmt}/bin/cabal-fmt --inplace"; + files = "\\.cabal$"; + }; + canonix = + { + name = "canonix"; + description = "Nix code prettifier."; + entry = "${tools.canonix}/bin/canonix"; + files = "\\.nix$"; + }; + nixfmt = + { + name = "nixfmt"; + description = "Nix code prettifier."; + entry = "${tools.nixfmt}/bin/nixfmt"; + files = "\\.nix$"; + }; + nixpkgs-fmt = + { + name = "nixpkgs-fmt"; + description = "Nix code prettifier."; + entry = "${tools.nixpkgs-fmt}/bin/nixpkgs-fmt -i"; + files = "\\.nix$"; + }; + elm-format = + { + name = "elm-format"; + description = "Format Elm files"; + entry = + "${tools.elm-format}/bin/elm-format --yes --elm-version=0.19"; + files = "\\.elm$"; + }; + shellcheck = + { + name = "shellcheck"; + description = "Format shell files"; + types = + [ + "bash" + ]; + entry = "${tools.shellcheck}/bin/shellcheck"; + }; + terraform-format = + { + name = "terraform-format"; + description = "Format terraform (.tf) files"; + entry = "${tools.terraform-fmt}/bin/terraform-fmt"; + files = "\\.tf$"; + }; + }; +} diff --git a/modules/pre-commit.nix b/modules/pre-commit.nix new file mode 100644 index 00000000..846aabd1 --- /dev/null +++ b/modules/pre-commit.nix @@ -0,0 +1,297 @@ +{ config, lib, pkgs, ... }: + +let + + inherit (lib) + attrNames + concatStringsSep + filterAttrs + literalExample + mapAttrsToList + mkIf + mkOption + types + ; + + inherit (pkgs) runCommand writeText git; + + cfg = config.pre-commit; + + hookType = + types.submodule ( + { config, name, ... }: + { + options = + { + enable = + mkOption { + type = types.bool; + description = "Whether to enable this pre-commit hook."; + default = false; + }; + raw = + mkOption { + type = types.attrsOf types.unspecified; + description = + '' + Raw fields of a pre-commit hook. This is mostly for internal use but + exposed in case you need to work around something. + + Default: taken from the other hook options. + ''; + }; + name = + mkOption { + type = types.str; + default = name; + defaultText = literalExample "internal name, same as id"; + description = + '' + The name of the hook - shown during hook execution. + ''; + }; + entry = + mkOption { + type = types.str; + description = + '' + The entry point - the executable to run. entry can also contain arguments that will not be overridden such as entry: autopep8 -i. + ''; + }; + language = + mkOption { + type = types.str; + description = + '' + The language of the hook - tells pre-commit how to install the hook. + ''; + default = "system"; + }; + files = + mkOption { + type = types.str; + description = + '' + The pattern of files to run on. + ''; + default = ""; + }; + types = + mkOption { + type = types.listOf types.str; + description = + '' + List of file types to run on. See Filtering files with types (https://pre-commit.com/#plugins). + ''; + default = [ "file" ]; + }; + description = + mkOption { + type = types.str; + description = + '' + Description of the hook. used for metadata purposes only. + ''; + default = ""; + }; + excludes = + mkOption { + type = types.listOf types.str; + description = + '' + Exclude files that were matched by these patterns. + ''; + default = []; + }; + }; + config = + { + raw = + { + inherit (config) name entry language files types; + id = name; + exclude = + if config.excludes == [] then "^$" else + "(${concatStringsSep "|" config.excludes})"; + }; + }; + } + ); + + enabledHooks = filterAttrs ( id: value: value.enable ) cfg.hooks; + processedHooks = + mapAttrsToList ( id: value: value.raw // { inherit id; } ) enabledHooks; + + precommitConfig = + { + repos = + [ + { + repo = ".pre-commit-hooks/"; + rev = "master"; + hooks = + mapAttrsToList ( id: _value: { inherit id; } ) enabledHooks; + } + ]; + }; + + hooksFile = + writeText "pre-commit-hooks.json" ( builtins.toJSON processedHooks ); + configFile = + writeText "pre-commit-config.json" ( builtins.toJSON precommitConfig ); + + hooks = + runCommand "pre-commit-hooks-dir" { buildInputs = [ git ]; } '' + HOME=$PWD + mkdir -p $out + ln -s ${hooksFile} $out/.pre-commit-hooks.yaml + cd $out + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + git init + git add . + git commit -m "init" + ''; + + run = + runCommand "pre-commit-run" { buildInputs = [ git ]; } '' + set +e + HOME=$PWD + cp --no-preserve=mode -R ${cfg.rootSrc} src + unlink src/.pre-commit-hooks || true + ln -fs ${hooks} src/.pre-commit-hooks + cd src + rm -rf src/.git + git init + git add . + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + git commit -m "init" + echo "Running: $ pre-commit run --all-files" + ${cfg.package}/bin/pre-commit run --all-files + exitcode=$? + git --no-pager diff --color + touch $out + [ $? -eq 0 ] && exit $exitcode + ''; + + # TODO: provide a default pin that the user may override + inherit (import (import ../nix/sources.nix).gitignore { inherit lib; }) + gitignoreSource + ; +in { + options.pre-commit = + { + + package = + mkOption { + type = types.package; + description = + '' + The pre-commit package to use. + ''; + default = pkgs.pre-commit; + defaultText = + literalExample '' + pkgs.pre-commit + ''; + }; + + tools = + mkOption { + type = types.attrsOf types.package; + description = + '' + Tool set from which nix-pre-commit will pick binaries. + + nix-pre-commit comes with its own set of packages for this purpose. + ''; + # This default is for when the module is the entry point rather than + # /default.nix. /default.nix will override this for efficiency. + default = (import ../nix {}).callPackage ../nix/tools.nix {}; + defaultText = + literalExample ''nix-pre-commit-hooks-pkgs.callPackage tools-dot-nix {}''; + }; + + hooks = + mkOption { + type = types.attrsOf hookType; + description = + '' + The hook definitions. + ''; + default = {}; + }; + + run = + mkOption { + type = types.package; + description = + '' + A derivation that tests whether the pre-commit hooks run cleanly on + the entire project. + ''; + readOnly = true; + default = run; + }; + + installationScript = + mkOption { + type = types.str; + description = + '' + A bash snippet that installs nix-pre-commit in the current directory + ''; + readOnly = true; + }; + + rootSrc = + mkOption { + type = types.package; + description = + '' + The source of the project to be checked. + ''; + defaultText = literalExample ''gitignoreSource config.root''; + default = gitignoreSource config.root; + }; + + }; + + config = + { + + pre-commit.installationScript = + '' + export PATH=$PATH:${cfg.package}/bin + if ! type -t git >/dev/null; then + # This happens in pure shells, including lorri + echo 1>&2 "WARNING: nix-pre-commit-hooks: git command not found; skipping installation." + else + # Avoid filesystem churn. We may be watched! + # This prevents lorri from looping after every interactive shell command. + if readlink .pre-commit-hooks >/dev/null \ + && [[ $(readlink .pre-commit-hooks) == ${hooks} ]]; then + echo 1>&2 "nix-pre-commit-hooks: hooks up to date" + else + echo 1>&2 "nix-pre-commit-hooks: updating $PWD" + + [ -L .pre-commit-hooks ] && unlink .pre-commit-hooks + ln -s ${hooks} .pre-commit-hooks + + # This can't be a symlink because its path is not constant, + # thus can not be committed and is invisible to pre-commit. + unlink .pre-commit-config.yaml + { echo '# DO NOT MODIFY'; + echo '# This file was generated by nix-pre-commit-hooks'; + ${pkgs.jq}/bin/jq . <${configFile} + } >.pre-commit-config.yaml + + pre-commit install + # this is needed as the hook repo configuration is cached + pre-commit clean + fi + fi + ''; + }; +} diff --git a/nix/packages.nix b/nix/packages.nix index 45f18ee2..b7a7af2f 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -1,19 +1,19 @@ -{ hlint, shellcheck, ormolu, hindent, cabal-fmt, canonix, elmPackages, niv -, gitAndTools, runCommand, writeText, writeScript, git, nixpkgs-fmt, nixfmt -, callPackage -}: +{ niv, gitAndTools, callPackage }: let - tools = - { - inherit hlint shellcheck ormolu hindent cabal-fmt canonix nixpkgs-fmt nixfmt; - inherit (elmPackages) elm-format; - terraform-fmt = callPackage ./terraform-fmt {}; - }; + tools = callPackage ./tools.nix {}; in tools // rec { inherit niv; inherit (gitAndTools) pre-commit; - run = import ./run.nix { inherit tools pre-commit runCommand writeText writeScript git; }; - pre-commit-check = run { src = ../.; }; + run = callPackage ./run.nix { inherit tools; }; + + # A pre-commit-check for nix-pre-commit itself + pre-commit-check = run { + src = ../.; + hooks = { + shellcheck.enable = true; + canonix.enable = true; + }; + }; } diff --git a/nix/project-module.nix b/nix/project-module.nix new file mode 100644 index 00000000..ec43244f --- /dev/null +++ b/nix/project-module.nix @@ -0,0 +1,12 @@ +/* + This module is picked up by project.nix but is also used internally + to find these imported modules. + */ +{ + imports = + [ + ../modules/all-modules.nix + ]; + + # TODO: move project.nix/modules/pre-commit.nix here +} diff --git a/nix/run.nix b/nix/run.nix index 3bbe2df2..fb328e50 100644 --- a/nix/run.nix +++ b/nix/run.nix @@ -1,116 +1,41 @@ -{ tools, pre-commit, git, runCommand, writeText, writeScript }: +{ pkgs, tools, pre-commit, git, runCommand, writeText, writeScript, lib }: { src -, hooks ? null +, hooks ? {} }: let - hooksYaml = - writeText "pre-commit-hooks" '' - - id: hlint - name: hlint - description: HLint gives suggestions on how to improve your source code. - entry: ${tools.hlint}/bin/hlint - language: system - files: '\.l?hs$' - - id: ormolu - name: ormolu - description: Haskell code prettifier. - entry: ${tools.ormolu}/bin/ormolu --mode inplace - language: script - files: '\.l?hs$' - - id: hindent - name: hindent - description: Haskell code prettifier. - entry: ${tools.hindent}/bin/hindent - language: script - files: '\.l?hs$' - - id: cabal-fmt - name: cabal-fmt - description: Format Cabal files - entry: ${tools.cabal-fmt}/bin/cabal-fmt --inplace - language: script - files: '\.cabal$' - - id: canonix - name: canonix - description: Nix code prettifier. - entry: ${tools.canonix}/bin/canonix - language: system - files: '\.nix$' - - id: nixfmt - name: nixfmt - description: Nix code prettifier. - entry: ${tools.nixfmt}/bin/nixfmt - language: script - files: '\.nix$' - - id: nixpkgs-fmt - name: nixpkgs-fmt - description: Nix code prettifier. - entry: ${tools.nixpkgs-fmt}/bin/nixpkgs-fmt -i - language: script - files: '\.nix$' - - id: elm-format - name: elm-format - description: Format Elm files - entry: ${tools.elm-format}/bin/elm-format --yes --elm-version=0.19 - language: script - files: \.elm$ - - id: shellcheck - name: shellcheck - description: Format shell files - types: [bash] - entry: ${tools.shellcheck}/bin/shellcheck - language: system - - id: terraform-format - name: terraform-format - description: Format terraform (.tf) files - entry: ${tools.terraform-fmt}/bin/terraform-fmt - files: '\.tf$' - language: script - ''; + sources = import ./sources.nix; - hooks = - runCommand "pre-commit-hooks-dir" { buildInputs = [ git ]; } '' - HOME=$PWD - mkdir -p $out - ln -s ${hooksYaml} $out/.pre-commit-hooks.yaml - cd $out - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - git init - git add . - git commit -m "init" - ''; + project = + lib.evalModules { + modules = + [ + ../modules/all-modules.nix + { + options = + { + root = + lib.mkOption { + description = "Internal option"; + default = src; + internal = true; + readOnly = true; + type = lib.types.unspecified; + }; + }; + config = + { + _module.args.pkgs = pkgs; + pre-commit.hooks = hooks; + pre-commit.tools = lib.mkDefault tools; + }; + } + ]; + }; + inherit (project.config.pre-commit) installationScript; - run = - runCommand "pre-commit-run" { buildInputs = [ git ]; } '' - set +e - HOME=$PWD - cp --no-preserve=mode -R ${src} src - unlink src/.pre-commit-hooks || true - ln -fs ${hooks} src/.pre-commit-hooks - cd src - rm -rf src/.git - git init - git add . - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - git commit -m "init" - echo "Running: $ pre-commit run --all-files" - ${pre-commit}/bin/pre-commit run --all-files - exitcode=$? - git diff - touch $out - [ $? -eq 0 ] && exit $exitcode - ''; in - run // { - shellHook = '' - [ -L .pre-commit-hooks ] && unlink .pre-commit-hooks - ln -s ${hooks} .pre-commit-hooks - export PATH=$PATH:${pre-commit}/bin - pre-commit install - # this is needed as the hook repo configuration is cached - pre-commit clean - ''; -} + project.config.pre-commit.run // { + shellHook = installationScript; + } diff --git a/nix/sources.json b/nix/sources.json index 2c99ae69..e7b8c79a 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -23,6 +23,18 @@ "url": "https://github.com/hercules-ci/canonix/archive/a3369542aef56366d3432e01e3e73374a310ca4a.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, + "gitignore": { + "branch": "master", + "description": "Nix function for filtering local git sources", + "homepage": "", + "owner": "hercules-ci", + "repo": "gitignore", + "rev": "f9e996052b5af4032fe6150bba4a6fe4f7b9d698", + "sha256": "0jrh5ghisaqdd0vldbywags20m2cxpkbbk5jjjmwaw0gr8nhsafv", + "type": "tarball", + "url": "https://github.com/hercules-ci/gitignore/archive/f9e996052b5af4032fe6150bba4a6fe4f7b9d698.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, "haskell.nix": { "branch": "master", "description": "Alternative Haskell Infrastructure for Nixpkgs", diff --git a/nix/tools.nix b/nix/tools.nix new file mode 100644 index 00000000..fd16db96 --- /dev/null +++ b/nix/tools.nix @@ -0,0 +1,10 @@ +{ hlint, shellcheck, ormolu, hindent, cabal-fmt, canonix, elmPackages, niv +, gitAndTools, runCommand, writeText, writeScript, git, nixpkgs-fmt, nixfmt +, callPackage +}: + +{ + inherit hlint shellcheck ormolu hindent cabal-fmt canonix nixpkgs-fmt nixfmt; + inherit (elmPackages) elm-format; + terraform-fmt = callPackage ./terraform-fmt {}; +}