Git Pre-Commit вместо лишнего стресса

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

Тема форматирования кода традиционно вызывает много споров среди инженеров. Сопутствующие конфликты могут отвлекать команду от разработки, снижать скорость поставок и качество кода. Так как для меня основным языком программирования является Python, в рамках данной статьи я буду раскрывать тему именно в его контексте. Однако основная идея и инструменты достаточно универсальны и применимы для других языков.

Меня зовут Юрий Бондаренко, я сотрудничаю с ЕРАМ в роли Senior Software Engineer и люблю делиться опытом в рамках различных митапов и материалов. А эту статью я начну с небольшой истории, которая описывает мое отношение к бесконечным спорам на эту тему.

Предыстория

Шел далекий 2002 год, я перешел в 9 класс. В расписании появился новый предмет — ДПЮ, что расшифровывалось как «Допризывная подготовка юношей» (даже интересно, есть ли нечто подобное в школах сейчас?). Так вот, не передать словами, насколько я ненавидел этот урок. В нашей школе его вел Петрович — отставной прапорщик, по совместительству наш учитель. Его любимым выражением было: «В армии все может быть безобразно, но должно быть однообразно». Так к чему же эта история и при чем тут Pre-Commit?

Проблематика

С определенной точки зрения разработка программного обеспечения — это не точная наука, а скорее творческий процесс, так как одну и туже задачу можно решить одновременно несколькими способами. Каждый разработчик, как писатель или художник, творит нечто уникальное и при этом у каждого есть свой собственный почерк. С одной стороны, это прекрасно, но с другой — хотелось бы иметь однородный код. По этой же причине в мире Python, к примеру, есть PEP-8. Но Python — не единственный инструмент, которым мы пользуемся, и PEP-8 во многом не исчерпывающий документ. Соответственно, в каждой команде или компании рождаются свои внутренние правила. Но этого мало.

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

  • Как в команде принято работать с GIT?
  • Какой паттерн именования веток?
  • Как принято оформлять комментарии к commit-ам?
  • Как форматировать код (максимальную вложенность, максимальную длину строк, кавычки, документацию....), сложность кода, импорты?
  • Какой принят допустимый уровень покрытия тестами?
  • Используют ли в команде mypy?

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

Контроль

Контроль можно разделить на два типа: мануальный и автоматизированный. В роли мануального обычно используется Code review, а в роли автоматизированного может выступать CI и Git Hooks.

Code Review

Code review — это неавтоматизированный процесс, в котором принимает участие автор кода и рецензенты. Последние анализируют код и принимаю решение о готовности его объединения с общей кодовой базой. Вводя code review, обычно преследуют несколько целей:

  • конструктивное и профессиональное обсуждение проблем;
  • поиск проблем в коде;
  • увеличение фактора автобуса;
  • обучение разработчиков.

На мой взгляд, code review — это не место, где должно присутствовать обсуждение именования веток, форматирования кода и прочей рутины. Причина в том, что это как минимум не продуктивно, а как максимум может сказаться на ментальном здоровье членов команды. Поэтому такие проверки лучше делать еще до того, как код попадет на Code Review.

CI (Continuous Integration)

Сontinuous Integration, если переводить дословно, то это «непрерывная интеграция». Обычно она рассматривается в парадигме CI/CD. Но нас сегодня интересует только малая часть этого мира. А именно — возможность тестировать и анализировать наш код на удалённом серверe по определенному событию. Это отличный инструмент, но мне бы хотелось иметь возможность делать какие-то несложные проверки, а может и вносить изменения в код, еще до того как он попадет на удаленный репозиторий. Пример применения CI/CD на примере Github Actions можно посмотреть в моей статье «Практический разбор PyPI для Python-инженеров».

GitHooks

Git hooks — это скрипты, которые запускаются автоматически каждый раз, когда в репозитории Git происходит определенное событие. Они позволяют настраивать внутреннее поведение Git и запускать настраиваемые действия на ключевых этапах жизненного цикла разработки. Вариантов использования хуков много: вы можете использовать их для автоматизации или оптимизации практически любого аспекта процесса разработки. Они могут выполнятся как локально, так и на стороне сервера.

Хуки находятся в каталоге .git / hooks каждого репозитория Git. В качестве языков могут использоваться практически любые скриптовые (и Python в их числе). Для наших целей можем использовать Git Pre-Commit Hook, который позволит нам выполнять скрипт еще до фиксации изменений. Но все не так просто, так как есть и ряд недостатков.

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

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

Так как же сделать использование Git Hooks удобным?

Pre-сommit framework

