Git-хуки, которые не дают коммитить плохой код
- воскресенье, 8 февраля 2026 г. в 00:00:09
Здравствуйте, коллеги программисты!
Большинство фейлов в CI — это мелочи: забытый console.log, форматирование, линт, сломанный импорт, файл без теста. Такие ошибки не должны доезжать до сборки или код-ревью.
Git-хуки позволяют запускать проверки прямо во время git commit и блокировать коммит, если были обнаружены нарушения.
В прошлой статье я рассказывал про скрипты, которые я использую для проверки качества кода в PHP/Laravel.
В этой статье я хочу рассказать о скриптах для JavaScript/TypeScript и Python — линтинг, форматирование, тесты, статический анализ и проверка наличия тестов.
Все скрипты описанные в статье находятся здесь - https://github.com/prog-time/git-hooks
Каждый скрипт — обычный .sh файл. Для каждого типа проверки я стараюсь делать 2 версии файла:
Скрипт который принимает список файлов.
Например: bash python/check_flake8.sh $FILES
Скрипт который запускает проверку на весь проект.
Например: bash python/check_flake8_all.sh
Каждый скрипт производит описанную проверку и возвращает код выхода:
0 — всё хорошо, коммит проходит;
1 — есть ошибки, коммит блокируется.
Версию скрипта с передачей файлов я использую для проверки через .git/hooks/pre-commit, где передаю список файлов добавленных в Git индекс. Версию с суффиксом *_all.sh я использую для проверки всего проекта в .git/hooks/pre-push или в CI.
Простой пример — вызов из pre-commit:
#!/bin/bash set -e # получаю все измененные файлы ALL_FILE_ARRAY=() while IFS= read -r line; do ALL_FILE_ARRAY+=("$line") done < <(git diff --cached --name-only --diff-filter=ACM || true) bash scripts/python/check_flake8.sh $ALL_FILE_ARRAY bash scripts/js/check_eslint_all.sh
Базовый уровень защиты кода заключается в том, чтобы сразу приводить его к единому стилю и ловить очевидные ошибки ещё до того, как они попадут в репозиторий. Это позволяет снизить число банальных проблем, которые обычно тормозят командную работу.
Скрипт check_eslint_all.sh — обёртка над ESLint, которая проверяет и автоматически исправляет ошибки по всему проекту.
Скрипт запускает npx eslint --fix по директориям указанным в переменной LINT_DIRS.
Автоисправимые проблемы чинятся автоматически. Если остаются ошибки (например, неиспользуемые переменные или сломанные импорты) — скрипт завершается с exit 1, и коммит блокируется.
#!/bin/bash # ------------------------------------------------------------------------------ # Runs ESLint with --fix on the entire project (app/, components/, lib/, types/). # No file arguments required — always checks all configured directories. # Exits 1 if ESLint fails to fix issues, 0 on success. # ------------------------------------------------------------------------------ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # Directories to check LINT_DIRS=("app/" "components/" "lib/" "types/") echo "=== ESLint (full project) ===" echo "Fixing ESLint issues..." if ! npx eslint "${LINT_DIRS[@]}" --fix; then echo "ERROR: Failed to fix ESLint issues" exit 1 fi echo "ESLint issues fixed!" exit 0
Для форматирования есть два варианта:
check_prettier_all.sh — форматирует весь проект;
check_prettier.sh — только конкретные файлы.
В pre-commit обычно используется второй вариант — форматируются только изменённые файлы. Это быстрее и не создаёт лишних диффов.
Скрипт просто прогоняет prettier --write, поэтому разработчику не нужно думать о пробелах, переносах и кавычках — стиль применяется автоматически.
Версии этих скриптов находятся здесь - https://github.com/prog-time/git-hooks/tree/main/javascript
Скрипт check_tsc_all.sh запускает проверку типов без сборки. Для настройки конфигурации используется tsconfig.check.json.
Это полезно, когда проект большой: можно случайно «уронить» типизацию в другом модуле, и обычный линт это не поймает.
#!/bin/bash # ------------------------------------------------------------------------------ # Runs TypeScript type checking on the entire project using tsconfig.check.json. # No file arguments required — checks all files configured in tsconfig. # Exits 1 if type check fails, 0 on success. # ------------------------------------------------------------------------------ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # TypeScript config TS_CONFIG="tsconfig.check.json" echo "=== TypeScript ===" echo "Running type check..." if ! npx tsc --project "$TS_CONFIG"; then echo "ERROR: TypeScript check failed" exit 1 fi echo "TypeScript check passed!" exit 0
Для работы данного скрипта необходимо в корне проекта создать файл tsconfig.check.json.
Пример реализации tsconfig.check.json:
{ "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "noUncheckedIndexedAccess": true, "plugins": [{ "name": "next" }], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }
Для Python набор Git-хуков выполняет ту же роль, что и для JavaScript/TypeScript, но с учётом особенностей экосистемы: динамическая типизация и разнообразие стилей кодирования требуют чуть более строгой проверки. Здесь два основных направления: линтинг и статический анализ типов.
Скрипт check_flake8.sh проверяет .py файлы на соответствие PEP8 и код-стайлу.
Flake8 ловит мелкие проблемы ещё до запуска тестов или деплоя: лишние пробелы, неверные отступы, слишком длинные строки, нарушения соглашений по именованию функций и переменных. Это снижает риск, что код будет выглядеть по-разному в разных модулях и усложнит чтение для команды.
#!/bin/bash # ---------------------------------------- # Python Code Style Checker # # This script checks Python files for style # issues using flake8. It runs directly on # the host (no Docker required). # ---------------------------------------- if [ $# -eq 0 ]; then echo "No files to check" exit 0 fi PY_FILES=() CHECKED_FILES=0 HAS_ERRORS=0 for file in "$@"; do # Skip non-Python files if [[ ! "$file" =~ \.py$ ]]; then continue fi # Skip if file doesn't exist if [ ! -f "$file" ]; then continue fi PY_FILES+=("$file") done # Check if there are any Python files to check if [ ${#PY_FILES[@]} -eq 0 ]; then echo "No Python files to check" exit 0 fi # Run flake8 linter on each file for file in "${PY_FILES[@]}"; do CHECKED_FILES=$((CHECKED_FILES + 1)) OUTPUT=$(flake8 --max-line-length=120 "$file" 2>&1) EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then HAS_ERRORS=1 echo "Style errors in: $file" echo "$OUTPUT" echo "" fi done # Final result if [ $HAS_ERRORS -ne 0 ]; then echo "----------------------------------------" echo "ERROR: Code style check failed!" echo "Total files checked: $CHECKED_FILES" echo "Fix the errors above before committing." exit 1 fi echo "Code style check passed! ($CHECKED_FILES files checked)" exit 0
Скрипт выводит ошибки стиля прямо в терминал. Например:
Style errors in: app/services/user_service.py app/services/user_service.py:42:1: E302 expected 2 blank lines, found 1 app/services/user_service.py:67:80: E501 line too long (132 > 120 characters)
Скрипт check_mypy.sh выполняет статический анализ типов.
Скрипт игнорирует тесты и вспомогательные файлы, чтобы фокусироваться только на продакшн-коде. Mypy позволяет выявлять потенциальные баги, которые в динамическом Python часто остаются незамеченными до выполнения кода.
#!/bin/bash # ------------------------------------------------------------ # Runs mypy static analysis for changed Python files locally. # Accepts file paths as args and checks only app/*.py files. # ------------------------------------------------------------ if [ $# -eq 0 ]; then echo "No files to check" exit 0 fi PY_FILES=() for file in "$@"; do # Skip non-Python files if [[ ! "$file" =~ \.py$ ]]; then continue fi # Only files inside app/ if [[ ! "$file" =~ ^app/ ]]; then continue fi # Skip if file doesn't exist if [ ! -f "$file" ]; then continue fi PY_FILES+=("$file") done if [ ${#PY_FILES[@]} -eq 0 ]; then echo "No Python files to check" exit 0 fi OUTPUT=$(mypy \ --pretty \ --show-error-codes \ --ignore-missing-imports \ --follow-imports=skip \ "${PY_FILES[@]}" 2>&1) EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then echo "----------------------------------------" echo "ERROR: Static analysis failed!" echo "----------------------------------------" echo "$OUTPUT" echo "----------------------------------------" echo "Total files checked: ${#PY_FILES[@]}" exit 1 fi echo "Static analysis passed! (${#PY_FILES[@]} files checked)" exit 0
Flake8 следит за стилем и базовыми ошибками, экономя время на код-ревью.
Mypy ловит ошибки типов, которые не видны при обычном запуске Python.
Вместе они создают первый фильтр качества: код не попадёт в коммит, пока не будет корректен по стилю и типам. Разработчик получает мгновенную обратную связь, а команда — стабильный и читаемый код, готовый к тестированию и деплою.
Следующий уровень защиты — убедиться, что новый код не только корректен по стилю и типам, но и покрыт тестами. В наборе хуков есть проверки для JavaScript/TypeScript и Python, которые гарантируют, что для изменённых файлов есть соответствующие тесты, а сами тесты проходят.
Процесс проверки тестирования делится на 2 стадии:
проверка наличия теста
запуск теста для измененного функционала
Скрипт check_tests_exist.sh проверяет наличие тестов для каждого .ts файла и блокирует коммит, если тест отсутствует. Исключения для конфигураций и вспомогательных файлов задаются в переменной SKIP_PATTERNS.
Данный скрипт, проверяет, чтобы для каждого файла существовал файл теста, который должен находиться в директории /tests. Например для файла app/api/v1/roles/route.ts обязательно должен существовать тест tests/app/api/v1/roles/route.test.ts.
#!/bin/bash # ------------------------------------------------------------------------------ # Checks that each staged TypeScript source file has a corresponding test file. # Receives files as arguments or reads from git staged files if none provided. # Exits 1 if any source file is missing its tests/...test.ts counterpart. # ------------------------------------------------------------------------------ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # Patterns to skip (no tests required) SKIP_PATTERNS=("tests/" ".test.ts" ".config.ts" ".config.mjs" "types/" ".d.ts" "layout.tsx" "page.tsx" "loading.tsx" "error.tsx" "globals.css" "providers/" "components/ui/" "prisma/") should_skip() { local file="$1" for pattern in "${SKIP_PATTERNS[@]}"; do [[ "$file" == *"$pattern"* ]] && return 0 done return 1 } # Get test path: source.ts -> tests/source.test.ts get_test_path() { local file="$1" local base="${file%.ts}" base="${base%.tsx}" echo "tests/${base}.test.ts" } # Get files to check FILES=() if [ $# -eq 0 ]; then while IFS= read -r line; do [[ "$line" == *.ts || "$line" == *.tsx ]] && FILES+=("$line") done < <(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true) [ ${#FILES[@]} -eq 0 ] && echo "No staged TypeScript files" && exit 0 else for arg in "$@"; do [[ "$arg" == *.ts || "$arg" == *.tsx ]] && FILES+=("$arg") done fi echo "=== Test Coverage Check ===" echo "" missing=() found=() skipped=() for file in "${FILES[@]}"; do [ ! -f "$file" ] && continue should_skip "$file" && { skipped+=("$file"); continue; } test_path=$(get_test_path "$file") if [ -f "$test_path" ]; then found+=("$file") else missing+=("$file → $test_path") fi done [ ${#found[@]} -gt 0 ] && echo -e "Has tests:" && printf ' %s\n' "${found[@]}" && echo "" [ ${#skipped[@]} -gt 0 ] && echo -e "Skipped:" && printf ' %s\n' "${skipped[@]}" && echo "" if [ ${#missing[@]} -gt 0 ]; then echo -e "Missing tests:" printf ' %s\n' "${missing[@]}" echo "" echo -e "ERROR: ${#missing[@]} file(s) missing tests" exit 1 fi echo -e "All files have tests!"
Также есть check_tests.sh, который находит конкретные тесты для изменённых файлов и запускает их.
Запуск тестов для измененных файлов - https://github.com/prog-time/git-hooks/blob/main/javascript/check_tests.sh
Запуск тестов для всего проекта - https://github.com/prog-time/git-hooks/blob/main/javascript/check_tests_all.sh
Для работы с Python используется похожий набор скриптов:
find_tests.sh - проверяет наличие тест файла
check_pytest.sh - запускает тесты для измененных файлов
Эти проверки делают pre-commit не просто линтером, а локальным гарантом качества кода: даже если разработчик забудет написать тест, коммит не пройдет, а команда получает стабильный и проверенный код.
Для Python-проектов, которые используют специфичные зависимости или системные библиотеки, иногда запуск линтеров и тестов на локальной машине даёт ошибки, которых нет в контейнере. Чтобы избежать расхождений окружений, набор хуков включает Docker-версии скриптов.
Для большинства скриптов, есть вариации скриптов, которые должны запускаться через Docker. Каждый такой скрипт имеет в название суффикс _in_docker.
Каждый инструмент имеет два варианта запуска:
Локальный — обычный скрипт (check_flake8.sh, check_mypy.sh, check_pytest.sh) работает на хосте.
Docker — скрипт запускается внутри контейнера (check_flake8_in_docker.sh, check_mypy_in_docker.sh, check_pytest_in_docker.sh).
Внутри контейнера запускается инструмент:
docker exec app_dev mypy /app/services/auth.py
Ошибки выводятся обратно с привычными хостовыми путями:
app/services/auth.py:15: error: Incompatible return value type
Если контейнер не запущен, скрипт выдаёт понятное сообщение:
ERROR: Container 'app_dev' is not running Start it with: docker-compose -f docker/dev/docker-compose.yml up -d
Консистентность окружения — проверки проходят в том же окружении, что и продакшн.
Без зависимости от локальной машины — версии Python, системных библиотек или расширений не влияют на результат.
Прямые пути в терминале — ошибки отображаются так же, как если бы вы работали локально.
Легкая интеграция с pre-commit — просто замените локальный скрипт на Docker-версию, остальная логика остаётся той же.
Моя сборка уникальна тем, что все скрипты разделены на отдельные подгруппы. Вы можете подключать только те проверки, которые реально нужны вашему проекту, и собирать собственную подборку хуков, без лишнего кода и лишних проверок.
Например, можно использовать только линтинг и Prettier для фронтенда, а для Python оставить только Flake8 и Mypy, либо подключить все проверки сразу — выбор за вами.
Кроме того, в проекте есть автотесты для всех shell-скриптов, которые используются в этой статье. Это значит, что вы можете:
Склонировать репозиторий: git clone https://github.com/prog-time/git-hooks.git
Настроить конфигурацию под свои директории, инструменты и правила.
Прогнать вашу версию через встроенные тесты, чтобы убедиться, что все скрипты работают корректно и блокируют коммит при ошибках.
Такой подход делает сборку надёжной и предсказуемой: вы точно знаете, что хуки сработают так, как задумано, а команда получает стабильный инструмент контроля качества кода, который можно адаптировать под любые проекты.
Релиз 2.0.0 превращает pre-commit в полноценный фильтр качества кода, который работает ещё до того, как изменения попадут в репозиторий.
JavaScript/TypeScript: ESLint, Prettier, проверка типов через TypeScript, запуск тестов (полный и по изменённым файлам), проверка наличия тестов.
Python: Flake8, Mypy, Pytest (локально и в Docker), проверка наличия тестов с контролем дубликатов.
Docker-поддержка: скрипты корректно работают в контейнере, автоматически маппят пути и выводят понятные ошибки.
Гибкость: скрипты разделены на подгруппы, можно подключать только нужные проверки и собирать свою подборку под конкретный проект.
Тестируемость: все скрипты покрыты автотестами, вы можете прогонять свою конфигурацию, чтобы убедиться в её надёжности.
Если вам понравилась эта сборка и она оказалась полезной для вашей команды — не забудьте поставить ⭐ на GitHub. Это помогает проекту развиваться, а мне — видеть, что работа приносит пользу разработчикам.
Если вы используете эту сборку и находите, что что-то можно улучшить, буду рад вашим Pull Request! Любые предложения по новым хукам, исправлениям или оптимизации скриптов помогут сделать проект ещё полезнее для всех.
Репозиторий: https://github.com/prog-time/git-hooks