Automatyczne aktualizowanie zawartości katalogu roboczego po wepchnięciu zmian do repozytorium Gita

GIT

Rozwlekły tytuł notki niejako obrazuje skalę mojego problemu – dłuuuugi czas męczyłem się z dziwnym zachowaniem Gita, który zamiast aktualizować aplikacje nad którymi pracowałem, aktualizował jedynie historię wersji, a katalog roboczy musiałem poprawiać ręcznie. Bardzo upierdliwa robota, gdy problem dotyczy wielu aplikacji, z których każda może mieć więcej niż jedną zdalną instancję…

To, co dla mnie było błędem Gita, okazało się przemyślaną strategią. Tak właśnie zachowuje się Git podczas wpychania zmian do aktywnej gałęzi – dla pewności zostawia nienaruszony katalog roboczy, a jego aktualizację należy wymusić ręcznie za pomocą komendy git checkout -f. Zupełnie nie skojarzyłem tej zależności i za każdym razem zastanawiałem się czemu raz katalog roboczy aktualizuje się wraz ze zmianą w repozytorium, a raz wymaga ode mnie bym logował się przez ssh i dokańczał robotę własnoręcznie. Dziś postanowiłem zwalczyć problem i proszę, sukces!

Z pomocą przychodzą tutaj Git Hooks, a konkretniej post-receive. Dzięki niemu możemy w prosty sposób skonfigurować repozytorium tak, by samoistnie aktualizowało zawartość katalogu roboczego po otrzymaniu pakietu commitów. Oczywiście, jak już wspomniałem, problem dotyczy tylko sytuacji gdy wpychamy do aktywnej gałęzi (co po wielokrotnym poszukiwaniu wyczytałem dopiero tutaj), co domyślnie jest zabronione, zatem jeśli chcemy mieć taką możliwość musimy odpowiednio skonfigurować repozytorium. W tym celu należy wydać komendę git config receive.denyCurrentBranch ignore lub wyedytować w odpowiedni sposób plik .git/config w naszym repo.

Kiedy już mamy możliwość wpychania commitów do aktywnej gałęzi w zdalnym repozytorium, możemy zabrać się za skrypt post-receive. Przede wszystkim należy wykasować .sample z nazwy pliku .git/hooks/post-receive.sample oraz nadać mu prawa do wykonywania: chmod +x .git/hooks/post-receive. W tym momencie hook jest gotowy i będzie wykonywany po każdym przyjęciu pakietu danych – oczywiście o ile coś do niego dopiszemy, bo domyślnie są tam same komentarze ;)

