diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 5aa4654..4d4a636 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,15 +1,17 @@ #!/bin/sh -# Blocks a commit when any staged file is unformatted or fails lint. +# Blocks a commit unless the whole codebase, exactly as it will be committed, is formatted and +# passes lint. # # oxfmt is the formatter (.oxfmtrc.json); oxlint is the linter (.oxlintrc.json). Wired up by # the "prepare" script in package.json (git config core.hooksPath .githooks) on npm install. # Emergency bypass: git commit --no-verify # -# This file is shared verbatim with jetstreamapp/soql-parser-js - keep the two copies in -# sync when changing it. +# This file is shared verbatim by jetstreamapp/sf-formula-parser, jetstreamapp/soql-parser-js, +# jetstreamapp/simple-excel and jetstreamapp/simple-xml - keep every copy in sync when changing it. -OXFMT="./node_modules/.bin/oxfmt" -OXLINT="./node_modules/.bin/oxlint" +root=$(git rev-parse --show-toplevel) || exit 1 +OXFMT="$root/node_modules/.bin/oxfmt" +OXLINT="$root/node_modules/.bin/oxlint" for bin in "$OXFMT" "$OXLINT"; do if [ ! -x "$bin" ]; then @@ -18,43 +20,46 @@ for bin in "$OXFMT" "$OXLINT"; do fi done -# Added, copied, modified and renamed paths only - deleted paths are not there to check. -staged=$(git diff --cached --name-only --diff-filter=ACMR) +# The checks run against a snapshot of the index rather than the working tree. The index is +# exactly what the commit will contain, so a file formatted on disk but never re-staged cannot +# slip through, and unstaged edits cannot block a clean commit. git points GIT_INDEX_FILE at the +# right index for `commit -a` and `commit ` too, and checkout-index honours it. Every +# tracked file is exported, so the whole codebase is checked, not only the files being changed; +# --ignore-skip-worktree-bits includes paths a sparse checkout or `update-index --skip-worktree` +# leaves out of the working tree. +snapshot=$(mktemp -d "${TMPDIR:-/tmp}/pre-commit.XXXXXX") || exit 1 +trap 'rm -rf "$snapshot"' EXIT +trap 'exit 1' HUP INT TERM -if [ -z "$staged" ]; then - exit 0 -fi +git -C "$root" checkout-index --all --ignore-skip-worktree-bits --prefix="$snapshot/" || exit 1 + +# Some lint rules resolve imports, so the snapshot shares the installed dependencies. +ln -s "$root/node_modules" "$snapshot/node_modules" || exit 1 -# Both tools skip what they do not handle, so the whole staged list goes to each rather than -# being filtered by extension here, which would drift as the tools gain file types. -# --no-error-on-unmatched-pattern: a staged list with nothing for that tool is not an error. -# The list is newline delimited, so convert to NUL to keep paths containing spaces intact. -check() { - printf '%s\n' "$staged" | tr '\n' '\0' | xargs -0 "$@" --no-error-on-unmatched-pattern -} +cd "$snapshot" || exit 1 # Both run even when the first fails, so one commit attempt reports everything to fix. status=0 -if ! check "$OXFMT" --check; then +if ! "$OXFMT" --check; then cat >&2 <<'MSG' -Commit blocked: the staged files listed above are not formatted. +Commit blocked: the files listed above are not formatted. npm run format # format the repo - git add -u # re-stage the result + git add -u # stage the result MSG status=1 fi -if ! check "$OXLINT"; then +if ! "$OXLINT"; then cat >&2 <<'MSG' -Commit blocked: the staged files listed above have lint errors. +Commit blocked: the files listed above have lint errors. npm run lint # review the full report npm run lint:fix # apply the fixes oxlint can make automatically - git add -u # re-stage the result + git add -u # stage the result MSG status=1 fi diff --git a/AGENTS.md b/AGENTS.md index 1e827fa..07662e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,8 +57,9 @@ under `/Applications`, Microsoft Excel with Automation permission (`oracle/excel **Always run `npm run format` and `npm run lint` after any change** and fix what lint reports. oxfmt settings are in `.oxfmtrc.json`, oxlint settings in `.oxlintrc.json` (scoped overrides for `scripts/`, `oracle/`, `bench/`, -`fixtures/` and tests; prefer fixing code over adding overrides). The `.githooks/pre-commit` hook blocks commits -that fail either check. Use conventional commit messages. +`fixtures/` and tests; prefer fixing code over adding overrides). The `.githooks/pre-commit` hook runs both checks +against a snapshot of the whole index, not the working tree, and blocks the commit if either fails. Use conventional +commit messages. ## Types and build