diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 000000000..6943ec361 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Run formatter from repository root regardless of current directory. +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "${REPO_ROOT}" + +# Capture the files already staged for commit so we only re-stage those +# paths after formatting. +staged_files=() +while IFS= read -r -d '' file; do + staged_files+=("${file}") +done < <(git diff --cached --name-only -z --diff-filter=ACMR) + +# Intentionally format all currently modified tracked C/C++ files. +# The helper handles no-op cases and exits 0 when nothing matches. +echo "Running clang-format fix before commit..." +./bin/clang-format-fix + +# Ensure formatting changes are included in the pending commit without +# staging unrelated tracked modifications from other files in the +# working tree. +if ((${#staged_files[@]})); then + git add -- "${staged_files[@]}" +fi \ No newline at end of file diff --git a/bin/clang-format-fix b/bin/clang-format-fix index d4902eb8d..aea136fee 100755 --- a/bin/clang-format-fix +++ b/bin/clang-format-fix @@ -14,6 +14,7 @@ fi set -euo pipefail GIT_LS_FILES_FLAGS="" +# -g scopes formatting to tracked files currently modified in git status. if [[ "${1:-}" == "-g" ]]; then GIT_LS_FILES_FLAGS="--modified" fi @@ -21,6 +22,7 @@ fi CLANG_FORMAT_VERSION_RAW="$(${CLANG_FORMAT_BIN} --version)" CLANG_FORMAT_MAJOR="$(printf '%s\n' "${CLANG_FORMAT_VERSION_RAW}" | grep -oE '[0-9]+' | head -n1)" +# Guard against local binaries older than the repo formatting config. if [[ -z "${CLANG_FORMAT_MAJOR}" || "${CLANG_FORMAT_MAJOR}" -lt 21 ]]; then echo "Error: ${CLANG_FORMAT_BIN} is too old: ${CLANG_FORMAT_VERSION_RAW}" echo "This repository's .clang-format requires clang-format 21 or newer." @@ -37,9 +39,14 @@ fi # --exclude-standard: ignores files in .gitignore # Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated. # Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated. +# Keep the no-match case non-fatal: grep returns 1 when no files match, +# which is expected when there are no modified C/C++ files. +set +o pipefail git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \ | grep -E '\.(c|cpp|h|hpp)$' \ | grep -v -E '^lib/EpdFont/builtinFonts/' \ | grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \ | grep -v -E '^lib/uzlib/' \ | xargs -r "${CLANG_FORMAT_BIN}" -style=file -i +# Restore strict pipeline failure handling for the rest of the script. +set -o pipefail diff --git a/docs/contributing/development-workflow.md b/docs/contributing/development-workflow.md index 66a18917d..ab7d64f83 100644 --- a/docs/contributing/development-workflow.md +++ b/docs/contributing/development-workflow.md @@ -6,6 +6,7 @@ This page defines the expected local workflow before opening a pull request. - Fork the repository to your own GitHub account - Clone your fork locally and add the upstream repository if needed +- Enable repo hooks once per clone: `git config core.hooksPath .githooks && chmod +x .githooks/pre-commit` - Branch from `master` - Keep each PR focused on one fix or feature area diff --git a/docs/contributing/getting-started.md b/docs/contributing/getting-started.md index 715ab18a4..299a23a1d 100644 --- a/docs/contributing/getting-started.md +++ b/docs/contributing/getting-started.md @@ -53,6 +53,13 @@ If you already cloned without submodules: git submodule update --init --recursive ``` +Enable the repository-managed Git hooks (required once per clone): + +```sh +git config core.hooksPath .githooks +chmod +x .githooks/pre-commit +``` + ## Build ```sh