Jako, że komenda git checkout -f powinna być wykonana z podaniem ścieżki do katalogu roboczego, to w skrypcie post-receive najwygodniej byłoby wprowadzić elastyczny sposób automatycznego wykrywania tejże ścieżki. Dzięki temu będzie on zawsze działał niezależnie od tego gdzie repozytorium będzie się znajdowało. Z pomocą oczywiście przyjdzie nam Bash (uwaga, przykładowe pliki Git Hooks zdeklarowane są do użycia powłoki shell, o czym świadczy #!/bin/sh, należy więc zmienić deklarację na #!/bin/bash) oraz „podrasowany” przeze mnie, znaleziony w sieci jednolinijkowiec.

Ostatecznie skrypt post-receive powinien zawierać:

1
2
3
4
5
6
#!/bin/bash

REPO_DIR="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"

# Update working directory after pushing to active branch
GIT_WORK_TREE=$REPO_DIR git checkout -f

REPO_DIR przyjmie jako wartość ścieżkę do katalogu zawierającego repozytorium (czyli o 2 poziomy wyżej od skryptu post-receive), dzięki czemu będzie możliwe wywołanie komendy aktualizującej katalog roboczy. Jeśli nam się kiedyś zechce przenieść aplikację gdzie indziej, nie będziemy musieli pamiętać o zmianie ścieżki w skrypcie post-receive, bo ona zawsze wykryje się samoistnie.

UWAGA: Powyższa metoda znajduje zastosowanie tylko dla repozytoriów z katalogiem roboczym (czyli nie tworzonych poprzez git init --bare), z domyślną strukturą katalog roboczy/repozytorium (czyli bez manipulacji z core.worktree, –work-tree, GIT_WORK_TREE, –git-dir, GIT_DIR). W innym wypadku należy odpowiednio zmodyfikować wykrywanie REPO_DIR, choć może to dobry temat na odrębną notkę, więc może kiedyś…

UWAGA 2: automatyczne aktualizowanie katalogu roboczego z użyciem git checkout -f wymazuje wszystkie lokalne zmiany w plikach! Przedstawiona metoda przydatna jest w przypadku, gdy instancja produkcyjna nie jest edytowana, a wszelakie prace rozwojowe prowadzone są w instancjach deweloperskich.

Komentarze do "Automatyczne aktualizowanie zawartości katalogu roboczego po wepchnięciu zmian do repozytorium Gita": (4)

  1. jezozwierzak:

    Proszę wybacz mi za banalne pytanie, ale nurtuje mnie to.

    Czym się różni wpychanie zmian do aktywnej gałęzi od zwykłego push-a poprzedzonego commit-em?

    • Wydaje mi się, że problem został wystarczająco dokładnie opisany w notce, ale postaram się dać odpowiedź w pigułce :)

      Załóżmy, że masz lokalne i zdalne repo, oba z 2 gałęziami master i develop. Edytujesz plik index.php, commitujesz go do developa i wpychasz do zdalnego repo. Tutaj w zależności od tego, która gałąź w zdalnym repo jest aktywna, w efekcie będziesz musiał po stronie zdalnego repo wykonać git merge develop dla aktywnego mastera (lub ewentualnie git checkout develop jeśli nie chcesz w tym momencie łączyć zmian), lub git checkout -f dla aktywnego developa (co „zatwierdzi” wrzucone zmiany, bo tak jak pisałem zawartość katalogu roboczego nie ulegnie zmianie automatycznie w momencie wepchnięcia commita i git status pokaże Ci zmiany w pliku index.php). I to jest właśnie ten szkopuł, że jeśli zdalna instancja pracuje na tej samej gałęzi, na której Ty pracujesz lokalnie, to w momencie wpychania commitów zmiany nie będą automatycznie widoczne w aplikacji. I żeby nie musieć ręcznie tych zmian „zatwierdzać” za każdym razem, można to właśnie obejść hookiem post-receive.

      Mam nadzieję, że wyjaśniłem wystarczająco dobrze :)

  2. jezozwierzak:

    Wyjaśniłeś bardzo dobrze.
    Aczkolwiek muszę rozpoznać temat dokładniej.
    Widziałem jeszcze coś co się nazywa post-commit.
    Moje niezrozumienie tematu wynika raczej z tego, że jeszcze nie sprawdzałem tego na żywym przykładzie i nie doczytałem dokładnie dokumentacji.
    W tym tygodniu powinienem dostać dane dostępowe do nowego serwera, na którym mam wdrożyć funkcjonalność, o której wydaje mi się, że jest tutaj mowa.

    Będę chciał zrobić 3 branch-e (produkcyjny, testowy, master) i każdy powinien być podpięty do odpowiedniej poddomeny. Oczywiście wszystko powinno się aktualizować zaraz po zrobieniu push-a przez developera.

    Dam znać jak poszło ;)

    • Niestety taka forma jaką proponujesz jest nie do zrealizowania z prostego powodu: aktywna może być tylko jedna gałąź, wobec czego nie mogą jednocześnie funkcjonować 3 wersje aplikacji w jednej jej instancji. Jeśli przełączysz się na gałąź master, to jednocześnie w 2 pozostałych domenach automatycznie będzie to samo.

      Jeśli chcesz mieć 3 poziomy instancji, o jakich mówisz, ja sugeruję taki schemat:
      – instancja produkcyjna (gałąź master i np. production, zaraz wyjaśnię dlaczego)
      – instancja testowa (liczba gałęzi dowolna, z tym, że jedna odpowiada gałęzi production z instancji produkcyjnej)
      – instancje developerskie

      Developerzy w miarę możliwości powinni mieć swoje instancje np. na localhoście, ewentualnie jeśli się nie da, to na serwerze, ale w osobnych lokalizacjach, tak by móc pracować jednocześnie nad czym innym. Swoje commity/gałęzie powinni wpychać do instancji testowej, która łączyłaby wszystkie instancje developerskie w jedną całość. Wszystkie udane zmiany powinny być merge’owane do gałęzi głównej (np. develop), która z kolei powinna być po przetestowaniu wpychana na produkcję do gałęzi production. I wracam do tego czemu tam są 2 gałęzie: gałąź master powinna zawierać zawsze wersję stabilną (master), druga gałąź wersję najnowszą (production). Z instancji testowej wpycha się do production, a w przypadku problemów przełącza się na gałąź master, co trwa ułamki sekund (względnie sekundy przy bardzo dużej ilości zmian) i w prosty sposób przywraca nam wersję działającą.

      Taka struktura oferuje po pierwsze niezależność – każdy z developerów może pracować nad innymi zmianami, które ostatecznie zostaną złączone w instancji testowej. Po drugie wystarczająco dobrze gwarantuje niezawodność – w przypadku wepchnięcia na produkcję błędnego kodu, przełączenie się na wersję stabilną przywraca działającą aplikację, a developerzy dalej mogą pracować nad poprawkami.

      Co do post-commit to jest to hook, który wykonywany jest lokalnie, w repozytorium do którego aktualnie dodawany jest commit. Różni się to od post-receive, bo ten drugi jest wykonywany po otrzymaniu paczki z innego repozytorium. Hooków jest sporo i wystarczy o nich poczytać, każdy może znaleźć inne zastosowanie, ograniczeniem jest chyba tylko wyobraźnia ;)

Skomentuj "Automatyczne aktualizowanie zawartości katalogu roboczego po wepchnięciu zmian do repozytorium Gita":

Musisz się zalogować, aby móc dodać komentarz.