Pre-commit — это удобный фреймворк для управления и поддержки Git Pre-Commit Hook.

Установку pre-commit можно делать через pip, что позволяет удобно добавлять его в зависимости от проекта.

pip install pre-commit

Чтобы убедится в правильность установки, можно выполнить команду:

pre-commit --version

Настройка

Для настройки pre-commit используется YAML-файл: pre-commit-config.yaml. Давайте разберемся с базовой структурой этого файла на основе настроек black:

repos:
  - repo: https://github.com/ambv/black
	rev: 21.10b0
	hooks:
  	- id: black
        language_version: python3.9
    	args: ["--line-length=100", "--target-version=py39"]

repos — список репозиториев которые будут использоваться;

repo — репозиторий, в рамках которого отработают хуки. Допустима ссылка на Git репозиторий, относительный путь или `local`;

hooks — список хуков, которые будут запущены в рамках репозитория;

id — идентификатор хука. Должен быть уникальным в рамках репозитория;

language_version — переопределенная версия языка, не является обязательным параметром;

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

Это минимальный набор настроек, который может понадобится. С полным списком можно ознакомиться в документации.

Чтобы инициировать наши скрипты, достаточно выполнить команду:

pre-commit install

Что касается тестирования конфигурации, то не обязательно создавать commit: достаточно выполнить команду pre-commit. Как видите, pre-commit позволяет в очень удобном формате использовать возможности Git Pre Commit Hook. Теперь давайте перейдем к решению нашей проблемы.

Форматирование кода

У каждого разработчика есть свой уникальный «почерк» и любимая IDE со своей системой автоматического форматирования кода. И как следствие — 100500 вариаций форматирования кода в проекте. Кстати, иногда даже без `git blame` можно со 100% уверенностью сказать, кто автор определенной части кода, а код всего проекта выглядит как одеяло в технике patchwork. В чем же здесь проблема?

Во-первых, мы теряем однородность кода, приложение выглядит как лоскутное покрывало.

Во-вторых, это усложняет использование всех благ IDE, которые стремятся увеличить нашу продуктивность и избавить от рутинных задач. Предположим, у нас есть гипотетический utils.py, в котором мы решили хранить какие-то утилиты. Один разработчик дополнил его функцией (f1), для форматирования он использовал возможности VS Code. А после этого второй разработчик дополнил файл еще одной функцией (f2), но для форматирования использовал PyCharm. Итог — в диффе коммита мы увидим не только f2 но и правки по f1. В реальной жизни это влечет за собой большие проблемы.

Поэтому было бы классно иметь инструмент, который может как встраиваться в IDE и заменять собой стандартный форматер кода, так и быть инструментом контроля. К примеру, этим инструментом может быть Black.

А его настройка для pre-commit может быть такой:

- repo: https://github.com/ambv/black
  rev: 21.10b0
  hooks:
	- id: black
      language_version: python3.9
  	args: ["--line-length=100", "--target-version=py39"]

Также в настройки black можно добавить возможность проводить проверку только в измененных файлах и проводить только проверку без форматирования --diff и --check соответственно.

Кроме того, можно использовать pyupgrade для автоматического обновления синтаксиса языка.

-   repo: https://github.com/asottile/pyupgrade
	rev: v2.29.0
	hooks:
	-   id: pyupgrade

Cтилистические ошибки

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

- repo: https://gitlab.com/pycqa/flake8
  rev: '3.7.9'
  hooks:
  -   id: flake8
  	args: ['--config=setup.cfg']
      additional_dependencies: [
          'flake8-bugbear==19.8.0',
          'flake8-coding==1.3.2',
          'flake8-comprehensions==3.0.1',
          'flake8-debugger==3.2.1',
          'flake8-deprecated==1.3',
          'flake8-docstrings==1.5.0',
          'flake8-isort==2.7.0',
          'flake8-pep3101==1.2.1',
          'flake8-polyfill==1.0.2',
          'flake8-print==3.1.4',
     	 'flake8-quotes==2.1.1',
      	'flake8-string-format==0.2.3',
  	]

Для контроля сложности кода можно еще использовать xenon:

- repo: https://github.com/yunojuno/pre-commit-xenon
	rev: v0.1
	hooks:
  	- id: xenon
    	args: ["-e=venv/*,.venv/*", "--max-average=A", "--max-modules=A", "--max-absolute=B"]

Контроль именования веток

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

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v2.2.1
  hooks:
  	- id: no-commit-to-branch
    	args: ['--pattern', '^(?!((fix|feature)\/[a-zA-Z0-9\-]+)$).*']

