Git pre commit windows

Запуск Git-хуков при помощи pre-commit

Умение работать с системой контроля версий Git — базовый навык для выживания разработчика (на любом языке программирования) в современных реалиях. Система контроля версий — это этакая машина времени для вашего проекта: всегда можно вернуться на любое прошлое состояние проекта, понять когда, как, что и кем менялось.

Интересный факт: согласно Google Trends, во всём мире Git фактически вытеснил другие системы контроля версий. Только в Китае почему-то до сих пор популярен Subversion (46% рынка). К чему бы это?

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

  • pre-commit — выполняется перед созданием коммита;
  • commit-msg — выполняется после добавления сообщения коммита;
  • post-checkout — выполняется после переключения ветки;
  • pre-push — выполняется перед загрузкой локальной истории на удалённый сервер.

Полный список хуков можно посмотреть в документации.

Как это работает? Рассмотрим, например, схему работы хука pre-commit :

  1. пользователь пишет в терминале git commit -v ;
  2. git пытается выполнить хук pre-commit локально, на машине разработчика;
  3. если хук завершается ошибкой, то операция коммита прерывается;
  4. если хук выполнился без ошибок, то операция коммита продолжается, открывается текстовый редактор для ввода сообщения.

Фишка в том, что хуки могут прерывать операции, за которые они отвечают, если что-то идёт не так. Это идеальное место, чтобы запускать какие-нибудь проверки качества кода, например, линтеры, форматтеры или тесты (если они работают быстро, конечно), или проверять сообщение коммита на соответствие конвенциям или на грамматические ошибки.

Пишем Git-хук на bash

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

В этом примере я использую Linux. Если вы пользуетесь Windows, то большинство описанный вещей будут работать без изменений через Git Bash, который устанавливается вместе с Git for Windows. Возможно, вам только придётся поменять путь до вашего bash в шебанге, как описано в конце этой статьи. Если же вы соберётесь писать в хуках что-то сложное и требующее интеграции с ОС, то, возможно, стоит вместо bash использовать PowerShell.

Создадим пустой репозиторий:

Git уже заботливо создал для нас шаблоны для написания хуков, которые лежат в специальной служебной директории:

Можно, например, просто переименовать файл pre-commit.sample в pre-commit , и этот хук вступит в силу. В моём случае там на bash реализована проверка имён файлов, которая не допустит добавление файлов с именами, содержащими не-ASCII символы.

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

Этот файл должен быть сохранён по пути .git/hooks/pre-commit . Файл нужно сделать исполняемым, иначе Git не сможет его запустить:

Теперь давайте создадим какой-нибудь файл, который точно не пройдет этих проверок. Обратите внимание на лишние пробелы рядом со скобками, которые не понравятся flake8 , и на одинарные кавычки, которые не понравятся black :

Попытаемся его закоммитить:

Наш умный хук выполнился и запустил flake8 , который нашел ошибки. Давайте удалим лишние пробелы и попытаемся закоммитить ещё раз:

В этот раз flake8 не нашёл ошибок, но операция всё равно завершилась ошибкой из-за black . Давайте отформатируем файл и наконец закоммитим его:

На этот раз всё срабатывает успешно.

Чтобы закоммитить, игнорируя хуки, можно было передать в команду флаг —no-verify или -n :

Как видите, писать git-хуки самостоятельно не так уж и сложно. Не обязательно использовать bash , хуки можно писать на чём угодно — любой исполняемый файл подойдет, хоть на интерпретируемом языке, хоть на компилируемом. Если бы нужно было писать какую-то более сложную логику, то я бы, например, предпочёл написать хук на Python.

У такого ручного способа есть несколько недостатков:

  • хук хранится в директории .git , которая является служебной для локальной копии репозитория и сама не сохраняется в систему контроля версий; это значит, что если вы захотите поделиться своим хуком с коллегами, то вы можете лишь каким-либо образом скинуть им исходный код, а им придётся самостоятельно класть его в правильное место и назначать права;
  • первая команда, завершившаяся ошибкой, прерывает выполнение хука и последующие команды не выполняются; это можно исправить, но получится сильно сложнее;
  • логика хука может быстро стать сложной;
  • всё нужно имплементить самостоятельно;
  • наш хук полагается на наличие команд flake8 и black , так что они либо должны быть установлены глобально, либо перед коммитом нужно активировать виртуальное окружение проекта.

