Git od podstaw – git add, git commit i historia zmian

by Patryk

Przy pierwszym kontakcie z Gitem szybko pojawiają się pytania: czym różni się git add od git commit, dlaczego nie wszystkie zmiany trafiają do commita i co właściwie oznacza HEAD? W tym artykule przedstawię Ci podstawy gita, które pomogą Ci zrozumieć sposób wersjonowania plików. Przyjrzymy się działaniu Working Directory i Staging Area, prześledzimy proces zapisywania zmian i nauczymy się analizować historię commitów. Ta wiedza przyda Ci się zarówno podczas pracy nad kodem, jak i przy wersjonowaniu skryptów czy konfiguracji w DevOps.

Czym jest GIT


Ucząc się IT na pewno nie raz spotkałeś się z pojęciem kontroli wersji. Git bo to on jest najbardziej znanym systemem wersjonowania dosłownie zdominował świat. Setki firm i programistów korzystają z niego aby śledzić zmiany, współpracować przy projektach oraz analizować problemy z wcześniejszych wersji kodu. W oficjalnej dokumentacji Gita możemy przeczytać:

System kontroli wersji śledzi wszystkie zmiany dokonywane na pliku (lub plikach) i umożliwia przywołanie dowolnej wcześniejszej wersji.

Brzmi logicznie prawda? Git to taki turbo przycisk zapisu, który nazywa się commitem. Podczas tworzenia commita tworzysz coś w stylu migawki wszystkich plików znajdujących się w folderze, w którym zainicjowałeś gita – znanym również jako repozytorium. Ta migawka zapisuje wszystko w aktualnym stanie , a do tego konkretnego commita możesz uzyskać dostęp później, jeśli coś zepsujesz.

Dlaczego warto uczyć się gita?


W tym momencie możesz zacząć się zastanawiać, skoro git zapisuje wersje danego projektu to po co mam go używać skoro mogę zapisywać kopie swojej pracy na Google Drive lub Dropbox. To troche pytanie w stylu dlaczego chirurg nie używa noża zamiast skalpela. Oczywiście, nóż może wykonać wiele zadań, ale w przypadku chirurga potrzebujesz bardziej precyzyjnego narzędzia.

Git jest właśnie takim skalpelem dla programisty. W chmurze po zapisaniu plików nie masz aż takiej kontroli nad historią zmian w nich. Musiałbyś pamiętać, jakie zmiany wprowadziłeś osobiście w danym pliku np w Kwietniu i Lipcu, a co jeśli tych plików w danym projekcie jest kilkaset i każdy ma po kilkaset linii kodu? Natomiast w git wszystkie zmiany jakie wprowadzasz są przechowywane wraz z informacją co się zmieniło od ostatniej zmiany. Za pomocą jednego polecenia dokładnie wiesz, które pliki i wiersze zostały zmienione.

Więcej informacji na temat gita, możesz znaleźć w darmowej książce.

Jak Git zarządza zmianami?


Na początek warto zrozumieć trzy obszary, z którymi pracujemy podczas korzystania z Gita:

    Working Directory
            |
            | git add
            v
       Staging Area
            |
            | git commit
            v
     Local Repository

Working directory

To katalog roboczy zawierający pliki projektu, nad którymi aktualnie pracujesz. To tutaj tworzysz nowe pliki, edytujesz kod, zmieniasz konfigurację lub usuwasz niepotrzebne elementy w projekcie.

Staging Area

Przechowuje stan plików przygotowanych do następnego commita. Aktualizujemy go poleceniem:

git add

Staging Area daje kontrolę nad tym, które zmiany chcemy zapisać w historii. Możesz przykładowo zmodyfikować pięć plików, ale przygotować do commita tylko dwa z nich. Pozostałe z nich zostaną w katalogu roboczym.

Co jest niezwykle ważne, staging area przechowuje konkretną zawartość plików z momentu ich dodania poleceniem git add. Jeżeli później ponownie zostanie zmodyfikowana ich zawartość nie zostaną automatycznie zaktualizowane w staging area. Nie przejmuj się jeśli na ten moment nie rozumiesz, pokaże to później na przykładzie.

Local Repository

Przechowuje historie commitów i aktualny stan projektu. W momencie gdy wykonujemy:

git commit

Git tworzy nowy commit na podstawie stanu plików dodanych do staging area.

Commit reprezentuje snapshot projektu, czyli zapisany stan śledzonych plików w określonym momencie historii.

