При code review ты указываешь разработчикам на забытые printf, console.log? С ужасом видишь код с синтаксической ошибкой? Или хочешь разграничить права на работу с ветками (как в Bitbucket), потому что джуниор путает порядок веток при слиянии? Хватит это терпеть! У нас же есть Git и Bash!

git push --force
git push --force
 

Легкий старт с 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 ты можешь самостоятельно.

Branch permissions в продукте Bitbucket Server
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 не пропустит твою операцию.

Сообщение от hook в среде IntelliJ IDEA
Сообщение от hook в среде IntelliJ IDEA
 

Проверка формата 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.

Правила проверки commit message в GitLab
Правила проверки commit message в GitLab

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 — бесплатную программу для проверки орфографии.

Автоматизируй рутинные действия и освободившееся время трать на более интересные вещи!

Оставить мнение