#!/usr/bin/env bash set -euo pipefail repo_root=$(git rev-parse --show-toplevel) cd "$repo_root" # shellcheck source=linux_configuration/.githooks/lib/common.sh source "${repo_root}/linux_configuration/.githooks/lib/common.sh" printf 'Verifying hook wiring and toolchain before committing...\n' ensure_hooks_path verify_toolchain printf ' ✓ toolchain complete\n' printf 'Running auto-fixers and shell_check before committing...\n' # Get list of staged shell files. zsh is deliberately excluded: neither shfmt # nor shellcheck parses zsh (SC1071), so .zsh files get `zsh -n` further down. mapfile -t staged_files < <(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(sh|bash)$' || true) if [[ ${#staged_files[@]} -gt 0 ]]; then printf 'Auto-fixing %d shell file(s)...\n' "${#staged_files[@]}" # Auto-fix with shfmt (guaranteed present by verify_toolchain) printf ' → Running shfmt...\n' for file in "${staged_files[@]}"; do if [[ -f $file ]]; then shfmt -w "$file" 2> /dev/null || true fi done # Re-stage the auto-fixed files for file in "${staged_files[@]}"; do if [[ -f $file ]]; then git add "$file" fi done printf ' ✓ Auto-fixes applied and staged\n' fi printf 'Running shell_check validation...\n' # Run shell_check to validate all checks # Note: We only validate the staged files since we just auto-formatted them # Validate only shellcheck, not shfmt (we already applied formatting) mapfile -d '' -t staged_shell_files < <(git diff --cached --name-only --diff-filter=ACM -z | grep -zE '\.(sh|bash)$' || true) if [[ ${#staged_shell_files[@]} -gt 0 ]]; then # Run shellcheck on staged files (guaranteed present by verify_toolchain) # -x: follow source directives # -S warning: only fail on warning or higher (not info-level SC1091) if ! shellcheck -x -S warning "${staged_shell_files[@]}" 2>&1; then printf '\nCommit aborted: shellcheck found issues.\n' >&2 printf 'Fix the remaining problems and retry the commit.\n' >&2 exit 1 fi fi # zsh files: syntax check only, with the shell that will actually run them. mapfile -d '' -t staged_zsh_files < <(git diff --cached --name-only --diff-filter=ACM -z | grep -zE '\.zsh$' || true) for file in "${staged_zsh_files[@]}"; do if ! zsh -n "$file"; then printf '\nCommit aborted: zsh -n found a syntax error in %s.\n' "$file" >&2 exit 1 fi done printf 'shell_check passed.\n' # Run duplicate code detection printf 'Running duplicate code detection...\n' JSCPD_BIN="${HOME}/.local/node_modules/.bin/jscpd" # Install jscpd if not present if [[ ! -x $JSCPD_BIN ]]; then printf ' → jscpd not found, installing...\n' if ! npm install --prefix ~/.local jscpd 2>&1; then printf '\nCommit aborted: failed to install jscpd.\n' >&2 printf 'Please install manually: npm install --prefix ~/.local jscpd\n' >&2 exit 1 fi printf ' ✓ jscpd installed\n' fi # Run jscpd and capture output # --min-lines 14: minimum 14 lines to consider a clone (ignores small boilerplate) # --min-tokens 25: minimum 25 tokens to consider a clone # --threshold 2: fail if more than 2% duplication # Excluded from the scan, and why: # .venv/ vendored third-party shell (virtualenv, tqdm completions). # .ci-mirror-venv/ # Not ours, and it alone pushed the repo over the bar: the # same scan reads 2.50% with it and 1.67% without. # .ci-mirror-venv/ is the SAME third-party shell under a # different name -- the clean-venv the ci-mirror pre-push gate # builds. It is untracked and generated, but jscpd walks the # working tree, not the index, so it was still being scanned. # Extracting digital_wellbeing shrank the denominator enough # for those virtualenv/tqdm clones to push the total to 2.09% # and block a commit that added no duplication at all. # lib/tests/ a fixture IS repetitive by design; the 2% rule exists to # stop duplicated production logic, not shared test setup. # Needed headroom for the Phase 3 coverage campaign. jscpd_output=$("$JSCPD_BIN" \ --pattern "**/*.sh" \ --min-lines 14 \ --min-tokens 25 \ --threshold 2 \ --reporters "console" \ --ignore "**/node_modules/**,**/.git/**,**/misc/**,**/*.txt,**/.venv/**,**/.ci-mirror-venv/**,**/lib/tests/**,**/tests/**" \ . 2>&1) || jscpd_exit=$? if [[ ${jscpd_exit:-0} -ne 0 ]]; then printf '\n%s\n' "$jscpd_output" printf '\nCommit aborted: duplicate code exceeds 2%% threshold.\n' >&2 printf 'Consider extracting common code to scripts/lib/common.sh\n' >&2 printf 'To see all duplicates: %s --pattern "**/*.sh" --min-lines 5 .\n' "$JSCPD_BIN" >&2 exit 1 fi printf ' ✓ Duplication check passed (under 2%% threshold)\n' # Run pre-commit framework hooks (.pre-commit-config.yaml) # This covers: Python (ruff, mypy, pylint, bandit, flake8, autoflake), # C/C++ (clang-format, cppcheck, flawfinder), TypeScript (eslint), # shell (shellcheck), and general checks (trailing-whitespace, etc.) printf '\nRunning pre-commit framework hooks...\n' if ! pre-commit run --hook-stage pre-commit; then printf '\nCommit aborted: pre-commit hooks failed.\n' >&2 printf 'Fix the issues above and retry the commit.\n' >&2 exit 1 fi printf ' ✓ pre-commit framework hooks passed\n' printf 'All checks passed. Proceeding with commit.\n'