Пошук уроків, статей та іншого контенту
Автоматизуйте перевірки перед комітом і push за допомогою Git hooks та інтегруйте їх із командними інструментами.
Git hooks — це скрипти, які Git автоматично запускає під час певних подій: створення коміту, push, злиття гілок тощо.
Найчастіше hooks використовують, щоб:
запускати форматування та статичний аналіз перед комітом;
перевіряти повідомлення коміту;
запускати тести перед push;
блокувати коміт або push, якщо перевірка завершилася помилкою;
інтегрувати Git із командними інструментами проєкту.
Hook керує результатом операції через код завершення:
0 — перевірка успішна, Git продовжує операцію;
ненульовий код — перевірка неуспішна, Git зупиняє операцію.
Git підтримує багато hooks, але для автоматизації перевірок найчастіше використовують такі:
pre-commit — запускається перед створенням коміту;
commit-msg — отримує шлях до файлу з повідомленням коміту;
pre-push — запускається перед відправленням комітів на віддалений репозиторій;
post-merge — запускається після успішного злиття;
pre-rebaserebaseУ цьому уроці зосередимося на pre-commit і pre-push.
За замовчуванням Git шукає hooks у каталозі:
.git/hooks/У цьому каталозі зазвичай є приклади файлів із суфіксом .sample. Git не запускає їх автоматично, доки файл не буде перейменовано та зроблено виконуваним.
Наприклад:
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitТакий підхід має суттєвий недолік: каталог .git не зберігається в репозиторії. Тому hooks потрібно налаштовувати окремо на кожному комп’ютері.
Для командної роботи зручно зберігати hooks у звичайному каталозі проєкту, наприклад:
.githooks/
├── pre-commit
└── pre-pushПісля цього потрібно повідомити Git використовувати цей каталог:
git config core.hooksPath .githooksНалаштування без параметра --global записується тільки для поточного репозиторію.
Перевірити активний шлях можна так:
git config --get core.hooksPathОскільки .githooks є частиною репозиторію, усі учасники команди отримують однакові скрипти після клонування. Однак виконувані права файлів залежать від файлової системи та способу клонування, тому їх варто перевірити:
chmod +x .githooks/pre-commit .githooks/pre-pushЗручно додати налаштування до скрипта встановлення проєкту:
git config core.hooksPath .githooks
chmod +x .githooks/*pre-commitpre-commit запускається після того, як Git підготував індекс, але до створення об’єкта коміту.
Це важливо: перевіряти потрібно не всі зміни у робочій директорії, а саме те, що додано до індексу командою git add.
Якщо hook аналізує всі файли проєкту, до коміту можуть потрапити результати перевірки файлів, які користувач не планував комітити. Тому для pre-commit бажано отримувати список staged-файлів.
pre-commitНижче hook для JavaScript/TypeScript-проєкту. Він:
отримує список змінених staged-файлів;
залишає лише файли JavaScript і TypeScript;
запускає ESLint тільки для них;
блокує коміт, якщо ESLint завершується з помилкою.
#!/usr/bin/env bash
set -euo pipefail
# Переходимо в корінь репозиторію незалежно від поточного каталогу.
cd "$(git rev-parse --show-toplevel)"
# Безпечне читання списку файлів, зокрема файлів із пробілами в назвах.
mapfile -d '' files < <(
git diff --cached \
--name-only \
--diff-filter=ACMR \
-z \
-- '*.js' '*.jsx' '*.ts' '*.tsx'
)
# Якщо відповідних файлів немає, перевірку можна пропустити.
if ((${#files[@]} == 0)); then
exit 0
fi
echo "Запуск ESLint для staged-файлів..."
# Помилка ESLint має ненульовий код і зупиняє створення коміту.
npx eslint -- "${files[@]}"Файл потрібно зберегти як .githooks/pre-commit і зробити виконуваним:
chmod +x .githooks/pre-commitgit diff --cachedКоманда:
git diff --cached --name-onlyпоказує файли, які вже додані до індексу.
Команда:
git diff --name-onlyпоказує unstaged-зміни, тобто зміни, які ще не будуть включені до поточного коміту. Для pre-commit це зазвичай не той список, який потрібно перевіряти.
Параметр --diff-filter=ACMR виключає видалені файли:
A — додані;
C — скопійовані;
M — змінені;
R — перейменовані.
Параметр -z повертає імена файлів, розділені нульовим байтом. Це безпечніше для назв із пробілами, лапками або спеціальними символами.
pre-pushpre-push запускається перед передаванням посилань на віддалений репозиторій. Якщо він завершується з помилкою, push не виконується.
Цей hook добре підходить для:
запуску тестів;
перевірки типів;
побудови проєкту;
перевірки всіх комітів, які мають бути відправлені.
На відміну від pre-commit, pre-push отримує дані через стандартний ввід. Кожен рядок містить чотири значення:
<local ref> <local sha1> <remote ref> <remote sha1>Наприклад:
refs/heads/feature/login 2f4a... refs/heads/feature/login 91ab...Саме тому hook може отримати SHA комітів, які відправляються.
pre-push#!/usr/bin/env bash
set -euo pipefail
# Переходимо в корінь репозиторію.
cd "$(git rev-parse --show-toplevel)"
echo "Запуск перевірки типів..."
npm run typecheck
echo "Запуск тестів..."
npm test
echo "Перевірки перед push успішні."Цей варіант запускає повний набір перевірок перед кожним push. Для невеликих проєктів цього достатньо. У великих проєктах тривалі перевірки можна оптимізувати, запускаючи їх лише для змінених пакетів або використовуючи кешування інструментів.
Приклад відповідних скриптів у package.json:
{
"scripts": {
"typecheck": "tsc --noEmit",
"test": "node --test"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}Назви команд мають відповідати конкретному стеку проєкту. Hook не повинен приховувати помилки: якщо npm run typecheck або npm test завершуються з ненульовим кодом, set -e зупиняє скрипт, а Git скасовує push.
Налаштування core.hooksPath не завжди передається іншому розробнику через сам репозиторій. Тому проєкту потрібна явна команда встановлення.
Наприклад, файл scripts/setup-git-hooks.sh:
#!/usr/bin/env bash
set -euo pipefail
# Переходимо в корінь репозиторію.
cd "$(git rev-parse --show-toplevel)"
git config core.hooksPath .githooks
chmod +x .githooks/*
echo "Git hooks налаштовано."Його можна запустити після клонування:
bash scripts/setup-git-hooks.shТаку команду варто включити в документацію проєкту або в загальний скрипт первинного налаштування.
Важливо, щоб hook був зрозумілим без додаткової конфігурації. Якщо він очікує встановлену команду, наприклад npm, node, eslint або pytest, це має бути частиною вимог проєкту.
Для контролю формату повідомлень використовується hook commit-msg. Git передає йому шлях до тимчасового файлу з повідомленням.
Наприклад, команда може вимагати префікс задачі:
PROJ-123: додати авторизаціюПриклад .githooks/commit-msg:
#!/usr/bin/env bash
set -euo pipefail
commit_message_file="$1"
commit_message="$(head -n 1 "$commit_message_file")"
if [[ ! "$commit_message" =~ ^[A-Z][A-Z0-9]+-[0-9]+:\ ]]; then
echo "Помилка: повідомлення коміту має починатися у форматі PROJ-123: текст"
exit 1
fi
exit 0У цьому випадку коміт із повідомленням на кшталт:
fix authбуде відхилено, а повідомлення:
PROJ-123: виправити перевірку токенапройде перевірку.
На відміну від pre-commit, commit-msg працює саме з текстом повідомлення, тому він підходить для перевірки шаблонів, довжини першого рядка або обов’язкових ідентифікаторів задач.
Hook може послідовно запускати кілька команд:
#!/usr/bin/env bash
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
echo "Перевірка форматування..."
npm run format:check
echo "Статичний аналіз..."
npm run lint
echo "Перевірка типів..."
npm run typecheck
echo "Усі перевірки перед комітом успішні."Порядок має значення:
швидкі перевірки краще запускати першими;
дорогі тести — після базових перевірок;
повідомлення про кожен етап допомагають швидко знайти причину помилки.
Команди мають повертати ненульовий код при помилці. Якщо інструмент лише виводить попередження, але завершується з кодом 0, Git вважатиме перевірку успішною.
Hook не повинен містити всю логіку проєкту. Краще зберігати основні команди в package.json, Makefile або іншому конфігураційному файлі проєкту, а з hook лише викликати їх.
Наприклад:
{
"scripts": {
"lint": "eslint .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"test": "node --test",
"verify": "npm run lint && npm run format:check && npm run typecheck && npm test"
}
}Тоді hook може бути коротким:
#!/usr/bin/env bash
set -euo pipefail
# Усі команди перевірки мають однакову поведінку локально та в CI.
npm run verifyПеревага такого підходу — одна й та сама команда доступна:
локально розробнику;
у Git hook;
у CI;
під час ручної діагностики помилки.
Git hooks працюють на машині розробника, тому вони не є повним механізмом захисту репозиторію.
Користувач може пропустити локальні перевірки:
git commit --no-verify
git push --no-verifyТакож hooks можуть бути не встановлені після клонування або не працювати в окремому середовищі.
Тому розподіл відповідальності зазвичай такий:
pre-commit — швидкі перевірки та зворотний зв’язок;
pre-push — дорожчі локальні перевірки перед відправленням;
CI — обов’язковий контроль, який не можна обійти локальним параметром;
захист гілки — заборона злиття без успішних перевірок CI.
Локальний hook підвищує зручність роботи, а CI забезпечує фактичне правило для всієї команди.
Git також має серверні hooks, зокрема:
pre-receive;
update;
post-receive.
Вони виконуються на Git-сервері під час приймання push. Серверний hook може відхилити зміни незалежно від того, чи запускав розробник локальні hooks.
Однак у хостингових сервісах можливості встановлення серверних hooks залежать від конкретної платформи. На практиці для командних репозиторіїв частіше використовують CI та правила захищених гілок.
Іноді локальні hooks не повинні запускати інтерактивні або дуже тривалі дії в CI. Для цього можна передбачити змінну середовища:
#!/usr/bin/env bash
set -euo pipefail
if [[ "${SKIP_LOCAL_HOOKS:-0}" == "1" ]]; then
echo "Локальні Git hooks вимкнено змінною SKIP_LOCAL_HOOKS."
exit 0
fi
npm run lint
npm testТаку можливість слід використовувати обережно. Якщо змінна дозволяє пропускати обов’язкові перевірки, вона не повинна бути єдиним захистом — остаточна перевірка має виконуватися в CI.
Якщо hook не запускається, послідовно перевірте:
Чи налаштований правильний каталог:
git config --get core.hooksPathЧи має файл правильне ім’я без суфікса .sh:
.githooks/pre-commitЧи має файл право на виконання:
ls -l .githooks/pre-commit
chmod +x .githooks/pre-commitЧи правильний інтерпретатор у shebang:
#!/usr/bin/env bashЧи запускається команда hook вручну:
.githooks/pre-commitЧи завершується команда з правильним кодом:
npm run lint
echo $?Для тимчасового трасування Bash-скрипта можна додати:
set -xВін покаже команди перед виконанням. Після діагностики цей режим краще прибрати, щоб не створювати зайвий шум.
Якщо pre-commit запускає аналіз усього проєкту, він може враховувати локальні незавершені зміни.
Для перевірок перед комітом отримуйте файли через:
git diff --cached --name-onlyGit ігнорує hook, якщо файл не є виконуваним:
chmod +x .githooks/pre-commitЯкщо в одному середовищі налаштовано:
git config core.hooksPath .githooksа в іншому Git очікує hooks у .git/hooks, скрипт не запускатиметься. Налаштування потрібно перевіряти командою:
git config --show-origin --get core.hooksPathОбробка списку файлів через простий цикл:
for file in $(git diff --cached --name-only); do
...
doneламається на пробілах і спеціальних символах у назвах. Для надійних Bash-скриптів використовуйте -z і масиви.
Автоматичне форматування файлу в pre-commit може змінити також unstaged-частину. У результаті індекс і робоче дерево можуть почати містити різні версії файлу.
Якщо hook змінює файли, він має чітко визначати, як оновлюється індекс. Безпечніший варіант для початку — використовувати перевірки без змін файлів, наприклад format:check.
pre-commitРозробники можуть почати обходити повільний hook через --no-verify. Швидкі перевірки варто залишати в pre-commit, а повний набір тестів запускати в pre-push і CI.
Hook виконується в середовищі користувача, тому команда може бути відсутня або мати іншу версію. Залежності проєкту потрібно встановлювати стандартним способом, а команди — викликати через локальний менеджер пакетів, наприклад npx або npm run.
Локальний hook можна пропустити, видалити або не встановити. Обов’язкові правила мають дублюватися в CI або на сервері.
Приклад структури репозиторію:
project/
├── .githooks/
│ ├── commit-msg
│ ├── pre-commit
│ └── pre-push
├── scripts/
│ └── setup-git-hooks.sh
├── package.json
└── README.mdПочаткове налаштування:
git config core.hooksPath .githooks
chmod +x .githooks/* scripts/setup-git-hooks.shПісля цього типовий цикл виглядає так:
Розробник змінює файли.
git add додає частину змін до індексу.
pre-commit перевіряє staged-файли.
commit-msg перевіряє повідомлення.
pre-push запускає повніші перевірки.
CI повторює критичні перевірки на сервері.
Git hooks запускають скрипти під час стандартних операцій Git.
pre-commit підходить для швидких перевірок staged-файлів.
commit-msg перевіряє формат повідомлення коміту.
pre-push підходить для тестів, перевірки типів і побудови проєкту.
Для командної роботи hooks варто зберігати в репозиторії та підключати через core.hooksPath.
Bash-скрипти hooks повинні мати право на виконання та коректно повертати коди завершення.
Локальні hooks можна пропустити через --no-verify, тому обов’язкові перевірки потрібно дублювати в CI.
Основні команди краще зберігати в інструментах проєкту, а hooks використовувати як тонкий шар інтеграції.