[GIT] Zaawansowane funkcje – cz. 1 – HEAD i utrata głowy

GIT: detached HEAD

W ostatnim czasie prowadziłem kilka szkoleń i konsultacji dla programistów. Wszyscy – bez wyjątku – używają GITa, większość programistów „mniej więcej” wie jak działa GIT, opisują to tak: „gdy dodajemy nową funkcję to tworzymy branch, pracujemy robiąc często commity, gdy skończymy to robimy merge do mastera, wtedy mogą być konflikty, a jak je rozwiążemy to wypychamy pullem zmiany do publicznego repo„.

Fajnie i nawet działa, ale przy większych projektach konieczne może się okazać bardziej dogłębne zaznajomienie z GITem, przydaj się to szczególnie w niestandardowych sytuacjach lub gdy chcemy wykorzystać możliwości drzemiące w GIT celem osiągnięcia bardziej wyrafinowanych celów.

Podczas rozmów rekrutayjnych zdarzało mi się zadać nast. pytania osobom, które zaznaczają 5/5 w pozycji GIT w CV:

  • W jakiej sytuacji możemy znaleźć się w pozycji z odłączonym HEAD? Co to znaczy, że HEAD jest odłączony?
  • Co to jest merge-commit, co to jest fast-forward?
  • Czy wykorzystywałeś kiedyś REBASE? W jakim celu?
  • Czy można zmienić kolejność commitów w GIT i edytować komentarze umieszczone kilka commitów temu? W jaki sposób zmodyfikować commit wykonany „4 commity temu„?
  • Po co używać squash?
  • Czy rebase można zastąpić kilkoma cherry-pickami?
  • Czy można wykonać „git add” i „git commit” w jednej komendzie? Jak zrobić to najprościej? Co to są aliasy GITa?
  • Czy commit może mieć 2 rodziców? Co to jest merge-commit?

A Ty znasz odpowiedzi? 🙂 Zapraszam do dalszej lektury serii artykułów o GIT.

W dzisiejszym odcinku przyjrzymy się głowie, czyli podstawie do dalszych rozważań!

Seria Artykułów o GIT – spis treści
  1. GIT HEAD – co to jest? Detached Head. Kiedy można stracić głowę?
  2. GIT fast-forward i Merge Commit. Co to właściwie jest?
  3. Git REBASE i Squash. Tworzymy ładną historię zmian.
  4. Przydatne narzędzia i oprogramowanie do pracy z GIT
  5. Przydatne Materiały o GIT plus BONUS – ściąga z poleceń GIT’a
  6. Podsumowanie.

Co to jest HEAD w GIT?

HEAD to po prostu wskaźnik (pointer) na gałąź (branch), na której aktualnie się znajdujemy. Przechowywany jest w wewnętrznej strukturze git, dokładniej tutaj:

cat .git/HEAD
ref: refs/heads/develop

Czy HEAD może wskazywać na konkretny commit? Tak! HEAD nie musi wskazywać zawsze na branch. Może wskazywać na dowolny commit.

Yyy, to co to jest właściwie ten branch?

W książce „Zostań ultra-samoukiem” Scott Young celnie zauważa, aby zadawać sobie pytania dotyczące podstaw, często zdamy sobie wtedy sprawę, że nie jesteśmy wcale tak zaznajomieni z tematem jak nam się wydawało. Jak wytłumaczyłbyś czym jest branch?

Niektórzy programiści myślą, że GIT ma jakąś wewnętrzną strukturę folderów i każdy branch to oddzielny, skompresowany folder zawierający kopie wszystkich plików naszego projektu, a git checkout powoduje wypakowanie tego archiwum. Nic bardziej mylnego.
GIT działa w dużo bardziej optymalny sposób, dane przechowuje w postaci struktury zbliżonej do grafu zawierającego snapshoty stanu zmodyfikowanych plików, a branch to po prostu wskaźnik na dany commit.

W większych szczegółach wygląda to tak:

Wewnętrzny sposób przechowywania struktury zmian w GIT
  • Obiekty blob reprezentują faktyczną zawartość danego pliku.
  • Obiekt tree zawiera informacje o plikach wchodzących w skład danego commitu i przyporządkowanie, w którym obiekcie blob znajduje się dany plik.
  • Obiekt commit zawiera wskaźnik na drzewo w ramach, którego przechowywany jest commit oraz jego metadane (autor, komentarz, itd.).

Każdy commit zawiera wskaźnik na swojego poprzednika (rodzica). Dzięki temu istnieje łątwa możliwość poruszania się w drzewie commitów:

Zakładając, że mamy 2 gałęzie: master i testing i pobraliśmy gałąź master:

git checkout master

Wewnętrzny stan repozytorium będzie wyglądałnastępująco:

Stan repozytorium po checkout master

Co tutaj widzimy? Gałąź master wskazuje na commit f30ab, gałąź testing wskazuje na commit 87ab2. Commit 87abc2 powstał z commitu f30ab (f30ab jest rodzicem commitu 87ab2). Head wskazuje na master, co znaczy, że wykonaliśmy checkout master.

