Что такое pre-commit hooks для Git и зачем они нужны

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Доброго времени суток, сегодня я хочу вам рассказать о том, что такое 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
  • Удобство в использовании
  • Единоразовая настройка

Из минусов:

  • Возрастает время на создание комита
  • Для больших проектов тесты могут бежать довольно долго

На этом всё. Спасибо, что дочитали до конца, надеюсь, это было полезно для Вас!

👍НравитсяПонравилось8
В избранноеВ избранном2
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Кльова стаття! Супер. Хуки дуже гнучкий інструмент. Pre-commit-message зручний.

У нас в команді пишуть у gedit, vs code, pycharm, geany.
В кожного редактора різне форматування по замовчуванню.
pre-commit + black чудово справляється (без тестів — тести в CI).
Пишеш код, комітиш. Блек все форматує.
ІМХО набагато пришвидчує роботу, коли не потрібно переживати за форматування. Плюс у всіх однаковий стиль.

Ставиш Сонар чи щось подібне і обов’язковий pr check та й все. Все, що залежить від людини — ненадійне

в девопсе это все проще, легче и приятнее

Вы забыли уточнить один важный минус использования pre-commit hooks, из-за которого этот функционал git совершенно не популярен: все ваши настройки не сохраняются в удаленном репозитории, и каждому человеку работающему с этим же репозиторием нужно будет мануально настроить хуки.
Это полностью сводит на нет пользу в использовании, ведь бежать к каждому разработчику и махать руками чтоб он обязательно поставил себе эти проверки, и не выпилил их когда какой-то тест не работает куда сложней, чем просто прикрутить эти проверки в CI.

Верное замечание, спасибо! Этот метод не пропогандирует отказ от CI/CD. Он больше для личного удобства конкретного разработчика

Сокращение количества фейлов на CI/CD

Но если у вас CI/CD то вы вряд ли используете чистый гит. Вы используете систему коде-ревью, типа гитлаб, геррит, битбакет, гитхаб — и там уже подобные вещи настраиваются со стороны этой системы, и вешаются уже на пуш или пулл реквест

зато вполне можно закоммитать какой-нить install_hooks.sh

Якщо людина працюватиме на проекті кілька років, в чому проблема один раз налаштувати? Це допоможе зекономити купу часу в майбутньому.

Та навіщо взагалі та автоматизація, заводиш одного робітника він все за всіх робить

в js для этого есть husky, один конфиг для всех разрабов

Это конечно очень полезная фича, только эти задачи лучше выполнять на этапах написания:
— Переформатирование кода по правилам делается автоматически плагинами VS Code при сохранении.
— Линт следит за стилем написания и подчёркивает, где нужно добавить описание к импортируемым элементам, или что-то исправить.
— При компиляции выводит сообщения о неиспользуемых переменных. Если код скомпилирован, значит неиспользуемых переменных там нет. Опять-таки есть плагины, которые на этапе написания показывают ошибки или какие-то косяки.

Единственное, что тесты автоматически не запускаются, но это как бы не проблема. Проблема покрыть тестами весь написанный функционал.

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

— Переформатирование кода по правилам делается автоматически плагинами VS Code при сохранении.

меня бы очень сильно бесило получать не-стилистические изменения в файлах на каждом сохранении. Например, удаление не используемых импортов. А вас — не?

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

есть куча различных кейсов юзать хуки, мне например больше всего нравится хук, который не дает закоммитить при наличии коментария // no-commit (js вариант), например при работе временно, чтоб не тормозило, мокаешь какой то внешний сервис, а потом при коммите не забыть переключится с мока

А почему и name и id должны быть уникальными? Зачем два поля с одинаковыми условиями?
Сразу второй вопрос

types: [python]

 — как система понимает какие файлы относятся к питону? по .py ?

id — это уникальный индитификатор хука
name — это то что Вы увидите при выполнении хука в консоли, по сути name не обязателен

по поводу types, совершенно верно, это ассоциация к *.py файлам. Подробнее описано тут — pre-commit.com/...​iltering-files-with-types

name — имя хука, должно быть уникальным среди всех хуков репозитория

Пустой 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

Подписаться на комментарии