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
1416for bin in " $OXFMT " " $OXLINT " ; do
1517 if [ ! -x " $bin " ]; then
@@ -18,43 +20,49 @@ for bin in "$OXFMT" "$OXLINT"; do
1820 fi
1921done
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.
3745status=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
4654MSG
4755 status=1
4856fi
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
5866MSG
5967 status=1
6068fi
0 commit comments