Как видите, есть большое количество репозиториев с хуками для pre-commit. Со списком самых популярных репозиториев можно ознакомится по ссылке. Также у каждого есть возможность написать свой собственных хук. Этот процесс несложный и описан в документации.

Заключение

Как я писал в начале статьи: «Пусть будет безобразно, но однообразно». Мораль этой фразы в данном случае такова, что не столь важно, какие кавычки или допустимую ширину кода вы используете, а важно чтобы это было решением команды, которому все следуют. В свою очередь, любые новые правила внутри команды должны преследовать такие цели:

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

И тогда всем нам будет счастье (хотя бы в работе).

Спасибо всем, кто дочитал. Делитесь своим опытом автоматизации контроля качества кода. Буду рад конструктивной дискуссии в комментариях.

Также хочу выразить благодарность своему другу и коллеге Павлу Шепетухе, который подготовил иллюстрации к этой статье. Спасибо, что уже больше 20 лет помогаешь с визуальной составляющей всех моих проектов!

👍НравитсяПонравилось10
В избранноеВ избранном6
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

Давно знав про цю фічу, але ні разу не використовував...до того моменту поки мене не задовбали тупі помилки в CI після 10-15 хв білду. Налаштував хук для flake8 та bandit, що використовується в CI, тепер стало комфортніше. Дуже зручна штука, але при умові, що користувач (програміст) розуміє нащо воно йому треба.

Такий flow з локальним тестуванням на pre commit хуці використовуєтся лише на проектах що називаються лайно мамонта, бо в сучасних проектах налаштовують повноцінний CI

Спасибо большое за Ваше мнение. Но хочу заметить, что ни кто не говорил про замену CI )

ни кто не говорил про замену CI

саме це і є мій поінт, налаштовувати remote ci прийдется в будьякому разі, бо ці хіпстори полюбляють remote ide;) так шо нема чого ускладнювати проект локальними хуками

Не знаю, как с Питоном, а Webstorm для JS имеет настройку «Run eslint —fix on save». Естественно, это не гарантирует, что все это у себя включать. Гарантирует шаг в пайплайне, который завалится и не даст смерджить в мастер :)
Не то, чтоб против хуков, но вот когда надо с чего-то незавершенного переключиться на срочную задачу — естественно, в другую ветку. И приходится на ходу исправлять форматирование тут и там, потому что не дает закоммитить — то смерть.

Один разработчик дополнил его функцией (f1), для форматирования он использовал возможности VS Code.

.

А после этого второй разработчик дополнил файл еще одной функцией (f2), но для форматирования использовал PyCharm.

я этого не понял. с ESLint так, что в любой IDE с одним и тем же конфигом даст то же самое форматирование. У Питона такого инструмента нет?

что в любой IDE с одним и тем же конфигом даст то же самое 

I doubt but ok

Можуть бути ньюанси, які автор і намагається нівелювати зовнішнім інструментом, який у всіх буде однаковий

I doubt but ok

От як воно працює: будь-яка IDE з підтримкою інтеграції ESLint просто перестає самодіяльність і делегує реформат/валілацію йому. Який і є «зовнішним інструментом». А в репо закомічений .eslintrc файл-конфіг. І хоч в Webstorm, хоч VSC буде однаковий результат.

Да все верно, но pre-coomit это по сути предохранитель. Кроме того Webstorm не единственная IDE для JS. И речь же не только про форматирование кода))

ну, так и ESLint тоже не только про форматирование. Это статический анализатор.
Или тут отсылка к валидации коммит мессаджей и бранчей? Но и их тоже гибче было б валидировать в рамках CI, а локально хоть пляши, хоть ветку «temp» называй и коммит «temp temp temp». Не?

Я не то, чтоб «категорически против», но сам юзал хуки, и чертыхался не раз, как надо было быстро переключаться, а —no-verify на языке не вертится, когда используешь раз в пятилетку. В итоге, при наличии шага в CI, это только лишний головняк. А при отстутствии CI — ложное чувство безопасности(как-то на «без-CI-ном» проекте чувак закинул в master коммит с неисправленными мердж конфликтами — вот эти все >>>>>; было весело, запомнил на века)

1. Это помогает новым ребятам в команде. Открыл пре-коммит и все понял))
2. Мне нравится проводить статический анализ до того как зафиксирую код, это экономит время, а держать в голове лишняя трата энергии ))
3. Можно добавить кастомных штук

pre-coomit это по сути предохранитель

который не обязательный))

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

Честно не помню уже всех нюансов, возможно что-то изменилось за пару лет ))

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

Как эту проблему решает Pre-сommit?

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

