Git hooks to skrypty, które wykonywane są automatycznie przed lub po określonych, „gitowych” czynnościach. Ogromną zaletą tego mechanizmu jest integralność z samym Git’em – nie ma potrzeby pobierania bądź instalowania czegokolwiek.
Poniżej zaprezentuję implementację oraz działanie trzech git hooks:
- commit-msg – uruchamiany przy próbie 
git commit, będzie sprawdzał czy wiadomość commita spełnia ustalone warunku - pre-commit – uruchamiany przy próbie 
git commit, będzie sprawdzał czy nasz kod spełnia standardy PSR-2 oraz czy wszystkie testy jednostkowe zakończyły się sukcesem - post-commit – uruchamiany po udanym 
git commit, będzie wysyłał wiadomość SMS na określony numer telefonu 
Do prezentacji powyższych hook’ów skorzystam z Symfony 4, PHPStan, PHP Code Sniffer oraz SMS API PHP Client. Dla zaczynających swoją przygodę z programowaniem, korzystających z innego framework’a lub język – nie zniechęcajcie się! Wszystko będzie bardzo dokładnie opisane. To tylko narzędzia, które mają za zadanie pokazać realne zastosowania w realnych projektach. Osoby po raz pierwszy widzące słowa „Symfony 4” oraz „PHP” zachęcam do zapoznania się z zakładką „Kursy„.
Cały, gotowy do uruchomienia i zabawy kod znajduję się tutaj: https://github.com/bedev-pl/githooks-sample
Hook commit-msg
Wiele zespołów korzysta z systemów do zarządzania projektami takimi jak Jira, RedMine czy CA. Dzięki odpowiedni regułom możemy wymusić na sobie lub na koledze z zespołu, że realizując zgłoszenie o nazwie i identyfikatorze „RBMK-1000: Wyłączenie artykułu powoduje jego wielokrotne wyświetlenie” w wiadomości commita napisze coś więcej niż „bug fix„. To o tyle ważne, iż pracując w projektach długofalowych warto po roku mieć możliwość sprawdzenia, dlaczego ta osoba napisała takie a nie inne poprawki.
Jeżeli kogoś to nie przekonało to niech sobie wyobrazi zgłoszenie w jakimś legacy projekcie dotyczące cofnięcia zmian sprzed pół roku kolegi, który jest na urlopie. Trzymając odpowiednie standardy w minutę będziemy w stanie dotrzeć do commita, który w swojej wiadomości ma identyfikator zgłoszenia. Gorzej jeżeli przez rok swojej pracy każdego commita nazywał „fix” lub „impl” – stracimy sporo czasu i nerwów.
Ustalmy więc, że realizacja zgłoszeń z naszego projektu musi mieć wiadomość zawierającą identyfikator zgłoszenia. W tym przypadku będzie to fraza RBMK-X gdzie X musi składać się z cyfr z zakresu 0-9. Pomijam fakt, czy taka nazwa jest dobra czy nie, ale zapewni nam jakiś minimalny standard.
Moim punktem wyjścia jest czysty projekt Symfony 4 z GIT’em.
Git hook commit-msg
Pierwszym krokiem, jest zmiana miejsca, w którym git będzie szukał hooków. Domyślnie jest to .git/hooks, które nie jest wersjonowane.  Nie musicie tego robić jeżeli chcecie trzymać git hooks w domyślnej lokalizacji. Utworzyłem w projekcie katalog githooks i wykonałem poniższą komendę:
git config core.hooksPath hooks
Zabieg ten umożliwi również dzielenie się nimi w łatwy sposób w osobami pracującymi nad projektem. Szczegóły znajdziecie na końcu tego artykułu.
W katalogu githooks należy utworzyć plik, którego nazwą będzie nazwa hooka – czyli w tym przypadku commit-msg. Musimy upewnić się, że plik jest wykonywalny.
sudo chmod 755 commit-msg
Okej, na chwile obecną mamy pusty plik z hookiem. Dodam do niego następujący kod:
#!/bin/bash echo "Nope" #wyświetlenie w terminalu tekstu "Nope" exit 1; #przekazanie kodu błędu
 Powyższy kod przy próbie commita powinien wyświetlić w terminale tekst „Nope” i anulować operację. Jeżeli ktoś nie rozumie linii 3 – ponownie zapraszam do zakładki kursy. Jeżeli nie chcecie, niech wam wystarczy że exit 0 to „OK” a exit 1 to „ERROR”.
