Wymień Husky na Lefthook i wejdź na nowy poziom znajomości Git Hooków
W świecie IT często wpadamy w rutynę — niezależnie od tego, czy jesteśmy developerami, testerami czy DevOpsami. Kolejny projekt to kolejny setup, w którym często wykorzystujemy te same narzędzia co w poprzednich, co jest dla nas przede wszystkim wygodne — wiemy, że coś działa, zostało sprawdzone przez społeczność i nie musimy się zbyt długo zastanawiać. Warto jednak wychodzić poza strefę komfortu — zazwyczaj kończy się to tylko dobrze dla samego projektu i rozwoju zespołu. IT to nie nauka geografii — tutaj szczyt górski dezaktualizuje się co kilku lat i musimy być na bieżąco.
Spis treści
Lewym sierpowym w rutynę
Dobrym przykładem rutyny w świecie JavaScriptu jest biblioteka do manipulacji daty i czasu — Moment.js, której autorzy sami mówią o tym, żeby jej nie używać i wymienić na nowszą formę np. Luxon (swoją drogą stworzoną przez tych samych developerów). Mimo to ciągle można spotkać nowo powstałe projekty, które domyślnie używają tej zależności, a manager paczek NPM informuje o 22 milionach pobrań tygodniowo.
W poniższym artykule chciałbym delikatnie podważyć szeroko stosowane rozwiązanie JavaScriptowe do obsługi Git Hooków — czyli Husky. A także zaproponować inne, w mojej opinii bardziej uniwersalne i prostsze rozwiązanie — czyli bibliotekę Lefthook.
Chciałbym pokazać też tym wpisem, że czasem „wymyślanie koła na nowo” ma sens, jeśli nowe rozwiązanie powala przeciwnika na deski.
Szybkie przypomnienie — czym są Git Hooki
Najprościej opisując, Git Hooki to customowe skrypty wywoływane podczas określonych operacji dokonywanych przy pracy z GITem. Pozwalają rozszerzać możliwości GIT-a i cały proces developmentu. Rozróżniamy hooki po stronie serwera i klienta, ale w poniższym wpisie skupimy się na tych drugich.
Najważniejsze hooki z perspektywy developera to:
pre-commit
– wywołanie przed commitem — mamy tutaj dostęp do plików w fazie „stage” — czyli tych, które mają trafić finalnie do commitaprepare-commit-msg
— wywoływane po otrzymaniu domyślnej wiadomości commita, tuż przed uruchomieniem edytora wiadomościcommit-msg
— tego hooka można wykorzystać do dostosowania wiadomości commita w celu zapewnienia zgodności z formatem lub odrzucenia na podstawie wybranych kryteriówpost-commit
— wywołanie po commiciepre-push
— wywołanie przed pushem
Warto przejrzeć folder z przykładami hooków od zespołu rozwijającego GIT-a — znajdują się one w każdym katalogu będącym repozytorium pod ścieżką .git/hooks
, przykłady posiadają sufix .sample
.
Żeby manualnie włączyć obsługę np. hooka commit-msg
należy posiadać plik w ścieżce .git/hooks/commit-msg
. Tak więc, aby włączyć przykładowy skrypt, wystarczy zmienić nazwę pliku commit-msg.sample
i usunąć sufix.
Główną zaletą bibliotek takich jak Husky czy Lefthook jest łatwość implementacji nowych hooków w naszym codebase. Ja widać powyżej, bez nich włączanie czy wyłączanie hooków wymaga manualnej modyfikacji plików w ukrytym i niewersjonowanym katalogu .git/hooks
. Kolejnym problemem jest ich upublicznianie — bez wspomnianych bibliotek udostępnianie innym członkom zespołu hooków wiąże się z użyciem dodatkowej dokumentacji, co jest zazwyczaj złym pomysłem.
Każda z dużych bibliotek wspierających implementację hooków rozszerza domyślną obsługę hooków w trochę inny sposób i tak:
- Husky — zmienia domyślną konfigurację GITa
config core.hooksPath
która przechowuje domyślną ścieżkę do katalogu z hookami (.git/hooks
) na własny katalog —.husky
w głównym katalogu repozytorium - Lefthook — wspierane hooki aktywowane są komendą np.
lefthook add -d pre-commit
– która tworzy plik w domyślnym katalogu.git/hooks
, ten „master hook script” deleguje operacje do wewnętrznych komend i skryptów — jeśli te są zdefiniowane w konfiguracji lefthooka
Dlaczego projekt od samego początku powinien korzystać z Git Hooków?
Git Hooki w mojej ocenie są szczególnie ważne w początkowych fazach projektów, gdy często zespół programistyczny jest okrojony, a hooki niskim kosztem mogą szybko wprowadzić różne standardy do naszego kodu. Więc przede wszystkim — automatyzacja. A co za tym idzie — oszczędność czasu i pieniędzy.
Szczególnie ciekawym rozwiązaniem może być wykorzystanie Git Hooków jako pierwszej wersji procesów Continuous Integration — w małym zespole i odpowiednim stacku technologicznym możemy odsunąć decyzje dotyczące pełnej integracji z narzędziami ciągłej integracji na późniejszy moment. Dzięki odpalaniu m.in. testów lokalnie z użyciem Git Hooków możemy bardzo szybko wprowadzić swego rodzaju „CI pipeline dla ubogich”, który pozwoli nam na samym początku projektu dostarczać bardziej jakościowy kod.
Oczywiście to nie jest idealne rozwiązanie — m.in. developer może pominąć wywołanie hooków lokalnie (flaga –no-verify w komendzie git commit), ale mówimy tutaj o sytuacjach i zespołach, które biorą odpowiedzialność za swoją pracę. Czasami też sam stack nas ogranicza i nie możemy podążyć powyżej opisaną drogą na skróty (testy niemożliwe do wywołania lokalnie).
Dlaczego przesiadka z Husky na Lefthook to więcej niż dobry pomysł
Uniwersalność i wsparcie
Lefthook możemy wykorzystywać nie dość, że w różnych obszarach developmentu jednocześnie (backend/frontend), ale także w różnych technologiach — jest multiplatformowy i posiada domyślne wsparcie dla: Ruby, Node.js, Go, Python, Swift, Scoop, Homebrew, Winget, Snap, Debian-based distro, RPM-based distro, Arch Linux.
Projekt jest stale rozwijany, nowe wersje pojawiają się każdego tygodnia.
Poniżej domyślny plik konfiguracyjny przedstawiający wsparcie dla wielu technologii:
lefthook.yml pre-push: commands: packages-audit: tags: frontend security run: yarn audit gems-audit: tags: backend security run: bundle audit pre-commit: parallel: true commands: eslint: glob: "*.{js,ts,jsx,tsx}" run: yarn eslint {staged_files} rubocop: tags: backend style glob: "*.rb" exclude: '(^|/)(application|routes)\.rb$' run: bundle exec rubocop --force-exclusion {all_files} govet: tags: backend style files: git ls-files -m glob: "*.go" run: go vet {files} scripts: "hello.js": runner: node "any.go": runner: go run
Konfiguracja dostarcza definicje dla takich operacji jak:
- pre-push — sprawdź audyt paczek dla frontendu i backendu
- pre-commit:
- wywołuj wszystkie operacje równolegle
- wywołaj formatowanie eslint dla plików z rozszerzeniem: js, ts, jsx, tsx
- wykonaj formatowanie kodu dla wszystkich plików Ruby z wyłączeniem plików o nazwach
application.rb
lubroutes.rb
gdziekolwiek się znajdują - wykonaj statyczną analizę kodu plików Go
- wywołaj 2 customowe skrypty: jeden napisany w Node.js, drugi w Go
Zero dependency
Chociaż biblioteka Husky sama w sobie nie posiada zależności, to żeby zaimplementować jeden z najtrywialniejszych sposobów użycia Git Hooków (podpięcie linterów przy hooku pre-commit
), najłatwiej i najszybciej zrobimy, to instalując kolejną bibliotekę, np. popularny lint-staged, i nagle w naszym node_modules pojawia się 36 zależności.
Lefthook dzięki rozbudowanym funkcjonalnościom bije na głowę podobne rozwiązania na rynku — powyżej opisany problem rozwiązujemy z użyciem wbudowanej zmiennej {staged_files}
która wstrzyknie nam wszystkie zestage’owane pliki z commita.
Zarządzanie listą plików
Z Lefthook możemy szybko sporządzić własną listę plików do procesowania w hooku, lub skorzystać z wbudowanych zmiennych. Aby zbudować własną listę plików, korzystamy z komendy files, a jeśli chcemy skorzystać z wbudowanych zmiennych mamy m.in. do wyboru:
- {files} — wynik polecenia komendy files
- {staged_files} — wspomniane w akapicie powyżej pliki staged, które próbujesz zacommitować
- {push_files} — pliki, które zostały zacommitowane, ale nie zpushowane
- {all_files} — wszystkie pliki śledzone przez git
Szybkość
Oparcie o język Go daje nam najszybszy na rynku zestaw do pracy z Git Hookami — w tym m.in. domyślne wsparcie dla procesowania równoległego (parallel execution) skryptów.
Jak włączyć procesowanie równoległe?
pre-push: parallel: true
Feature szczególnie przydatny w erze monorepo — możemy wykonywać w tym samym czasie sprawdzenie poprawności składni kilkunastu katalogów i np. dodatkowo wywołać jeszcze testy jednostkowe.
Domyślna synchroniczność w Husky nie daje nam takich możliwości out of the box.
Filtrowanie plików i regexy
Lefthook domyślnie udostępnia nam mechanizmy pozwalające filtrować pliki o określonych rozszerzeniach, poniżej przykład z procesowaniem tylko wybranych rozszerzeń plików w linterze:
pre-commit: commands: lint: glob: "*.{js,ts,jsx,tsx}" run: yarn eslint {staged_files}
Możemy dodać też dodatkowy filtr wykluczający wybrane pliki używając wyrażeń regularnych.
Wszystkie te filtrowania muszą zostać samodzielnie opracowane w Husky w oparciu m.in. o zaawansowane wykorzystanie komendy git diff
.
Personalizacja
Bardzo ciekawą funkcją Lefthooka jest możliwość posiadania osobistej konfiguracji, która może być formą naszego prywatnego hookowego sanktuarium. Jeśli mamy jakieś prywatne hooki, które chcemy używać tylko podczas osobistego developmentu, wystarczy stworzyć plik lefthook-local.yml
i dzięki temu nadpisać domyślną konfigurację z lefthook.yml
, w tym np. zmienić domyślnie stworzone kroki lub całkowicie je pominąć.
Na poniższym przykładzie wykluczamy z domyślnej konfiguracji wywołanie wszystkich hooków oznaczonych tagiem frontend
.
lefthook-local.yml pre-push: exclude_tags: - frontend
Nie zapomnij dodać lefthook-local.yml
do .gitignore
!
I wiele innych…
Poza wspomnianymi wyżej funkcjami w Lefthook znajdziemy także m.in.:
- Custom scripts — możemy napisać własne skrypty np. w czystym Node.js lub bash i podpiąć je pod hooki
- Tagi — jeśli chcemy zgrupować komendy możemy sięgnąć po tagi
- Wsparcie dockera — jeśli w naszym projekcie opieramy się mocno na kontenerach i chcemy używać takich samych narzędzi podczas wyzwalania hooków, Lefthook wspiera to dzięki zmiennej {cmd}
- Cała dostępna konfiguracja Lefthook
Przykład — dodanie walidacji Conventional Commits do wiadomości commita
Poniżej 7 kroków, jak zainstalować Lefthook i dodać walidację popularnego standardu dla tworzonych wiadomości commitów, czyli Conventional Commits.
1. Zainstaluj Lefthook jako dev dependency: npm install lefthook --save-dev
2. Zainstaluj walidator Conventional Commits w JS: npm install --save-dev @commitlint/{cli,config-conventional}
3. Dodaj definicję hooka commit-msg: lefthook add -d commit-msg
4. Stwórz plik będący skryptem bashowym: .lefthook/commit-msg/commitlint.sh
wraz z zawartością:
echo $(head -n1 $1) | npx commitlint --color
5. Dodaj do konfiguracji lefthook.yml
definicję hooka commit-msg
commit-msg: scripts: "commitlint.sh": runner: bash
6. Przetestuj walidator — spróbuj dodać commita bez tematu, np. git commit -am "test"
7. Efektem powinien być błąd walidatora Conventional Commits:
Przydatne linki
- Strona paczki na NPM – https://www.npmjs.com/package/lefthook
- Github – https://github.com/evilmartians/lefthook
- Konfiguracja – https://github.com/evilmartians/lefthook/blob/599459eb87d1dab4fd56707fd1ad3a4209c133c8/docs/configuration.md
- Lefthook: knock your team’s code back into shape – https://evilmartians.com/chronicles/lefthook-knock-your-teams-code-back-into-shape
- Git hooks fun — lolcommits — wykonaj zdjęcie kamerką podczas tworzenia commita – https://github.com/lolcommits/lolcommits
- Git hooks fun — podema — dodaje losowe emoji do wiadomości commita – https://github.com/bmwant/podmena