Software Development

Jak Git działa za kulisami

Marzec 5, 2021 0
Podziel się:

Git jest narzędziem niezwykle pomocnym przy codziennej pracy w zespole. Chcąc nauczyć się jego obsługi, sięgamy po przeróżne kursy. Często przedstawiają one sposób działania Gita, stosując pewne uproszczenia i abstrakcje. Nie jest to złe, gdyż znacznie ułatwia nam przyswojenie podstawowej wiedzy. Ma jednak istotną wadę – nieraz prowadzi do wyrobienia sobie błędnych założeń. Nie stanowi to problemu przy wykonywaniu prostych czynności. Jednak podczas codziennej pracy w zespole, szybko okazuje się, że konieczne jest wyjście poza ramy podstawowej wiedzy. Często natrafiamy na problem, który wymaga od nas nieco większego pojęcia o regułach rządzących Gitem. Czasami takie problemy zdarza się nam spowodować samemu, wychodząc z błędnych założeń, które wynikają z niepełnej wiedzy.

Niezwykle przydatne jest zrozumienie, jak Git działa pod osłoną tych wszystkich uproszczeń. Ułatwia to zdecydowanie pracę w zespole i pozwala uniknąć czyhających na nas pułapek. Taka wiedza pozwala nam nabrać większej pewności siebie i swobody przy posługiwaniu się Gitem. Pozwala nam tworzyć przejrzystą historię podczas codziennej pracy przy projekcie.

Git wybacza sporo błędów, więc żeby bezpowrotnie utracić jakieś zmiany, trzeba się naprawdę postarać. Jednak po co przysparzać niepotrzebnie nerwów sobie i zespołowi? Jeśli wiemy jakie mechanizmy zadziałają, gdy wykonamy konkretne polecenie, to jesteśmy w pełni świadomi efektów naszych działań. Mamy pewność, że nie wprowadzimy żadnych komplikacji i osiągniemy zamierzony cel.

W tym artykule zajrzymy w zakamarki Gita, które są ukryte dla użytkowników, dopóki ci nie wykażą zainteresowania nimi. Dzięki temu dowiemy się, w jaki sposób Git przechowuje informacje i jak nimi manipuluje. Żeby zrozumieć treści zawarte w dalszej części artykułu, potrzebna będzie przynajmniej podstawowa znajomość Gita, bo skupimy się na bardziej zaawansowanych kwestiach.

Czym jest repozytorium?

Nie jest trudno domyślić się, skąd Git wie, że jakiś folder na dysku jest repozytorium lokalnym. Znakiem rozpoznawczym jest podfolder o nazwie .git, który znajduje się w każdym repozytorium. Jego zadaniem jest przechowywanie wszystkich danych, które powstają podczas pracy z Gitem. Znajduje się tam m. in. historia projektu. Folder będący repozytorium lokalnym jest równocześnie katalogiem roboczym. To znaczy, że zawiera pliki projektu, na których pracujemy i które modyfikujemy. Pośród nich znajduje się wcześniej wspomniany folder .git. Można bez przeszkód zajrzeć do jego wnętrza (co zresztą zrobimy w jednym z kolejnych paragrafów). Pozwoli nam to lepiej zrozumieć specyfikę działania Gita. Nie ma jednak potrzeby wykonywać manualnie jakichkolwiek operacji na tym specjalnym folderze. Służy on Gitowi i to Git powinien zajmować się jego zarządzaniem. Jako użytkownicy dostajemy od Gita szeroką gamę narzędzi, które pokrywają większość naszych potrzeb. Dzięki temu nie musimy ingerować w wewnętrzne struktury Gita.

Istotną informacją jest dla nas fakt, że Git przechowuje historię zmian plików projektu za pomocą ich kopii. Oznacza to, że kiedy dokonamy jakichś zmian w pliku i utworzymy commit, Git automatycznie utworzy sobie kopię tego pliku. Będzie ona dokładnym odwzorowaniem pliku z katalogu roboczego. Następnie zostanie umieszczona w folderze .git i pozostanie w nim już niezmieniona.

