From e4d8d1283437543934a18c9688bc12c0ae326448 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 10 Oct 2026 11:18:46 -0500 Subject: [PATCH 1/2] fix(hooks): check the whole codebase as staged in pre-commit The hook passed staged paths to oxfmt and oxlint, which read the working tree. A file formatted on disk but never re-staged could slip through, and unstaged edits could block a clean commit. It now exports the index to a temporary directory and checks every tracked file there, so it sees exactly what the commit will contain. The header names all four repos sharing the hook, so every copy is byte-identical. --- .githooks/pre-commit | 49 +++++++++++++++++++++++--------------------- AGENTS.md | 5 +++-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 5aa4654..a61d20f 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,44 @@ 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. +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 --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 From ac0a93152ee8b65657dd6e65352fe75b04d84295 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 10 Oct 2026 12:09:49 -0500 Subject: [PATCH 2/2] fix(hooks): export skip-worktree entries into the pre-commit snapshot --- .githooks/pre-commit | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index a61d20f..4d4a636 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -24,12 +24,14 @@ done # 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. +# 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 -git -C "$root" checkout-index --all --prefix="$snapshot/" || exit 1 +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