Skip to content

Commit 2e1ebe1

Browse files
authored
Merge pull request #303 from jetstreamapp/fix/pre-commit-index-snapshot
fix(hooks): check the whole codebase as staged in pre-commit
2 parents 0801f8b + 449ed9b commit 2e1ebe1

2 files changed

Lines changed: 36 additions & 26 deletions

File tree

‎.githooks/pre-commit‎

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
#!/bin/sh
2-
# Blocks a commit when any staged file is unformatted or fails lint.
2+
# Blocks a commit unless the whole codebase, exactly as it will be committed, is formatted and
3+
# passes lint.
34
#
45
# oxfmt is the formatter (.oxfmtrc.json); oxlint is the linter (.oxlintrc.json). Wired up by
56
# the "prepare" script in package.json (git config core.hooksPath .githooks) on npm install.
67
# Emergency bypass: git commit --no-verify
78
#
8-
# This file is shared verbatim with jetstreamapp/sf-formula-parser - keep the two copies in
9-
# sync when changing it.
9+
# This file is shared verbatim by jetstreamapp/sf-formula-parser, jetstreamapp/soql-parser-js,
10+
# jetstreamapp/simple-excel and jetstreamapp/simple-xml - keep every copy in sync when changing it.
1011

11-
OXFMT="./node_modules/.bin/oxfmt"
12-
OXLINT="./node_modules/.bin/oxlint"
12+
root=$(git rev-parse --show-toplevel) || exit 1
13+
OXFMT="$root/node_modules/.bin/oxfmt"
14+
OXLINT="$root/node_modules/.bin/oxlint"
1315

1416
for bin in "$OXFMT" "$OXLINT"; do
1517
if [ ! -x "$bin" ]; then
@@ -18,43 +20,49 @@ for bin in "$OXFMT" "$OXLINT"; do
1820
fi
1921
done
2022

21-
# Added, copied, modified and renamed paths only - deleted paths are not there to check.
22-
staged=$(git diff --cached --name-only --diff-filter=ACMR)
23+
# The checks run against a snapshot of the index rather than the working tree. The index is
24+
# exactly what the commit will contain, so a file formatted on disk but never re-staged cannot
25+
# slip through, and unstaged edits cannot block a clean commit. git points GIT_INDEX_FILE at the
26+
# right index for `commit -a` and `commit <path>` too, and checkout-index honours it. Every
27+
# tracked file is exported, so the whole codebase is checked, not only the files being changed;
28+
# --ignore-skip-worktree-bits includes paths a sparse checkout or `update-index --skip-worktree`
29+
# leaves out of the working tree.
30+
snapshot=$(mktemp -d "${TMPDIR:-/tmp}/pre-commit.XXXXXX") || exit 1
31+
# A relative TMPDIR yields a relative path, which the EXIT trap would resolve from inside the
32+
# snapshot after the cd below, leaving it behind - anchor it to where git ran the hook.
33+
case $snapshot in /*) ;; *) snapshot="$PWD/$snapshot" ;; esac
34+
trap 'rm -rf "$snapshot"' EXIT
35+
trap 'exit 1' HUP INT TERM
2336

24-
if [ -z "$staged" ]; then
25-
exit 0
26-
fi
37+
git -C "$root" checkout-index --all --ignore-skip-worktree-bits --prefix="$snapshot/" || exit 1
38+
39+
# Some lint rules resolve imports, so the snapshot shares the installed dependencies.
40+
ln -s "$root/node_modules" "$snapshot/node_modules" || exit 1
2741

28-
# Both tools skip what they do not handle, so the whole staged list goes to each rather than
29-
# being filtered by extension here, which would drift as the tools gain file types.
30-
# --no-error-on-unmatched-pattern: a staged list with nothing for that tool is not an error.
31-
# The list is newline delimited, so convert to NUL to keep paths containing spaces intact.
32-
check() {
33-
printf '%s\n' "$staged" | tr '\n' '\0' | xargs -0 "$@" --no-error-on-unmatched-pattern
34-
}
42+
cd "$snapshot" || exit 1
3543

3644
# Both run even when the first fails, so one commit attempt reports everything to fix.
3745
status=0
3846

39-
if ! check "$OXFMT" --check; then
47+
if ! "$OXFMT" --check; then
4048
cat >&2 <<'MSG'
4149
42-
Commit blocked: the staged files listed above are not formatted.
50+
Commit blocked: the files listed above are not formatted.
4351
4452
npm run format # format the repo
45-
git add -u # re-stage the result
53+
git add -u # stage the result
4654
MSG
4755
status=1
4856
fi
4957

50-
if ! check "$OXLINT"; then
58+
if ! "$OXLINT"; then
5159
cat >&2 <<'MSG'
5260
53-
Commit blocked: the staged files listed above have lint errors.
61+
Commit blocked: the files listed above have lint errors.
5462
5563
npm run lint # review the full report
5664
npm run lint:fix # apply the fixes oxlint can make automatically
57-
git add -u # re-stage the result
65+
git add -u # stage the result
5866
MSG
5967
status=1
6068
fi

‎AGENTS.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ entry in `overrides` (by file glob) rather than disabling it repo-wide, and reco
3838

3939
## Commit hook
4040

41-
A `pre-commit` hook in `.githooks/` runs both the format check and oxlint against staged files and
42-
**blocks the commit** if either fails, so skipping those steps will stop the commit rather than slip
43-
through. The hook is wired up by the `prepare` script (`git config core.hooksPath .githooks`) on
41+
A `pre-commit` hook in `.githooks/` runs both the format check and oxlint against the whole codebase
42+
and **blocks the commit** if either fails, so skipping those steps will stop the commit rather than
43+
slip through. It checks a snapshot of the index, not the working tree, so it sees exactly what the
44+
commit will contain: a file formatted on disk but not re-staged still blocks, and unstaged edits
45+
never do. The hook is wired up by the `prepare` script (`git config core.hooksPath .githooks`) on
4446
`npm install`. Emergency bypass: `git commit --no-verify`.
4547

4648
## Types

0 commit comments

Comments
 (0)