W repozytorium istnieje wiele wersji jednego pliku, które nieco się od siebie różnią. Są to różnice wynikające z wykonywanych zmian podczas pracy nad projektem. Jedną wersję pliku znajdziemy przede wszystkim w katalogu roboczym. To ją modyfikujemy, pracując nad projektem. Jeżeli dodamy plik do przechowalni (ang. staging area) za pomocą git add, to Git utworzy jego kopię. Umieści ją w folderze .git, który będzie zawierał pozostałe wersje pliku. Innymi słowy, Git posiada osobną wersję pliku dla każdego commita, w którym ten plik został zmodyfikowany. Dzięki temu przełączanie się między commitami jest bardzo wydajne.

W innych systemach kontroli wersji, gdy chcemy cofnąć się do odległego commita z przeszłości, konieczne jest przejście po wszystkich nowszych od niego commitach. Po drodze odczytywane są kolejne zmiany, jakie były wykonane na pliku, aż do momentu, gdy zostanie przywrócona pożądana wersja. W Gicie ten proces jest znacznie prostszy, gdyż posiada on zapisaną wersję pliku dla interesującego nas commita. Przełączając się na ten commit, wystarczy w folderze .git znaleźć plik z tą wersją. Jest to znacznie szybsze niż w innych systemach kontroli wersji.

Do pracy na lokalnym repozytorium niepotrzebne jest aktywne połączenie internetowe z repozytorium zdalnym. Jest ono wymagane tylko podczas pobierania i wysyłania zmian. Ponadto nie jest nawet konieczne, żeby repozytorium zdalne istniało, możliwa jest praca jedynie na repozytorium lokalnym. Nie ma żadnych przeciwwskazań, żeby utworzyć repozytorium zdalne później i wtedy połączyć je z lokalnym, a co więcej, możliwe jest też połączenie z więcej niż jednym repozytorium zdalnym. Taki mechanizm wykorzystują m.in. Github czy GitLab by udostępnić funkcję fork.

Czym jest commit?

Git przechowuje commity w postaci plików o niewielkich rozmiarach. Każdy commit zawiera ustalony z góry zestaw informacji. Są to:

  • opis
  • autor i twórca commita
  • data utworzenia commita i data jego modyfikacji
  • skrót commita rodzica (albo rodziców)
  • skrót pliku tree

Każdy commit posiada również unikalny identyfikator, który nazywany jest też skrótem. Jest to ciąg 40 liczb szesnastkowych. Skrót wyliczany jest za pomocą algorytmu SHA-1 na podstawie zawartości, jaka znajduje się w pliku commita. Każdy skrót jest inny, ponieważ każdy commit będzie zawierał nieco inne dane. Skrót przypisany commitowi nie będzie się też nigdy zmieniał, gdyż commity są niemodyfikowalne – nie jesteśmy w stanie żadnym poleceniem Gita zmienić istniejącego commita. Niektóre polecenia jak np. git commit --amend dają nam poczucie, że wykonaliśmy zmiany w commicie, ale tak naprawdę tworzą one nowe commity na wzór już istniejących. W efekcie wykonania tych komend w repozytorium będą znajdować się dwa commity. Ten pierwszy nie zostanie usunięty, ale najczęściej utracimy do niego odniesienie. Nadal będzie istniał w repozytorium, ale dostęp do niego będzie utrudniony, co jest dość schludnym rozwiązaniem. Otrzymujemy nowy commit w miejsce starego, który jednak nie przepada bezpowrotnie i zawsze możemy go odzyskać, jeśli zajdzie taka potrzeba. Efekt wykonania polecenia git commit --amend widać na rysunku poniżej. Pokazuje on, że została utworzona kopia commita C2, na którym wykonano operację amend.

01 amend git 300x90 - Jak Git działa za kulisami

Większość commitów posiada odniesienie do swoich commitów-rodziców, bo w ten sposób Git buduje historię – jako ciąg następujących po sobie commitów. Istnieją także commity bez rodzica. Najczęstszym takim przypadkiem jest po prostu pierwszy commit w historii repozytorium, który nie ma swojego poprzednika. Możliwe jest również samodzielne utworzenie takiego commita, który nie posiada rodzica. Często natomiast commity miewają dwoje rodziców. Są to merge-commity, powstałe w wyniku łączenia (ang. merging) dwóch gałęzi. Właściwie od standardowych commitów różnią się jedynie posiadaniem drugiego rodzica. Poza tym plik merge-commita posiada dokładnie taki sam zestaw informacji jak zwykły commit.

