#!/usr/bin/env bash
set -euo pipefail
unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOOK="$ROOT/scripts/pre-commit"
BACKEND_FORMATTER="$ROOT/scripts/backend-python-format"
TMPDIR="$(mktemp -d)"
cleanup() { rm -rf "$TMPDIR"; }
trap cleanup EXIT
REPO="$TMPDIR/repo"
STUBS="$TMPDIR/stubs"
mkdir -p "$REPO/web/frontend/src" "$REPO/app/lib" "$REPO/.github/workflows" "$REPO/.github/scripts" "$REPO/scripts" "$STUBS"
cp "$BACKEND_FORMATTER" "$REPO/scripts/backend-python-format"
git -C "$REPO" init -q
git -C "$REPO" config user.email test@example.com
git -C "$REPO" config user.name test
cat >"$REPO/.github/scripts/check_git_author_identity.py" <<'PY'
import sys
sys.exit(0)
PY
printf 'jobs:\n flutter:\n steps:\n - uses: subosito/flutter-action@v2\n with:\n flutter-version: 9.9.9\n' >"$REPO/.github/workflows/repo-checks.yml"
printf '{\n "devDependencies": {\n "eslint-plugin-prettier": "^4.2.1",\n "prettier": "^2.8.8",\n "prettier-plugin-tailwindcss": "^0.3.0"\n }\n}\n' >"$REPO/web/frontend/package.json"
cat >"$REPO/web/frontend/package-lock.json" <<LOCK
{
"name": "fixture",
"version": "0.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "fixture",
"version": "0.0.0"
},
"node_modules/prettier": {
"version": "2.8.8"
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.3.0"
}
}
}
LOCK
printf 'const a = 0\n' >"$REPO/web/frontend/src/a.ts"
printf 'void main() {}\n' >"$REPO/app/lib/main.dart"
git -C "$REPO" add -A
git -C "$REPO" commit -qm base
make_prettier_stub() {
target_dir="$1"
version="$2"
mkdir -p "$target_dir/node_modules/.bin"
cat >"$target_dir/node_modules/.bin/prettier" <<STUB
#!/bin/sh
if [ "\$1" = "--version" ]; then echo "$version"; exit 0; fi
shift
for f in "\$@"; do printf 'PRETTIER_${version}_FORMATTED\n' >"\$f"; done
STUB
chmod +x "$target_dir/node_modules/.bin/prettier"
}
make_prettier_lock() {
target_dir="$1"
version="$2"
cat >"$target_dir/package-lock.json" <<LOCK
{
"name": "fixture",
"version": "0.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "fixture",
"version": "0.0.0"
},
"node_modules/prettier": {
"version": "$version"
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.3.0"
}
}
}
LOCK
}
make_prettier_plugin() {
target_dir="$1"
version="${2:-0.3.0}"
mkdir -p "$target_dir/node_modules/prettier-plugin-tailwindcss"
printf '{"name": "prettier-plugin-tailwindcss", "version": "%s"}\n' "$version" >"$target_dir/node_modules/prettier-plugin-tailwindcss/package.json"
}
make_flutter_stub() {
cat >"$STUBS/flutter" <<STUB
#!/bin/sh
# Mimic Flutter's Git-based SDK-version resolution: a leaked hook GIT_DIR (as in
# a linked worktree) makes the probe report nothing even for a valid install.
if [ -n "\${GIT_DIR:-}" ]; then exit 0; fi
echo "Flutter $1 • channel stable"
STUB
cat >"$STUBS/dart" <<'STUB'
for f in "$@"; do
case "$f" in
*.dart) printf 'DART_FORMATTED\n' >"$f" ;;
esac
done
STUB
chmod +x "$STUBS/flutter" "$STUBS/dart"
}
make_black_stub() {
cat >"$STUBS/black" <<'STUB'
if [ "${1:-}" = "--version" ]; then
echo "black, 26.5.1 (compiled: yes)"
exit 0
fi
for arg in "$@"; do
case "$arg" in
*.py) printf 'PYTHON_FORMATTED\n' >"$arg" ;;
esac
done
STUB
chmod +x "$STUBS/black"
}
run_hook() {
( cd "$REPO" && env PATH="$STUBS:$PATH" "$@" sh "$HOOK" )
}
expect_refusal() {
label="$1"
shift
out="$TMPDIR/out.txt"
if run_hook "$@" >"$out" 2>&1; then
echo "FAIL: $label — hook exited 0 instead of refusing" >&2
cat "$out" >&2
exit 1
fi
if ! grep -q "mismatch\|refus\|unresolved" "$out"; then
echo "FAIL: $label — refusal message is not actionable" >&2
cat "$out" >&2
exit 1
fi
}
printf 'const a = {b:1}\n' >"$REPO/web/frontend/src/a.ts"
git -C "$REPO" add web/frontend/src/a.ts
rm -rf "$REPO/web/frontend/node_modules"
expect_refusal "missing pinned prettier"
test "$(cat "$REPO/web/frontend/src/a.ts")" = "const a = {b:1}"
make_prettier_stub "$REPO/web/frontend" 3.4.2
expect_refusal "prettier version mismatch"
test "$(cat "$REPO/web/frontend/src/a.ts")" = "const a = {b:1}"
make_prettier_stub "$REPO/web/frontend" 2.0.0
expect_refusal "prettier same-major stale lock"
test "$(cat "$REPO/web/frontend/src/a.ts")" = "const a = {b:1}"
make_prettier_stub "$REPO/web/frontend" 2.8.8
make_prettier_plugin "$REPO/web/frontend" 0.3.0
run_hook >/dev/null
grep -q 'PRETTIER_2.8.8_FORMATTED' "$REPO/web/frontend/src/a.ts"
git -C "$REPO" reset -q --hard
printf 'const a = {b:1}\n' >"$REPO/web/frontend/src/a.ts"
git -C "$REPO" add web/frontend/src/a.ts
make_prettier_stub "$REPO/web/frontend" 2.8.8
make_prettier_plugin "$REPO/web/frontend" 0.2.0
expect_refusal "prettier plugin version mismatch"
test "$(cat "$REPO/web/frontend/src/a.ts")" = "const a = {b:1}"
git -C "$REPO" reset -q --hard
printf 'const a = {b:1}\n' >"$REPO/web/frontend/src/a.ts"
git -C "$REPO" add web/frontend/src/a.ts
cat >"$REPO/web/frontend/package-lock.json" <<LOCK
{
"name": "fixture",
"version": "0.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "fixture",
"version": "0.0.0"
},
"node_modules/prettier": {
"version": "2.8.8"
}
}
}
LOCK
git -C "$REPO" add web/frontend/package-lock.json
make_prettier_stub "$REPO/web/frontend" 2.8.8
make_prettier_plugin "$REPO/web/frontend" 0.2.0
expect_refusal "declared prettier plugin unresolved in lockfile"
test "$(cat "$REPO/web/frontend/src/a.ts")" = "const a = {b:1}"
git -C "$REPO" reset -q --hard
printf 'void main() { }\n' >"$REPO/app/lib/main.dart"
git -C "$REPO" add app/lib/main.dart
expect_refusal "no flutter toolchain"
test "$(cat "$REPO/app/lib/main.dart")" = "void main() { }"
make_flutter_stub 8.8.8
expect_refusal "flutter version mismatch"
test "$(cat "$REPO/app/lib/main.dart")" = "void main() { }"
make_flutter_stub 9.9.9
run_hook >/dev/null
grep -q 'DART_FORMATTED' "$REPO/app/lib/main.dart"
printf 'void main() { }\n' >"$REPO/app/lib/main.dart"
git -C "$REPO" add app/lib/main.dart
run_hook GIT_DIR="$REPO/.git" >/dev/null
grep -q 'DART_FORMATTED' "$REPO/app/lib/main.dart"
git -C "$REPO" reset -q --hard
printf 'const c = {d:1}\n' >"$REPO/web/frontend/src/a.ts"
printf 'void main() { }\n' >"$REPO/app/lib/main.dart"
git -C "$REPO" add -A
rm -rf "$REPO/web/frontend/node_modules" "$STUBS/flutter"
run_hook OMI_SKIP_WEB_FORMAT=1 OMI_SKIP_DART_FORMAT=1 >/dev/null
test "$(cat "$REPO/web/frontend/src/a.ts")" = "const c = {d:1}"
test "$(cat "$REPO/app/lib/main.dart")" = "void main() { }"
git -C "$REPO" reset -q --hard
for webdir in web/app web/admin; do
mkdir -p "$REPO/$webdir/src"
printf '{\n "devDependencies": {\n "prettier": "^2.8.8",\n "prettier-plugin-tailwindcss": "^0.3.0"\n }\n}\n' >"$REPO/$webdir/package.json"
make_prettier_lock "$REPO/$webdir" 2.8.8
make_prettier_stub "$REPO/$webdir" 2.8.8
make_prettier_plugin "$REPO/$webdir" 0.3.0
printf 'const %s = {b:1}\n' "$(basename "$webdir")" >"$REPO/$webdir/src/a.ts"
git -C "$REPO" add -A
run_hook >/dev/null
grep -q 'PRETTIER_2.8.8_FORMATTED' "$REPO/$webdir/src/a.ts"
git -C "$REPO" reset -q --hard
rm -rf "$REPO/$webdir"
done
mkdir -p "$REPO/web/unpinned/src"
printf '{ "devDependencies": {} }\n' >"$REPO/web/unpinned/package.json"
printf 'const x = {b:1}\n' >"$REPO/web/unpinned/src/a.ts"
git -C "$REPO" add -A
expect_refusal "unpinned web dir"
test "$(cat "$REPO/web/unpinned/src/a.ts")" = "const x = {b:1}"
mkdir -p "$REPO/web/admin/src"
printf '{\n "devDependencies": {\n "prettier": "^2.8.8",\n "prettier-plugin-tailwindcss": "^0.3.0"\n }\n}\n' >"$REPO/web/admin/package.json"
make_prettier_lock "$REPO/web/admin" 2.8.8
make_prettier_stub "$REPO/web/admin" 2.8.8
make_prettier_plugin "$REPO/web/admin" 0.3.0
git -C "$REPO" add web/admin/package.json web/admin/package-lock.json
printf 'const staged = {b:1}\n' >"$REPO/web/admin/src/a.ts"
git -C "$REPO" add web/admin/src/a.ts
printf 'const staged = {b:1}\nconst unstaged = 2\n' >"$REPO/web/admin/src/a.ts"
expect_refusal "web file with unstaged edits"
test "$(cat "$REPO/web/admin/src/a.ts")" = "const staged = {b:1}
const unstaged = 2"
git -C "$REPO" add web/admin/src/a.ts
rm -rf "$REPO/web/unpinned"
git -C "$REPO" reset -q -- web/unpinned
run_hook >/dev/null
grep -q 'PRETTIER_2.8.8_FORMATTED' "$REPO/web/admin/src/a.ts"
mkdir -p "$REPO/web/admin/lib/services/omi-api"
printf 'export const omiApi = { untouched: 1 }\n' >"$REPO/web/admin/lib/services/omi-api/omiApi.generated.ts"
printf 'const gen = {b:1}\n' >"$REPO/web/admin/src/b.ts"
git -C "$REPO" add web/admin/lib/services/omi-api/omiApi.generated.ts web/admin/src/b.ts
run_hook >/dev/null
grep -q 'PRETTIER_2.8.8_FORMATTED' "$REPO/web/admin/src/b.ts"
test "$(cat "$REPO/web/admin/lib/services/omi-api/omiApi.generated.ts")" = "export const omiApi = { untouched: 1 }"
git -C "$REPO" reset -q --hard
mkdir -p "$REPO/web/nolock/src"
printf '{\n "devDependencies": {\n "prettier": "^2.8.8",\n "prettier-plugin-tailwindcss": "^0.3.0"\n }\n}\n' >"$REPO/web/nolock/package.json"
make_prettier_stub "$REPO/web/nolock" 2.0.0
make_prettier_plugin "$REPO/web/nolock" 0.3.0
printf 'const nolock = {b:1}\n' >"$REPO/web/nolock/src/a.ts"
git -C "$REPO" add web/nolock/src/a.ts web/nolock/package.json
expect_refusal "missing lockfile with same-major prettier"
test "$(cat "$REPO/web/nolock/src/a.ts")" = "const nolock = {b:1}"
git -C "$REPO" reset -q --hard
mkdir -p "$REPO/backend"
printf 'x= 1\n' >"$REPO/backend/example.py"
git -C "$REPO" add backend/example.py
make_black_stub
run_hook >/dev/null
test "$(cat "$REPO/backend/example.py")" = "PYTHON_FORMATTED"
git -C "$REPO" diff --exit-code -- backend/example.py >/dev/null
git -C "$REPO" diff --cached -- backend/example.py | grep -q 'PYTHON_FORMATTED'
echo "pre-commit pinned-toolchain refusal tests passed"