From 0a5b959a76b75b45b3f2d7096898edab50bd5b61 Mon Sep 17 00:00:00 2001 From: Malix - Alix Brunet Date: Thu, 10 Sep 2026 17:27:34 +0200 Subject: [PATCH] feat: integrate with nix fmt Expose a `formatter` package that wraps `pre-commit run` for use with `nix fmt`. - Provide `formatter` option in `modules/pre-commit.nix` and inherit it in `run.nix`. - In `flake-module.nix`, expose `pre-commit.formatter` and set `formatter = lib.mkDefault cfg.settings.formatter;`. - Handle pre-commit exit codes when files are reformatted so that `nix fmt` succeeds when changes are applied. - Update template and README documentation. --- README.md | 13 ++---------- flake-module.nix | 8 ++++++++ modules/pre-commit.nix | 43 +++++++++++++++++++++++++++++++++++++++ nix/run.nix | 2 +- nix/tests/hook-config.nix | 2 ++ template/flake.nix | 2 ++ 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 097a5cba..d1615767 100644 --- a/README.md +++ b/README.md @@ -88,16 +88,7 @@ Given the following `flake.nix` example: { # Run the hooks with `nix fmt`. formatter = forEachSystem ( - system: - let - pkgs = nixpkgs.legacyPackages.${system}; - config = self.checks.${system}.pre-commit-check.config; - inherit (config) package configFile; - script = '' - ${pkgs.lib.getExe package} run --all-files --config ${configFile} - ''; - in - pkgs.writeShellScriptBin "pre-commit-run" script + system: self.checks.${system}.pre-commit-check.formatter ); # Run the hooks in a sandbox with `nix flake check`. @@ -162,7 +153,7 @@ nix fmt ### flake-parts -If your flake uses [flake-parts](https://flake.parts/), we provide a flake-parts module as well. Checkout [`./template/flake.nix`](https://github.com/cachix/git-hooks.nix/blob/master/template/flake.nix) for an example. +If your flake uses [flake-parts](https://flake.parts/), we provide a flake-parts module as well. It automatically configures `checks..pre-commit` and `formatter.` to format your code using the enabled hooks when running `nix fmt`. Checkout [`./template/flake.nix`](https://github.com/cachix/git-hooks.nix/blob/master/template/flake.nix) for an example. ## Nix diff --git a/flake-module.nix b/flake-module.nix index 7405e5e9..d1adf690 100644 --- a/flake-module.nix +++ b/flake-module.nix @@ -68,10 +68,18 @@ in description = "A development shell with the git hooks installed and all the packages made available."; readOnly = true; }; + formatter = mkOption { + type = types.package; + description = "A formatter package that runs pre-commit hooks, suitable for flake-parts `formatter`."; + default = cfg.settings.formatter; + defaultText = lib.literalExpression "config.pre-commit.settings.formatter"; + readOnly = true; + }; }; }; config = { checks = lib.optionalAttrs cfg.check.enable { pre-commit = cfg.settings.run; }; + formatter = lib.mkDefault cfg.settings.formatter; pre-commit.settings = { pkgs, ... }: { rootSrc = self.outPath; package = lib.mkDefault pkgs.pre-commit; diff --git a/modules/pre-commit.nix b/modules/pre-commit.nix index 22df4c1f..706d7025 100644 --- a/modules/pre-commit.nix +++ b/modules/pre-commit.nix @@ -117,6 +117,36 @@ let ''; }; + formatter = + pkgs.writeShellScriptBin "pre-commit-fmt" '' + set -euo pipefail + export PATH="${lib.makeBinPath ([ cfg.gitPackage cfg.package ] ++ enabledExtraPackages)}:$PATH" + + exitcode=0 + if [ "$#" -gt 0 ]; then + ${lib.getExe cfg.package} run -c ${cfg.configFile} --files "$@" || exitcode=$? + else + if [ -n "''${PRJ_ROOT:-}" ]; then + cd "$PRJ_ROOT" + fi + ${lib.getExe cfg.package} run -c ${cfg.configFile} --all-files || exitcode=$? + fi + + # pre-commit returns 1 when files were modified by hooks. + # For a formatter (`nix fmt`), modifying files is the intended outcome. + # If exit code was 1, re-run to distinguish between successful formatting changes (clean on 2nd pass) + # and actual errors/syntax failures (fails again on 2nd pass). + if [ "$exitcode" -eq 1 ]; then + if [ "$#" -gt 0 ]; then + ${lib.getExe cfg.package} run -c ${cfg.configFile} --files "$@" + else + ${lib.getExe cfg.package} run -c ${cfg.configFile} --all-files + fi + else + exit "$exitcode" + fi + ''; + failedAssertions = builtins.map (x: x.message) (builtins.filter (x: !x.assertion) config.assertions); performAssertions = @@ -279,6 +309,19 @@ in defaultText = lib.literalExpression ""; }; + formatter = + mkOption { + type = types.package; + description = + '' + A wrapper script that runs pre-commit on all files or specified files, + suitable for use as the flake's `formatter` output (`nix fmt`). + ''; + readOnly = true; + default = formatter; + defaultText = lib.literalExpression ""; + }; + shellHook = mkOption { type = types.str; diff --git a/nix/run.nix b/nix/run.nix index b1c861d8..a3fd821b 100644 --- a/nix/run.nix +++ b/nix/run.nix @@ -33,5 +33,5 @@ let in project.config.run // { inherit (project) config extendModules; - inherit (project.config) enabledPackages shellHook; + inherit (project.config) enabledPackages shellHook formatter; } diff --git a/nix/tests/hook-config.nix b/nix/tests/hook-config.nix index 6c19bf4d..7897c5e7 100644 --- a/nix/tests/hook-config.nix +++ b/nix/tests/hook-config.nix @@ -106,6 +106,8 @@ runCommand "hook-config-test" { } ( lib.concatStrings (lib.mapAttrsToList runHookTest hookTests) + lib.concatStrings (lib.mapAttrsToList runAssertionTest assertionTests) + '' + ${lib.optionalString (!lib.isDerivation (run { src = null; addGcRoot = false; }).formatter) "echo 'FAILED: res.formatter is not a derivation'; exit 1"} + ${lib.optionalString (!lib.isDerivation (eval { }).formatter) "echo 'FAILED: (eval {}).formatter is not a derivation'; exit 1"} echo "All hook config tests passed" > $out '' ) diff --git a/template/flake.nix b/template/flake.nix index 274b4313..39977bc8 100644 --- a/template/flake.nix +++ b/template/flake.nix @@ -24,6 +24,8 @@ # Equivalent to inputs'.nixpkgs.legacyPackages.hello; packages.hello = pkgs.hello; pre-commit.settings.hooks.nixpkgs-fmt.enable = true; + # NOTE: `formatter` is automatically configured to run enabled hooks via `nix fmt`. + # You can also use `config.pre-commit.formatter` explicitly if needed. # NOTE: You can also use `config.pre-commit.devShell` devShells.default = pkgs.mkShell { shellHook = ''