Но вы предлагаете инструмент, который точно так же надо будет подключать/устанавливать.

В общем из статьи я не понял, зачем просить всех устанавливать левую утилиту, если достаточно просто добавить в инструкцию команду

$ cp pre-commit .git/hooks/
?

Спасибо большое за комментарий.

Как эту проблему решает Pre-сommit?

ни как не решает эту проблему, и не должен)) Нам всегда нужны все 3 шага: самоконтроль, CI и Code Review.

Но вы предлагаете инструмент, который точно так же надо будет подключать/устанавливать.

Да можно использовать хуки без обертки, удобно ли это решать уже каждой команде отдельно )

В общем из статьи я не понял, зачем просить всех устанавливать левую утилиту

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

Один разработчик дополнил его функцией (f1), для форматирования он использовал возможности VS Code. А после этого второй разработчик дополнил файл еще одной функцией (f2), но для форматирования использовал PyCharm. Итог — в диффе коммита мы увидим не только f2 но и правки по f1. В реальной жизни это влечет за собой большие проблемы.

Чтоб этого не было нужно разделять коммиты:
первый коммит — форматирование функции f1.
второй — добавление функции f2.

да можно сделать, но это не ускорит работу команды, мы будем тратить время на переформатирование с PyCharm в VS Code с VS Code в black стили и так бесконечно. И это простой пример ))

А тимлид вам зачем?
При обнаружении такой ситуации он должен взять линейку и надавать обоим участникам этого пинг-понга по пальцам.

вот ему нефиг делать следить за каждым комитом, какой-то микроменеджмент

Зачем следить?
Один из разработчиков, вполне может пожаловаться на второго.

> Контроль именования веток

Можно просто сделать commit-msg hook, который при отсутствии имени ветки будет его принудительно добавлять. Например, вот такой.

#!/bin/bash
#
# Automatically adds branch name and branch description to every commit message.
#
NAME=$(git branch | grep '*' | sed -e 's/\* //' -e 's#^.*/##' )
DESCRIPTION=$(git config branch."$NAME".description)

echo "$NAME"': '"$(cat $1)" > "$1"
if [ -n "$DESCRIPTION" ] 
then
   echo "" >> "$1"
   echo $DESCRIPTION >> "$1"
fi 

да, можно и так )) Спасибо.

Настраивать нужно не на стороне клиента, а на стороне сервера, тогда у вас все коммиты будут проверятся всегда.

Да git hooks это не замена CI, а дополнение к ней

Повірте, це дуже пришвидшує розробку, коли ти не заморочуєшся над форматуванням, а при коміті все виправляється без додаткових зусиль.

Ну хорошо, представим себе, что у разработчика остался коммит в котором «def f ( a,b ,c) :»
На сервере пришёл скрипт переделал это в «def f(a, b, c):»
По правилам Git, коммиты будут разные (разное содержимое и разные id). Когда разработчик сделает pull на ветку с сервера, с коммитами что случится?
Хорошо, если rebase, тогда локальный коммит превратится в перестановку пробелов (и потом должен быть убит, о чём отдельный спич). А если merge? Сольются две версии? И какая выиграет?

Вывод: нет, коммит на сервере должен приниматься 1:1 как он был послан конкретным разработчиком, или не приниматься.
На сервере могут теми же вызовами проверять, что код не поменялся, но не модифицировать.

Но чтобы разработчик мог сделать гарантированно корректный по текущим правилам коммит — ему желательно помочь автоматизацией.

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

хм, я бы про автоформат на сервере и не подумал бы даже.

Если сделали требование его использования — то проверять на сервере, что код соответвует форматтеру, достаточно логично. Точно так же как всяким линтерам, если они есть.

Ну а на «клиентской» стороне дать возможность поставить нужные тулзы с нужными опциями (дать конфиг и, если надо, точную версию).

Завтыкал — завалился пайплайн — не смерджишь в мастер.

Ну или любую другую ветку, где как устроено.

Git commit —no-verify

это и плюс и минус git hook-ов.

Просто нужен аналогичных хук на pre-receive на стороне хранилища. Тогда не будет проходить push.
И отключение или —no-verify уже не помогут.

А есть такая возможность?
Сам пару раз неосознанно пушил без тестов и тд. просто потому, что не были установлены нод модули)

А есть такая возможность?

У вас какая задача — чтобы проверялось или чтобы обойти проверку?

Наличие хуков на стороне хранилища зависит от, собственно говоря, хранилища.

github и gitlab — имеют. Как настраивать искать по github/gitlab pre-receive hook.

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