Перейти к содержимому

8. Git Hooks

![Иллюстрация к уроку](/lessons/git-git-hooks.png)
Git Hooks – это как стражники вашего репозитория, автоматически проверяющие и выполняющие действия перед или после определенных событий в Git. Они помогают автоматизировать задачи и поддерживать качество кода.
## Что такое Git Hooks?
Git Hooks – это скрипты, которые Git выполняет автоматически до или после событий, таких как коммит, пуш или мерж. Они позволяют вам настраивать рабочий процесс Git, добавляя собственные проверки, уведомления и автоматические действия. Представьте, что это автоматические помощники, которые помогают вам следить за порядком в вашем коде.
Git Hooks хранятся в каталоге `.git/hooks` внутри вашего репозитория. По умолчанию, там находятся примеры скриптов с расширением `.sample`. Чтобы активировать hook, нужно убрать расширение `.sample` и сделать файл исполняемым (например, `chmod +x pre-commit`).
## Типы Git Hooks и примеры
Существует множество Git Hooks, которые срабатывают в разных точках рабочего процесса. Рассмотрим некоторые из них:
* **`pre-commit`**: Выполняется перед созданием коммита. Часто используется для проверки стиля кода, запуска тестов или проверки наличия комментариев.
```bash
#!/bin/sh
# Проверяем, что сообщение коммита не пустое
if [ -z "$(git commit -m ' ' --allow-empty | grep 'No commit message')" ]; then
echo "Пожалуйста, добавьте сообщение к коммиту."
exit 1
fi
  • prepare-commit-msg: Выполняется перед открытием редактора для ввода сообщения коммита. Можно использовать для автоматического добавления информации в сообщение коммита.

    #!/bin/sh
    # Добавляем номер задачи из Jira в сообщение коммита
    echo "#JIRA-123" >> $1
  • commit-msg: Проверяет сообщение коммита. Можно использовать для обеспечения соответствия сообщения коммита определенному формату.

    #!/bin/sh
    # Проверяем, что сообщение коммита начинается с префикса
    if ! grep -q "^feat\|^fix\|^docs\|^style\|^refactor\|^test\|^chore" "$1"; then
    echo "Сообщение коммита должно начинаться с одного из префиксов: feat, fix, docs, style, refactor, test, chore"
    exit 1
    fi
  • pre-push: Выполняется перед отправкой изменений на удаленный репозиторий. Можно использовать для запуска тестов или проверки соответствия кода стандартам.

    #!/bin/sh
    # Запускаем тесты перед отправкой
    npm test
    if [ $? -ne 0 ]; then
    echo "Тесты не прошли. Отправка отменена."
    exit 1
    fi
  • post-receive: Выполняется на удаленном репозитории после получения изменений. Можно использовать для развертывания кода на сервере или отправки уведомлений.

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

  • pre-commit: Скрипт запускает линтер (например, ESLint для JavaScript) и форматировщик кода (например, Prettier). Если линтер находит ошибки или код не отформатирован, коммит будет отменен.
  • pre-push: Скрипт запускает все автоматизированные тесты. Если хотя бы один тест не проходит, отправка на удаленный репозиторий будет отменена.
  • commit-msg: Скрипт проверяет, что сообщение коммита соответствует соглашению Conventional Commits (например, “feat: add new feature”).

Такой подход позволяет поддерживать высокое качество кода и предотвращать попадание ошибок в основную ветку разработки. Многие фреймворки и библиотеки, такие как React, Vue.js и Laravel, используют Git Hooks для автоматизации задач и обеспечения качества кода.

  • Git Hooks – это скрипты, которые выполняются автоматически до или после определенных событий в Git.
  • Они позволяют автоматизировать задачи, такие как проверка стиля кода, запуск тестов и проверка сообщений коммитов.
  • Git Hooks хранятся в каталоге .git/hooks внутри репозитория.
  • Использование Git Hooks помогает поддерживать качество кода и предотвращать попадание ошибок в основную ветку разработки.
  • Можно использовать различные языки программирования для написания Git Hooks, например, Bash, Python или JavaScript.
## Интерактивный пример
Как работают Git Hooks — триггеры на события:
<Playground client:load
template="vite-react"
files={{
"/App.jsx": `import { useState, useRef } from "react";
const HOOKS = [
{ name: "pre-commit", desc: "Lint & format check" },
{ name: "commit-msg", desc: "Validate commit message format" },
{ name: "pre-push", desc: "Run tests before push" },
{ name: "post-commit", desc: "Notify team (Slack webhook)" },
];
export default function App() {
const [statuses, setStatuses] = useState(HOOKS.map(() => "idle"));
const [log, setLog] = useState([]);
const running = useRef(false);
const simulate = () => {
if (running.current) return;
running.current = true;
setStatuses(HOOKS.map(() => "idle"));
setLog([]);
const results = HOOKS.map(() => Math.random() > 0.15);
let i = 0;
const step = () => {
if (i >= HOOKS.length) {
setLog((l) => [...l, "Commit successful!"]);
running.current = false;
return;
}
setStatuses((s) => s.map((v, j) => j === i ? "running" : v));
setLog((l) => [...l, "Running " + HOOKS[i].name + "..."]);
const idx = i;
setTimeout(() => {
if (results[idx]) {
setStatuses((s) => s.map((v, j) => j === idx ? "pass" : v));
setLog((l) => [...l, HOOKS[idx].name + ": passed"]);
i++;
step();
} else {
setStatuses((s) => s.map((v, j) => j === idx ? "fail" : v));
setLog((l) => [...l, HOOKS[idx].name + ": FAILED! Commit aborted."]);
running.current = false;
}
}, 600);
};
step();
};
const colors = { idle: "#334155", running: "#eab308", pass: "#22c55e", fail: "#ef4444" };
const icons = { idle: "--", running: "...", pass: "OK", fail: "XX" };
return (
<div style={{ fontFamily: "monospace", background: "#0f172a", color: "#e2e8f0", padding: 16, minHeight: "100vh" }}>
<h3 style={{ color: "#818cf8", marginBottom: 12 }}>Git Hooks Lifecycle</h3>
<button onClick={simulate} style={{ padding: "7px 16px", borderRadius: 6, border: "none", cursor: "pointer", fontWeight: 700, fontSize: 12, background: "#6366f1", color: "#fff", marginBottom: 10 }}>git commit -m "fix: bug"</button>
<div style={{ display: "flex", flexDirection: "column", gap: 6, marginBottom: 12 }}>
{HOOKS.map((h, i) => (
<div key={h.name} style={{ display: "flex", alignItems: "center", gap: 10, background: statuses[i] === "running" ? "#1c1917" : "#1e293b", border: "1px solid " + colors[statuses[i]], borderRadius: 8, padding: "8px 12px", fontSize: 12, transition: "all .3s" }}>
<span style={{ color: "#22d3ee", fontWeight: 700, minWidth: 100 }}>{h.name}</span>
<span style={{ color: "#94a3b8", flex: 1 }}>{h.desc}</span>
<span style={{ color: colors[statuses[i]], fontWeight: 700, fontSize: 11 }}>{icons[statuses[i]]}</span>
</div>
))}
</div>
<div style={{ background: "#1e293b", border: "1px solid #334155", borderRadius: 8, padding: 10, fontSize: 11, color: "#94a3b8", maxHeight: 80, overflowY: "auto" }}>
{log.length ? log.map((l, i) => <div key={i}>{l}</div>) : <div>Нажми кнопку, чтобы запустить симуляцию</div>}
</div>
</div>
);
}
`
}}
/>