Wiemy już, jakie informacje wchodzą w skład pliku commita. Na tej podstawie możemy stwierdzić, że jego rozmiar nie zależy od ilości zmian w projekcie jakie zawiera. W takim razie skąd Git właściwie wie, jakie zmiany zostały wykonane w ramach commita? W tym celu wykorzystuje pliki tree. Każdy commit posiada odniesienie do jednego z takich plików. Odniesieniem jest oczywiście skrót SHA-1, wyliczony na podstawie zawartości pliku tree, tak jak w przypadku commita. Plik tree zawiera listę plików i podfolderów, jakie znajdują się w konkretnym folderze projektu. Commit wskazuje na plik tree, który odpowiada zawartości katalogu roboczego. Plik tree przyporządkowuje każdemu podfolderowi inny plik tree, a każdemu plikowi plik blob. Pliki blob to kopie plików projektowych zrobione w momencie tworzenia commita. Pliki tree tworzą drzewiastą strukturę, odpowiadającą strukturze katalogu roboczego. Obrazuje to poniższy rysunek – po lewej stronie pokazana jest struktura folderów, a po prawej powiązania w plikach tree i blob.

02 tree blob diagram git e1611576005171 - Jak Git działa za kulisami

Podczas tworzeniu commita, Git dodaje nowe pliki tree i blob tylko dla plików projektowych, które uległy zmianie. Dla pozostałych plików wykorzystywane są istniejące już pliki tree i blob. Git przechowuje je w postaci skompresowanej. Jeżeli znamy skrót interesującego nas pliku, to możemy z łatwością odczytać jego zawartość za pomocą komendy:
git cat-file -p SKROT

Zachęcam do przetestowania jej na dowolnym repozytorium Gita. Żeby znaleźć skrót ostatniego commita, wystarczy użyć polecenia git log -1

Czym jest gałąź

Gałęzie w Gicie są jednym z elementów, które najbardziej odróżniają go od innych systemów kontroli wersji. Często mylnie uważa się, że gałąź jest zbiorem commitów, co można by zobrazować w następujący sposób:

03 branch git 300x290 - Jak Git działa za kulisami

Nie jest to jednak zgodne z prawdą. Gałąź w Gicie jest tak naprawdę tylko wskaźnikiem na pojedynczy commit. Git przechowuje gałęzie w postaci plików, co nie powinno już być zaskoczeniem. W dodatku plików o niewielkich rozmiarach, bo zawierających jedynie 40 znaków. Brzmi znajomo, prawda? Jest to oczywiście skrót SHA-1 commitu. Czyni to pracę z gałęziami naprawdę wydajną. Podczas tworzenia nowej gałęzi nie są wykonywane żadne wymagające operacje ani nie następuje kopiowanie jakichkolwiek plików, jak dzieje się w innych systemach kontroli wersji. Git ogranicza się do utworzenia jednego niewielkiego pliku. Przełączanie się między gałęziami też jest szybką operacją. Ten proces opisałem szerzej w paragrafie „Co dzieje się za kulisami”.

Gałąź, na której pracujemy, podąża za kolejnymi commitami. To znaczy, że po utworzeniu commita, Git aktualizuje wskaźnik gałęzi, tak by wskazywał na nowy commit. By ten mechanizm był możliwy, istnieje specjalny wskaźnik HEAD, który… również jest plikiem. HEAD przechowuje informację o tym, na jakiej obecnie gałęzi się znajdujemy. Czyli HEAD wskazuje na gałąź, która wskazuje na commit. Commit z kolei wskazuje na plik tree, a plik tree zawiera odniesienia do plików blob. Pliki blob to konkretne wersje plików projektowych.

Chociaż najczęściej przełączamy się na konkretną gałąź, możliwe jest też przełączenie się na wskazany commit. Wystarczy podać skrót commita, używając polecenia git checkout SKROT. Wtedy plik HEAD będzie zawierał skrót tego commita, a nie nazwę gałęzi. Taki stan nosi nazwę detached HEAD. Git ostrzega nas jednak przed wykonywaniem tej czynności, gdyż łatwo można utracić wszystkie odniesienia do nowych commitów. W tym stanie, HEAD wskazuje na konkretny commit. Jeżeli utworzymy nowy commit, to HEAD zostanie zaktualizowany i będzie na niego wskazywać. Będzie to jednak jedyne odniesienie do nowego commita, gdyż żadna gałąź nie będzie na niego wskazywać. Po przełączeniu się na inną gałąź, HEAD nie będzie już wskazywał nowego commita i nie będzie istniało do niego już żadne inne odniesienie. Git go nie usunie (a przynajmniej nie od razu) więc ciągle możliwe będzie jego użycie. Niemniej, dostęp do niego będzie utrudniony.