Do zapamiętania: gałęzie to wskaźniki na commity, a HEAD informuje, którą gałąź mamy aktualnie pobraną. Czyli HEAD zazwyczaj wskazuje na branch nad, którym pracujemy.

To co to jest ten odłączony HEAD?

Odłącznie głowy brzmi jak coś mało przyjemnego… W GIT nie jest to jednak nic nadzwyczajnego, ba – to całkiem przydatny stan, nie traktujmy go więc jako awarię.

Z odłączonym HEAD mamy do czynienie podczas gdy HEAD nie wskazuje na żadną gałąź (branch) tylko na konkretny commit z przeszłości! Tyle!

Wszyscy znają komendę git checkout z nazwą gałęzi, ale nie wszyscy wiedzą, że możemy równie łatwo cofnąć się do konkretnego commita. Możemy to zrobić na kilka sposobów:

  • wskazać hash danego commitu, np. git checkout 34ac2,
  • skorzystać z zapisu relatywnego, np.: git checkout HEAD~2 – oznacza, że chcemy cofnąć się o 2 commity w odniesieniu do aktualnego położenia HEAD. Druga opcja to cofanie się po merge-commitach za pomocą ^, ale to zdecydowanie rzadsza opcja.

git checkout HEAD^4 <=cofamy się o 4 comity licząc od aktualnego położenia HEAD

Spowoduje to odłącznie HEAD. Head przestanie wskazywać na gałąź, a zacznie wskazywać na jakiś konkretny commit z przeszłości. Takie zachowanie jest przydatne do weryfikacji zmian wprowadzonych przez konkretny commit oraz podczas poszukiwania, „który commit spowodował wadliwe działanie aplikacji”.

Poprawki commitów w głównej gałęzi

Po odłączeniu HEAD (czyli po przełączeniu się na dowolny commit z przeszłości), mamy możliwość wprowadzenia zmian. Wyobraźmy sobie sytuację, że funkcja naszej aplikacji rozwijana w gałęzi develop zachowuje się wadliwie ze względu na zmiany wprowadzone 2 commity temu. W takiej sytuacji cofamy się do trzeciego commitu, wprowadzamy poprawki następnie poprawki te zachowujemy w nowej gałęzi.

git checkout develop
git checkout HEAD^2 <=cofamy się 2 commity wstecz
git branch fix_download_data <=w tym miejscu "wychodzimy nowym branchem"
git checkout fix_download_data <= pobieramy nową gałąź, (tak znam git checkout -b :))
git add * 
git commit -m "poprawki w pobieraniu danych" <=zatwierdzamy poprawki
git checkout master
git merge fix_download_data <=naprawiamy developa

W powyższy sposób wyszliśmy branchem fix_download_data, z kodu wprowadzonego 2 commity przed HEAD, wprowadziliśmy poprawki i dodaliśmy je do mastera.
Jest to wygodny sposób na wprowadzenie poprawek i dodanie ich do głównej gałęzi w sytuacji gdy aktualny stan gałęzi został popsuty przez zmiany dodane kilka commitów temu.
Są oczywiście inne sposoby jak interaktywny rebase i cherry-peak, niemniej ten powyżej często okazuje się najszybszy.

Ważne jest aby utworzyć gałąź fix_download_data, ponieważ w innej sytuacji utracimy uchwyt na nasze zmiany wprowadzone po odłączeniu HEAD. Zmiany oczywiście zostaną zachowane ale jak do nich dotrzeć skoro nie wskazuje na nie żaden branch? Jedynym sposobem aby do nich sięgnąć będzie checkout po haszu commita.
Głównie z tego powodu GIT ostrzega nas przed stanem odłączonego HEAD.

Porzucanie zmian

Możemy znaleźć się też w drugiej sytuacji, w której eksperymentalne cofniemy się, dokonamy weryfikacji i zmian w kodzie po czym uznamy, że chcemy te wprowadzone zmiany porzucić; np. aplikacja nadal nie działa (cofamy się 3 commity przed HEAD, dokonujemy niewielkich zmian, sprawdzamy czy aplikacja zacznie funkcjonować poprawnie, po wszystkim chcemy te eksperymentalne zmiany porzucić). Jak to zrobić? Standardowo:

git reset --hard

Komenda reset z parametrem hard powoduje usunięcie wprowadzonych zmian i przywrócenie plików do stanu wejściowego. Następnie możemy już standardowo przejść na naszą gałąź i „dołączyć HEAD z powrotem do gałęzi”:

git checkout master (lub inna gałąź)

W kolejnym artykule przyjrzymy się nieco bliżej funkcji fast-forward i ustalimy czy na pewno wiemy czym jest scalanie trójstronne! Zapraszam do subskrypcji i obserwacji bloga.

3 odpowiedzi na “[GIT] Zaawansowane funkcje – cz. 1 – HEAD i utrata głowy”

  1. Pingback: dotnetomaniak.pl

Leave a Reply