git commit -m "fix" Nope
Jak widać kod zadziałał zgodnie z przewidywaniami. Pamiętając nasze założenia, skrypt sprawdzający reguły naszej wiadomości przy commicie będzie wyglądał następująco:
#!/bin/bash pattern='^(\bRBMK)-[0-9]+' #wyrażenie regularne msg="Aborting commmit - message doesn't match pattern!" #wiadomość błędu if ! grep -iqE "$pattern" "$1"; then # sprawdzenie, czy wiadomość spełnia reguły wyrażenia echo "$msg" # jeżeli nie, wyświetlamy wiadomość exit 1; # i przekazujemy kod błędu fi echo "Commit message OK!"
Poniżej widzimy efekty próby commita ze złą wiadomością:
git commit -m "fix" Aborting commit. Your commit message does not match required pattern!
Sukces! Spróbujmy się poprawić i wpisać poprawną wiadomość:
git commit -m "RBMK-1000: bug fix" Commit message OK! [master 42051cb] RBMK-1000: bug fix 1 file changed, 10 insertions(+) create mode 100755 hooks/commit-msg
Ponownie – wszystko zadziałało zgodnie z pierwotnymi założeniami. Skoro mamy zabezpieczoną wiadomość commita przed „impl” i „fix„, przejdźmy do kolejnego git hooka.
Git hook pre-commit
W tym przykładzie, użyjemy narzędzia które opisałem w poprzednim artykule – PHPStan. Pomijam więc jego opis, wszystkie informację znajdziecie tutaj. W jednym zdaniu – to narzędzie do sprawdzania poprawności kodu.
Analogicznie do poprzedniego „haczyka”, na samym początku należy utworzyć plik o nazwie danego hooka – czyli pre-commit i zmienić jego uprawniania na np. 775. Następnie dodajemy PHPStana do projektu:
composer require phpstan/phpstan
Zakładając, że przy każdym commicie chcemy sprawdzać wszystkie pliki z katalogu src na poziomie 7, skrypt będzie wyglądał następująco:
#!/bin/bash
printf "PHPStan started..."
# ustawienie analizy dla folderów tests i src, format błędów 1 bład per linia
# analiza na levelu 7 - najwyższym możliwym poziomie
output=$(php vendor/bin/phpstan analyse src --error-format=raw --level 7)
if [ -z "$output" ] 			# true, jeżeli $output jest pusty
then
	printf "\nGood job!\n"
    exit 0 						# wszystko ok
else
    printf "$output" 			# wyświetlenie błędu
    printf "\nCommit aborted, fix your code!\n"
    exit 1						# przekazanie kodu błędu
fiTeraz musimy dodać jakiś plik, który będzie można sprawdzić. Dodaję więc IndexController.php który wygląda tak:
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
class IndexController
{
    public function index(): Response
    {
        return 1;
    }
}W jedynej funkcji w naszej klasie mamy oczywisty błąd – zwracany typ integer nie jest zgodny z deklaracją zwracanego typu Response. PHPStan z pewnością nam o tym powie. Sprawdźmy więc:
git commit -m "RBMK-1001: index controller implementation" 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% /home/bedevpl/repos/githooks-sample/src/Controller/IndexController.php:9:Method App\Controller\IndexController::index() should return Symfony\Component\HttpFoundation\Response but returns int. Commit aborted, fix your code!
Analiza i hook zadziałały jak trzeba – błąd został wykryty, a commit anulowany. Poprawiony kontroler będzie wyglądał następująco:
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
class IndexController
{
    public function index(): Response
    {
        return new Response();
    }
}A próba commita zakończy się sukcesem:
git commit -m "RBMK-1001: index controller implementation" 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% PHPStan finished. Good job! Commit message OK! [master 816c60f] RBMK-1001: index controller implementation 6 files changed, 959 insertions(+), 157 deletions(-) create mode 100755 hooks/pre-commit create mode 100644 src/Controller/IndexController.php
Commit się udał. W praktyce analiza całego kodu może być po pierwsze nie efektywna przy dużych projektach, a po drugie prowadzić nawet do lekkich spięć w zespole.  Wystarczy że jedna osoba nie będzie korzystała z git hooks lub po prostu zacommituje zmiany z parametrem --no-verify, przy użyciu którego pre-commit i commit-msg nie są uruchamiane. Poniżej ponownie funkcja zwraca zły typ w stosunku do deklaracji:
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
class IndexController
{
    public function index(): Response
    {
        return 1;
    }
}Zapiszmy zmiany z parametrem --no-verify:
git commit -m "RMBK-1002: hotfix" --no-verify [master 9df7c4c] RMBK-1002: hotfix 1 file changed, 1 insertion(+), 1 deletion(-)
A teraz dodajmy w pełni poprawny kontroler o nazwie UserController:
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
class UserController
{
    public function addUser(): Response
    {
        return new Response();
    }
}A gdy ktoś git hooks nie używa…
Przejmujemy projekt po koledze. Dodajemy własną funkcjonalność, próbujemy zapisać nasze zmiany pod poprawną nazwą:
git commit -m "RMBK-1003: user add impl" 3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% /home/bedevpl/repos/githooks-sample/src/Controller/IndexController.php:9:Method App\Controller\IndexController::index() should return Symfony\Component\HttpFoundation\Response but returns int. Commit aborted, fix your code!
Na pewno nie tego się spodziewaliśmy. Mamy teraz kilka opcji:
- poprawiamy nie swój kod całkowicie niezwiązany z naszą funkcjonalnością
 - prosimy kolegę aby poprawił swój kod
 - również korzystamy z 