Dobrym sposobem na zrozumienie mechanizmów pracy z gałęziami w Gicie jest wizualizowanie sobie całego procesu. Na stronie dostępny jest interaktywny kurs, który wyświetla strukturę gałęzi w postaci animowanego grafu. Wykonując kolejne operacje, widzimy na bieżąco jakie zmiany zachodzą w gałęziach. Zachęcam do zapoznania się chociaż z początkowymi lekcjami tego kursu.

Jak zbudowany jest folder .git?

Folder .git jest bazą danych Gita. Zawiera wszystkie informacje potrzebne do pracy z repozytorium. Git przechowuje je w formie plików. Część z nich ma postać skompresowaną, by oszczędzać miejsce na dysku.

Folder .git zawiera następujące elementy:

  • hooks – folder zawierający skrypty, uruchamiane automatycznie po wykonaniu określonych akcji
  • info – folder zawierający plik exclude z listą ignorowanych plików
  • logs – folder zawierający historię operacji na gałęziach
  • objects – folder zawierający pliki tree i blob
  • refs – folder zawierający pliki gałęzi i tagów
  • config – plik z lokalną konfiguracją repozytorium
  • HEAD – plik z nazwą bieżącej gałęzi
  • index – plik binarny zawierający listę plików w przechowalni

Z naszego punktu widzenia interesujący jest folder objects, gdzie Git przechowuje pliki tree i blob. Ciekawy jest zwłaszcza sposób w jaki Git to robi. Gdy zajrzymy do folderu objects, znajdziemy w nim w większości podfoldery, których nazwa składa się z dokładnie dwóch znaków – liczb szesnastkowych. Są to dwa pierwsze znaki skrótów plików tree i blob, które znajdują się w tych folderach. Jest to rozwiązanie poprawiające wydajność przy wyszukiwaniu konkretnych plików po ich skrótach, gdyż ogranicza liczbę plików jaką trzeba sprawdzić, by znaleźć ten właściwy.

W folderze objects znajduje się również podfolder o nazwie pack. Przechowuje on pliki binarne zwane packfile. Żeby wytłumaczyć jaka jest ich rola, trzeba najpierw wyjaśnić w jaki sposób Git zapisuje zmiany w projekcie. Jak wspominałem wcześniej, Git używa w tym celu plików blob. Są to po prostu kopie plików z katalogu roboczego. Jeżeli commit zawiera zmianę tylko w jednej linijce pliku, to Git i tak wykona kopię całego tego pliku. Może stać się to problematyczne, gdy plik jest dużych rozmiarów i wprowadzane są do niego często zmiany. Wtedy kopie takiego pliku będą niepotrzebnie zabierały miejsce na dysku. Żeby temu zapobiec, Git posiada mechanizm zbierający takie zmiany w jednym pliku. Tym plikiem jest właśnie packfile. Przechowuje on zmiany w postaci różnic. To znaczy, że zapisywane są tylko te fragmenty, które uległy zmianie, a nie całe kopie pliku. Pliki blob odpowiadające tym zmianom mogą zostać usunięte, co zwalnia miejsce na dysku.

Warto jeszcze wspomnieć o pliku index, który znajduje się bezpośrednio w folderze .git. Git zapisuje zawartość plików w momencie dodawania ich do staging area. W tym celu korzysta ze znanych już plików blob. Zatem każdy plik dodany do staging area ma swój plik blob. Plik index zawiera listę plików w staging area wraz z odpowiadającym im plikami blob.

Co dzieje się za kulisami

Warto podsumować zebrane do tej pory informacje. Prześledźmy zmiany, jakie zachodzą w repozytorium lokalnym, podczas codziennej pracy z Gitem. Będzie to dobry sposób na uporządkowanie wiedzy. Zacznijmy od pustego folderu przeznaczonego na repozytorium projektu. Pierwszym krokiem jest sklonowanie repozytorium zdalnego za pomocą komendy git clone. W efekcie w folderze lokalnym jest tworzony folder .git ze znaną już nam zawartością. W pliku .git/config zostaje zapisane odniesienie do zdalnego repozytorium z domyślną nazwą origin. Dzięki temu Git wie, jak się z nim połączyć. Plik .git/refs/remotes/origin/HEAD wskazuje domyślną gałąź w repozytorium zdalnym. Najczęściej jest to master. Git sprawdza jaki commit jest wskazywany przez tę gałąź, a następnie znajduje plik tree, wskazywany przez ten commit. Następnie umieszcza pliki i foldery w katalogu roboczym zgodnie ze wskazaniami pliku tree. To znaczy, że katalog roboczy zawiera pliki w takiej wersji, w jakiej były podczas tworzenia commita.