Cały mechanizm sprowadza się do dwóch operacji:

  • git add przygotowuje zawartość plików do zapisu.
  • git commit zapisuje przygotowany stan projektu w historii repozytorium.

Rozdzielenie tych operacji pozwala świadomie decydować, co powinno znaleźć się w danym commicie.

Tworzenie pierwszego repozytorium


Do ćwiczeń wykorzystamy katalog devops-lab, który utworzymy na maszynie wirtualnej z Debianem.

Najpierw sprawdźmy, czy Git jest zainstalowany:

git --version

Jeśli nie jest, możesz go zainstalować w debianie poleceniem:

apt install git

Następnie utwórzmy katalog na potrzeby laba i przejdźmy do niego:

mkdir devops-lab
cd devops-lab

Aby zacząć korzystać z Gita, najpierw musisz zainicjować repozytorium w folderze, który Git będzie śledził w celu kontroli wersji. Będąc w katalogu devops-lab uruchom następujące polecenie:

git init

To polecenie tworzy ukryty podfolder .git, który zawiera wszystkie dane śledzenia i metadane Gita dla repozytorium. Możemy go zobaczyć za pomocą polecenia:

ls -la

Po zainicjowaniu repozytorium możesz rozpocząć śledzenie plików za pomocą podstawowych poleceń Gita.

Warto wiedzieć, że usunięcie katalogu .git nie usuwa zwykłych plików projektu. Tracimy jednak lokalne dane repozytorium, w tym jego historię i konfigurację.

Konfiguracja użytkownika

Zanim utworzymy pierwszy commit, skonfigurujmy dane autora. Każde zatwierdzenie w Git ma przypisanego autora, z podanym imieniem i nazwiskiem oraz adresem e-mail. Aby ustawić te dane, użyj poniższych poleceń.

Możemy ustawić je dla bieżącego repozytorium:

git config user.name "Jan Kowalski"
git config user.email "jan@example.com"

Jeśli chcemy używać tych samych danych domyślnie we wszystkich repozytoriach, dodajemy opcję --global:

git config --global user.name "Jan Kowalski"
git config --global user.email "jan@example.com"

Aktualne wartości globalne sprawdzimy poleceniami:

git config --global user.name
git config --global user.email

Konfiguracja lokalna repozytorium ma pierwszeństwo przed globalną.

Podane dane będą zapisywane w metadanych commitów. Nie są one hasłem ani mechanizmem uwierzytelniania do GitHuba.

Pierwszy plik i git status

Jedną z najczęsciej używanych komend Gita jest:

git status

Pokazuje ona stan repozytorium: pliki nieśledzone, zmiany przygotowane do commita oraz modyfikacje, które pozostają w katalogu roboczym. Na ten moment zobaczysz komunikat, że na gałęzi master nie ma jeszcze żadnych zapisów.

Utwórzmy więc pierwszy plik:

echo 'DevOps Lab' > README.md

Sprawdźmy stan repozytorium:

git status

Git pokaże README.md w sekcji:

Nieśledzone pliki:
    README.md

Plik istnieje w katalogu projektu, ale Git jeszcze go nie śledzi.

Możemy to sprawdzić:

git ls-files

Polecenie nie wyświetli README.md, ponieważ plik nie został jeszcze dodany do staging area.

Dodawanie plików przez git add

Aby dodać pliki do staging area, które mają zostać zatwierdzone należy skorzystać z polecenia git add.

git add README.md

Następnie:

git status

Teraz zobaczymy, że plik trafił do staging area:

Zmiany do złożenia:
    nowy plik: README.md

Po ponownym uruchomieniu polecenia git ls-files zobaczysz, że git już śledzi plik, a jego aktualna zawartość jest przygotowana do zapisu:

git ls-files

Możemy też dodawać kilka plików jednocześnie:

git add file1 file2

Możemy również przygotować wszystkie zmiany z bieżącego katalogu i jego podkatalogów:

git add .

Ta komenda jest wygodna, ale wymaga ostrożności, uruchomiona z katalogu głównego repozytorium może dodać nowe pliki, modyfikacje oraz usunięcia, których nie planowaliśmy uwzględniać w jednym commicie. Domyślnie pomija pliki ignorowane przez Gita. Dlatego przed zapisem zmian warto sprawdzić, co znajduje się w staging area.

Tworzenie pierwszego commita

Przed wykonaniem commita sprawdźmy przygotowane zmiany:

