Dzisiaj o ciekawym mechanizmie wbudowanym bezpośrednio w gita – git hooks. Jest to mechanizm pozwalający na wykonywanie określonych, oskryptowanych wcześniej zadań podczas wykonywania podstawowych operacji występujących w tym systemie kontroli wersji. Możemy wykonywać automatyczne akcje np. przed stworzeniem commita albo wypchnięciem zmian na serwer.

Jednym z moich ulubionych przykładów na użycie hooków po stronie klienta (są dozwolone także akcje, które odbywają się po stronie serwera – po więcej informacji zapraszam do oficjalnej dokumentacji) jest sprawdzenie zmienionych plików code snifferem i w razie niepoprawnego formatowania - odrzucenie takiego commita.

Przygotowanie środowiska

Stwórzmy sobie pusty projekt. Zainicjujemy w nim puste repozytorium git:

git init

następnie dodajmy plik composer.json z chęcią załadowania PHP Code Sniffera:

{
    "name": "bartl/git-hooks",
    "authors": [
        {
            "name": "Bartłomiej Romanek",
            "email": "b.romanek@example.com"
        }
    ],
    "require": {},
    "require-dev": {
        "squizlabs/php_codesniffer": "^3.5"
    }
}

Dodatkowo stwórzmy plik wykonywalny, który NIE PRZECHODZI statycznej analizy kodu, może to być plik index.php:

<?php

class A {
    public function __construct() {

    }
}

Następnie uruchamiamy wąchacza i sprawdzamy, jak się zachowa:

./vendor/bin/phpcs --standard=PSR12 index.php

Otrzymujemy wynik analizy - negatywny – i to jest w porządku:

FILE: /var/www/html/index.php
----------------------------------------------------------------------------------------------------------------------
FOUND 4 ERRORS AFFECTING 3 LINES
----------------------------------------------------------------------------------------------------------------------
 3 | ERROR | [ ] Each class must be in a namespace of at least one level (a top-level vendor name)
 3 | ERROR | [x] Opening brace of a class must be on the line after the definition
 4 | ERROR | [x] Opening brace should be on a new line
 6 | ERROR | [x] Function closing brace must go on the next line following the body; found 1 blank lines before brace
----------------------------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 3 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------------------------------------------------------

Time: 1.53 secs; Memory: 6MB

Stworzenie git hooka

Hooki trzymane są domyślnie w katalogu .git/hooks czyli tam, gdzie znajduje się nasz projekt. Katalog ten jest ukryty. Jeżeli do niego przejdziesz, to zobaczysz tam kilka plików:

root@b9581e100681:/var/www/html/.git/hooks# ls -l
total 32
-rwxrwxrwx 1 root root  478 Jul 28 18:35 applypatch-msg.sample
-rwxrwxrwx 1 root root  896 Jul 28 18:35 commit-msg.sample
-rwxrwxrwx 1 root root 3327 Jul 28 18:35 fsmonitor-watchman.sample
-rwxrwxrwx 1 root root  189 Jul 28 18:35 post-update.sample
-rwxrwxrwx 1 root root  424 Jul 28 18:35 pre-applypatch.sample
-rwxrwxrwx 1 root root 1642 Jul 28 18:35 pre-commit.sample
-rwxrwxrwx 1 root root 1348 Jul 28 18:35 pre-push.sample
-rwxrwxrwx 1 root root 4898 Jul 28 18:35 pre-rebase.sample
-rwxrwxrwx 1 root root  544 Jul 28 18:35 pre-receive.sample
-rwxrwxrwx 1 root root 1492 Jul 28 18:35 prepare-commit-msg.sample
-rwxrwxrwx 1 root root 3610 Jul 28 18:35 update.sample

Są to przykładowe skrypty, które możesz wykorzystać do przygotowania własnych zadań. Nas interesuje akcja wykonywana przed commitem, dlatego otwórzmy sobie plik pre-commit.sample. Na samej górze widzimy opis hooka oraz parametry, jakie on przyjmuje (w tym przypadku nie przyjmuje żadnego argumentu).

Skrypty napisane są w bashu. Bash jaki jest, każdy widzi. Sam nie za bardzo za nim przepadam 🙂. Nie stoi nic na przeszkodzie, aby pisać zadania w innym języku skryptowym, np. naszym ulubionym PHP. Stwórzmy nowy plik o nazwie pre-push (plik bez rozszerzenia – dopiero wtedy taki skrypt zostanie rozpoznany jako git hook):

#!/bin/php
<?php

declare(strict_types=1);

system("php vendor/bin/phpcs --standard=PSR12 index.php", $returnCode);

if ($returnCode !== 0) {
    echo PHP_EOL . 'PHPCS verification failed!' . PHP_EOL;
}

exit ($returnCode);

Próbujemy zrobić commit. W wyniku tego otrzymujemy:

root@b9581e100681:/var/www/html/# git commit -am "first commit"

FILE: /var/www/html/index.php
----------------------------------------------------------------------
FOUND 4 ERRORS AFFECTING 3 LINES
----------------------------------------------------------------------
 3 | ERROR | [ ] Each class must be in a namespace of at least one
   |       |     level (a top-level vendor name)
 3 | ERROR | [x] Opening brace of a class must be on the line after
   |       |     the definition
 4 | ERROR | [x] Opening brace should be on a new line
 6 | ERROR | [x] Function closing brace must go on the next line
   |       |     following the body; found 1 blank lines before brace
----------------------------------------------------------------------
PHPCBF CAN FIX THE 3 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 113ms; Memory: 6MB

PHPCS verification failed!

Zmiany nie zostały zapisane! Poprawmy teraz błędy w pliku index.php:

<?php

declare(strict_types=1);

namespace N;

class A
{
    public function __construct()
    {
    }
}

I znowu commit... tym razem przeszło bez problemu 😀.

Operowanie na dynamicznych wartościach

Powyższy przykład działa, ale tylko chwilowo. Wynika to z tego, że na sztywno sprawdzany jest plik index.php. A co w przypadku modyfikowania innych plików? Można to rozwiązać np. pobierając zmiany za pomocą git diff:

#!/bin/php
<?php

declare(strict_types=1);

exec("git diff --name-only", $files);

$filesJoined = implode (" ", $files);

system("php vendor/bin/phpcs --standard=PSR12 $filesJoined", $returnCode);

if ($returnCode !== 0) {
    echo PHP_EOL . 'PHPCS verification failed!' . PHP_EOL;
}

exit ($returnCode);

Na koniec kilka ciekawych kwestii:

  • git hooki nie są przesyłane na serwer podczas pusha; dlatego każdy programista w zespole musi je u siebie ręcznie skonfigurować;
  • jeżeli hook otrzymuje jakieś dodatkowe parametry (np. pre-push otrzymuje nazwę remote), to w skrypcie można się do nich odwołać tak jak do każdego parametru z linii poleceń - w skryptach PHP będzie to możliwe za pomocą tablicy $argv;
  • git hook zatrzymuje wykonywanie akcji, jeżeli exit code skryptu będzie inny niż 0 (oznacza to błąd wykonywania skryptu);
  • w nagłówku zamiast #!/bin/php można dać #!/usr/bin/env php.

Co uważacie o mechanizmie git hooks? Fajnie się dowiedzieć jeszcze przed commitem, że nie spełniamy jakichś założeń, prawda? To o wiele lepsza sytuacja, niż dowiedzieć się tego samego z czerwonego pipeline w CI 😊.