--no-verifydo czasu, aż ktoś poprawi swój kod lub skończymy pracować w projekcie - edytujemy hooka aby sprawdzał tylko i wyłącznie pliki, w których dokonaliśmy zmian
 
O ile pierwsze trzy opcję mnie całkowicie nie przekonują, o tyle opcja ostatnia wydaje się najrozsądniejsza. Oczywiście jeżeli nasz współpracownik edytował ten sam plik to wybrane rozwiązanie i tak nam nie pomoże. Przechodząc do samego skryptu, będzie wyglądać następująco:
#!/bin/bash
printf "PHPStan started..."
filesToCheck=$(git diff --name-only --diff-filter=d HEAD src)
output=$(php vendor/bin/phpstan analyse $filesToCheck --error-format=raw --level 7)
if [ -z "$output" ]
then
    printf "\nGood job!\n"
    exit 0
else
    printf "$output\n"
    printf "\nCommit aborted, fix your code!\n"
    exit 1
fiW linii 3 do zmiennej filesToCheck przekazujemy rezultatgit diff dla katalogu src. Zmodyfikowaliśmy (dodaliśmy) tylko jeden plik – nasz nowy kontroler, więc tym razem analiza powinna dotyczyć tylko niego.
git commit -m "RBMK-1003: user add impl" 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% Good job! Commit message OK! [master ca81501] RBMK-1003: user add impl 1 file changed, 11 insertions(+) create mode 100644 src/Controller/UserController.php
Tylko nowy plik został poddany analizie, natomiast błędny plik bez zmian został zignorowany – czyli dokładnie to, o co chodziło.
Git hook post-commit
Lepszym zastosowaniem na wysyłkę wiadomości SMS byłby „haczyk” post-recievie po stronie serwera, jednak po przez uproszczenie niektórych rzeczy, pokażę go przy post-commit. Powód znajdziecie na końcu tego artykułu.
Do tego przykładu wykorzystam serwis SMSApi. Zakładając darmowe konto (jedyny minus to weryfikacja po przez numer telefonu) dostaniecie 50 SMS’ów do testów. Wystarczy się zarejestrować, zalogować, przejść do zakładki „Ustawienia API” -> „Tokeny API (OAUTH)” -> „Generuj token”.
Dodajemy paczkę do naszego projektu:
composer require smsapi/php-client
Następnie dodajmy plik hooka o nazwie post-commit – przypominam o ustawieniu uprawnień. Zawartość pliku będzie bardzo krótka, ponieważ ograniczy się do wykonania innego skryptu:
#!/bin/bash php post-commit-script.php printf "\nSMS sent!\n"
„Haczyk” uruchomi program o nazwie post-commit-script.php, którego zawartość wygląda następująco (pamiętamy o uprawnieniach):
<?php declare(strict_types=1); require_once 'vendor/autoload.php'; use Smsapi\Client\SmsapiHttpClient; use Smsapi\Client\Feature\Sms\Bag\SendSmsBag; $apiToken = ''; //paste your token $phoneNumber = ""; // enter your phone number $message = "FEEL THE COMMIT POWER!"; //write your message $sms = SendSmsBag::withMessage($phoneNumber, $message); $service = (new SmsapiHttpClient())->smsapiPlService($apiToken); $service->smsFeature()->sendSms($sms);
Wystarczy że w liniach 9, 10 i 11 uzupełnimy kolejno token wygenerowany w panelu SMSApi, numer telefonu odbiorcy, oraz wiadomość. Tylko tyle! Zapiszmy swoje zmiany i sprawdźmy czy dostaniemy wiadomość:
git commit -m "RBMK-000: post-commit hook" 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% Good job! Commit message OK! SMS sent!
Od tej chwili po każdym udanym commicie na numer telefonu podany w skrypcie, otrzymamy taką oto wiadomość:

Szybko poszło. Oczywiście chcąc stosować wszystkie praktyki dobrego programowania docelowy kod wyglądałby inaczej, natomiast chciałem pokazać mniej doświadczonym, że takie rzeczy są na prawdę banalnie proste!
Podsumowanie
Jak widać w powyższym artykule, jeżeli chodzi o pomysły na zastosowanie git hooków prawie nic nas nie ogranicza. Dzięki temu możemy nie tylko niskim kosztem znacznie podnieść jakość pisanego przez nas kodu, ale również automatyzować niektóre czynności. Nic nie stoi na przeszkodzie, aby hook post-commit od razu wrzucał nam zmiany na repozytorium, a post-receive na serwerze odpalał testy jednostkowe, robił nam wdrożenie na produkcję oraz informował o tym naszego przełożonego.
Jeżeli chodzi o ciekawe zastosowania, to więcej znajdziecie tutaj. Moim zdaniem całkowitym „must have” jeżeli chodzi o programy podłączone pod hook post-commit jest lolcommits. Dzięki niemu za każdym razem gdy commit nam się powiedzie, kamera w naszym laptopie zrobi nam zdjęcie i podpisze tytułem commita. Możemy również robić projektowe timelapsy i gify. Więcej szczegółów znajdziecie tutaj.
Minusem jest utrudnione dzielenie się hookami, które działają lokalnie – niestety nawet gdy są wersjonowane, każdy chcąc ich użyć musi ustawić w gicie ich lokalizację. Najlepiej wrzucić konfigurację do makefile’a.
Dodatkowo nikogo nikogo nie zmusimy do korzystania z nich – jeżeli ktoś się uprze, to dalej będzie wypychał zmiany z parametrem --no-verify podpisane „impl” . W tym przypadku jest to jednak problem z tym kimś, a nie z samymi hookami.
Mam nadzieję że w jakimś stopniu przybliżyłem Wam ten temat. Jednak musiałem pójść na mały kompromis – pominąłem konfigurację i implementację git hooks server side, ponieważ artykuł znacznie by się wydłużył. Część mniej doświadczonych pewnie by się zniechęciła widząc ilość potrzebnej konfiguracji. W planach mam również artykuły dla na prawdę zaawansowanych, jednak na to potrzebuję jeszcze trochę czasu :).

			
Świetny wpis. Myślę, że najlepsze rozwiązanie tyczy się commit message. Bardzo często widzę błedy w nich, szczególnie gdy korzystamy z jakiegoś narzędzia do tasków. Potem robi się niezły bałagan. Dzięki za wpis i liczę na więcej!
Według mnie sprawdzanie commit-msg to podstawa. Sam kiedyś commitowałem tak jak w artykule („fix”) i wiem jak się sam na siebie wściekałem. Dzięki za miłe słowa i zapraszam częściej.
[…] To tak jak z jazdą samochodem – można oglądać jak jeżdżą inni, ale jeżeli sami nie spróbujemy to praktycznie nic nam to nie da. Dlatego dołącz do swojej aplikacji na stanowisko link do swojego GITa. Jeżeli nie wiesz czym jest Git – Google IT! Natomiast jeżeli chodzi o pierwsze projekty – najfajniej pisze się coś, czego możemy użyć lub wykorzystać praktycznie. W moim przypadku był to prosty skrypt, który sprawdzał czy na stronie uczelni pojawił się nowy wpis odnośnie wyjazdu na CeBIT (gdzie obowiązywała zasada kto pierwszy ten lepszy) i informował mnie o nim SMS’em (o praktycznym użyciu SMS API przeczytasz tutaj). […]