Something went wrong. Try again.
A small, fast, drop-in replacement for git-hooks.nix
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273{pkgs}: let inherit (pkgs) lib;
hookSpec = import ./hook-spec.nix {inherit lib;}; scriptGen = import ./script-gen.nix {inherit lib;}; tangledGen = import ./tangled-gen.nix {inherit lib;}; githubActionsGen = import ./github-actions-gen.nix {inherit lib;};
# built from the caller's own `pkgs`; flake consumers get one of these per # system via nixhooks.lib.<system>.presets (see flake.nix). presets = import ./presets.nix {inherit pkgs;};
runtimeLib = builtins.readFile ./hooks-runtime.sh; driverPreCommit = builtins.readFile ./drivers/pre-commit.sh; driverPrePush = builtins.readFile ./drivers/pre-push.sh; driverCommitMsg = builtins.readFile ./drivers/commit-msg.sh; driverRunHooks = builtins.readFile ./drivers/run-hooks.sh;
# lets install-hooks tell a nixhooks-installed hook and other hooks apart marker = "# managed-by: nixhooks -- do not edit directly";
genFunction = calls: '' nixhooks_generated_hooks() { ${ if calls == "" then " :" else calls } } '';
stageBody = driver: calls: '' ${marker} set -euo pipefail ${runtimeLib} ${genFunction calls} ${driver} '';
mkStageScript = name: driver: calls: pkgs.writeShellScriptBin name (stageBody driver calls);
# same script with a non-Nix shebang, for committing into the repo mkPortableScript = name: driver: calls: pkgs.writeTextFile { name = "${name}-portable"; executable = true; text = '' #!/usr/bin/env bash ${stageBody driver calls} ''; };
mkHooks = { hooks, # attrset of name -> hook config, see lib/hook-spec.nix for fields. settings ? {}, # optional {tangled, githubActions, parallel, portable}; see below. }: let tangled = settings.tangled or {}; # optional Tangled pipeline config, see lib/tangled-gen.nix for fields. githubActions = settings.githubActions or {}; # optional GitHub Actions workflow config, see lib/github-actions-gen.nix for fields. parallel = settings.parallel or false; # optionally determines if hooks are ran in parallel # optional hook scripts committed to the repo for non-Nix users portable = { enable = false; dir = ".nixhooks"; # relative to the repo root } // settings.portable or {}; in assert lib.assertMsg (builtins.isBool parallel) "nixhooks: settings.parallel must be a bool"; assert lib.assertMsg (builtins.isBool portable.enable) "nixhooks: settings.portable.enable must be a bool"; assert lib.assertMsg (builtins.isString portable.dir && portable.dir != "" && !lib.hasPrefix "/" portable.dir) "nixhooks: settings.portable.dir must be a non-empty relative path"; let normalized = hookSpec.normalizeHooks hooks; # resolve the top-level switch + per-hook escape hatch into one # effective flag before codegen so lib/script-gen.nix only ever # reads an already-decided hook.parallel scheduled = lib.mapAttrs (_: h: h // {parallel = parallel && !h.serial;}) normalized; tangledCfg = tangledGen.normalizeTangled tangled; githubActionsCfg = githubActionsGen.normalizeGithubActions githubActions;
stageDrivers = { pre-commit = driverPreCommit; pre-push = driverPrePush; commit-msg = driverCommitMsg; }; stageCalls = lib.mapAttrs (stage: _: scriptGen.genStageCalls stage scheduled) stageDrivers;
preCommitHook = mkStageScript "pre-commit-hook" driverPreCommit stageCalls.pre-commit; prePushHook = mkStageScript "pre-push-hook" driverPrePush stageCalls.pre-push; commitMsgHook = mkStageScript "commit-msg-hook" driverCommitMsg stageCalls.commit-msg; runHooks = mkStageScript "run-hooks" driverRunHooks (scriptGen.genAllCalls scheduled);
portableScripts = lib.mapAttrs (stage: driver: mkPortableScript stage driver stageCalls.${stage}) stageDrivers;
# runs body once per stage with $stage, $src (generated) and $dest set portableLoop = body: lib.concatStrings (lib.mapAttrsToList (stage: src: '' stage=${stage} src=${src} dest="$dir/${stage}" ${body} '') portableScripts);
portablePrelude = name: '' set -euo pipefail
if ! git_root=$(git rev-parse --show-toplevel 2>/dev/null); then echo "${name}: not inside a git repository" >&2 exit 1 fi dir="$git_root/"${lib.escapeShellArg portable.dir} '';
genPortableHooks = pkgs.writeShellScriptBin "gen-portable-hooks" '' ${portablePrelude "gen-portable-hooks"} ${portableLoop '' if [[ -e "$dest" ]] && ! grep -q ${lib.escapeShellArg marker} "$dest" 2>/dev/null; then echo "gen-portable-hooks: $dest already exists and is not managed by nixhooks, refusing to overwrite" >&2 exit 1 fi ''} mkdir -p "$dir" ${portableLoop '' install -m 0755 "$src" "$dest" echo "gen-portable-hooks: wrote $dest" ''} '';
checkPortableHooks = pkgs.writeShellScriptBin "check-portable-hooks" '' ${portablePrelude "check-portable-hooks"} stale="" ${portableLoop '' cmp -s "$src" "$dest" || stale="$stale $stage" ''} if [[ -n "$stale" ]]; then echo "nixhooks: portable hooks in "${lib.escapeShellArg portable.dir}" are out of date:$stale" >&2 echo "nixhooks: regenerate with ${genPortableHooks}/bin/gen-portable-hooks" >&2 exit 1 fi '';
installHooks = pkgs.writeShellScriptBin "install-hooks" '' set -euo pipefail
if ! git_dir=$(git rev-parse --git-dir 2>/dev/null); then echo "install-hooks: not inside a git repository" >&2 exit 1 fi
install_one() { local stage="$1" src="$2" local dest="$git_dir/hooks/$stage" if [[ -L "$dest" && "$(readlink "$dest")" == "$src" ]]; then return 0 fi if [[ -e "$dest" && ! -L "$dest" ]] && ! grep -q ${lib.escapeShellArg marker} "$dest" 2>/dev/null; then echo "install-hooks: $dest already exists and is not managed by nixhooks, refusing to overwrite" >&2 exit 1 fi mkdir -p "$git_dir/hooks" ln -sf "$src" "$dest" echo "install-hooks: installed $stage -> $src" }
install_one "pre-commit" "${preCommitHook}/bin/pre-commit-hook" install_one "pre-push" "${prePushHook}/bin/pre-push-hook" install_one "commit-msg" "${commitMsgHook}/bin/commit-msg-hook" '';
tangledPipeline = pkgs.writeText "hooks.yml" '' ${marker} ${tangledGen.mkPipelineText tangled} '';
genTangledPipeline = pkgs.writeShellScriptBin "gen-tangled-pipeline" '' set -euo pipefail
if ! git_root=$(git rev-parse --show-toplevel 2>/dev/null); then echo "gen-tangled-pipeline: not inside a git repository" >&2 exit 1 fi
dest="$git_root/.tangled/workflows/hooks.yml" if [[ -e "$dest" ]] && ! grep -q ${lib.escapeShellArg marker} "$dest" 2>/dev/null; then echo "gen-tangled-pipeline: $dest already exists and is not managed by nixhooks, refusing to overwrite" >&2 exit 1 fi
mkdir -p "$(dirname "$dest")" install -m 0644 ${tangledPipeline} "$dest" echo "gen-tangled-pipeline: wrote $dest" ''; githubActionsWorkflow = pkgs.writeText "hooks.yml" '' ${marker} ${githubActionsGen.mkWorkflowText githubActions} '';
genGithubActionsWorkflow = pkgs.writeShellScriptBin "gen-github-actions-workflow" '' set -euo pipefail
if ! git_root=$(git rev-parse --show-toplevel 2>/dev/null); then echo "gen-github-actions-workflow: not inside a git repository" >&2 exit 1 fi
dest="$git_root/.github/workflows/hooks.yml" if [[ -e "$dest" ]] && ! grep -q ${lib.escapeShellArg marker} "$dest" 2>/dev/null; then echo "gen-github-actions-workflow: $dest already exists and is not managed by nixhooks, refusing to overwrite" >&2 exit 1 fi
mkdir -p "$(dirname "$dest")" install -m 0644 ${githubActionsWorkflow} "$dest" echo "gen-github-actions-workflow: wrote $dest" '';
hookOutputs = { pre-commit-hook = preCommitHook; pre-push-hook = prePushHook; commit-msg-hook = commitMsgHook; run-hooks = runHooks; install-hooks = installHooks; } // lib.optionalAttrs tangledCfg.enable { tangled-pipeline = tangledPipeline; gen-tangled-pipeline = genTangledPipeline; } // lib.optionalAttrs githubActionsCfg.enable { github-actions-workflow = githubActionsWorkflow; gen-github-actions-workflow = genGithubActionsWorkflow; } // lib.optionalAttrs portable.enable { gen-portable-hooks = genPortableHooks; check-portable-hooks = checkPortableHooks; }; shellHook = '' ${installHooks}/bin/install-hooks '' + lib.optionalString portable.enable '' ${checkPortableHooks}/bin/check-portable-hooks || true ''; # deprecated: derivations used to sit at the top level of the result. removedTopLevel = lib.mapAttrs (name: _: throw "nixhooks: `hooks.${name}` has been removed; use `hooks.packages.${name}` instead") hookOutputs; in removedTopLevel // { inherit shellHook; packages = hookOutputs;
# returns a copy of `shell` that installs hooks on entry wrapShell = shell: pkgs.mkShell { inputsFrom = [shell]; inherit shellHook; };
apps = builtins.mapAttrs (_: drv: { type = "app"; program = lib.getExe drv; }) (builtins.removeAttrs hookOutputs ["tangled-pipeline" "github-actions-workflow"]); };in { inherit mkHooks presets; inherit (hookSpec) normalizeHooks normalizeHook defaultHook; inherit (tangledGen) normalizeTangled defaultTangled; inherit (githubActionsGen) normalizeGithubActions defaultGithubActions;}