Kiedy tworzymy nową gałąź poleceniem git branch nowa_galaz, Git automatycznie tworzy nowy plik .git/refs/heads/nowa_galaz zawierający skrót bieżącego commita. Po przełączeniu się na tę gałąź za pomocą komendy git checkout nowa_galaz, Git aktualizuje plik HEAD. Będzie wskazywał na nową gałąź, na którą się przełączyliśmy.

Po wprowadzeniu zmian do projektu, dodajemy wybrane pliki do staging area za pomocą polecenia git add. Dla wszystkich dodanych w ten sposób plików, Git tworzy ich kopie w postaci plików blob. Następnie zapisuje do pliku index listę dodanych plików i skróty odpowiadających im plików blob. Tylko zmiany, które znajdą się w pliku index zostaną uwzględnione w commicie.

Wykonując commit korzystamy z polecenia git commit.

  1. Git tworzy plik commit oraz pliki tree odpowiadające ścieżkom plików z pliku index.
  2. Pliki tree wskazują na pliki blob, tworząc drzewiastą strukturę odpowiadającą katalogowi roboczemu. Git w tym momencie nie tworzy nowych plików blob tylko wykorzystuje już istniejące, powstałe podczas dodawania plików do staging area.
  3. Następnie Git sprawdza na jaką gałąź wskazuje plik HEAD i aktualizuje ją, by wskazywała na nowo utworzony commit. Oznacza to, że modyfikowany jest plik .git/refs/heads/nowa_galaz.
  4. Na koniec wcielamy zmiany z naszej gałęzi do gałęzi master. W tym celu przełączmy się na niego (zmienia się plik HEAD). Wykorzystajmy komendę git merge nowa_galaz. Często okazuje się, że w międzyczasie powstały na nim już jakieś commity, przez co nie jest możliwe użycie mechanizmu fast-forward. Oznacza to, że Git musi utworzyć nowy merge-commit. Powstają odpowiednie pliki tree, natomiast wszystkie pliki blob już istnieją.

Jaka jest różnica między merge a rebase?

To pytanie często pada na rozmowach kwalifikacyjnych, więc warto znać na nie odpowiedź. Przyjrzyjmy się, jakie mechanizmy zadziałają, podczas wykonywania tych poleceń.

Zadaniem obu, jest połączenie zmian z dwóch gałęzi. Robią to jednak w inny sposób. Działanie komendy rebase polega na skopiowaniu commitów. Wskazujemy gałąź docelową:
git rebase galaz_docelowa

Commity wybierane są z gałęzi, na której się znajdujemy. Wybierane są jedynie te commity, których nie ma na gałęzi docelowej. Mówiąc dokładniej, są to commity, do których nie można dotrzeć, poruszając się po kolejnych rodzicach commita wskazywanego przez gałąź docelową. Oprócz kopiowania commitów, ważną operacją wykonywaną podczas rebase, jest przeniesienie wskaźnika bieżącej gałęzi na najnowszy skopiowany commit. Rysunki poniżej przedstawiają sytuację przed i po wykonaniu polecenia:
git rebase docelowa

04 rebase before git - Jak Git działa za kulisami

05 rebase after git 300x93 - Jak Git działa za kulisami

Listę commitów, które zostaną skopiowane możemy znaleźć za pomocą polecenia git log docelowa..zrodlowa

Komenda merge także służy do łączenia zmian z dwóch gałęzi, ale w przeciwieństwie do rebase, nie kopiuje żadnych commitów. Merge może zostać wykonany na dwa sposoby. Pierwszy to fast-forward merge. Git wykonuje go automatycznie, ale tylko jeśli jedna z gałęzi zawiera już wszystkie commity z drugiej. Wtedy jedyną wykonaną operacją jest aktualizacja wskaźnika bieżącej gałęzi, tak by wskazywał na ten sam commit, co gałąź docelowa. Poniżej widać sytuację przed i po wykonaniu merge z użyciem fast-forward:

