Git – przypadki użycia

Do efektywnej i bezproblemowej pracy z Gitem nie wystarczy znajomość komend, te można zresztą w każdej chwili odnaleźć w dokumentacji – przede wszystkim trzeba rozumieć filozofię stojącą za tym potężnym narzędziem. Zatem na początek garść przydatnych informacji teoretycznych.

Czym w istocie jest Git i w jaki sposób przechowuje dane

Najprościej rzecz ujmując, Git to baza danych, która przechowuje stany projektu na kolejnych etapach pracy. Git nie zapisuje różnic w plikach – zapisuje widok (ang. snapshot) całego projektu. Widok ten, zwany commitem bądź rewizją, identyfikowany jest poprzez swoją sumę kontrolną SHA-1. Polecenia Gita z reguły w mniej czy bardziej jawny sposób operują na rewizjach: porównując je ze sobą, scalając, synchronizując z nimi katalog roboczy itp.

Historia zmian, gałęzie

Każdy commit zawiera odniesienie do swojego poprzednika, zatem zbiór następujących po sobie rewizji tworzy rodzaj łańcucha. Takie podejście umożliwia strukturyzację całej historii zmian trzymanej w Gicie.

cs-git-sceariusze-zycia-creativestyle

 

Powyższy schemat przedstawia historię zmian złożoną z dwóch równoległych łańcuchów commitów oraz trzech wskaźników. Łańcuch rewizji stanowi gałąź, jeśli do jego elementu odwołuje się nazwany wskaźnik (w tym wypadku feature lub master). Łańcuch jest gałęzią aktywną, jeśli dodatkowo na ten element wskazuje HEAD – wówczas katalog roboczy zsynchronizowany jest z odpowiadającą wskaźnikowi rewizją (tj. wszystkie zmiany w plikach śledzone są w stosunku do niej).

Określanie rewizji oraz zakresów rewizji

W trakcie pracy z Gitem możemy odwoływać się do rewizji na rozmaite sposoby:

  • bezpośrednio poprzez SHA-1 (np. f8519fd29909ed37b1e83fad6f259d3e6ed9a9ae)
  • bezpośrednio poprzez skrót SHA-1 (np. 6bc603b), o ile jest on jednoznaczny
  • poprzez wskaźnik HEAD oraz wskaźniki reprezentujące gałęzie (np. master, origin/master)
  • relatywnie do wskaźników (np. HEAD^, HEAD~3, master~1)
  • odwołując się do przeszłych stanów wskaźników (HEAD@{3}, master@{yesterday}, master@{<date>})
  • oraz poprzez zakresy (master..HEAD, origin/master..master, feature…master)

Modyfikowanie historii zmian

Podczas pracy nad obszernymi funkcjonalnościami pojawia się problem kontroli nad dotychczas napisanym kodem: łatwo się pogubić we wprowadzonych zmianach bądź coś omyłkowo nadpisać lub skasować. Rozwiązaniem tego problemu jest częste commitowanie, choćby niewielkich zmian – takie „cząstkowe” commity mogą doskonale posłużyć jako punkty asekuracyjne, jeśli coś pójdzie źle.

Warto zauważyć, że podstawową zaletą Gita w stosunku do scentralizowanych systemów kontroli wersji jest możliwość lokalnej pracy na prywatnych gałęziach i dowolnego przeredagowywania historii wprowadzanych zmian – tak długo, jak długo nie były one umieszczane w publicznym repozytorium. Oto przykładowa historia zmian w lokalnej gałęzi:

Jak widać, wykonywana praca na swych poszczególnych etapach opisywana była dość swobodnym stylem, niekoniecznie dopuszczalnym w głównym repozytorium. Załóżmy ponadto, że ustalenia zespołu wymagają zawierania całej funkcjonalności w jak najmniejszej liczbie commitów, opisanych w języku angielskim. Z pomocą przychodzi polecenie rebase z opcją interactive:

W tym wypadku polecenie umożliwi przepisanie całej historii począwszy od miejsca w gałęzi master, z którego wywiedziona została gałąź feature (mogliśmy także odwołać się do dowolnej innej rewizji z tej gałęzi). Interaktywny rebase umożliwia takie operacje, jak łączenie commitów, zmianę ich kolejności, edycję opisów i tym podobne. Historia po przeredagowaniu może wyglądać tak:

Gałąź feature w obecnym kształcie może zostać scalona z gałęzią master i zgłoszona do publicznego repozytorium.

Użycie kodu z innej gałęzi

Załóżmy teraz, że gałąź feature nie mogła jednak z jakichś względów na razie zostać scalona, chcemy natomiast, aby w masterze znalazł się wykonany refactoring klas foo i bar (tj. commit fed82d9). Z pomocą przyjdzie nam polecenie:

które zaaplikuje dany commit w gałęzi master. Co jednak, jeśli chcielibyśmy użyć wyłącznie zmian z jednego pliku? Możemy wykorzystać do tego polecenie checkout:

Plik bar.class został umieszczony w staging area i zostanie uwzględniony w najbliższym commicie.

Dlaczego jednak użyliśmy polecenia, które służy przecież do zmiany gałęzi? Otóż zmiana gałęzi jest, co prawda, najczęstszym przypadkiem użycia polecenia checkout, ale jest to przypadek szczególny. Generalnie polecenie to służy do synchronizowania plików w katalogu roboczym z ich stanem w danej rewizji.

Użycie fragmentu pliku

Może zdarzyć się sytuacja, że wprowadziliśmy w jednym pliku bardzo dużą liczbę niezwiązanych ze sobą tematycznie zmian (cokolwiek to oznacza w kontekście jednego pliku ;)) bądź też po prostu chcemy szybko zrobić commit zawierający tylko część zmian, a resztą zająć się później. Najprostszym rozwiązaniem w takim wypadku jest przełącznik -p polecenia add:

Git pozwoli nam na zaakceptowanie krok po kroku bloków kodu, które zostaną użyte w najbliższej rewizji. Efekt można sprawdzić poleceniem:

W wyniku otrzymamy dwie wersje pliku z różnymi zmianami – wersja z zaakceptowanymi przez nas blokami znajdować się będzie w staging area, pozostałe bloki pozostaną w zmodyfikowanym pliku w katalogu roboczym.

Historia stanu gałęzi

Zaznaczyłem wcześniej, że do rewizji możemy się odwołać poprzez przeszły stan wskaźników (HEAD bądź gałęzi). Uwaga: historia zmian stanu wskaźników nie jest tożsama z historią zmian projektu, reprezentowaną przez łańcuch rewizji. Aby zilustrować ten problem, wyobraźmy sobie scalenie gałęzi feature z gałęzią master, w wyniku którego master „zyskał” trzy commity – jego stan zmienił się jednak tylko raz. Aby odtworzyć historię zmian stanu danej gałęzi (innymi słowy, rewizje, do których w toku pracy odwoływał się reprezentujący ją wskaźnik), korzystamy z polecenia reflog:

Analogicznie uzyskujemy historię innych gałęzi. Polecenie reflog bez parametrów wyświetli historię zmian wkaźnika HEAD. Do stanów gałęzi odwołujemy się poprzez referencje typu: master@{1}, master@{2}, feature@{4}, HEAD@{10}, a także używając określeń czasu: master@{yesterday} czy master@{2 months ago}. Przykładowo polecenie:

umożliwi przejrzenie ówczesnej wersji pojedynczego pliku.

W praktyce ten sposób odwołania do rewizji najczęściej używany jest do przywracania stanu gałęzi po wykonaniu nieudanego polecenia zmieniającego jej historię (np. merge czy rebase). Wystarczy komenda:

by przywrócić stan gałęzi sprzed niefortunnej zmiany.

Detached HEAD

Na koniec załóżmy, że chcemy szybko przetestować działanie projektu lub podejrzeć cały kod konkretnej rewizji. W tym celu możemy skorzystać z polecenia checkout, np.:

Polecenia te przenoszą nas w tryb „detached HEAD”, co oznacza, że nie znajdujemy się obecnie w żadnej gałęzi, ale katalog roboczy został zsynchronizowany z rewizją określoną przez parametr.

Bibliografia:

Więcej informacji o mechanizmach przechowywania danych przez Gita, metodach pracy z gałęziami, możliwych sposobach odwoływania się do rewizji oraz o wykorzystaniu polecenia rebase – można znaleźć w podręczniku i w dokumentacji:

http://git-scm.com/book
https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html
https://www.kernel.org/pub/software/scm/git/docs/git-rebase.html

 

 

pluswerk