git diff --staged

Ponieważ repozytorium nie ma jeszcze żadnego commita, Git porówna przygotowaną zawartość z pustym stanem początkowym. W wyniku zobaczymy fragment:

+# DevOps Lab

Znak + oznacza dodaną linię.

Po dodaniu plików do staging area możesz zatwierdzić go za pomocą polecenia git commit. Zatwierdzenie to zapis zmian wprowadzonych w repozytorium. Aby zatwierdzić pliki z staging area z czytelnym komunikatem, użyj następującego polecenia:

git commit -m "Add initial README"

Git zapisze stan przygotowany w staging area jako pierwszy commit na gałęzi master.

Sprawdźmy status:

git status

Powinniśmy zobaczyć:

nic do złożenia, drzewo robocze czyste

Oznacza to, że nie mamy oczekujących zmian do zapisania. Zawartość śledzonych plików w katalogu roboczym i staging area odpowiada aktualnemu commitowi. Od tego momentu każda zmiana wprowadzona w dowolnym pliku w folderze będzie rejestrowana i porównywana z tą początkową migawką.

Co naprawdę dzieje się po git add?


Różnicę pomiędzy working directory a staging area najlepiej zobaczyć na konkretnym przykładzie.

Mamy plik README.md o zawartości:

# DevOps Lab

Dodajmy kolejną linię:

echo 'Line A' >> README.md

Następnie przygotujmy plik do commita:

git add README.md

W staging area znajduje się teraz wersja:

# DevOps Lab
Line A

Dopiszmy jeszcze jedną linię:

echo 'Line B' >> README.md

Tym razem nie wykonujemy ponownie git add.

W katalogu roboczym mamy:

# DevOps Lab
Line A
Line B

Staging area nadal przechowuje wcześniejszą wersję:

# DevOps Lab
Line A

W uproszczeniu wygląda to tak:

Working Directory
-----------------
# DevOps Lab
Line A
Line B

        |
        | Ostatnie git add
        v

Staging Area
------------
# DevOps Lab
Line A

        |
        | git commit
        v

Local Repository
----------------
Commit zapisze Line A,
ale nie zapisze Line B

Jeżeli teraz wykonamy:

git commit -m "Update README"

commit zapisze:

Line A

ale nie:

Line B

Dlaczego się tak dzieje? Ponieważ git add aktualizuje staging area na podstawie zawartości pliku w chwili wykonania polecenia git add. Późniejsza edycja pozostaje zmianą w working directory, dopóki ponownie jej nie przygotujemy poleceniem git add.

Porównywanie zmian za pomocą polecenia git diff

Git udostępnia dwa podstawowe sposoby sprawdzania różnic podczas pracy nad plikami.

git diff

Poniższe polecenie porównuje stan śledzonych plików w working directory ze staging area:

git diff

W naszym przykładzie zobaczymy:

 # DevOps Lab
Line A
+Line B

Git pokazuje Line B, ponieważ tej zmiany nie ma jeszcze w staging area. Line A występuje w obu wersjach pliku, więc nie jest oznaczona jako nowa zmiana w tym porównaniu.

git diff –staged

Teraz uruchommy:

git diff --staged

To polecenie porównuje staging area z ostatnim commitem, wskazywanym przez HEAD. Wynik będzie wyglądał tak:

 # DevOps Lab
+Line A

Widzimy wyłącznie Line A, ponieważ tylko ta zmiana została przygotowana do commita.

Różnice między komendami można przedstawić następująco:

       HEAD
(ostatni commit)
|
| git diff --staged
v
Staging Area
|
| git diff
v
Working Directory

W codziennej pracy git diff --staged przydaje się szczególnie przed wykonaniem commita. Pozwala zobaczyć, jakie różnice względem poprzedniego commita zostały przygotowane do zapisu.

Zapisujemy przygotowaną zmianę

Wykonajmy:

git commit -m "Add first README note"

Commit zapisze wersję pliku zawierającą Line A.

Możemy to sprawdzić:

git show HEAD:README.md

Wynik:

# DevOps Lab
Line A

Z kolei:

cat README.md

pokaże:

# DevOps Lab
Line A
Line B

Ostatnia linia nadal znajduje się w katalogu roboczym. Git jej nie usunął. Po prostu nie uwzględnił jej w commicie.

Zapiszmy ją w osobnym commicie:

git add README.md
git commit -m "Add second README note"

