Что такое pre-commit hooks для Git и зачем они нужны
Доброго времени суток, сегодня я хочу вам рассказать о том, что такое pre-commit hooks для системы контроля версий Git.
Итак, для начала нам надо разобраться с тем, что такое pre-commit hooks. В системе контроля версий Git есть специальный механизм для запуска скриптов и/или команд по определенному событию (см. рисунок ниже), благодаря которому мы можем автоматизировать некоторые рутинные операции.

Допустим, что перед каждым commit мы должны выполнять следующие шаги:
- Переформатирование кода, согласно правилам форматирования кода (black).
- Удаление неиспользуемых импортов библиотек и неиспользуемых переменных (autoflake).
- Апгрейд кода под новый стиль написания, который добавлен в новых версиях языка (pyupgrade).
- Переформатирование импортов согласно правил форматирования (reorder-python-imports).
- Проверить все переформатирования на соответствие стандартам форматирования (flake8).
- Запуск тестов.
Исходя из количества степов, легко забыть что-то из перечня. Ну что же, давайте автоматизировать!
Из всех хуков, которые предоставляет система контроля версий GIT (см. рисунок выше) нам больше всего подходит pre-commit hook на это событие мы и будем подписываться.
Для упрощения конфигурации я буду использовать пакет pre-commit который предоставляет удобные конфиги в формате YAML.
Далее, нам необходимо создать файл .pre-commit-config.yml в корневом каталоге нашего проекта, рядом с папкой .git .
Давайте разберем конфиг на примере первой задачи «Переформатирование кода, согласно правилам форматирования кода».
- repo: local hooks: - id: black name: black entry: black language: system types: [python] args: [--line-length=200, --target-version=py37]
- repo — репозиторий для которого будет отрабатывать данный хук, в нашем примере — это локальный репозиторий
- hooks — перечень хуков которые будут отрабатывать для репозитория обозначенного выше.
- id — идинтификатор хука, должен быть уникальным среди всех хуков в репозитории
- name — имя хука, должно быть уникальным среди всех хуков репозитория
- entry — команда или скрипт, который должен быть запущен по событию, в нашем случае это команда black
- language — язык, при помощи которого этот хук будет установлен, в нашем случае это system, так как после установки black становиться системной командой
- types — тип файла к которому данный хук должен быть применен, в нашем случае это python файлы
- args — дополнительные параметры запуска команды которую мы обозначили в entry
Таким образом, у нас получился небольшой, компактный конфиг для нашего pre-commit hook. Но одного конфига мало для того, чтобы это заработало, после того как с конфигом мы закончили необходимо выполнить интеграцию (инсталляцию) всех наших хуков непосредственно в репозиторий. Для этого используем команду:
pre-commit install
Полный конфиг для нашей задачи будет выглядеть так:
- repo: local hooks: - id: black name: black entry: black language: system types: [python] args: [--line-length=200, --target-version=py37] - id: autoflake name: autoflake entry: autoflake language: system types: [python] args: [--in-place, --remove-all-unused-imports, --remove-duplicate-keys] - id: pyupgrade name: pyupgrade entry: pyupgrade language: system types: [python] args: [--py37-plus] - id: reorder-python-imports name: reorder-python-imports entry: reorder-python-imports language: system types: [python] args: [--py37-plus] - id: flake8 name: flake8 entry: flake8 language: system types: [python] args: [--max-line-length=200] - id: tests name: Run tests entry: "source .env/bin/activate && python -m pytest -v tests" language: system verbose: true
Так же важно понимать, что при commit хуки применяются не ко всем файлам в проекте, а только к тем, которые попали в commit, то есть те, которые были изменены или добавлены.
Из плюсов можно выделить следующее:
- Автоматизация рутины
- Сокращение количества фейлов на CI/CD
- Удобство в использовании
- Единоразовая настройка
Из минусов:
- Возрастает время на создание комита
- Для больших проектов тесты могут бежать довольно долго
На этом всё. Спасибо, что дочитали до конца, надеюсь, это было полезно для Вас!
37 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівПлохая практика, если вы — не единственный разработчик.
Отличная практика, если однажды уже выполнена вся подготовка (например, форматирование) и все разработчики обязаны применять эти методы.
А ещё лучше, если CI контролирует результат у себя тоже.
Запускати тести через
pre-commitоднозначно крінжово. Всі обгортають вtoxчи щось подібне, бо тестування ж може відбуватися в багатьох енвах, і треба ціла матриця.Додавати в локальний ґіт реп дуже сумнівна ідея. Крім того, зараз є ґітхаб-апка, яка запускається в пул-реквестах і навіть зміни комітить назад: pre-commit.ci.
Ні, гарна.
Бо якщо сервер переробляє зміни по-своєму, то потім їх буде складно прийняти тому ж розробнику взад, буде суцільний конфлікт.
Ну якщо вам локальна робота не важлива з моменту пуша PR, то тоді ок. Але це само по собі якось дивно, мʼяко кажучи.
Кльова стаття! Супер. Хуки дуже гнучкий інструмент. Pre-commit-message зручний.
У нас в команді пишуть у gedit, vs code, pycharm, geany.
В кожного редактора різне форматування по замовчуванню.
pre-commit + black чудово справляється (без тестів — тести в CI).
Пишеш код, комітиш. Блек все форматує.
ІМХО набагато пришвидчує роботу, коли не потрібно переживати за форматування. Плюс у всіх однаковий стиль.
Ставиш Сонар чи щось подібне і обов’язковий pr check та й все. Все, що залежить від людини — ненадійне
в девопсе это все проще, легче и приятнее
Девопёс предпочитает не вкладывать в себя код.
Вы забыли уточнить один важный минус использования pre-commit hooks, из-за которого этот функционал git совершенно не популярен: все ваши настройки не сохраняются в удаленном репозитории, и каждому человеку работающему с этим же репозиторием нужно будет мануально настроить хуки.
Это полностью сводит на нет пользу в использовании, ведь бежать к каждому разработчику и махать руками чтоб он обязательно поставил себе эти проверки, и не выпилил их когда какой-то тест не работает куда сложней, чем просто прикрутить эти проверки в CI.
Верное замечание, спасибо! Этот метод не пропогандирует отказ от CI/CD. Он больше для личного удобства конкретного разработчика
Но если у вас CI/CD то вы вряд ли используете чистый гит. Вы используете систему коде-ревью, типа гитлаб, геррит, битбакет, гитхаб — и там уже подобные вещи настраиваются со стороны этой системы, и вешаются уже на пуш или пулл реквест
А кто то еще использует «чистый гит» ?
зато вполне можно закоммитать какой-нить
install_hooks.shЯкщо людина працюватиме на проекті кілька років, в чому проблема один раз налаштувати? Це допоможе зекономити купу часу в майбутньому.
Та навіщо взагалі та автоматизація, заводиш одного робітника він все за всіх робить
в js для этого есть husky, один конфиг для всех разрабов
У Gerrit рекомендация на такой случай обычно выглядит как wget http://где-там-сервер/hooks.sh && sh hooks.sh если запустить из локальной рабочей копии. Банально и тоже можно оскриптовать.
CI проверяет — выполняет тоже и сравнивает у себя. Может также проверяться обязательный элемент (как Change-Id в Gerrit).
Выполнение локально полезно потому, что если сервер начнёт перепиливать коммиты под себя, локальному разработчику надо будет, если он делает что-то хоть чуть большее чем один коммит, сложным образом мержить и ребейзить. Поэтому сервер должен или принимать, или не принимать коммиты, но в таком виде, как их запушили.
Это конечно очень полезная фича, только эти задачи лучше выполнять на этапах написания:
— Переформатирование кода по правилам делается автоматически плагинами VS Code при сохранении.
— Линт следит за стилем написания и подчёркивает, где нужно добавить описание к импортируемым элементам, или что-то исправить.
— При компиляции выводит сообщения о неиспользуемых переменных. Если код скомпилирован, значит неиспользуемых переменных там нет. Опять-таки есть плагины, которые на этапе написания показывают ошибки или какие-то косяки.
Единственное, что тесты автоматически не запускаются, но это как бы не проблема. Проблема покрыть тестами весь написанный функционал.
Согласен, эти задачи можно решить и другими способами, но как вариант. Ну и кк тому же — не все языки компилируються. Но по поводу плагинов — согласен полностью
меня бы очень сильно бесило получать не-стилистические изменения в файлах на каждом сохранении. Например, удаление не используемых импортов. А вас — не?
Скорее наоборот, получать лишние сообщения при компиляции о неиспользуемых импортах будет бесить. Всё равно их придётся удалить, либо закомментировать. Ну и кроме того, наоборот, используемые импорты автоматически вставляются.
есть куча различных кейсов юзать хуки, мне например больше всего нравится хук, который не дает закоммитить при наличии коментария // no-commit (js вариант), например при работе временно, чтоб не тормозило, мокаешь какой то внешний сервис, а потом при коммите не забыть переключится с мока
А почему и name и id должны быть уникальными? Зачем два поля с одинаковыми условиями?
Сразу второй вопрос
— как система понимает какие файлы относятся к питону? по .py ?
id — это уникальный индитификатор хука
name — это то что Вы увидите при выполнении хука в консоли, по сути name не обязателен
по поводу types, совершенно верно, это ассоциация к *.py файлам. Подробнее описано тут — pre-commit.com/...iltering-files-with-types
Пустой name не считается неуникальным? =)
Уникальность тут нужна чисто для визуала, програмной ошибки он не вызовет, в статье указал на уникальность что бы не возникло путоницы
Спасибо за труд, Алексей! Очень полезно!
Личный опыт: тесты лучше запускать на pre-push.
Главная причина: когда надо срочно переключиться на другую бранчу, временно закомитав незавершенную работу, будет очень бесить «ой, тесты упали, фиг тебе, а не коммит». Также, лично для меня, нормальный флоу когда коммитаю фичу по частям, в конце обновляю тесты, а уже потом делаю squash. Что также не реально при тестах в pre-commit.
Резонное замечание по поводу тестов, но тесты — это как пример того что можно. А вот если надо переключиться на другую бранчу не лучше ли использовать stash что бы не плодить коммиты?
stash же глобальный — один на все-все бранчи. Даже если задавать мессадж(что занимает время и ментальные силы — «как назвать, чтоб потом точно понял к чему?»), возможно, вернешься не через час, а через день, два, неделю. И в стеше уже с полдюжины таких «потом еще вернусь».
это личные предпочтения, или количество коммитов может влиять на что-то?
История коммитов, довольно часто используеться в релизнотах. А что бы закомитить не надо меседж писать и время тратить? Ну тут холивар — ради хотливара, дело вкуса и процессов.
fixup + autosquash
Даже не при переключении на бранчу, а если большой кусок работы. Лучше коммититься каждый день, а не раз в месяц. Иначе навернется диск, или пожар в офисе — и овердофига работы потеряно, с соплями, депресняком и выгоранием.
Так понятное дело что большей кусок работы нужно комитить. Повторюсь, тесты в статье — как пример, а не как призыв.
Комитить можно так: «git commit —no-verify», тогда прекомитные хуки не будут запускаться.
—no-verify
This option bypasses the pre-commit and commit-msg hooks
Можно ещё так : git commit -n