diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 21bba7ec..e1f54097 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 project.nix +{ + "repos": [ + { + "hooks": [ + { + "id": "canonix" + }, + { + "id": "shellcheck" + } + ], + "repo": ".pre-commit-hooks/", + "rev": "master" + } + ] +} 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..b8df26c8 --- /dev/null +++ b/modules/pre-commit.nix @@ -0,0 +1,319 @@ +{ 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 + ''; + + srcStr = toString ( config.root.origSrc or config.root ); + + # TODO: provide a default pin that the user may override + inherit (import (import ../nix/sources.nix).gitignore { inherit lib; }) + gitignoreSource + ; +in { + options.pre-commit = + { + + enable = + mkOption { + type = types.bool; + default = false; + description = + '' + Whether to enable pre-commit integration. + + https://pre-commit.com/ + ''; + }; + + enableAutoInstall = + mkOption { + type = types.bool; + default = true; + description = + '' + Whether to auto install pre-commit when invoking nix-shell in the + project root. + ''; + }; + + 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. + ''; + }; + + 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; + }; + + rootSrc = + mkOption { + type = types.package; + description = + '' + The source of the project to be checked. + ''; + defaultText = literalExample ''gitignoreSource config.root''; + default = gitignoreSource config.root; + }; + + }; + + config = + mkIf cfg.enable { + + shell.packages = [ cfg.package ]; + + activation.hooks = + mkIf cfg.enableAutoInstall [ + '' + 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 + ( + # We use srcStr to protect against installing pre-commit hooks + # in the wrong places such as for example ./. when invoking + # nix-shell ../../other-project/shell.nix + cd ${lib.escapeShellArg srcStr} && { + # 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" ${lib.escapeShellArg srcStr} + + [ -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 project.nix'; + ${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..a939a55b 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -14,6 +14,14 @@ 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/run.nix b/nix/run.nix index 3bbe2df2..7e32f048 100644 --- a/nix/run.nix +++ b/nix/run.nix @@ -1,116 +1,42 @@ -{ 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" - ''; + # TODO upstream this and add tests to support this use case + project-nix-core = + map ( f: sources."project.nix" + ("/" + f) ) [ + "modules/root.nix" + "modules/activation.nix" + "modules/shell.nix" + "modules/nixpkgs.nix" + ]; + + project = + lib.evalModules { + modules = + project-nix-core ++ [ + { + root = src; + nixpkgs.pkgs = pkgs; + pre-commit.tools = lib.mkDefault tools; + pre-commit.enable = true; + pre-commit.hooks = hooks; + } + ../modules/pre-commit.nix + ../modules/hooks.nix + ]; + }; + inherit (project.config.shell.shell) shellHook activationHook; - 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 = '' + activationHook=${lib.escapeShellArg activationHook} + ${shellHook} + ''; + } diff --git a/nix/sources.json b/nix/sources.json index 2c99ae69..9a5b053f 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -106,5 +106,17 @@ "type": "tarball", "url": "https://github.com/tweag/ormolu/archive/a7076c0f83e5c06ea9067b71171859fa2ba8afd9.tar.gz", "url_template": "https://github.com///archive/.tar.gz" + }, + "project.nix": { + "branch": "pre-commit", + "description": "A configuration manager for your projects", + "homepage": null, + "owner": "hercules-ci", + "repo": "project.nix", + "rev": "ec17ac79904a296f358961ffcff7a13948c2b92e", + "sha256": "1gkb9k995ly53d5sxn5hr4lyzh57sq25j3i2cw8zva92g8x69gb5", + "type": "tarball", + "url": "https://github.com/hercules-ci/project.nix/archive/ec17ac79904a296f358961ffcff7a13948c2b92e.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" } }