Po wykonaniu:

git status

Powinniśmy zobaczyć czyste drzewo robocze. W ten sposób utworzyliśmy trzy commity, z których każdy reprezentuje inny stan projektu.

Jeden commit, jedna logiczna zmiana

Przy tworzeniu commitów warto trzymać się zasady, że każdy z nich powinien mieć konkretny cel.

Add system information script
Add application configuration
Document installation process

Powyższe commity są łatwiejsze do przeanalizowania niż nic nie znaczący komentarz “changes”. Nie chodzi o to, żeby każdy commit zawierał jak najmniej linii kodu. Jeśli wprowadzenie jednej funkcjonalności wymaga zmiany kilku plików, mogą one znaleźć się w jednym commicie. Ważne, żeby zmiany były ze sobą logicznie powiązane.

Przydaje się to podczas code review, szukania regresji czy wycofywania błędnych zmian.

Przeglądanie historii przez git log

Do wyświetlenia historii commitów służy polecenie:

git log

Polecenie to pokazuje szczegółowe informacje, m.in. pełne identyfikatory, autorów, daty i wiadomości commitów. Na co dzień wygodniejsza bywa wersja skrócona:

git log --oneline

W naszym repozytorium zobaczymy historię podobną do tej:

de3b0b5 Add second README note
c8fe47b Add first README note
db54ab3 Add initial README

Identyfikatory są przykładowe. W Twoim repozytorium będą inne.

Każda linia reprezentuje jeden commit. Po lewej znajduje się jego skrócony identyfikator, a po prawej wiadomość opisująca zmianę.

Graficzne przedstawienie historii

Warto poznać jeszcze jedno polecenie:

git log --oneline --decorate --graph --all

Dla naszej liniowej historii wynik będzie wyglądał mniej więcej tak:

* de3b0b5 (HEAD -> main) Add second README note
* c8fe47b Add first README note
* db54ab3 Add initial README

Na razie graf jest prosty. Gdy zaczniemy tworzyć branche i wykonywać merge, pojawią się rozgałęzienia. Wtedy ta komenda staje się szczególnie przydatna.

Hash commita

Każdy commit ma swój identyfikator służący do odróżnienia go od innych. Pełny 40-znakowy identyfikator aktualnego commita wyświetlimy poleceniem:

git rev-parse HEAD

Hash pozwala odwołać się do konkretnego commita. Możemy go wykorzystać np. w poleceniu:

git show ID

W praktyce należy podać identyfikator istniejący w naszym repozytorium. Przydaje się to podczas analizowania błędów, porównywania wersji oraz przeglądania starszych zmian.

HEAD i branch master


Podczas przeglądania historii wielokrotnie spotkamy nazwę HEAD.

W typowej sytuacji HEAD wskazuje aktualnie wybraną gałąź, a ta wskazuje konkretny commit.

HEAD
 |
 v
master
 |
 v
Commit C

Jesteśmy na branchu master, który wskazuje na commit C.

Co się dzieje po utworzeniu nowego commita?

Załóżmy, że mamy historię:

A <--- B <--- C
              ^
             master
              ^
             HEAD

Wprowadzamy zmianę i tworzymy commit D.

Historia będzie wyglądała tak:

A <--- B <--- C <--- D
                    ^
                   master
                    ^
                   HEAD

Git przesuwa wskaźnik master na nowy commit. HEAD nadal wskazuje na master.

Poruszanie się po historii: HEAD~1, HEAD~2

Git umożliwia odwoływanie się do wcześniejszych commitów bez podawania ich hashy.

Weźmy historię:

A <--- B <--- C <--- D
                    ^
                   HEAD

Możemy odwołać się do commitów w następujący sposób:

HEAD    = D
HEAD~1  = C
HEAD~2  = B
HEAD~3  = A

Przykładowo aby wyświetlić zawartość poprzedniego commita możemy skorzystać z poniższego polecenia:

git show HEAD~1

Z kolei jeśli chcemy wyświetlić commit oddalony o dwa kroki wstecz możemy skorzystać z poniższego:

git show HEAD~2

Porównywanie commitów przez git diff


Git pozwala porównywać nie tylko working directory i staging area, ale również dowolne dwa punkty historii.

git diff <starszy-commit> <nowszy-commit>

Załóżmy, że historia wygląda tak:

A <--- B <--- C <--- D
      ^               ^
   starszy          nowszy

Wykonując:

