fix(hooks): native pre-commit script — lefthook движок виснет на Windows+кириллица

lefthook 2.1.x не завершает pre-commit при git commit на пути
"C:\моя\проекты\портал crm\Документация" (кириллица+пробел): проверки
проходят, но движок виснет на git stash/index.lock и плодит node-зомби.

Решение (выбор заказчика «свой простой скрипт»):
- tools/git-hooks/pre-commit.sh — нативная замена, зеркалит джобы lefthook.yml
  (gitleaks/markdownlint/cspell/stylelint/pint/larastan/squawk/eslint), но
  вызывает инструменты напрямую (node <entry>, не npx) и НЕ модифицирует index
  (нет git add/--fix) → нет конфликта за .git/index.lock. Явный exit.
- .git/hooks/pre-commit (локальный, не в git) → диспетчер на этот скрипт.
- lefthook.yml: npx→node в md/cspell/stylelint джобах + убран stage_fixed
  (markdownlint/pint) — кросс-платформенно безопасно, для CI/Linux где lefthook
  работает штатно (lefthook.yml остаётся источником истины конфигурации).
- lefthook 2.1.6→2.1.8.

post-commit (status-md) и pre-push lefthook работают штатно — не трогаю.
Bypass: LEFTHOOK=0 git commit ...
This commit is contained in:
Дмитрий
2026-05-23 09:39:22 +03:00
parent 3fde7f1dd5
commit a296a499d9
3 changed files with 100 additions and 11 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/bin/sh
# =============================================================================
# tools/git-hooks/pre-commit.sh — нативная замена lefthook-движка
# =============================================================================
# Зачем: lefthook 2.1.x виснет при `git commit` на этой Windows-машine
# (путь с кириллицей + пробелом: "C:\моя\проекты\портал crm\Документация").
# Сами проверки отрабатывают и проходят, но движок lefthook не завершается
# и плодит node-зомби (см. CHANGELOG / memory feedback_environment q.107+).
# Заменено 23.05.2026 по решению заказчика «свой простой скрипт».
#
# Этот скрипт зеркалит pre-commit джобы lefthook.yml, но:
# - вызывает инструменты напрямую (node <entry>, не npx → нет зомби-обёрток)
# - НЕ модифицирует index (нет git add / git stash / --fix) → нет конфликта
# за .git/index.lock с родительским git commit (корень зависаний lefthook)
# - имеет явный exit-код, ничего не висит
#
# Источник истины КОНФИГУРАЦИИ проверок — lefthook.yml (для CI/Linux, где
# lefthook работает штатно). Этот скрипт — локальная Windows-реализация.
#
# Bypass (как у lefthook): LEFTHOOK=0 git commit ...
# =============================================================================
[ "$LEFTHOOK" = "0" ] && exit 0
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT" || exit 1
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
[ -z "$STAGED" ] && exit 0
FAIL=0
note() { printf '\n[pre-commit] %s\n' "$1"; }
# 1. gitleaks — секреты / ПДн / токены в staged (§5.2). Нативный exe.
note "gitleaks (secrets)"
./bin/gitleaks.exe protect --staged --config .gitleaks.toml --no-banner || { note "gitleaks FAILED"; FAIL=1; }
# 2+3. markdownlint + cspell на staged .md (исключая вендоренные скилы).
# Без --fix: pre-commit не модифицирует файлы. Авто-fix — `npm run lint:md:fix`.
MD=$(printf '%s\n' "$STAGED" | grep -E '\.md$' | grep -vE '^\.claude/skills/(mermaid|ccpm|data-scientist|marketingskills)/')
if [ -n "$MD" ]; then
note "markdownlint"
node node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs $MD || { note "markdownlint FAILED — запусти 'npm run lint:md:fix'"; FAIL=1; }
note "cspell"
node node_modules/cspell/bin.mjs --no-progress --no-summary --no-gitignore $MD || { note "cspell FAILED — добавь слово в cspell-words.txt или поправь"; FAIL=1; }
fi
# 4. Stylelint на staged .html (CSS в прототипах).
HTML=$(printf '%s\n' "$STAGED" | grep -E '\.html$')
if [ -n "$HTML" ]; then
note "stylelint"
node node_modules/stylelint/bin/stylelint.mjs $HTML || { note "stylelint FAILED"; FAIL=1; }
fi
# 5+6. Pint (--test, без авто-fix) + Larastan на staged app/**/*.php.
PHP=$(printf '%s\n' "$STAGED" | grep -E '^app/.*\.php$')
if [ -n "$PHP" ]; then
PHP_REL=$(printf '%s\n' "$PHP" | sed 's#^app/##')
note "pint --test"
( cd app && php vendor/bin/pint --test $PHP_REL ) || { note "pint FAILED — запусти 'cd app && composer pint'"; FAIL=1; }
note "larastan"
( cd app && php vendor/bin/phpstan analyse --no-progress --memory-limit=512M ) || { note "larastan FAILED — 'cd app && composer stan'"; FAIL=1; }
fi
# 7. squawk на staged *.sql (миграции PostgreSQL).
SQL=$(printf '%s\n' "$STAGED" | grep -E '\.sql$')
if [ -n "$SQL" ]; then
note "squawk"
./bin/squawk.exe $SQL || { note "squawk FAILED"; FAIL=1; }
fi
# 8. ESLint на staged app/resources/js/**/*.{ts,vue}.
VUE=$(printf '%s\n' "$STAGED" | grep -E '^app/resources/js/.*\.(ts|vue)$')
if [ -n "$VUE" ]; then
VUE_REL=$(printf '%s\n' "$VUE" | sed 's#^app/##')
note "eslint"
( cd app && node node_modules/eslint/bin/eslint.js $VUE_REL ) || { note "eslint FAILED"; FAIL=1; }
fi
if [ "$FAIL" = "1" ]; then
note "ОТКЛОНЕНО — проверки не пройдены (см. выше). Обход: LEFTHOOK=0 git commit ..."
exit 1
fi
note "OK — все проверки пройдены"
exit 0