diff --git a/README.md b/README.md index c307ac7..5da4d9f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # nixhooks -A small, fast, drop-in replacement for [git-hooks.nix](https://github.com/cachix/git-hooks.nix) +A small, fast, drop-in replacement for [git-hooks.nix](https://github.com/cachix/git-hooks.nix). No flake required, no `flake-parts`, no `systems`, no Python, no slop, just plain ol' Nix and Bash. @@ -73,11 +73,31 @@ hooks. = { stages = [ "pre-commit" ]; # subset of [ "pre-commit" "pre-push" ] pass_filenames = true; # append matched files as trailing args always_run = false; # run once with no file filtering/passing + path = [ ]; # packages whose bin/ dirs are prepended to PATH for entry }; ``` `` must match `[A-Za-z0-9_.-]+` +`path` is for tools that internally dispatch to a sibling binary via `PATH` +(e.g. `cargo` finding `cargo-clippy`) -- `entry` itself is always invoked by +absolute path regardless of `path`. + +## Presets +`presets` is a small built-in catalog of common tool configs -- plain +`hooks.`-shaped attrsets you can reference directly or override: + +```nix +nixhooks.mkHooks { + hooks = { + shellcheck = nixhooksLib.presets.shellcheck; + alejandra = nixhooksLib.presets.alejandra // { stages = [ "pre-push" ]; }; + }; +} +``` + +See [lib/presets.nix](./lib/presets.nix) for the full list of presets. + ## Tangled pipelines `mkHooks` can also generate a [Tangled](https://tangled.org) Spindle pipeline: diff --git a/flake.nix b/flake.nix index affaa0c..c7bebe3 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - outputs = {self}: let + outputs = {...}: let npinsSources = import ./npins; systems = [ "x86_64-linux" @@ -17,20 +17,13 @@ perSystem = system: let pkgs = import npinsSources.nixpkgs {inherit system;}; - hooks = (import ./default.nix {inherit pkgs;}).mkHooks { + nixhooksLib = import ./default.nix {inherit pkgs;}; + hooks = nixhooksLib.mkHooks { hooks = { - shellcheck = { - entry = "${pkgs.shellcheck}/bin/shellcheck"; - files = "\\.sh$"; - stages = ["pre-push"]; - }; - - alejandra-check = { - entry = "${pkgs.alejandra}/bin/alejandra"; - args = ["--check"]; - files = "\\.nix$"; - stages = ["pre-push"]; - }; + shellcheck = nixhooksLib.presets.shellcheck // {stages = ["pre-push"];}; + alejandra = nixhooksLib.presets.alejandra // {stages = ["pre-push"];}; + shfmt = nixhooksLib.presets.shfmt // {stages = ["pre-push"];}; + deadnix = nixhooksLib.presets.deadnix // {stages = ["pre-push"];}; }; tangled = { @@ -57,7 +50,7 @@ devShells = builtins.mapAttrs (_: d: { default = d.pkgs.mkShell { - packages = [d.pkgs.npins d.pkgs.alejandra d.pkgs.bats]; + packages = [d.pkgs.shfmt d.pkgs.npins d.pkgs.alejandra d.pkgs.bats]; shellHook = '' ${d.hooks.install-hooks}/bin/install-hooks ''; diff --git a/lib/default.nix b/lib/default.nix index 9af4d0e..4ff7664 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -4,6 +4,7 @@ hookSpec = import ./hook-spec.nix {inherit lib;}; scriptGen = import ./script-gen.nix {inherit lib;}; tangledGen = import ./tangled-gen.nix {inherit lib;}; + presets = import ./presets.nix {inherit pkgs;}; runtimeLib = builtins.readFile ./hooks-runtime.sh; driverPreCommit = builtins.readFile ./drivers/pre-commit.sh; @@ -122,7 +123,7 @@ (builtins.removeAttrs hookOutputs ["tangled-pipeline"]); }; in { - inherit mkHooks; + inherit mkHooks presets; inherit (hookSpec) normalizeHooks normalizeHook defaultHook; inherit (tangledGen) normalizeTangled defaultTangled; } diff --git a/lib/drivers/pre-push.sh b/lib/drivers/pre-push.sh index fe6bb3a..a5a7ec2 100644 --- a/lib/drivers/pre-push.sh +++ b/lib/drivers/pre-push.sh @@ -1,15 +1,15 @@ # shellcheck shell=bash while read -r local_ref local_sha remote_ref remote_sha; do - # local_ref/remote_ref are part of git's pre-push stdin protocol but unused - # here; shellcheck disable=SC2034 keeps the field positions self-documenting. - : "$local_ref" "$remote_ref" - # local_sha all-zero: this is a ref *deletion* (e.g. `git push origin :branch`), - # nothing local to check. - [[ "$local_sha" =~ ^0+$ ]] && continue + # local_ref/remote_ref are part of git's pre-push stdin protocol but unused + # here; shellcheck disable=SC2034 keeps the field positions self-documenting. + : "$local_ref" "$remote_ref" + # local_sha all-zero: this is a ref *deletion* (e.g. `git push origin :branch`), + # nothing local to check. + [[ "$local_sha" =~ ^0+$ ]] && continue - nixhooks_collect_prepush_files "$remote_sha" "$local_sha" - nixhooks_generated_hooks + nixhooks_collect_prepush_files "$remote_sha" "$local_sha" + nixhooks_generated_hooks done nixhooks_summary diff --git a/lib/hook-spec.nix b/lib/hook-spec.nix index ea06aca..bf1151c 100644 --- a/lib/hook-spec.nix +++ b/lib/hook-spec.nix @@ -7,6 +7,7 @@ stages = ["pre-commit"]; pass_filenames = true; always_run = false; + path = []; # extra packages whose bin/ dirs are prepended to PATH for entry }; validStages = [ @@ -29,7 +30,9 @@ "nixhooks: hook '${name}' has invalid stage(s) in ${builtins.toJSON merged.stages}; valid stages are: ${builtins.concatStringsSep ", " validStages}"; assert lib.assertMsg ( merged.stages != [] - ) "nixhooks: hook '${name}' must declare at least one stage"; merged; + ) "nixhooks: hook '${name}' must declare at least one stage"; + assert lib.assertMsg (builtins.isList merged.path) + "nixhooks: hook '${name}' 'path' must be a list of packages"; merged; in { inherit defaultHook normalizeHook; normalizeHooks = hooks: lib.mapAttrs normalizeHook (lib.filterAttrs (_: h: h.enable or true) hooks); diff --git a/lib/hooks-runtime.sh b/lib/hooks-runtime.sh index 9036734..5477273 100644 --- a/lib/hooks-runtime.sh +++ b/lib/hooks-runtime.sh @@ -1,168 +1,173 @@ # shellcheck shell=bash -# nixhooks runtime library +# nixhooks runtime library NIXHOOKS_FAILED=0 NIXHOOKS_FILES=() _nixhooks_skip_list() { - IFS=',' read -ra _nixhooks_skip <<<"${SKIP:-}" + IFS=',' read -ra _nixhooks_skip <<<"${SKIP:-}" } _nixhooks_skip_list should_skip() { - local name="$1" s - for s in "${_nixhooks_skip[@]}"; do - [[ "$s" == "$name" ]] && return 0 - done - return 1 + local name="$1" s + for s in "${_nixhooks_skip[@]}"; do + [[ "$s" == "$name" ]] && return 0 + done + return 1 } -# Args: name files_regex exclude_regex pass_filenames(0|1) always_run(0|1) entry [extra_args...] -# Matches against $NIXHOOKS_FILES populated by the caller before invocation +# Args: name files_regex exclude_regex pass_filenames(0|1) always_run(0|1) entry path_prefix [extra_args...] +# path_prefix (possibly empty) is prepended to PATH so entry can find sibling +# binaries it dispatches to internally. Matches against $NIXHOOKS_FILES +# populated by the caller before invocation run_hook() { - local name="$1" files_re="$2" exclude_re="$3" pass_filenames="$4" always_run="$5" entry="$6" - shift 6 - local extra_args=("$@") - - if should_skip "$name"; then - echo "nixhooks: skip $name (SKIP)" - return 0 - fi - - local matched=() f - if [[ "$always_run" != 1 ]]; then - for f in "${NIXHOOKS_FILES[@]}"; do - [[ "$f" =~ $files_re ]] || continue - [[ "$exclude_re" != '^$' && "$f" =~ $exclude_re ]] && continue - matched+=("$f") - done - if [[ ${#matched[@]} -eq 0 ]]; then - echo "nixhooks: skip $name (no matching files)" - return 0 - fi - fi - - echo "nixhooks: run $name" - local cmd=("$entry" "${extra_args[@]}") - if [[ "$pass_filenames" == 1 && "$always_run" != 1 ]]; then - cmd+=("${matched[@]}") - fi - - if ! "${cmd[@]}"; then - echo "nixhooks: FAIL $name" - NIXHOOKS_FAILED=1 - fi + local name="$1" files_re="$2" exclude_re="$3" pass_filenames="$4" always_run="$5" entry="$6" path_prefix="$7" + shift 7 + local extra_args=("$@") + + if should_skip "$name"; then + echo "nixhooks: skip $name (SKIP)" + return 0 + fi + + local matched=() f + if [[ "$always_run" != 1 ]]; then + for f in "${NIXHOOKS_FILES[@]}"; do + [[ "$f" =~ $files_re ]] || continue + [[ "$exclude_re" != '^$' && "$f" =~ $exclude_re ]] && continue + matched+=("$f") + done + if [[ ${#matched[@]} -eq 0 ]]; then + echo "nixhooks: skip $name (no matching files)" + return 0 + fi + fi + + echo "nixhooks: run $name" + local cmd=("$entry" "${extra_args[@]}") + if [[ "$pass_filenames" == 1 && "$always_run" != 1 ]]; then + cmd+=("${matched[@]}") + fi + + local run_path="$PATH" + [[ -n "$path_prefix" ]] && run_path="$path_prefix:$PATH" + + if ! PATH="$run_path" "${cmd[@]}"; then + echo "nixhooks: FAIL $name" + NIXHOOKS_FAILED=1 + fi } nixhooks_summary() { - if [[ "$NIXHOOKS_FAILED" -ne 0 ]]; then - echo "nixhooks: one or more hooks failed" >&2 - exit 1 - fi + if [[ "$NIXHOOKS_FAILED" -ne 0 ]]; then + echo "nixhooks: one or more hooks failed" >&2 + exit 1 + fi } -# File discovery +# File discovery nixhooks_collect_precommit_files() { - local against - if git rev-parse --verify -q HEAD >/dev/null; then - against=HEAD - else - against="$(git hash-object -t tree /dev/null)" - fi - NIXHOOKS_FILES=() - while IFS= read -r -d '' f; do - NIXHOOKS_FILES+=("$f") - done < <(git diff --cached --name-only --diff-filter=ACM -z "$against") + local against + if git rev-parse --verify -q HEAD >/dev/null; then + against=HEAD + else + against="$(git hash-object -t tree /dev/null)" + fi + NIXHOOKS_FILES=() + while IFS= read -r -d '' f; do + NIXHOOKS_FILES+=("$f") + done < <(git diff --cached --name-only --diff-filter=ACM -z "$against") } nixhooks_collect_repo_files() { - NIXHOOKS_FILES=() - while IFS= read -r -d '' f; do - NIXHOOKS_FILES+=("$f") - done < <(git ls-files -z) + NIXHOOKS_FILES=() + while IFS= read -r -d '' f; do + NIXHOOKS_FILES+=("$f") + done < <(git ls-files -z) } nixhooks_collect_prepush_files() { - local remote_sha="$1" local_sha="$2" - NIXHOOKS_FILES=() - if [[ "$remote_sha" =~ ^0+$ ]]; then - # new branch / no upstream: no meaningful base to diff against - while IFS= read -r -d '' f; do - NIXHOOKS_FILES+=("$f") - done < <(git ls-files -z) - else - while IFS= read -r -d '' f; do - NIXHOOKS_FILES+=("$f") - done < <(git diff --name-only -z "$remote_sha" "$local_sha") - fi + local remote_sha="$1" local_sha="$2" + NIXHOOKS_FILES=() + if [[ "$remote_sha" =~ ^0+$ ]]; then + # new branch / no upstream: no meaningful base to diff against + while IFS= read -r -d '' f; do + NIXHOOKS_FILES+=("$f") + done < <(git ls-files -z) + else + while IFS= read -r -d '' f; do + NIXHOOKS_FILES+=("$f") + done < <(git diff --name-only -z "$remote_sha" "$local_sha") + fi } -# partial-staging isolation +# partial-staging isolation # Hooks run against a throwaway worktree checked out from what's staged NIXHOOKS_WORKTREE_DIR="" NIXHOOKS_STAGED_TREE="" NIXHOOKS_REPO_ROOT="" nixhooks_worktree_cleanup() { - if [[ -n "$NIXHOOKS_WORKTREE_DIR" ]]; then - git worktree remove --force "$NIXHOOKS_WORKTREE_DIR" 2>/dev/null || rm -rf "$NIXHOOKS_WORKTREE_DIR" - NIXHOOKS_WORKTREE_DIR="" - fi + if [[ -n "$NIXHOOKS_WORKTREE_DIR" ]]; then + git worktree remove --force "$NIXHOOKS_WORKTREE_DIR" 2>/dev/null || rm -rf "$NIXHOOKS_WORKTREE_DIR" + NIXHOOKS_WORKTREE_DIR="" + fi } -# Must run after nixhooks_collect_precommit_files (uses the real repo's index/HEAD). +# Must run after nixhooks_collect_precommit_files (uses the real repo's index/HEAD). nixhooks_enter_staged_worktree() { - NIXHOOKS_REPO_ROOT="$(pwd)" - NIXHOOKS_STAGED_TREE="$(git write-tree)" - - local commit - if git rev-parse --verify -q HEAD >/dev/null; then - commit="$(git commit-tree "$NIXHOOKS_STAGED_TREE" -p HEAD -m "nixhooks: staged snapshot")" - else - commit="$(git commit-tree "$NIXHOOKS_STAGED_TREE" -m "nixhooks: staged snapshot")" - fi - - # git invokes hooks with GIT_INDEX_FILE. Once we cd into the - # throwaway worktree below, that stale relative path would be misresolved - # against the new cwd/gitdir, so we save and clear them here - NIXHOOKS_SAVED_GIT_INDEX_FILE="${GIT_INDEX_FILE-}" - NIXHOOKS_SAVED_GIT_DIR="${GIT_DIR-}" - NIXHOOKS_SAVED_GIT_WORK_TREE="${GIT_WORK_TREE-}" - unset GIT_INDEX_FILE GIT_DIR GIT_WORK_TREE - - NIXHOOKS_WORKTREE_DIR="$(mktemp -d)" - rmdir "$NIXHOOKS_WORKTREE_DIR" # git worktree add creates the dir itself - trap nixhooks_worktree_cleanup EXIT - git worktree add --quiet --detach "$NIXHOOKS_WORKTREE_DIR" "$commit" - cd "$NIXHOOKS_WORKTREE_DIR" || exit 1 + NIXHOOKS_REPO_ROOT="$(pwd)" + NIXHOOKS_STAGED_TREE="$(git write-tree)" + + local commit + if git rev-parse --verify -q HEAD >/dev/null; then + commit="$(git commit-tree "$NIXHOOKS_STAGED_TREE" -p HEAD -m "nixhooks: staged snapshot")" + else + commit="$(git commit-tree "$NIXHOOKS_STAGED_TREE" -m "nixhooks: staged snapshot")" + fi + + # git invokes hooks with GIT_INDEX_FILE. Once we cd into the + # throwaway worktree below, that stale relative path would be misresolved + # against the new cwd/gitdir, so we save and clear them here + NIXHOOKS_SAVED_GIT_INDEX_FILE="${GIT_INDEX_FILE-}" + NIXHOOKS_SAVED_GIT_DIR="${GIT_DIR-}" + NIXHOOKS_SAVED_GIT_WORK_TREE="${GIT_WORK_TREE-}" + unset GIT_INDEX_FILE GIT_DIR GIT_WORK_TREE + + NIXHOOKS_WORKTREE_DIR="$(mktemp -d)" + rmdir "$NIXHOOKS_WORKTREE_DIR" # git worktree add creates the dir itself + trap nixhooks_worktree_cleanup EXIT + git worktree add --quiet --detach "$NIXHOOKS_WORKTREE_DIR" "$commit" + cd "$NIXHOOKS_WORKTREE_DIR" || exit 1 } # Re-stages anything a hook mutated inside the isolated worktree. If the real -# working tree file was identical to what was staged beforehand also updates +# working tree file was identical to what was staged beforehand also updates # the real working tree file nixhooks_reconcile_worktree() { - cd "$NIXHOOKS_REPO_ROOT" || exit 1 - [[ -n "$NIXHOOKS_SAVED_GIT_INDEX_FILE" ]] && export GIT_INDEX_FILE="$NIXHOOKS_SAVED_GIT_INDEX_FILE" - [[ -n "$NIXHOOKS_SAVED_GIT_DIR" ]] && export GIT_DIR="$NIXHOOKS_SAVED_GIT_DIR" - [[ -n "$NIXHOOKS_SAVED_GIT_WORK_TREE" ]] && export GIT_WORK_TREE="$NIXHOOKS_SAVED_GIT_WORK_TREE" - - local f mode orig_blob new_hash real_blob - for f in "${NIXHOOKS_FILES[@]}"; do - orig_blob="$(git rev-parse "$NIXHOOKS_STAGED_TREE:$f" 2>/dev/null)" || continue - new_hash="$(git hash-object "$NIXHOOKS_WORKTREE_DIR/$f" 2>/dev/null)" || continue - [[ "$new_hash" == "$orig_blob" ]] && continue # hook didn't touch this file - - new_hash="$(git hash-object -w "$NIXHOOKS_WORKTREE_DIR/$f")" - mode="$(git ls-tree "$NIXHOOKS_STAGED_TREE" -- "$f" | cut -d' ' -f1)" - - real_blob="$(git hash-object "$f" 2>/dev/null || true)" - if [[ "$real_blob" == "$orig_blob" ]]; then - cp "$NIXHOOKS_WORKTREE_DIR/$f" "$f" - fi - - git update-index --cacheinfo "$mode,$new_hash,$f" - echo "nixhooks: re-staged $f (modified by a hook)" - done - - nixhooks_worktree_cleanup + cd "$NIXHOOKS_REPO_ROOT" || exit 1 + [[ -n "$NIXHOOKS_SAVED_GIT_INDEX_FILE" ]] && export GIT_INDEX_FILE="$NIXHOOKS_SAVED_GIT_INDEX_FILE" + [[ -n "$NIXHOOKS_SAVED_GIT_DIR" ]] && export GIT_DIR="$NIXHOOKS_SAVED_GIT_DIR" + [[ -n "$NIXHOOKS_SAVED_GIT_WORK_TREE" ]] && export GIT_WORK_TREE="$NIXHOOKS_SAVED_GIT_WORK_TREE" + + local f mode orig_blob new_hash real_blob + for f in "${NIXHOOKS_FILES[@]}"; do + orig_blob="$(git rev-parse "$NIXHOOKS_STAGED_TREE:$f" 2>/dev/null)" || continue + new_hash="$(git hash-object "$NIXHOOKS_WORKTREE_DIR/$f" 2>/dev/null)" || continue + [[ "$new_hash" == "$orig_blob" ]] && continue # hook didn't touch this file + + new_hash="$(git hash-object -w "$NIXHOOKS_WORKTREE_DIR/$f")" + mode="$(git ls-tree "$NIXHOOKS_STAGED_TREE" -- "$f" | cut -d' ' -f1)" + + real_blob="$(git hash-object "$f" 2>/dev/null || true)" + if [[ "$real_blob" == "$orig_blob" ]]; then + cp "$NIXHOOKS_WORKTREE_DIR/$f" "$f" + fi + + git update-index --cacheinfo "$mode,$new_hash,$f" + echo "nixhooks: re-staged $f (modified by a hook)" + done + + nixhooks_worktree_cleanup } diff --git a/lib/presets.nix b/lib/presets.nix new file mode 100644 index 0000000..66e497f --- /dev/null +++ b/lib/presets.nix @@ -0,0 +1,94 @@ +{pkgs}: { + shellcheck = { + entry = "${pkgs.shellcheck}/bin/shellcheck"; + files = "\\.sh$"; + }; + + alejandra = { + entry = "${pkgs.alejandra}/bin/alejandra"; + args = ["--check"]; + files = "\\.nix$"; + }; + + statix = { + entry = "${pkgs.statix}/bin/statix"; + args = ["check"]; + files = "\\.nix$"; + pass_filenames = false; + always_run = true; + }; + + deadnix = { + entry = "${pkgs.deadnix}/bin/deadnix"; + args = ["--fail"]; + files = "\\.nix$"; + }; + + typos = { + entry = "${pkgs.typos}/bin/typos"; + }; + + harper = { + entry = "${pkgs.harper}/bin/harper-cli"; + args = ["lint" "--no-color"]; + files = "\\.(md|txt)$"; + }; + + shfmt = { + entry = "${pkgs.shfmt}/bin/shfmt"; + args = ["-d"]; + files = "\\.sh$"; + }; + + taplo = { + entry = "${pkgs.taplo}/bin/taplo"; + args = ["fmt" "--check"]; + files = "\\.toml$"; + }; + + yamllint = { + entry = "${pkgs.yamllint}/bin/yamllint"; + files = "\\.ya?ml$"; + }; + + prettier = { + entry = "${pkgs.prettier}/bin/prettier"; + args = ["--check"]; + files = "\\.(html|css|scss|less|js|jsx|ts|tsx)$"; + }; + + eslint = { + entry = "${pkgs.eslint}/bin/eslint"; + files = "\\.(js|jsx|ts|tsx)$"; + }; + + rustfmt = { + entry = "${pkgs.rustfmt}/bin/rustfmt"; + args = ["--check"]; + files = "\\.rs$"; + }; + + clippy = { + # cargo dispatches `clippy` to `cargo-clippy` via PATH + entry = "${pkgs.cargo}/bin/cargo"; + args = ["clippy" "--" "-D" "warnings"]; + path = [pkgs.clippy]; + files = "\\.rs$"; + pass_filenames = false; + always_run = true; + }; + + govet = { + entry = "${pkgs.go}/bin/go"; + args = ["vet" "./..."]; + files = "\\.go$"; + pass_filenames = false; + always_run = true; + }; + + zigfmt = { + entry = "${pkgs.zig}/bin/zig"; + args = ["fmt" "--check"]; + files = "\\.zig$"; + }; +} diff --git a/lib/script-gen.nix b/lib/script-gen.nix index 861f420..da2aacf 100644 --- a/lib/script-gen.nix +++ b/lib/script-gen.nix @@ -16,6 +16,7 @@ else "0" ) hook.entry + (lib.makeBinPath hook.path) ] ++ hook.args; in " run_hook ${lib.concatMapStringsSep " " lib.escapeShellArg args}"; diff --git a/tests/hooks-runtime.bats b/tests/hooks-runtime.bats index 56c9aa7..1c8c5b4 100644 --- a/tests/hooks-runtime.bats +++ b/tests/hooks-runtime.bats @@ -34,7 +34,7 @@ teardown() { @test "run_hook passes only files matching 'files' to the entry" { NIXHOOKS_FILES=("a.sh" "b.txt") - run_hook myhook '\.sh$' '^$' 1 0 "$RECORDER" + run_hook myhook '\.sh$' '^$' 1 0 "$RECORDER" '' run cat "$RECORD_FILE" [[ "$output" == *"a.sh"* ]] [[ "$output" != *"b.txt"* ]] @@ -42,7 +42,7 @@ teardown() { @test "run_hook excludes files matching 'exclude'" { NIXHOOKS_FILES=("a.sh" "vendor/b.sh") - run_hook myhook '\.sh$' '^vendor/' 1 0 "$RECORDER" + run_hook myhook '\.sh$' '^vendor/' 1 0 "$RECORDER" '' run cat "$RECORD_FILE" [[ "$output" == *"a.sh"* ]] [[ "$output" != *"vendor/b.sh"* ]] @@ -50,13 +50,13 @@ teardown() { @test "run_hook does not invoke the entry when nothing matches" { NIXHOOKS_FILES=("a.txt") - run_hook myhook '\.sh$' '^$' 1 0 "$RECORDER" + run_hook myhook '\.sh$' '^$' 1 0 "$RECORDER" '' [ ! -e "$RECORD_FILE" ] } @test "run_hook with always_run=1 invokes the entry with no files matched" { NIXHOOKS_FILES=() - run_hook myhook '.*' '^$' 1 1 "$RECORDER" + run_hook myhook '.*' '^$' 1 1 "$RECORDER" '' [ -e "$RECORD_FILE" ] # $(...) strips the recorder's trailing newline, so this is empty iff no # arguments were passed (printf '%s\n' with zero args still emits one blank line). @@ -65,31 +65,69 @@ teardown() { @test "run_hook with pass_filenames=0 invokes the entry without filenames" { NIXHOOKS_FILES=("a.sh") - run_hook myhook '\.sh$' '^$' 0 0 "$RECORDER" + run_hook myhook '\.sh$' '^$' 0 0 "$RECORDER" '' [ -e "$RECORD_FILE" ] [ -z "$(cat "$RECORD_FILE")" ] } +# --- run_hook: path_prefix ------------------------------------------------- + +@test "run_hook prepends path_prefix to PATH before invoking the entry" { + local extra_bin_dir="$TEST_REPO/extra_bin" + mkdir -p "$extra_bin_dir" + cat >"$extra_bin_dir/sibling-tool" <<'EOF' +#!/bin/sh +exit 0 +EOF + chmod +x "$extra_bin_dir/sibling-tool" + + local checker="$TEST_REPO/checker.sh" + cat >"$checker" <<'EOF' +#!/bin/sh +command -v sibling-tool +EOF + chmod +x "$checker" + + NIXHOOKS_FILES=() + run run_hook myhook '.*' '^$' 1 1 "$checker" "$extra_bin_dir" + [ "$status" -eq 0 ] + [[ "$output" == *"$extra_bin_dir/sibling-tool"* ]] +} + +@test "run_hook does not touch PATH when path_prefix is empty" { + local checker="$TEST_REPO/checker.sh" + cat >"$checker" <<'EOF' +#!/bin/sh +command -v sibling-tool +EOF + chmod +x "$checker" + + NIXHOOKS_FAILED=0 + NIXHOOKS_FILES=() + run_hook myhook '.*' '^$' 1 1 "$checker" '' + [ "$NIXHOOKS_FAILED" -eq 1 ] +} + # --- run_hook: SKIP and failure aggregation ------------------------------- @test "run_hook honors SKIP and never invokes the entry" { _nixhooks_skip=("myhook") NIXHOOKS_FILES=("a.sh") - run_hook myhook '\.sh$' '^$' 1 0 "$RECORDER" + run_hook myhook '\.sh$' '^$' 1 0 "$RECORDER" '' [ ! -e "$RECORD_FILE" ] } @test "run_hook sets NIXHOOKS_FAILED when the entry fails, without aborting" { NIXHOOKS_FAILED=0 NIXHOOKS_FILES=("a.sh") - run_hook myhook '\.sh$' '^$' 1 0 false + run_hook myhook '\.sh$' '^$' 1 0 false '' [ "$NIXHOOKS_FAILED" -eq 1 ] } @test "run_hook leaves NIXHOOKS_FAILED untouched when the entry succeeds" { NIXHOOKS_FAILED=0 NIXHOOKS_FILES=("a.sh") - run_hook myhook '\.sh$' '^$' 1 0 true + run_hook myhook '\.sh$' '^$' 1 0 true '' [ "$NIXHOOKS_FAILED" -eq 0 ] }