javascript

Git-хуки, которые не дают коммитить плохой код

  • воскресенье, 8 февраля 2026 г. в 00:00:09
https://habr.com/ru/articles/993870/

Здравствуйте, коллеги программисты!

Большинство фейлов в CI — это мелочи: забытый console.log, форматирование, линт, сломанный импорт, файл без теста. Такие ошибки не должны доезжать до сборки или код-ревью.

Git-хуки позволяют запускать проверки прямо во время git commit и блокировать коммит, если были обнаружены нарушения.

В прошлой статье я рассказывал про скрипты, которые я использую для проверки качества кода в PHP/Laravel.

В этой статье я хочу рассказать о скриптах для JavaScript/TypeScript и Python — линтинг, форматирование, тесты, статический анализ и проверка наличия тестов.

Все скрипты описанные в статье находятся здесь - https://github.com/prog-time/git-hooks

Как это работает

Каждый скрипт — обычный .sh файл. Для каждого типа проверки я стараюсь делать 2 версии файла:

  1. Скрипт который принимает список файлов.
    Например: bash python/check_flake8.sh $FILES

  2. Скрипт который запускает проверку на весь проект.
    Например: 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

JavaScript: Форматирование и линтинг

Базовый уровень защиты кода заключается в том, чтобы сразу приводить его к единому стилю и ловить очевидные ошибки ещё до того, как они попадут в репозиторий. Это позволяет снизить число банальных проблем, которые обычно тормозят командную работу.

ESLint

Скрипт 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

Prettier

Для форматирования есть два варианта:

  • check_prettier_all.sh — форматирует весь проект;

  • check_prettier.sh — только конкретные файлы.

В pre-commit обычно используется второй вариант — форматируются только изменённые файлы. Это быстрее и не создаёт лишних диффов.

Скрипт просто прогоняет prettier --write, поэтому разработчику не нужно думать о пробелах, переносах и кавычках — стиль применяется автоматически.

Версии этих скриптов находятся здесь - https://github.com/prog-time/git-hooks/tree/main/javascript

TSC

Скрипт 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: линтинг и статический анализ

Для Python набор Git-хуков выполняет ту же роль, что и для JavaScript/TypeScript, но с учётом особенностей экосистемы: динамическая типизация и разнообразие стилей кодирования требуют чуть более строгой проверки. Здесь два основных направления: линтинг и статический анализ типов.

Линтинг с Flake8

Скрипт 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)

Статический анализ с Mypy

Скрипт 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

  • Flake8 следит за стилем и базовыми ошибками, экономя время на код-ревью.

  • Mypy ловит ошибки типов, которые не видны при обычном запуске Python.

Вместе они создают первый фильтр качества: код не попадёт в коммит, пока не будет корректен по стилю и типам. Разработчик получает мгновенную обратную связь, а команда — стабильный и читаемый код, готовый к тестированию и деплою.

Тесты и проверка покрытия

Следующий уровень защиты — убедиться, что новый код не только корректен по стилю и типам, но и покрыт тестами. В наборе хуков есть проверки для JavaScript/TypeScript и Python, которые гарантируют, что для изменённых файлов есть соответствующие тесты, а сами тесты проходят.

JavaScript: проверки наличия тестов

Процесс проверки тестирования делится на 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: проверки наличия тестов

Для работы с Python используется похожий набор скриптов:

  • find_tests.sh - проверяет наличие тест файла

  • check_pytest.sh - запускает тесты для измененных файлов

Эти проверки делают pre-commit не просто линтером, а локальным гарантом качества кода: даже если разработчик забудет написать тест, коммит не пройдет, а команда получает стабильный и проверенный код.

Docker-варианты скриптов

Для 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

Преимущества Docker-скриптов

  • Консистентность окружения — проверки проходят в том же окружении, что и продакшн.

  • Без зависимости от локальной машины — версии Python, системных библиотек или расширений не влияют на результат.

  • Прямые пути в терминале — ошибки отображаются так же, как если бы вы работали локально.

  • Легкая интеграция с pre-commit — просто замените локальный скрипт на Docker-версию, остальная логика остаётся той же.

Гибкость и тестируемость сборки

Моя сборка уникальна тем, что все скрипты разделены на отдельные подгруппы. Вы можете подключать только те проверки, которые реально нужны вашему проекту, и собирать собственную подборку хуков, без лишнего кода и лишних проверок.

Например, можно использовать только линтинг и Prettier для фронтенда, а для Python оставить только Flake8 и Mypy, либо подключить все проверки сразу — выбор за вами.

Кроме того, в проекте есть автотесты для всех shell-скриптов, которые используются в этой статье. Это значит, что вы можете:

  1. Склонировать репозиторий: git clone https://github.com/prog-time/git-hooks.git

  2. Настроить конфигурацию под свои директории, инструменты и правила.

  3. Прогнать вашу версию через встроенные тесты, чтобы убедиться, что все скрипты работают корректно и блокируют коммит при ошибках.

Такой подход делает сборку надёжной и предсказуемой: вы точно знаете, что хуки сработают так, как задумано, а команда получает стабильный инструмент контроля качества кода, который можно адаптировать под любые проекты.

Итого

Релиз 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