Содержание статьи
При code review ты указываешь разработчикам на забытые printf
, console.log
? С ужасом видишь код с синтаксической ошибкой? Или хочешь разграничить права на работу с ветками (как в Bitbucket), потому что джуниор путает порядок веток при слиянии? Хватит это терпеть! У нас же есть Git и Bash!
Xakep #205. Взлом Single Sign-On
Легкий старт с Git hooks
У систем контроля версий есть механизм хуков — скрипты-callback’и, запускаемые Git, когда происходит определенное действие. В некотором смысле это шаблон Observer из ООП.
В качестве действия могут выступать commit
, apply patch
, merge
, push
, rebase
и другие операции Git. Многие думают, что хук можно использовать только для проверки формата commit message, но их область применения ограничена только твоей ленью и/или фантазией. Примеры в статье написаны для Git, но по аналогии можно сделать и для других систем контроля версий — Subversion, Mercurial, Bazaar.
Теория хуков
Хуки бывают двух типов:
- Клиентские — находятся на машине конечного контрибьютора (developer, lieutenant, dictator). Каждый контрибьютор настраивает хуки только для себя в своей копии существующего репозитория, и они не повлияют на других контрибьюторов. Для того чтобы добавить хук, достаточно изменить файл в директории
$PROJECT_DIR/.git/hooks/
. Изменение в этой директории не может быть закоммичено и будет влиять только на текущего пользователя. Такие хуки можно игнорировать, используя дополнительный флагgit commit --no-verify
. - Серверные — хуки, исполняемые угадай где :). Они будут применяться ко всем контрибьюторам проекта.
Для установки такого хука также достаточно изменить файл в директории$PROJECT_DIR/hooks/
, но делать это нужно в удаленном репозитории. Пропустить исполнение таких хуков нельзя. Также при клонировании репозитория не получится выкачать хуки, они останутся на удаленном репозитории.
Писать Git hooks можно на любом скриптовом языке: Python, PHP, Ruby, PowerShell и прочих. В статье будет использован Bash.
Hook уведомляет о результате своего выполнения с помощью кода возврата, и если хук возвращает код, отличный от 0, то исполнение прервется и выполнить операцию (
git commit
, git push
и так далее) не получится.Встроенные хуки
При создании репозитория командой git init
также создаются примеры хуков, которые можно посмотреть в одноименной директории.
$ captain@jolly-roger:/PONY/.git/hooks$ ls
applypatch-msg.sample post-update.sample pre-commit.sample
pre-rebase.sample update.sample commit-msg.sample
pre-applypatch.sample pre-push.sample prepare-commit-msg.sample
Для того чтобы начать их использовать, достаточно скопировать интересующий файл, убрав постфикс .sample (cp pre-commit.sample pre-commit
), и убедиться, что для него выставлены права на исполнение; если нет — выполняем chmod +x pre-commit
.
INFO
Полный список возможных видов hooks можно посмотреть на сайте Git или набрав в командной строке
man githooks
.Хук слева, хук справа
С теорией разобрались. Самое время попрактиковаться на реальных примерах.
Запрет на push в ветку
Начнем с серверного хука с названием pre-receive, который исполняется перед тем, как в удаленном репозитории зафиксируются изменения, когда разработчик делает git push
. Если хук отдаст код возврата, отличный от 0, то исполнение прервется и зафиксировать изменения в репозитории не удастся.
Предположим, что на нашем проекте есть люди (джуниоры), которые не должны иметь возможность фиксировать изменения в master-ветке, пока не вникнут в проект.
Чтобы избавить их от соблазна, можно добавить pre-receive hook с проверкой по black-list:
#!/bin/bash
changedBranch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
blockedUsers=(junior1 junior2)
if [[ ${blockedUsers[*]} =~ $USER ]]; then
if [ $changedBranch == "master" ]; then
echo "You are not allowed commit changes in this branch"
exit 1
fi
fi
Код получился достаточно простым. В переменную changedBranch записывается ветка, с которой разработчик хочет произвести изменения. Если имя пользователя находится в массиве blockedUsers
, тогда мы возвращаем код возврата 1 и не даем зафиксировать изменения в blessed repository.
По аналогии можно сделать проверку по white-list, когда только у конкретных людей (Release managers) есть возможность фиксировать изменения в master-ветке. Теперь реализовать фичу branch permissions продукта Bitbucket Server ты можешь самостоятельно.
INFO
Неслучайно master-ветку нужно защищать от прямого доступа всем желающим. Все популярные branching workflow, будь то gitflow
или GitHub flow, первым постулатом определяют, что master-ветка должна быть всегда готова к релизу на продакшен сервера.
printf shall not pass
Многие разработчики по-прежнему отлаживают свои программы, используя технику Print debugging (или, по-научному, Tracing) и часто забывают удалить отладочный код при коммите. Чтобы обезопасить наш blessed repository, можно использовать следующий Git hook (как pre-commit, так и pre-receive):
#!/bin/bash
blackList="console.info\|console.log\|alert\|var_dump"
result=0
while read FILE; do
# check that file not removed(also can be implemented using --diff-filter)
if [[ -f $FILE ]]; then
if [[ "$FILE" =~ ^.+(php|html|js)$ ]]; then
RESULT=$(grep -i -m 1 "$blackList" "$FILE")
if [[ ! -z $RESULT ]]; then
echo "$FILE contains denied word: $RESULT"
result=1
fi
fi
fi
done << EOT
$(git diff --cached --name-only)
EOT
if [ $result -ne 0 ]; then
echo "Aborting commit due to denied words"
exit $result
fi
Команда git diff --cached --name-only
вернет имена всех измененных файлов, в том числе удаленных или переименованных, поэтому в цикле по файлам у нас есть условие [[ -f $FILE ]]
, которое проверяет, что файл существует в системе. Далее грепаем в каждом файле запрещенные фразы. В случае, если наш любимый программист решит внести свои изменения с отладочным кодом в репозиторий, его попытка будет предотвращена. А при code review тебе не нужно будет указывать ему на лишний отладочный код:
$ git commit -m "fix critical bug"
debug.js contains denied word: console.info("hm, is this dead code?");
debug.php contains denied word: var_dump($a);
Aborting commit due to denied words
Такой вот текст увидит наш бедолага и пойдет удалять отладочные строчки кода. Если ты пользуешься средой разработки от JetBrains, то тоже увидишь поясняющее сообщение, если hook не пропустит твою операцию.
Проверка формата commit message
Очень удобно, когда commit message содержит ссылку на задачу из issue tracker’a, например JIRA. Для этого есть специальный встроенный хук с названием commit-msg, и он может быть как клиентским, так и серверным. На вход ему подается один аргумент — текст из параметра -m команды git commit -m "commit message"
.
Пусть наш проект называется PONY. Тогда содержимое хука может быть следующим:
#!/bin/bash
commitRegex='^(PONY-[0-9]+|merge|hotfix)'
if ! grep -qE "$commitRegex" "$1"; then
echo "Aborting according commit message policy. Please specify JIRA issue PONY-XXXX."
exit 1
fi
Также мы оставили возможность костылить хотфиксить и сливать ветки. Система управления репозиториями GitLab предоставляет графический интерфейс для проверки формата commit message.
INFO
При желании можно проверить, что такая задача действительно существует в JIRA. Или можно добавить проверку, чтобы указание ссылки на code review было обязательным.
Проверка синтаксиса исходного кода
Бывает так, что ты написал фичу, несколько раз все проверил и успешно прогнал unit-тесты, но перед коммитом случайно нажал на клавиатуру, и в код попал лишний символ. Согласись, неприятно. Следующий хук проверяет синтаксис PHP, Ruby, Go, поддержку своего любимого языка добавить самостоятельно не составит труда.
#!/bin/sh
result=0
# check php/ruby syntax
while read FILE
do
# check that file not removed(also can be implemented using --diff-filter)
if [[ -f $FILE ]]; then
if [[ "$FILE" =~ ^.+(php|html)$ ]]; then
php -l "$FILE"
if [ $? -ne 0 ]; then
result=1
fi
elif [[ "$FILE" =~ ^.+(rb)$ ]]; then
ruby -c "$FILE"
if [ $? -ne 0 ]; then
result=1
fi
elif [[ "$FILE" =~ ^.+(go)$ ]]; then
gofmt -l -e "$FILE"
if [ $? -ne 0 ]; then
result=1
fi
fi
fi
done <<EOT
$(git diff --cached --name-only)
EOT
if [ $result -ne 0 ]; then
echo "Aborting commit due to files with syntax errors" >&2
exit $result
fi
Рассмотрим, как работает данный код. Пусть в staging area находятся два файла. Первый — t.php
со следующим содержимым:
<?php
define("const", "value")
Второй — t.rb
с одной строчкой кода:
puts "Hello world'
При попытке зафиксировать изменения Git hook не пропустит синтаксически неверный код:
$ git commit -m "magic commit message"
PHP Parse error: parse error in t.php on line 4
Errors parsing t.php
t.rb:1: unterminated string meets end of file
Aborting commit due to files with syntax errors
Также мы сразу увидим, в каких файлах и на каких строках у нас синтаксические ошибки.
Запуск Jenkins-задач
И наконец, классический пример continuous integration, который стоит рассмотреть.
Допустим, у нас есть проект в Jenkins, который умеет выгружать из репозитория последние изменения и запускать сборку проекта, прогон интеграционных тестов и прочее. Если ты используешь Git plugin для Jenkins, то для запуска сборки достаточно вызвать всего одну команду:
curl https://jenkins.your_company.com/git/notifyCommit?url=https://repository.your_company.com/PROJECT
Для этого стоит использовать другой серверный hook с названием post-receive, который запустится после того, как изменения будут приняты в удаленный репозиторий. Более популярное решение — плагин Build Token Root Plugin, где процесс аналогичен.
Собираем все вместе
Для того чтобы обеспечить модульность хуков, можно каждый держать в отдельном файле в директории $GIT_DIR/hooks
и подключать их в нужном pre-commit или pre-receive hook.
#!/bin/bash
"$(dirname "$0")"/check-syntax
"$(dirname "$0")"/block-debugging-code
"$(dirname "$0")"/unit-tests
Такой подход облегчит поддержку. Кроме того, тебе будет намного проще отключить какой-то из хуков, закомментировав одну строчку.
Что делать, если нет доступа к удаленному репозиторию?
В идеале хуки должны быть серверными и применяться ко всем участникам проекта, но это не всегда бывает возможным. Например, GitHub не позволяет создавать серверные хуки или у тебя не получается договориться с отделом системных администраторов насчет предоставления ssh до системы управления репозиториями. В таком случае придется использовать клиентские хуки и обеспечить их актуальность на машине каждого разработчика. Чтобы не бегать с флешкой по всем машинам, можно использовать Puppet, либо закоммитить hooks в репозиторий и устанавливать их, используя Grunt.
Что осталось за кадром
Ты можешь использовать Git hooks для автоматизации действий, которые необходимы проекту. Вот еще несколько примеров:
- Если ты придерживаешься жестких стандартов форматирования исходного кода в проекте, то можно по аналогии создать хук, который будет запускать php codesniffer, gofmt, pep8 или любую другую утилиту для измененных файлов. Благодаря этому при code review тебе не придется отправлять код на доработку из-за открывающей фигурной скобки не на той строке.
- Хочешь сделать снапшоты в веб-камере во время коммита? Проект lolcommits с открытым исходным кодом доступен на GitHub. Там же есть небольшая галерея.
- Настоящий граммар-наци может сделать хук, который контролирует правописание разработчиков, используя GNU Aspell — бесплатную программу для проверки орфографии.
Автоматизируй рутинные действия и освободившееся время трать на более интересные вещи!