Используем готовый инструмент — pre-commit

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

Читайте также:  Windows 10 домашняя минимальные требования

Установка

pre-commit написан на Python (ещё бы, иначе б я не был его фанатом 😁), поэтому установить его можно через pip . Он должен быть установлен глобально, а не в виртуальном окружении проекта, где вы собираетесь его использовать. Рекомендую использовать метод установки в домашнюю директорию пользователя (см. заметку про виртуальные окружения за подробностями):

Настройка

pre-commit спроектирован с прицелом на удобное использование сторонних переиспользуемых хуков, но может исполнять и вообще любые команды. Уже написаны сотни полезных хуков для разных языков и задач, из которых, как из конструктора, можно собрать практически любой нужный вам вокрфлоу. Выбирайте те, которые вам понравятся. Для примера я возьму всё те же flake8 и black , и ещё несколько других хуков сверху (а что, бесплатно же).

pre-commit конфигурируется на уровне репозитория при помощи YAML-файла. Файл должен называться .pre-commit-config.yaml и находиться в корне репозитория. Давайте сгенерируем базовый конфиг:

И допилим примерно до такого состояния:

Теперь включим pre-commit в текущем репозитории.

Рекомендую добавить эту команду в документацию для разработчиков. Это всё, что нужно будет сделать вашим коллегам, чтобы хуки заработали и у них тоже.

Можно убедиться, что pre-commit заменил наш старый хук на свой:

При этом старый хук был переименован в pre-commit.legacy . Умный pre-commit будет запускать и его тоже, чтобы не сломать текущее поведение. Если это не желательно, то проще всего удалить этот старый файл:

Проверим, что конфигурационный файл валиден, а заодно и что всё нынешнее содержимое репозитория удовлетворяет описанными правилам:

При первом запуске pre-commit скачает и закэширует все файлы, необходимые для выполнения проверок. Для проверок, которые написаны на Python, будут созданы изолированные виртуальные окружения, так что они никак не будут зависеть от виртуального окружения проекта. Первый раз из-за скачивания зависимостей эта команда может выполняться секунд 30 (в зависимости от скорости интернета), но перезапустите её ещё раз и она завершится за секунду.

Возможно, pre-commit найдёт проблемы или даже исправит некоторые файлы.

Если проверка отработала без ошибок, то конфиг нужно добавить в Git:

Плагины/зависимости для проверок

Из-за того, что для проверок, написанных на Python, создаются отдельные виртуальные окружения, может быть не совсем понятно, как устанавливать плагины для таких программ, как, например, flake8 . Для flake8 важно, чтобы все его плагины были установлены с ним в одно виртуальное окружение, иначе он просто не сможет их найти. Специально для этого у pre-commit предусмотрена настройка additional_dependencies , которая используется вот таким образом:

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

Использование

Теперь можно вообще забыть про существование pre-commit и просто пользоваться Git как обычно, а pre-commit будет выполнять все описанные проверки, изредка беспокоя вас прерванными операциями, если будут найдены какие-нибудь проблемы. Давайте снова попробуем закоммитить тот сломанный файл с пробелами и кавычками:

Ожидаемо, операция прервалась, но в этот раз мы получили все нужные сообщения об ошибках. Кроме того, файл даже был автоматически отформатирован при помощи black , так что ничего даже вручную делать не нужно. Просто ещё раз запускаем те же команды, и в этот раз проверки должны пройти.

На YAML программировать намного приятнее, чем на bash ! Да, честно говоря, практически на чём угодно писать приятнее, чем на bash .

Альтернативы pre-commit

Мне проще всего использовать инструменты, написанные на знакомом мне языке, но вообще pre-commit далеко не уникальный инструмент и имеет множество альтернатив. Если вы пишете не на Python, то может быть вам будут ближе другие инструменты, хотя все они имеют примерно схожий функционал:

Возможно, вы также найдете для себя что-то полезное на странице “Awesome Git hooks”.

Заключение

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

Представьте ситуацию, когда сидишь и ждёшь результатов проверок от CI, которые могут быть достаточно долгими (на проекте, где я сейчас работаю, тесты выполняются 8-10 минут), видишь красный крестик, идёшь посмотреть, что же там сломалось, а там всё почти отлично — тесты прошли, но только flake8 нашёл лишний пробел. Чинишь лишний пробел и снова ждёшь 10 минут, чтобы получить свою зелёную галочку. Дак вот хуки спасают от таких ситуаций, потому что все тривиальные проблемы обнаруживаются за несколько секунд локально на моей машине и никогда не попадают в историю Git.

Настоятельно рекомендую пользоваться Git-хуками. Это позволит вам не тратить время на ерунду, и в итоге быть более эффективным и довольным разработчиком.

Примеры из поста можно найти здесь.

Если понравилась статья, то подпишитесь на уведомления о новых постах в блоге, чтобы ничего не пропустить!

Автоматизируем проверку кода или еще немного о pre-commit hook’ах

Думаю, нет нужды рассказывать хабрапользователю что такое Git / GitHub, pre-commit и как наносить ему hook справа. Перейдем сразу к делу.

Читайте также:  Автоблокировка windows 10 по времени

В сети много примеров хуков, большинство из них на shell’ах, но ни один автор не уделил внимание одному важному моменту — хук приходится таскать из проекта в проект. На первый взгляд — ничего страшного. Но вдруг появляется необходимость внести изменения в хук, который уже живет в 20 проектах… Или внезапно нужно переносить разработку с Windows на Linux, а хук на PowerShell’е… Что делать? . PROFIT

«Лучше так: 8 пирогов и одна свечка!»

Примеры, конечно, сильно утрированы, но с их помощью выявлены неудобства, которых хотелось бы избежать. Хочется, чтобы хук не требовалось таскать по всем проектам, не приходилось часто «допиливать», но чтобы при этом он умел:

  • выполнять проверку отправляемого в репозиторий кода на валидность (например: соответствие требованиям PEP8, наличие документации итд);
  • выполнять комплексную проверку проекта (юнит-тесты итд);
  • прерывать операцию commit’а в случае обнаружения ошибок и отображать подробный журнал для разбора полетов.

И выглядел приблизительно так:

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

pre-commit.py

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

Этими параметрами будем задавать основное поведение скрипта:

  • -c или —check[скрипт1… скриптN] — запуск скриптов проверки на валидность. Скрипт должен располагаться в том же каталоге, что и pre-commit.py. Иначе — нужно указать полный путь. Каждому скрипту будут «скармливаться» файлы из текущего коммита.
  • -t или —test[тест1… тестN] — запуск юнит-тестов и прочих скриптов, которым не требуются файлы текущего коммита. Тест должен располагаться в каталоге текущего проекта. Иначе — нужно указать полный путь.

Оба параметра будут необязательными (для возможности оставить только один тип проверки), но если не указать ни один из них, pre-commit.py завершит работу с кодом «1» (ошибка).

И добавим вспомогательные параметры (все необязательные):

  • -e или —execпуть_к_интерпретатору — полный путь (с именем файла) к интерпретатору, который будет выполнять скрипты из —check и —test. Если параметр не указать — будет использован интерпретатор, которым выполняется pre-commit.py.
  • -v или —verbose — включает подробное логирование. Если не указан — в лог записывается консольный вывод тех скриптов, выполнение которых завершилось с кодом ошибки.
  • -o или —openlogпуть_к_просмотрщику — полный путь (с именем файла) к программе, которой будем просматривать лог.
  • -f или —forcelog — принудительное открытие лога. Если не указан — лог открывается только в случае обнаружения ошибок. Параметр применим, если указан —openlog.

Логика ясна, теперь можно приступать к написанию самого скрипта.

Параметры командной строки

Для начала настроим парсер параметров командной строки. Здесь будем использовать модуль argparse (или «на пальцах» неплохо объясняют здесь и здесь), так как он входит в базовый пакет Python.

Запустим скрипт со следующими параметрами:

И выведем содержимое params на экран:

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

Все хорошо, но можно немного упростить себе жизнь, без ущерба гибкости. Мы знаем, что в 99% случаев скрипт валидации один и называется он, к примеру, ‘pep8.py’, а скрипт юнит-тестов в нашей власти каждый раз называть одинаково (и часто он тоже будет один). Аналогично с отображением лога — всегда будем использовать одну и ту же программу (пусть это будет «Блокнот»). Внесем изменения в конфигурацию парсера:

И добавим установку значений по умолчанию:

После внесения изменений код настройки парсера должен выглядеть так:

Теперь строка запуска скрипта стала короче:
содержимое params:

Параметры победили, едем дальше.

Настроим объект лога. Файл лога ‘pre-commit.log’ будет создаваться в корне текущего проекта. Для Git рабочим каталогом является корень проекта, поэтому путь к файлу не указываем. Также, укажем режим создания нового файла при каждой операции (нам нет необходимости хранить предыдущие логи) и зададим формат лога — только сообщение:

Последней строкой кода еще немного упростим себе жизнь — создаем алиас, которым будем пользоваться дальше по коду вместо logging.info.

Shell

Нам потребуется неоднократно запускать дочерние процессы и считывать их вывод в консоль. Для реализации данной потребности напишем функцию shell_command. В ее обязанности будет входить:

  • запуск подпроцесса (с помощью Popen);
  • считывание данных с консоли подпроцесса и их преобразования;
  • запись считанных данных в лог, если подпроцесс завершился с кодом ошибки.

Функция будет принимать аргументы:

  • command — аргумент для Popen. Собственно то, что будет запускать в Shell’е. Но вместо цельной строки («python main.py») рекомендуют задавать списком ([‘python’, ‘main.py’]);
  • force_report — управление выводом в лог. Может принимать значения: True — принудительный вывод в лог, False — вывод, если получен код ошибки, None — запретить вывод в лог.
Читайте также:  Ssh linux server from windows

Head revision

Список файлов текущего commit’а легко получается с помощью консольной команды Git — «diff». В нашем случае потребуются измененные или новые файлы:

В результате targets будет содержать нечто подобное:

Самый мучительный этап завершен — дальше будет проще.

Проверка на валидность

Здесь все просто — пройдемся по всем скриптам, заданным в —check, и запустим каждый со списком targets:

Пример содержимого лога на коде не прошедшем проверку на валидность:
[ SHELL ] C:\python34\python.exe c:\dev\projects\pre-commit-tool\pep8.py C:\dev\projects\example\demo\daemon_example.py (code: 1):
C:\dev\projects\example\demo\daemon_example.py:8:80: E501 line too long (80 > 79 characters)

Запуск тестов

Аналогично поступаем и с юнит-тестами, только без targets:

[UPD] Отображаем лог

В зависимости от глобального кода результата и параметров —openlog и —forcelog, принимаем решение — отображать лог или нет:

Примечание. Работает в версиях Python 2.6 (и выше) и 3.х. На версиях, ниже 2.6 — тесты не проводились

И не забываем в конце скрипта вернуть в оболочку Git код результата:

Все. Скрипт готов к использованию.

Корень зла

Хук — это файл с именем «pre-commit» (без расширения), который нужно создать в каталоге: /.git/hooks/

Для корректного запуска на Windows есть пара важных моментов:
1. Первая строка файла должна быть: #!/bin/sh
Иначе увидем такую ошибку:
GitHub.IO.ProcessException: error: cannot spawn .git/hooks/pre-commit: No such file or directory

2. Использование стандартного разделителя при указании пути приводит к подобной ошибке:
GitHub.IO.ProcessException: C:\python34\python.exe: can’t open file ‘c:devprojectspre-commit-toolpre-commit.py’: [Errno 2] No such file or directory

Лечится тремя способами: используем двойной обратный слеш, либо берем весь путь в двойные кавычки, либо используем «/». К примеру, Windows съедает это и не давится:

Конечно, так делать не рекомендуется 🙂 Используйте любой способ, который вам нравится, но один.

Приемочные испытания

Тренироваться будем «на кошках»:

Тестовый commit имеет новые, переименованные\измененные и удаленные файлы. Также, включены файлы, не содержащие код; сам код содержит ошибки оформления и не проходит один из юнит-тестов. Создадим хук с валидацией, тестами и открытием подробного лога:

И пробуем выполнить commit. Подумав пару секунд, Git desktop просигналит об ошибке:

А в соседнем окне блокнот отобразит следующее:

[ SHELL ] git diff —cached —name-only —diff-filter=ACM (code: 0):
.gitattributes1
demo/daemon_example.py
main.py
test.py
test2.py

[ SHELL ] C:\python34\python.exe c:\dev\projects\pre-commit-tool\pep8.py C:\dev\projects\example\demo\daemon_example.py C:\dev\projects\example\main.py C:\dev\projects\example\test.py C:\dev\projects\example\test2.py (code: 1):
C:\dev\projects\example\demo\daemon_example.py:8:80: E501 line too long (80 > 79 characters)
C:\dev\projects\example\demo\daemon_example.py:16:5: E303 too many blank lines (2)
C:\dev\projects\example\demo\daemon_example.py:37:5: E303 too many blank lines (2)
C:\dev\projects\example\demo\daemon_example.py:47:5: E303 too many blank lines (2)
C:\dev\projects\example\main.py:46:80: E501 line too long (90 > 79 characters)
C:\dev\projects\example\main.py:59:80: E501 line too long (100 > 79 characters)
C:\dev\projects\example\main.py:63:80: E501 line too long (115 > 79 characters)
C:\dev\projects\example\main.py:69:80: E501 line too long (105 > 79 characters)
C:\dev\projects\example\main.py:98:80: E501 line too long (99 > 79 characters)
C:\dev\projects\example\main.py:115:80: E501 line too long (109 > 79 characters)
C:\dev\projects\example\main.py:120:80: E501 line too long (102 > 79 characters)
C:\dev\projects\example\main.py:123:80: E501 line too long (100 > 79 characters)

[ SHELL ] C:\python34\python.exe test.py (code: 1):
Test 1 — passed
Test 2 — passed
[!] Test 3 FAILED

[ SHELL ] C:\python34\python.exe test2.py (code: 0):
Test 1 — passed
Test 2 — passed

Повторим этот же commit, только без подробного лога:

[ SHELL ] C:\python34\python.exe c:\dev\projects\pre-commit-tool\pep8.py C:\dev\projects\example\demo\daemon_example.py C:\dev\projects\example\main.py C:\dev\projects\example\test.py C:\dev\projects\example\test2.py (code: 1):
C:\dev\projects\example\demo\daemon_example.py:8:80: E501 line too long (80 > 79 characters)
C:\dev\projects\example\demo\daemon_example.py:16:5: E303 too many blank lines (2)
C:\dev\projects\example\demo\daemon_example.py:37:5: E303 too many blank lines (2)
C:\dev\projects\example\demo\daemon_example.py:47:5: E303 too many blank lines (2)
C:\dev\projects\example\main.py:46:80: E501 line too long (90 > 79 characters)
C:\dev\projects\example\main.py:59:80: E501 line too long (100 > 79 characters)
C:\dev\projects\example\main.py:63:80: E501 line too long (115 > 79 characters)
C:\dev\projects\example\main.py:69:80: E501 line too long (105 > 79 characters)
C:\dev\projects\example\main.py:98:80: E501 line too long (99 > 79 characters)
C:\dev\projects\example\main.py:115:80: E501 line too long (109 > 79 characters)
C:\dev\projects\example\main.py:120:80: E501 line too long (102 > 79 characters)
C:\dev\projects\example\main.py:123:80: E501 line too long (100 > 79 characters)

[ SHELL ] C:\python34\python.exe test.py (code: 1):
Test 1 — passed
Test 2 — passed
[!] Test 3 FAILED

Исправим ошибки, повторим commit, и — вот он, долгожданный результат: Git desktop не ругается, а блокнот показывает пустой pre-commit.log. PROFIT.

[UPD] Вместо заключения

Конечно, данный скрипт — не панацея. Он полезен, когда все необходимые проверки ограничиваются локальным запуском проверочных скриптов. В комплексных проектах обычно применяется концепция Непрерывной интеграции (или CI), и здесь на помощь приходят Travis (для Linux и OS X) и его аналог AppVeyor (для Windows).

[UPD] Еще одна альтернатива — overcommit. Довольно функциональный инструмент для управления хуками Git. Но есть нюансы — для работы overcommit необходимо локально развернуть интерпретатор Ruby.

Всем приятного кодинга и корректных коммитов.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Оцените статью