#!/usr/bin/env bash # # pre-commit — format and lint staged Swift files before commit. # Refreshes the index only for fully staged files; partially staged files must be re-staged manually. set -euo pipefail ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || true)" if [[ -z "${ROOT_DIR}" ]]; then echo "pre-commit: could not determine git repository root." exit 1 fi cd "${ROOT_DIR}" if ! command -v mint >/dev/null 2>&1; then echo "pre-commit: mint is not installed." echo "Run ./scripts/bootstrap (recommended) or: brew install mint && mint bootstrap" exit 1 fi # BrewUILint runs over the WHOLE tree (Homebrew + Sources), not just the staged files. Its # nonisolated-extension rule needs to see every `nonisolated` type declaration to flag a bad # extension on it, and that declaration may live in an unstaged file in another package. Tests # (Tests/) are intentionally excluded. Builds the tool first (cached in Tools/BrewUILint/.build). echo "pre-commit: running BrewUILint (whole tree)..." if ! xcrun swift build --package-path Tools/BrewUILint -c release >/dev/null 2>&1; then echo "pre-commit: failed to build BrewUILint (Tools/BrewUILint) — fix the toolchain/build and retry." exit 1 fi brewuilint_bin="$(xcrun swift build --package-path Tools/BrewUILint -c release --show-bin-path)/BrewUILint" set +e brewuilint_output="$(find Homebrew HomebrewUpgradeHelper Sources -name '*.swift' -print0 | xargs -0 "${brewuilint_bin}" 2>&1)" brewuilint_status=$? set -e if [[ ${brewuilint_status} -ne 0 ]]; then echo "" echo "pre-commit: commit blocked due to BrewUILint violations:" echo "" echo "${brewuilint_output}" exit 1 fi swift_files=() while IFS= read -r -d '' file; do if [[ "${file}" == *.swift ]]; then swift_files+=("${file}") fi done < <(git diff --cached --name-only --diff-filter=ACMR -z) if [[ ${#swift_files[@]} -eq 0 ]]; then echo "pre-commit: no staged Swift files, skipping format/lint checks." exit 0 fi # Files with unstaged hunks must not be blanket `git add`ed — that would defeat selective staging. partially_staged_files=() worktree_hashes_before=() for file in "${swift_files[@]}"; do if ! git diff --quiet -- "${file}"; then partially_staged_files+=("${file}") fi worktree_hashes_before+=("$(git hash-object -w "${file}")") done echo "pre-commit: formatting staged Swift files (mint run swiftformat)..." mint run swiftformat "${swift_files[@]}" echo "pre-commit: applying auto-fixes (mint run swiftlint --fix)..." set +e mint run swiftlint lint --fix --quiet "${swift_files[@]}" fix_status=$? set -e if [[ ${fix_status} -ne 0 ]]; then echo "pre-commit: swiftlint auto-fix reported unresolved issues; running final validation." fi files_needing_manual_stage=() for index in "${!swift_files[@]}"; do file="${swift_files[index]}" worktree_hash_before="${worktree_hashes_before[index]}" worktree_hash_after="$(git hash-object -w "${file}")" if [[ "${worktree_hash_before}" == "${worktree_hash_after}" ]]; then continue fi is_partial=false if ((${#partially_staged_files[@]} > 0)); then for partial in "${partially_staged_files[@]}"; do if [[ "${file}" == "${partial}" ]]; then is_partial=true break fi done fi if [[ "${is_partial}" == true ]]; then files_needing_manual_stage+=("${file}") else git add -- "${file}" fi done if [[ ${#files_needing_manual_stage[@]} -gt 0 ]]; then echo "" echo "pre-commit: formatter/linter updated partially staged file(s)." echo "Re-stage your intended changes (e.g. git add -p) and commit again:" printf ' - %s\n' "${files_needing_manual_stage[@]}" exit 1 fi echo "pre-commit: validating lint status..." set +e lint_output="$(mint run swiftlint lint --strict --quiet "${swift_files[@]}" 2>&1)" lint_status=$? set -e if [[ ${lint_status} -ne 0 ]]; then echo "" echo "pre-commit: commit blocked due to unresolved SwiftLint violations." echo "Fix the issues below, stage changes, and commit again:" echo "" echo "${lint_output}" exit 1 fi echo "pre-commit: checks passed." exit 0