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 лет помогаешь с визуальной составляющей всех моих проектов!
37 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів