Something went wrong. Try again.
A small, fast, drop-in replacement for git-hooks.nix
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259# shellcheck shell=bash# nixhooks runtime library
NIXHOOKS_FAILED=0NIXHOOKS_FILES=()
NIXHOOKS_PARALLEL_PIDS=()NIXHOOKS_PARALLEL_NAMES=()NIXHOOKS_PARALLEL_LOGS=()NIXHOOKS_PARALLEL_JOB_DIR=""
# files_re/exclude_re pair -> path of a file holding the matched filenames.# A file, not a variable, because $(...) strips embedded NULs and the# matches are NUL-delimited. Lets hooks that share a pattern reuse one match# instead of re-scanning $NIXHOOKS_FILES each; see nixhooks_matched_files.declare -gA NIXHOOKS_MATCH_CACHE=()NIXHOOKS_MATCH_CACHE_DIR=""NIXHOOKS_MATCH_CACHE_COUNTER=0
_nixhooks_skip_list() { 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}
# Populates the caller's array (named by $3) with $NIXHOOKS_FILES entries# matching files_re and not exclude_re ("^$" means exclude nothing).# Computed once per pair and cached in NIXHOOKS_MATCH_CACHE.nixhooks_matched_files() { local files_re="$1" exclude_re="$2" local -n out_matched="$3" local key="${files_re}"$'\x1e'"${exclude_re}"
if [[ -z "${NIXHOOKS_MATCH_CACHE[$key]+set}" ]]; then [[ -z "$NIXHOOKS_MATCH_CACHE_DIR" ]] && NIXHOOKS_MATCH_CACHE_DIR="$(mktemp -d)" local cache_file="$NIXHOOKS_MATCH_CACHE_DIR/$((NIXHOOKS_MATCH_CACHE_COUNTER++))" if [[ "$exclude_re" != '^$' ]]; then printf '%s\0' "${NIXHOOKS_FILES[@]}" | { grep -zE -- "$files_re" || true; } | { grep -zvE -- "$exclude_re" || true; } \ >"$cache_file" else printf '%s\0' "${NIXHOOKS_FILES[@]}" | { grep -zE -- "$files_re" || true; } \ >"$cache_file" fi NIXHOOKS_MATCH_CACHE["$key"]="$cache_file" fi
# shellcheck disable=SC2034 # written via nameref; read by the caller mapfile -d '' out_matched <"${NIXHOOKS_MATCH_CACHE[$key]}"}
# Warms NIXHOOKS_MATCH_CACHE for a pair before any hooks run, so parallel# jobs (forked copies of this process) inherit it instead of each# recomputing their own. Called by generated scripts; see script-gen.nix.nixhooks_precompute_matches() { local -a _nixhooks_discard nixhooks_matched_files "$1" "$2" _nixhooks_discard}
# 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 invocationrun_hook() { 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=() if [[ "$always_run" != 1 ]]; then nixhooks_matched_files "$files_re" "$exclude_re" matched 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 return 1 fi return 0}
# Args: same as run_hook.run_hook_parallel() { local name="$1" if [[ -z "$NIXHOOKS_PARALLEL_JOB_DIR" ]]; then NIXHOOKS_PARALLEL_JOB_DIR="$(mktemp -d)" fi local logfile="$NIXHOOKS_PARALLEL_JOB_DIR/$name.log" run_hook "$@" >"$logfile" 2>&1 & NIXHOOKS_PARALLEL_PIDS+=("$!") NIXHOOKS_PARALLEL_NAMES+=("$name") NIXHOOKS_PARALLEL_LOGS+=("$logfile")}
# Waits for every job launched via run_hook_parallel flushing each one's# buffered output once it finishes and folding a nonzero exit into# NIXHOOKS_FAILED. Always exits 0.nixhooks_wait_parallel() { local i st content for i in "${!NIXHOOKS_PARALLEL_PIDS[@]}"; do st=0 wait "${NIXHOOKS_PARALLEL_PIDS[$i]}" || st=$? content="$(<"${NIXHOOKS_PARALLEL_LOGS[$i]}")" [[ -n "$content" ]] && printf '%s\n' "$content" [[ "$st" -ne 0 ]] && NIXHOOKS_FAILED=1 done rm -f "${NIXHOOKS_PARALLEL_LOGS[@]}" NIXHOOKS_PARALLEL_PIDS=() NIXHOOKS_PARALLEL_NAMES=() NIXHOOKS_PARALLEL_LOGS=() [[ -n "$NIXHOOKS_PARALLEL_JOB_DIR" ]] && rmdir "$NIXHOOKS_PARALLEL_JOB_DIR" 2>/dev/null NIXHOOKS_PARALLEL_JOB_DIR=""}
nixhooks_summary() { [[ -n "$NIXHOOKS_MATCH_CACHE_DIR" ]] && rm -rf "$NIXHOOKS_MATCH_CACHE_DIR" if [[ "$NIXHOOKS_FAILED" -ne 0 ]]; then echo "nixhooks: one or more hooks failed" >&2 exit 1 fi}
# File discoverynixhooks_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")}
nixhooks_collect_repo_files() { NIXHOOKS_FILES=() while IFS= read -r -d '' f; do NIXHOOKS_FILES+=("$f") done < <(git ls-files -z)}
nixhooks_collect_commitmsg_files() { NIXHOOKS_FILES=("$1")}
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 --diff-filter=ACM -z "$remote_sha" "$local_sha") fi}
# partial-staging isolation# Hooks run against a throwaway worktree checked out from what's stagedNIXHOOKS_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}
# 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}
# 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# the real working tree filenixhooks_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}