06 merge before git 300x63 - Jak Git działa za kulisami

07 merge after git 300x45 - Jak Git działa za kulisami

Drugi sposób, w jaki może zadziałać merge, polega na utworzeniu nowego commitu. Jest to merge-commit, który posiada dwoje rodziców. Są nimi commity wskazywane przez łączone ze sobą gałęzie. Ten sposób jest stosowany przez Gita, jeśli obie gałęzie zawierają różne commity. Poniżej widać sytuację przed i po wykonaniu merge z utworzeniem merge-commita.

08 merge commit before git 300x94 - Jak Git działa za kulisami

09 merge commit after git 300x82 - Jak Git działa za kulisami

Jaka jest różnica między fetch pull?

W repozytorium lokalnym znajdują się specjalne gałęzie zdalne, których zadaniem jest odzwierciedlanie zmian w repozytorium zdalnym. To znaczy, że będą zawierać te same commity, co gałęzie w repozytorium zdalnym, które śledzą. Kiedy pobieramy commity z repozytorium zdalnego, trafiają one do odpowiednich gałęzi zdalnych. Nazwy tych gałęzi zawierają przedrostek z nazwą repozytorium zdalnego, np. origin/master. Origin to domyślna nazwa repozytorium zdalnego, a master nazwa śledzonej gałęzi w tym repozytorium.

Gałąź lokalna i jej zdalny odpowiednik nie muszą zawsze wskazywać tego samego commitu. Jeżeli będąc na gałęzi master, utworzyliśmy nowy commit, a nie wysłaliśmy go jeszcze do repozytorium zdalnego, to origin/master nie będzie wskazywał tego commitu. Może też zdarzyć się odwrotna sytuacja, gdy pobierzemy najnowsze zmiany z repozytorium zdalnego i znajdą się one tylko na gałęzi origin/master, ale nie będzie ich jeszcze na gałęzi master.

Taki efekt osiągniemy przy pomocy komendy git fetch. Pobiera ona commity z repozytorium zdalnego i umieszcza je w odpowiednich gałęziach zdalnych. Sytuację po wykonaniu git fetch przedstawia poniższy rysunek:

10 fetch git - Jak Git działa za kulisami

Zazwyczaj chcemy pobrane commity umieścić również w gałęziach lokalnych, aby odzwierciedlić je w katalogu roboczym. Wystarczy wykonać merge zdalnej gałęzi do jej lokalnego odpowiednika:
git checkout master
git merge origin/master

Po tej operacji, lokalna gałąź master będzie zawierała zmiany ze zdalnego repozytorium:

11 fetch after git 300x137 - Jak Git działa za kulisami

Pobieranie zmian w ten sposób wymaga każdorazowego wykonywania kilku poleceń. Git udostępnia jednak dodatkową komendę, która łączy w sobie powyższe polecenia. Komenda git pull najpierw pobiera commity ze zdalnego repozytorium i umieszcza je w gałęziach zdalnych (fetch), a następnie aktualizuje bieżącą gałąź lokalną, by również zawierała te commity (merge). Jest to ułatwienie dla użytkowników, by mogli sprawniej pobierać zmiany z repozytorium zdalnego.

Podsumowanie

Przyjrzeliśmy się dokładniej funkcjonalności oferowanej przez Gita. Poznaliśmy sporo mechanizmów, jakimi się posługuje. Dowiedzieliśmy się w jaki sposób przechowywane są commity i zmiany w plikach projektowych. Przyjrzeliśmy się również wykorzystywanym strukturom danych, m.in. plikom tree i blob. Poza tym zdefiniowaliśmy precyzyjnie, czym są gałęzie i zobaczyliśmy, jak Git wykonuje na nich operacje. Wyjaśniliśmy także różnice w działaniu merge i rebase.

Wszystko to jest wiedzą, która wykracza poza podstawową znajomość Gita. Nabyte umiejętności sprawdzą się przy codziennej pracy i pozwolą znacznie pewniej i swobodniej posługiwać się poleceniami Gita. Pomogą również w zrozumieniu trudniejszych pojęć, jeśli zajdzie taka potrzeba. Myślę, że naprawdę warto mieć świadomość, jakie operacje Git wykonuje za nas automatycznie i jakie rodzi to konsekwencje. Dzięki temu będziemy wiedzieli, jakie rezultaty przyniosą nasze działania.

4.3 / 5
Tagi: , ,

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

Zostaw komentarz