130 lines
4.8 KiB
Bash
130 lines
4.8 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# breeze/githooks/pre-commit
|
|
#
|
|
# Runs clang-format and clang-tidy against staged C/C++ files before each
|
|
# commit — no Python, no pre-commit framework, just bash and (optionally)
|
|
# the clang tools themselves.
|
|
#
|
|
# Neither tool is required to have this hook installed: if a binary isn't
|
|
# on PATH, this prints a warning and skips that check instead of blocking
|
|
# the commit. Only *actual formatting/lint issues* from a tool that IS
|
|
# installed will block a commit.
|
|
#
|
|
# Platforms:
|
|
# - Linux / macOS: runs under the system bash (or Homebrew bash on macOS)
|
|
# with no extra setup.
|
|
# - Windows: runs automatically through Git for Windows' bundled Git Bash —
|
|
# git.exe recognizes the "#!/usr/bin/env bash" line above and executes
|
|
# hooks through it directly, so no WSL, Cygwin, or separate bash install
|
|
# is needed. Everything this script uses (mktemp, diff, grep, printf,
|
|
# command -v) ships with Git for Windows out of the box.
|
|
#
|
|
# One-time setup per clone (same command on every OS):
|
|
# chmod +x breeze/githooks/pre-commit (no-op on Windows, harmless either way)
|
|
# git config core.hooksPath breeze/githooks
|
|
|
|
set -uo pipefail
|
|
|
|
# Colors only when writing to an interactive terminal — avoids raw escape
|
|
# codes in CI logs, redirected output, or terminals that don't support ANSI.
|
|
if [ -t 1 ]; then
|
|
RED=$'\033[0;31m'
|
|
YELLOW=$'\033[0;33m'
|
|
GREEN=$'\033[0;32m'
|
|
RESET=$'\033[0m'
|
|
else
|
|
RED=''
|
|
YELLOW=''
|
|
GREEN=''
|
|
RESET=''
|
|
fi
|
|
|
|
exit_code=0
|
|
|
|
# --- Collect staged C/C++ files (added/copied/modified) ---
|
|
# ':!vendor/*' excludes third-party code vendored under vendor/ (see
|
|
# breeze/cmake/Dependencies.cmake in the parent project) -- it isn't this
|
|
# project's code, so it isn't held to this style guide. Not part of the
|
|
# toolkit as shipped; added for this project's vendor/ convention.
|
|
files=()
|
|
while IFS= read -r line; do
|
|
[ -n "$line" ] && files+=("$line")
|
|
done < <(git diff --cached --name-only --diff-filter=ACM -- \
|
|
'*.c' '*.h' '*.cpp' '*.hpp' '*.cc' '*.cxx' '*.tpp' ':!vendor/*')
|
|
|
|
if [ ${#files[@]} -eq 0 ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# On macOS, Homebrew's llvm (the formula that ships clang-tidy) is keg-only:
|
|
# installed but not on PATH. Look there too, AFTER the normal PATH so Apple's
|
|
# tools stay first. (breeze/scripts/_common.sh's _pmn_extend_tool_path does the same
|
|
# for the scripts; a git hook is kept standalone, so it's repeated here.)
|
|
if [ "$(uname -s)" = Darwin ]; then
|
|
for _llvm_bin in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do
|
|
if [ -d "$_llvm_bin" ]; then PATH="$PATH:$_llvm_bin"; break; fi
|
|
done
|
|
fi
|
|
|
|
# --- clang-format: check staged files are already formatted ---
|
|
if command -v clang-format >/dev/null 2>&1; then
|
|
unformatted=()
|
|
for f in "${files[@]}"; do
|
|
[ -f "$f" ] || continue
|
|
if ! diff -q <(clang-format "$f") "$f" >/dev/null 2>&1; then
|
|
unformatted+=("$f")
|
|
fi
|
|
done
|
|
|
|
if [ ${#unformatted[@]} -gt 0 ]; then
|
|
echo "${RED}clang-format: these files aren't formatted:${RESET}"
|
|
printf ' %s\n' "${unformatted[@]}"
|
|
echo "Fix with: clang-format -i ${unformatted[*]}"
|
|
exit_code=1
|
|
fi
|
|
else
|
|
echo "${YELLOW}warning: clang-format not found on PATH — skipping format check.${RESET}"
|
|
fi
|
|
|
|
# --- clang-tidy: static analysis, needs a compile database ---
|
|
if command -v clang-tidy >/dev/null 2>&1; then
|
|
if [ ! -f compile_commands.json ]; then
|
|
echo "${YELLOW}warning: compile_commands.json not found — skipping clang-tidy.${RESET}"
|
|
echo "${YELLOW}Generate one with: cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...${RESET}"
|
|
else
|
|
# A template, because BSD mktemp (macOS) wants one where GNU's doesn't.
|
|
tidy_output=$(mktemp "${TMPDIR:-/tmp}/pmn-clang-tidy.XXXXXX")
|
|
tidy_had_issues=0
|
|
|
|
for f in "${files[@]}"; do
|
|
[ -f "$f" ] || continue
|
|
if ! clang-tidy --quiet "$f" >>"$tidy_output" 2>&1; then
|
|
tidy_had_issues=1
|
|
fi
|
|
done
|
|
|
|
if grep -q "warning:\|error:" "$tidy_output" 2>/dev/null; then
|
|
echo "${RED}clang-tidy found issues:${RESET}"
|
|
cat "$tidy_output"
|
|
exit_code=1
|
|
elif [ "$tidy_had_issues" -eq 1 ]; then
|
|
# Non-zero exit with no warning/error text usually means clang-tidy
|
|
# itself couldn't run (e.g. missing headers) — surface it, but
|
|
# don't block the commit on a tooling problem rather than a style one.
|
|
echo "${YELLOW}warning: clang-tidy exited with an error unrelated to style:${RESET}"
|
|
cat "$tidy_output"
|
|
fi
|
|
|
|
rm -f "$tidy_output"
|
|
fi
|
|
else
|
|
echo "${YELLOW}warning: clang-tidy not found on PATH — skipping static analysis.${RESET}"
|
|
fi
|
|
|
|
if [ $exit_code -eq 0 ]; then
|
|
echo "${GREEN}pre-commit checks passed.${RESET}"
|
|
fi
|
|
|
|
exit $exit_code
|