git diff B D

otrzymamy różnicę pomiędzy stanem projektu w commicie B a stanem w commicie D.

Porównanie HEAD~2 z HEAD

W naszym repozytorium utworzyliśmy trzy commity:

A <--- B <--- C
^             ^
HEAD~2       HEAD

Commit A zawierał tylko nagłówek README. W B dodaliśmy Line A, a w C dodaliśmy Line B.

Uruchommy:

git diff HEAD~2 HEAD

Wynik będzie zawierał:

 # DevOps Lab
+Line A
+Line B

Git porównał stan z pierwszego commita ze stanem aktualnym.

Dlaczego historia Gita jest przydatna w DevOps?


W DevOps Git często służy do wersjonowania znacznie większej liczby elementów niż sam kod aplikacji. W repozytoriach mogą znajdować się skrypty automatyzujące zadania, konfiguracje usług, Dockerfile, pliki Terraform czy definicje pipeline’ów CI/CD. Załóżmy, że aplikacja działała poprawnie przez kilka wdrożeń, ale po ostatnim deploymencie zaczęła zgłaszać błędy.

Historia wygląda tak:

A --- B --- C --- D --- E
            |           |
         Wersja       Wersja
         działająca   z problemem

Wiemy, że commit C odpowiadał działającej wersji, a po wdrożeniu E wystąpił problem.

Możemy porównać oba stany:

git diff <commit-C> <commit-E>

Git pokaże zmiany we wszystkich śledzonych plikach między wskazanymi commitami. Mogą to być na przykład:

Dockerfile
docker-compose.yml
.github/workflows/deploy.yml
terraform/main.tf
config/application.yml

W ten sposób możemy zauważyć zmodyfikowaną wartość konfiguracyjną, zmianę nazwy zmiennej środowiskowej czy poprawkę w procesie wdrożenia.

Praktyczny workflow pracy z Gitem


Na podstawie omówionych przykładów możemy ułożyć prosty schemat codziennej pracy.

Najpierw sprawdzamy stan repozytorium:

git status

Następnie wprowadzamy potrzebne zmiany w plikach i sprawdzamy ich zakres:

git diff

Gdy mamy już gotowe zmiany, przygotowujemy wybrane pliki:

git add README.md

Przed zapisaniem commita sprawdzamy staging area:

git diff --staged

Jeśli wszystko się zgadza, tworzymy commit:

git commit -m "Update project documentation"

Na koniec kontrolujemy stan repozytorium i historię:

git status
git log --oneline

Schemat wygląda następująco:

Sprawdzenie statusu
        |
        v
   Edycja plików
        |
        v
     git diff
        |
        v
      git add
        |
        v
  git diff --staged
        |
        v
     git commit
        |
        v
     git status

Z czasem część tych operacji staje się nawykiem. Szczególnie przydatne jest sprawdzanie zawartości staging area przed każdym commitem.

Ściągawka z komend

KomendaDo czego służy
git initTworzy repozytorium
git statusPokazuje stan repozytorium
git status --shortWyświetla skrócony status plików
git ls-filesWyświetla pliki śledzone w staging area
git add <plik>Przygotowuje wskazany plik do commita
git add .Przygotowuje zmiany z bieżącego katalogu i podkatalogów
git diffPokazuje zmiany nieprzygotowane do commita
git diff --stagedPokazuje zmiany znajdujące się w staging area
git commit -m "message"Tworzy commit z przygotowanego stanu
git logWyświetla historię commitów
git log --onelinePokazuje skróconą historię
git log --oneline --decorate --graph --allWyświetla graficzną historię
git rev-parse HEADZwraca pełny identyfikator aktualnego commita
git show HEADPokazuje szczegóły aktualnego commita
git show HEAD~1Pokazuje poprzedni commit wzdłuż pierwszego rodzica
git diff HEAD~2 HEADPorównuje aktualny snapshot ze starszym
git diff HASH1 HASH2Porównuje dwa wskazane stany projektu

Następny krok: Znasz już podstawy działania Gita, wiesz, jak przygotowywać zmiany do commita i analizować historię repozytorium. W kolejnej lekcji połączymy lokalne repozytorium z GitHubem. Poznasz komendy clone, remote, push, fetch i pull oraz dowiesz się, jak przesyłać i pobierać zmiany ze zdalnego repozytorium.

← Wróć do spisu kursu DevOps

Może Cię również zainteresować

Dodaj komentarz