#!/bin/sh
# Fail before minting a commit if this clone still has a test-fixture Git identity.
# A leftover repo-local user.email (Ratchet Test / *.invalid) overrides the global
# identity, and GitHub then will not attribute the commit to the person who pushed.
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 "$ROOT/.github/scripts/check_git_author_identity.py" --pending || exit 1
# Universal source hygiene: fail fast on unresolved merge conflict markers.
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if [ -n "$STAGED_FILES" ]; then
conflict_found=0
while IFS= read -r file; do
[ -n "$file" ] || continue
if git grep --cached -n -I -E '^(<<<<<<<|>>>>>>>)' -- "$file"; then
conflict_found=1
fi
done <<EOF
$STAGED_FILES
EOF
if [ "$conflict_found" -eq 1 ]; then
echo "Unresolved merge conflict markers found in staged files" >&2
exit 1
fi
fi
# Dart formatting for app/
STAGED_DART_FILES=$(git diff --cached --name-only --diff-filter=ACM "app/**.dart" | grep -v -e '\.gen\.dart$' -e '\.g\.dart$' -e '^app/lib/l10n/app_localizations.*\.dart$')
if [ -n "$STAGED_DART_FILES" ] && [ "${OMI_SKIP_DART_FORMAT:-}" = "1" ]; then
echo "OMI_SKIP_DART_FORMAT=1: leaving staged Dart files unformatted."
STAGED_DART_FILES=""
fi
if [ -n "$STAGED_DART_FILES" ]; then
ROOT="$(git rev-parse --show-toplevel)"
PINNED_FLUTTER=$(git show :.github/workflows/repo-checks.yml 2>/dev/null | sed -n 's/^[[:space:]]*flutter-version:[[:space:]]*//p' | head -1 | tr -d "\"' ")
if [ -z "$PINNED_FLUTTER" ]; then
PINNED_FLUTTER=$(sed -n 's/^[[:space:]]*flutter-version:[[:space:]]*//p' "$ROOT/.github/workflows/repo-checks.yml" 2>/dev/null | head -1 | tr -d "\"' ")
fi
if [ -z "$PINNED_FLUTTER" ]; then
echo "Could not read the pinned flutter-version from .github/workflows/repo-checks.yml." >&2
echo "Refusing to run 'dart format' with an unknown toolchain. Set OMI_SKIP_DART_FORMAT=1 to commit unformatted." >&2
exit 1
fi
if command -v flutter >/dev/null 2>&1; then
# Git exports linked-worktree metadata to hooks. Flutter resolves its own
# SDK version through Git, so those variables must not leak into the probe.
LOCAL_FLUTTER=$(
unset GIT_DIR GIT_WORK_TREE
flutter --version 2>/dev/null | sed -n 's/^Flutter \([0-9][^ ]*\).*/\1/p' | head -1
)
else
LOCAL_FLUTTER=""
fi
if [ "$LOCAL_FLUTTER" != "$PINNED_FLUTTER" ]; then
echo "Dart formatter toolchain mismatch: repo pins Flutter $PINNED_FLUTTER, found ${LOCAL_FLUTTER:-no flutter on PATH}." >&2
echo "A different Dart SDK reformats untouched files into a shape CI rejects, so formatting is refused." >&2
echo "Install Flutter $PINNED_FLUTTER (fvm use $PINNED_FLUTTER), or set OMI_SKIP_DART_FORMAT=1 to commit unformatted." >&2
exit 1
fi
DART_BIN="$(dirname "$(command -v flutter)")/dart"
if [ ! -x "$DART_BIN" ]; then
echo "Dart formatter sibling not found: expected $DART_BIN next to the validated flutter." >&2
echo "Refusing to run an unrelated 'dart' from PATH. Set OMI_SKIP_DART_FORMAT=1 to commit unformatted." >&2
exit 1
fi
echo "Formatting staged Dart files in app/ with Flutter $PINNED_FLUTTER..."
echo "$STAGED_DART_FILES" | xargs "$DART_BIN" format --line-length 120
echo "$STAGED_DART_FILES" | xargs git add
fi
# Python formatting for backend/
STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM "backend/**.py")
if [ -n "$STAGED_PYTHON_FILES" ]; then
echo "Formatting staged Python files in backend/ with the pinned formatter..."
echo "$STAGED_PYTHON_FILES" | xargs "$ROOT/scripts/backend-python-format" --write
echo "$STAGED_PYTHON_FILES" | xargs git add
fi
# ARB (l10n JSON) formatting for app/lib/l10n/
STAGED_ARB_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.arb$')
if [ -n "$STAGED_ARB_FILES" ]; then
if command -v jq >/dev/null 2>&1; then
echo "Formatting ARB files to 4-space JSON indent..."
for f in $STAGED_ARB_FILES; do
jq --indent 4 '.' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
done
echo "$STAGED_ARB_FILES" | xargs git add
else
echo "jq is not installed. Please run 'sudo apt install jq' or 'brew install jq'" >&2
exit 1
fi
fi
# Web (JS/TS/JSX/TSX) formatting with Prettier
# Auto-generated TypeScript (e.g. the admin OpenAPI client) is excluded: a
# regenerated artifact must not be swept through Prettier's format-and-add.
STAGED_WEB_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx)$' | grep -E '^web/' | grep -v '\.generated\.ts$')
if [ -n "$STAGED_WEB_FILES" ] && [ "${OMI_SKIP_WEB_FORMAT:-}" = "1" ]; then
echo "OMI_SKIP_WEB_FORMAT=1: leaving staged web files unformatted."
STAGED_WEB_FILES=""
fi
if [ -n "$STAGED_WEB_FILES" ]; then
echo "Formatting staged web files with Prettier..."
for dir in $(echo "$STAGED_WEB_FILES" | cut -d/ -f1-2 | sort -u); do
DIR_FILES=$(echo "$STAGED_WEB_FILES" | grep "^$dir/")
if [ -z "$DIR_FILES" ]; then
continue
fi
PINNED=$(git show :"$dir/package.json" 2>/dev/null | sed -n 's/.*"prettier"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
if [ -z "$PINNED" ]; then
PINNED=$(sed -n 's/.*"prettier"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$dir/package.json" 2>/dev/null | head -1)
fi
if [ -z "$PINNED" ]; then
echo "$dir/package.json does not pin prettier; refusing to format $dir." >&2
echo "Pin prettier there, or set OMI_SKIP_WEB_FORMAT=1 to commit unformatted." >&2
exit 1
fi
PRETTIER_BIN="$dir/node_modules/.bin/prettier"
if [ -x "$PRETTIER_BIN" ]; then
LOCAL_VERSION=$("$PRETTIER_BIN" --version 2>/dev/null)
else
LOCAL_VERSION=""
fi
RESOLVED_VERSION=$(git show :"$dir/package-lock.json" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print((d.get('packages',{}).get('node_modules/prettier') or {}).get('version',''))" 2>/dev/null)
if [ -n "$RESOLVED_VERSION" ]; then
if [ -z "$LOCAL_VERSION" ] || [ "$LOCAL_VERSION" != "$RESOLVED_VERSION" ]; then
echo "Prettier toolchain mismatch in $dir: package-lock resolves $RESOLVED_VERSION, found ${LOCAL_VERSION:-no $dir/node_modules/.bin/prettier}." >&2
echo "A floating prettier reformats files into a shape CI rejects, so formatting is refused." >&2
echo "Run 'npm ci' in $dir, or set OMI_SKIP_WEB_FORMAT=1 to commit unformatted." >&2
exit 1
fi
else
echo "Prettier lockfile pin missing or unresolvable in $dir: refusing to format $dir." >&2
echo "A missing, empty, or unparseable package-lock.json lets a floating prettier reformat files into a shape CI rejects." >&2
echo "Run 'npm ci' in $dir, or set OMI_SKIP_WEB_FORMAT=1 to commit unformatted." >&2
exit 1
fi
for plugin in prettier-plugin-tailwindcss; do
PLUGIN_RESOLVED=$(git show :"$dir/package-lock.json" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print((d.get('packages',{}).get('node_modules/' + sys.argv[1]) or {}).get('version',''))" "$plugin" 2>/dev/null)
PLUGIN_DECLARED=$(git show :"$dir/package.json" 2>/dev/null | sed -n "s/.*\"$plugin\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1)
if [ -z "$PLUGIN_DECLARED" ]; then
PLUGIN_DECLARED=$(sed -n "s/.*\"$plugin\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$dir/package.json" 2>/dev/null | head -1)
fi
if [ -z "$PLUGIN_RESOLVED" ]; then
if [ -n "$PLUGIN_DECLARED" ]; then
echo "Prettier plugin toolchain unresolved in $dir: package.json declares $plugin $PLUGIN_DECLARED, package-lock.json has no entry for it." >&2
echo "Prettier auto-loads any installed plugin, so a stale or missing one reformats files into a shape CI rejects; formatting is refused." >&2
echo "Run 'npm ci' in $dir, or set OMI_SKIP_WEB_FORMAT=1 to commit unformatted." >&2
exit 1
fi
continue
fi
PLUGIN_PKG="$dir/node_modules/$plugin/package.json"
if [ -f "$PLUGIN_PKG" ]; then
PLUGIN_LOCAL=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('version',''))" "$PLUGIN_PKG" 2>/dev/null)
else
PLUGIN_LOCAL=""
fi
if [ -z "$PLUGIN_LOCAL" ] || [ "$PLUGIN_LOCAL" != "$PLUGIN_RESOLVED" ]; then
echo "Prettier plugin toolchain mismatch in $dir: package-lock resolves $plugin $PLUGIN_RESOLVED, found ${PLUGIN_LOCAL:-no $dir/node_modules/$plugin}." >&2
echo "A missing or stale plugin reformats files into a shape CI rejects, so formatting is refused." >&2
echo "Run 'npm ci' in $dir, or set OMI_SKIP_WEB_FORMAT=1 to commit unformatted." >&2
exit 1
fi
done
for file in $DIR_FILES; do
if ! git diff --quiet -- "$file"; then
echo "$file has unstaged edits; refusing to format-and-add because it would sweep them into the commit." >&2
echo "Stage or commit the remaining edits first, or set OMI_SKIP_WEB_FORMAT=1 to commit unformatted." >&2
exit 1
fi
done
echo "$DIR_FILES" | xargs "$PRETTIER_BIN" --write
echo "$DIR_FILES" | xargs git add
done
fi
# C/C++ formatting for firmware/
STAGED_C_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(c|cpp|cc|cxx|h|hpp)$' | grep -E '^(omi/|omiGlass/)')
if [ -n "$STAGED_C_FILES" ]; then
echo "Formatting staged C/C++ files in firmware..."
if ! command -v clang-format >/dev/null 2>&1; then
echo "clang-format is not installed. Please install it first" >&2
exit 1
fi
echo "$STAGED_C_FILES" | xargs clang-format -i
echo "$STAGED_C_FILES" | xargs git add
fi
# Swift formatting for desktop/macos/Desktop/
STAGED_SWIFT_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^desktop/macos/Desktop/.*\.swift$' | grep -v '/Generated/' || true)
if [ -n "$STAGED_SWIFT_FILES" ]; then
echo "Formatting staged Swift files in desktop/macos/Desktop/..."
if ! command -v xcrun >/dev/null 2>&1; then
echo "xcrun not found — Swift formatting requires macOS with Xcode" >&2
echo "Bootstrap: desktop/macos/scripts/swift-format-wrapper.sh bootstrap" >&2
exit 1
fi
ROOT="$(git rev-parse --show-toplevel)"
WRAPPER="$ROOT/desktop/macos/scripts/swift-format-wrapper.sh"
echo "$STAGED_SWIFT_FILES" | xargs "$WRAPPER" format -i
echo "$STAGED_SWIFT_FILES" | xargs git add
fi
exit 0