Podstawy Git w praktyce: jak nie pogubić się w commitach i branchach

0
90
Rate this post

Nawigacja:

Od chaosu do kontroli: po co w ogóle Git?

Typowa scena z życia początkującego programisty

Kuba kończy projekt na studia. W folderze ma już: projekt_final_v2, projekt_final_v3_poprawione, a na samym końcu ląduje perełka: projekt_final_v3_poprawione_ostateczne_KUBA. Nagle prowadzący prosi o cofnięcie się do wersji sprzed tygodnia, bo „tamto rozwiązanie było lepsze”. I zaczyna się polowanie na pliki.

Git rozwiązuje dokładnie ten problem. Zamiast dziesięciu kopii tego samego projektu w osobnych folderach, masz jedno repozytorium Git – z pełną historią zmian, możliwością cofnięcia się do dowolnej wersji i czytelnymi informacjami, co zostało zmienione, kiedy i przez kogo. Nawet w jednoosobowym projekcie to natychmiast porządkuje pracę.

Co faktycznie daje Git w codziennej pracy

Git jest rozproszonym systemem kontroli wersji. W praktyce oznacza to kilka bardzo konkretnych korzyści:

  • Historia zmian – każdy commit zapisuje stan projektu w danym momencie: które pliki się zmieniły, co dokładnie w nich poprawiono oraz kto to zrobił.
  • Cofanie błędów – jeśli coś zepsujesz, możesz cofnąć się do poprzedniego commita albo odwrócić konkretną zmianę.
  • Eksperymentowanie bez strachu – tworzysz osobny branch, testujesz pomysł, a jeśli okaże się bez sensu, po prostu go usuwasz. Główny kod pozostaje czysty.
  • Współpraca – każdy pracuje w swoim branchu, zmiany są łączone w kontrolowany sposób, a konflikty są widoczne i rozwiązywane jawnie.
  • Śledzenie problemów – widzisz, który commit wprowadził błąd, możesz prześledzić jego historię i przywrócić poprzedni stan.

To nie jest magia. Git po prostu zapisuje „migawki” projektu w przemyślany sposób i pozwala się między nimi przemieszczać. Cała reszta to kwestia kilku nawyków: sensowne commity, oddzielne branche na funkcje, unikanie pracy „na żywca” w głównej gałęzi.

Czym Git jest, a czym nie jest

Git bywa mylony z Dropboxem, Google Drive czy innym „folderem w chmurze”. Różnica jest fundamentalna:

  • Git – system kontroli wersji, który śledzi historię plików, umożliwia rozgałęzianie tej historii i łączenie różnych linii rozwoju.
  • Chmura / dysk sieciowy – po prostu miejsce przechowywania plików, często z prymitywną historią wersji (czasem tylko ostatnich kilku).

Git:

  • nie jest kopią zapasową w klasycznym sensie (chociaż pomaga ją utrzymać, gdy używasz zdalnego repozytorium),
  • nie zastępuje dysku ani chmury, tylko zapisuje i zarządza wersjami kodu,
  • działa lokalnie – możesz pracować bez internetu, commitować, robić branche, rebase, a dopiero później wypchnąć wszystko na serwer.

Gdy podłączysz Git do GitHuba, GitLaba, Bitbucketa czy innej platformy, dostajesz dodatkowo pull requesty (merge requesty), code review, pipeline’y CI/CD itd. Rdzeń i tak stanowi lokalne repozytorium Git na twoim komputerze.

Lokalne i zdalne repozytorium – obraz całości

W większości projektów funkcjonują równocześnie dwa poziomy:

  • Repozytorium lokalne – katalog na twoim komputerze z ukrytym folderem .git. Tu robisz commity, branche, mergujesz.
  • Repozytorium zdalne (remote) – np. na GitHubie. Służy jako „wspólny punkt odniesienia” dla zespołu i dodatkowe zabezpieczenie.

Przepływ pracy jest prosty:

  1. Robisz zmiany lokalnie, commitujesz je.
  2. Od czasu do czasu wysyłasz zmiany na zdalne repozytorium (git push).
  3. Ściągasz zmiany od innych (git pull lub git fetch + git merge/rebase).

Git nie wymaga ciągłego połączenia z internetem. Większość prac wykonujesz lokalnie, a zdalne repozytorium jest tylko punktem synchronizacji i współpracy.

Git sam z siebie nie rozwiąże bałaganu w projekcie, ale daje bardzo solidne ramy. Jeśli trzymać się kilku prostych zasad – sensowne commity, osobne branche na funkcje, jasne komunikaty – kontrola nad historią projektu przestaje być problemem, nawet gdy projekt rośnie.

Pierwsze kroki: instalacja, konfiguracja i słowniczek pojęć

Instalacja Git na Windows, macOS i Linux

Najprostsza droga, żeby zacząć, zależy od systemu operacyjnego.

Windows

  • Wejdź na git-scm.com i pobierz instalator „Git for Windows”.
  • Uruchom instalator, akceptuj domyślne ustawienia (są sensowne dla większości użytkowników).
  • Po instalacji będziesz mieć dostęp do „Git Bash” – terminala, w którym możesz od razu używać komend git.

macOS

  • Najprościej: zainstaluj Homebrew, a potem w terminalu wpisz: brew install git.
  • Alternatywnie: po wpisaniu git w terminalu system często zaproponuje instalację „Command Line Tools” od Apple – zawierają one Git.

Linux

  • Na Debian/Ubuntu: sudo apt-get install git
  • Na Fedora: sudo dnf install git
  • Na Arch: sudo pacman -S git

Po instalacji sprawdź wersję, żeby upewnić się, że wszystko działa:

git --version

Podstawowa konfiguracja: co ustawić od razu

Raz sensownie ustawiona konfiguracja Git potrafi oszczędzić wiele frustracji. Najważniejsze rzeczy:

  • Imię i e-mail – będą dołączane do każdego commita.
  • Edytor – używany np. przy pisaniu dłuższych wiadomości commitów.
  • Alias na częste komendy – skraca codzienną pracę.

W terminalu wpisz:

git config --global user.name "Twoje Imię"
git config --global user.email "twoj.email@example.com"

git config --global core.editor "nano"   # albo "code --wait", "vim", itp.

# przykładowe aliasy
git config --global alias.st "status -sb"
git config --global alias.co "checkout"
git config --global alias.br "branch"
git config --global alias.cm "commit -m"
git config --global alias.lg "log --oneline --graph --decorate --all"

Opcja --global oznacza, że ustawienia dotyczą wszystkich repozytoriów danego użytkownika. Można też konfigurować Git lokalnie w ramach jednego projektu, ale na start wystarczy konfiguracja globalna.

Słowniczek pojęć Git, które naprawdę trzeba znać

Kilka terminów przewija się w każdej rozmowie o Git. Bez nich ciężko zrozumieć, co się dzieje.

  • Repozytorium (repository) – katalog z projektem + ukryty folder .git, w którym Git trzyma całą historię zmian. To „świat” twojego projektu.
  • Commit – zapis stanu plików w danym momencie. Taka migawka z opisem: co zostało zmienione i dlaczego.
  • Branch (gałąź) – nazwana linia rozwoju historii commitów. Domyślnie masz main lub master; inne branche służą m.in. do nowych funkcji.
  • HEAD – wskaźnik na „aktualny” commit/branch, na którym pracujesz. Gdy przełączasz się na inny branch, HEAD zmienia swoje położenie.
  • Staging area (index) – „poczekalnia” zmian przed commitem. Tu trafia to, co zostało dodane przez git add.
  • Remote – zdalne repozytorium (np. na GitHubie), powiązane z lokalnym.

Working directory, staging area i repozytorium – prosta analogia

Dobrym obrazem trzech poziomów Gita jest praca nad referatem:

  • Working directory – twoje brudne notatki, plik, który edytujesz. Tu powstają zmiany, często w nieuporządkowanej formie.
  • Staging area – zaznaczasz na marginesie, które fragmenty są „gotowe” i chcesz je wstawić do ostatecznej wersji. Gitowo: używasz git add, żeby wskazać konkretne linie/pliki.
  • Repozytorium (commity) – kolejne „czyste wydania” twojego referatu z przypiętą datą, autorem i notatką, co się zmieniło.

W praktyce przepływ wygląda tak:

  1. Edytujesz pliki – zmiany są tylko w working directory.
  2. Wybierasz, co ma wejść do nowego commita: git add plik – zmiany trafiają do staging area.
  3. Tworzysz commit: git commit -m "opis" – staging area jest zapisywana w repozytorium jako nowy commit.

To rozdzielenie pozwala robić bardzo precyzyjne commity. Możesz wprowadzić wiele zmian naraz, ale do konkretnego commita dodać tylko część z nich.

Już sama poprawnie ustawiona konfiguracja i zrozumienie różnic między working directory, staging area i repozytorium sprawia, że Git przestaje straszyć. Kolejne kroki to po prostu oswojenie kilku podstawowych komend na żywym projekcie.

Tworzenie repozytorium i pierwsze commity krok po kroku

git init vs git clone – dwie drogi startu

Na początku trzeba odpowiedzieć na jedno pytanie: czy zaczynasz nowy projekt, czy dołączasz do istniejącego?

Nowy projekt – użyj git init

  1. Utwórz katalog na projekt: mkdir moj-projekt && cd moj-projekt
  2. Uruchom: git init

W katalogu pojawi się ukryty folder .git – to znaczy, że katalog stał się repozytorium Git. Na razie jednak nie ma żadnych commitów.

Istniejący projekt – użyj git clone

Jeśli projekt już jest np. na GitHubie, klonujesz go:

git clone https://github.com/uzytkownik/projekt.git

Git:

  • pobierze całe repozytorium z historią,
  • utworzy katalog o nazwie repozytorium,
  • ustawi domyślnego remote o nazwie origin wskazującego na zdalny adres.

W praktyce: git init – dla świeżych pomysłów, git clone – gdy dołączasz do innych.

Dodawanie plików i pierwszy commit – co się dzieje pod spodem

Zakładając, że masz już repozytorium, pora na pierwsze zmiany.

  1. Sprawdź status:
    git status

    Zobaczysz pliki nieśledzone (untracked), czyli takie, których Git jeszcze nie zna.

  2. Dodaj pliki do staging area:
    git add plik1.py
    git add folder/plik2.js
    # albo wszystko:
    git add .
  3. Jeszcze raz sprawdź status:
    git status

    Teraz część plików będzie miała status „changes to be committed” – są przygotowane do commita.

  4. Utórz pierwszy commit:
    git commit -m "Inicjalny commit: dodanie bazowych plików"

W tym momencie w repozytorium pojawił się pierwszy commit. Git przypisał mu unikalny identyfikator (hash), zapisał stan śledzonych plików, datę, autora i wiadomość.

.gitignore – filtr na zbędne pliki

Wiele plików w projekcie nie powinno trafiać do repozytorium: pliki tymczasowe, katalogi IDE, katalogi z zależnościami, pliki konfiguracyjne z danymi lokalnymi. Do zarządzania tym służy .gitignore.

Utwórz plik .gitignore w głównym katalogu projektu. Przykładowe wpisy:

# katalogi edytorów/IDE
.vscode/
.idea/

# systemowe
.DS_Store
Thumbs.db

# Python
__pycache__/
*.pyc

# Node.js
node_modules/

# logi
*.log

Zawartość .gitignore możesz generować z gotowych szablonów, np. na stronie gitignore.io (wpisujesz technologie, których używasz, np. „python, visualstudiocode, macOS”).

Typowe problemy przy pierwszych commitach

Scenariusz jest prosty: coś naprawiasz „na szybko”, dorzucasz małe usprawnienie, jeszcze jedną poprawkę… a po godzinie robisz jednego wielkiego commita „fixy”. Po tygodniu nie wiesz już, co tam właściwie było. Brzmi znajomo?

Kilka potknięć powtarza się u większości osób na starcie:

  • Jeden commit, milion zmian – trudno potem znaleźć konkretną poprawkę, trudno też zrobić revert tylko części.
  • Commity „śmietniki” typu „poprawki”, „test”, „działa?” – w historii nic nie mówią.
  • Dodawanie wszystkiego na siłę (git add .) – razem z plikami tymczasowymi, eksperymentalnymi notatkami czy lokalnymi konfiguracjami.

Im szybciej ograniczysz te nawyki, tym spokojniej będziesz patrzeć na historię projektu po kilku miesiącach.

Programista w czarnej bluzie pisze kod na laptopie w biurze
Źródło: Pexels | Autor: Sora Shimazaki

Commit bez magii: jak zapisywać zmiany z głową

Małe, logiczne porcje zamiast wielkich zlepków

Dobra zasada: jeden commit powinien odpowiadać na pytanie „co konkretnie zmieniłem?” w jednym, spójnym obszarze. Niech będzie to:

  • „Dodanie walidacji formularza rejestracji”, a nie „rejestracja + poprawa logowania + refaktor CSS”.
  • „Refaktor funkcji calculate_total” zamiast „refaktor kilku plików + zmiana tekstów na stronie”.

Takie porcjowanie ma dwie konsekwencje:

  • łatwiej prześledzić, dlaczego coś przestało działać (git blame/log),
  • łatwiej cofnąć tylko jeden fragment zmian, bez ruszania reszty.

git status i git diff jako codzienna kontrola

Zanim cokolwiek zacommitujesz, spójrz, co naprawdę zmieniłeś. Dwie komendy robią tu większość roboty:

git status
git diff

git status pokaże ogólną sytuację:

  • które pliki są zmienione,
  • które czekają już w staging area,
  • które są całkowicie nowe (untracked).

git diff natomiast pokazuje konkretne różnice w kodzie. Krótka pętla:

  1. modyfikujesz plik,
  2. uruchamiasz git diff,
  3. sprawdzasz, czy zmieniłeś tylko to, co planowałeś.

Dzięki temu nie wciągasz do repozytorium przypadkowo wciśniętych spacji czy debugowych logów.

Dodawanie zmian „na raty” – git add -p

Często w jednym pliku ląduje kilka różnych pomysłów: poprawka błędu, mały refaktor, zmiana komentarza. Da się to sensownie podzielić na osobne commity, nawet jeśli wszystko siedzi w jednym pliku. Służy do tego:

git add -p plik.py

Git pokazuje zmiany w „hunkach” (fragmentach) i pyta, co z nimi zrobić:

  • y – dodaj ten fragment do staging area,
  • n – pomiń,
  • s – podziel fragment na mniejsze kawałki, jeśli to możliwe,
  • q – wyjdź z trybu interaktywnego.

Przykład z życia: naprawiasz błąd w funkcji i przy okazji poprawiasz literówki w komentarzach. Możesz:

  1. dodać do jednego commita tylko poprawkę błędu (fragmenty z kodem),
  2. do drugiego – same kosmetyczne zmiany w komentarzach.

Takie drobiazgi robią ogromną różnicę, gdy za pół roku będziesz śledzić historię po konkretnym błędzie.

Dobre wiadomości commitów: konkret, nie poezja

Wiadomość commita to nie jest miejsce na literackie popisy, ale też nie musi być lakonicznym „fix”. Dobrze sprawdza się prosty format:

  • imperatyw w pierwszej linii – „Dodaj…”, „Popraw…”, „Usuń…” jakbyś opisywał, co commit robi,
  • ewentualne rozwinięcie w kolejnych liniach – gdy zmiana jest bardziej złożona.

Przykład:

Popraw walidację adresu e-mail w formularzu rejestracji

Obecnie akceptowane są adresy bez domeny najwyższego poziomu.
Dodaj dodatkowe sprawdzenie w regexie oraz testy jednostkowe
dla kilku problematycznych przykładów.

Jeśli commit ma jasno opisaną intencję, zwykle nawet nie trzeba zaglądać w diff, żeby zrozumieć, o co chodziło.

Poprawianie ostatniego commita: git commit –amend

Częsty przypadek: zacommitowałeś zmiany, po czym zauważyłeś literówkę albo zapomniałeś o jednym pliku. Nie trzeba robić kolejnego commita „oops”. Możesz poprawić ostatni:

# dodajesz brakujący plik
git add brakujacy_plik.py

# nadpisujesz ostatni commit (treść i/lub wiadomość)
git commit --amend

W efekcie powstaje „nowy” ostatni commit, który zawiera i stare, i nowe zmiany. Jedno zastrzeżenie: gdy pracujesz z innymi, nie używaj --amend na commitach, które już wypchnąłeś na zdalne repozytorium, bo komplikujesz historię innym osobom.

Wycofywanie zmian przed commitem

Zanim coś trafi do historii, często chcesz się z czegoś wycofać. Jest kilka scenariuszy.

Cofnięcie zmian w pliku w working directory

# przywrócenie pliku do stanu z ostatniego commita
git restore nazwa_pliku

Usunięcie pliku ze staging area (ale zachowanie zmian)

git restore --staged nazwa_pliku

Efekt: commit będzie „czystszy”, a ty możesz spokojnie przygotować osobny commit na resztę zmian.

Branch jako „plac zabaw”: podstawy pracy na gałęziach

Po co w ogóle branch, skoro można na mainie?

Kto choć raz zepsuł działający projekt zmianą „na produkcji”, ten docenia branche. Typowy obrazek: użytkownicy zgłaszają błąd, ty w panice grzebiesz w kodzie na głównej gałęzi, po drodze wpada ci nowy pomysł na usprawnienie. Po godzinie nie wiesz, co było poprawką, a co eksperymentem.

Branch pozwala wydzielić sobie bezpieczną przestrzeń:

  • main (albo master) – to „stabilna linia”. Tu trafia kod, który ma działać.
  • oddzielne gałęzie dla funkcji, poprawek błędów, eksperymentów – można w nich mieszać ile się chce, bez ryzyka popsucia głównej linii.

Tworzenie i przełączanie się między branchami

Podstawowe operacje na gałęziach są proste:

# lista gałęzi
git branch

# utworzenie nowej gałęzi (bez przełączania)
git branch feature/logowanie

# przełączenie na istniejącą gałąź
git switch feature/logowanie

# skrót: utworzenie i przełączenie za jednym razem
git switch -c feature/logowanie

W starszych materiałach często pojawia się git checkout. Nadal działa, ale dla czytelności lepiej korzystać z git switch i git restore – są bardziej jednoznaczne.

Strategia „gałąź na zadanie”

Przydatny nawyk: każde zadanie/funkcję/buga traktuj jak osobny „temat” i dawaj mu własną gałąź. Przykład nazewnictwa:

  • feature/rejestracja-uzytkownika
  • bugfix/naprawa-paginacji
  • chore/aktualizacja-dependencji

Schemat pracy jest wtedy powtarzalny:

  1. przełączasz się na aktualnego main,
  2. tworzysz gałąź pod konkretne zadanie,
  3. robisz na niej commity,
  4. po zakończeniu łączysz gałąź z powrotem z main (merge lub rebase),
  5. opcjonalnie usuwasz gałąź, jeśli swoje zrobiła.

Sprawdzanie, na czym stoisz: git log i przydatne aliasy

Gdy gałęzi robi się więcej, sam git log bywa mało czytelny. Dlatego na początku dobrym nawykiem jest ustawienie aliasu, który już zostało zasugerowane:

git lg

Jeśli alias jest skonfigurowany jak wcześniej (log --oneline --graph --decorate --all), zobaczysz od razu:

  • rozgałęzienia i łączenia historii,
  • aktualną pozycję HEAD,
  • które commity należą do której gałęzi.

Taki wykres to świetny „radar” – pomaga nie zgubić się w tym, co już zostało połączone, a co jeszcze wisi na bocznej gałęzi.

Praca równoległa: zmiana zadania w trakcie

Częsta sytuacja: pracujesz nad nową funkcją na gałęzi feature/a, ale nagle pojawia się krytyczny bug do naprawy. Zamiast mieszać wszystko na jednej gałęzi:

  1. Zacommituj aktualny stan feature/a (nawet jako „WIP”, jeśli pracujesz sam).
  2. Przełącz się na main: git switch main.
  3. Utwórz gałąź na poprawkę błędu: git switch -c bugfix/krytyczny-bug.
  4. Napraw bug, zacommituj, połącz z main.
  5. Wróć spokojnie do feature/a.

Zadania nie mieszają się w historii, a ty nie musisz pamiętać, które linijki były od czego.

Porządkowanie starych gałęzi

Z czasem w repozytorium pojawia się wiele gałęzi, z których część nie jest już potrzebna. Po połączeniu zmian z main możesz śmiało usuwać gałęzie lokalne:

git branch -d feature/stara-funkcja

Jeśli z jakiegoś powodu chcesz usunąć gałąź, która nie została jeszcze w pełni połączona (świadomie rezygnujesz z tamtych zmian), użyj:

git branch -D feature/porzucony-pomysl

Czysta lista gałęzi to mniej rozpraszaczy i mniejsze ryzyko, że przypadkiem wrócisz do dawno porzuconej wersji funkcji.

Łączenie zmian: merge, rebase i codzienne scenariusze

Merge – łączenie historii „tak jak było”

Wyobraź sobie, że kończysz pracę nad funkcją na gałęzi feature/rejestracja. Na main w tym czasie ktoś dodał poprawkę błędu. Teraz trzeba to wszystko połączyć. Najbardziej bezpośrednia droga to:

git switch main
git merge feature/rejestracja

Git spróbuje automatycznie połączyć zmiany. Jeśli się uda:

  • powstaje commit scalający (merge commit),
  • historia pokazuje dwa ramiona, które schodzą się w jednym punkcie.

Zaleta: zachowujesz pełny „pamiętnik” wydarzeń – widać dokładnie, kiedy rozwidliła się praca i kiedy z powrotem się zeszła.

Konflikty merge – gdy dwie osoby zmieniły to samo

Konflikt to nie katastrofa, tylko sygnał: „Git nie potrafi zdecydować za ciebie”. Dzieje się tak, gdy:

  • w tym samym fragmencie pliku są różne zmiany,
  • Git nie jest w stanie ich automatycznie połączyć.

Przy konflikcie zobaczysz komunikat i zmienioną zawartość plików, np.:

<<<<<<< HEAD
wersja kodu z aktualnej gałęzi
=======
wersja kodu z gałęzi łączonej
>>>>>>> feature/rejestracja

Twoje zadanie:

  1. Otworzyć plik i zdecydować, która wersja (albo jaki kompromis) jest poprawna.
  2. Usunąć znaczniki <<<<<, =======, >>>>>.
  3. Dodać poprawiony plik do staging area: git add plik.
  4. Po rozwiązaniu wszystkich konfliktów zakończyć merge: git commit (czasem Git zrobi to automatycznie po dodaniu plików).

Większość edytorów (VS Code, IntelliJ, itp.) ma wizualne narzędzia do rozwiązywania konfliktów, które pokazują wersje obok siebie i pozwalają klikać „accept current / incoming / both”.

Rebase – „przepisanie” historii na bardziej liniową

Wyobraź sobie, że kolega otwiera historię projektu i widzi pięć gałęzi, każda przeplatana commitami, które w połowie się powtarzają. Niby wszystko działa, ale śledzenie „co z czego wynika” zamienia się w grę w statki. Właśnie w takich momentach przydaje się rebase.

Technicznie rebase to przeniesienie serii commitów na inny „punkt startowy”. Klasyczny scenariusz:

  1. Na bazie main tworzysz feature/rejestracja.
  2. Na obu gałęziach powstają nowe commity.
  3. Przed połączeniem chcesz, żeby twoje commity wylądowały „na końcu” aktualnego main, jakby powstały po nim.

Wtedy:

git switch feature/rejestracja
git rebase main

Git bierze twoje commity z gałęzi funkcjonalnej i odtwarza je jeden po drugim tak, jakby zostały zrobione na najnowszym main. Historia jest ładna, liniowa, bez merge commitów.

Rebase a merge – kiedy co wybrać

Najprościej patrzeć na to jak na wybór między:

  • merge – zachowaj pamiętnik „tak, jak było”,
  • rebase – uporządkuj notatki, jakby były pisane po kolei.

Kilka praktycznych zasad:

  • Na gałęziach prywatnych (nad którymi pracujesz sam) rebase jest wygodny – czyści historię, zanim ją pokażesz innym.
  • Dla głównej gałęzi zespołu częściej zostawia się merge, bo pokazuje rzeczywisty przebieg prac.
  • Rebase jest przyjemny do „aktualizacji” swojej gałęzi przed wysłaniem MR/PR:
    git switch feature/rejestracja
    git fetch origin
    git rebase origin/main
    

Drobny nawyk: przed rebase zrób git status i upewnij się, że nie masz nic niezacommitowanego. Rebase przestawia commity; zmiany „na brudno” tylko przeszkadzają.

Rozwiązywanie konfliktów podczas rebase

Przy dłuższych gałęziach rebase rzadko przechodzi całkiem „na zielono”. Scenariusz często wygląda tak: w połowie rebase pojawia się konflikt, Git zatrzymuje się i każe ci go ręcznie wyczyścić.

Procedura jest podobna jak przy konflikcie merge:

  1. Otwierasz pliki z konfliktami, widzisz znaczniki <<<<< itd.
  2. Wybierasz właściwą wersję lub łączysz obie.
  3. Dodajesz pliki: git add plik.

Różnica jest w ostatnim kroku – zamiast git commit robisz:

git rebase --continue

Jeśli po drodze uznasz, że jednak to za dużo roboty lub coś poszło nie tak, możesz wycofać całe rebazowanie:

git rebase --abort

Wniosek po kilku razach jest zwykle podobny: im drobniejsze i częstsze rebase’y, tym mniej bolesne konflikty. Lepiej robić to co dzień niż raz na dwa tygodnie.

Rebase interaktywny: sprzątanie w commitach

Wyobraź sobie gałąź z serią commitów: „fix”, „poprawka”, „znowu fix” i jednym „dodaj formularz rejestracji”. Kod jest OK, ale historia wygląda jak notatnik z wieczornego debugowania. Przed połączeniem z main możesz ją uporządkować rebase’em interaktywnym.

git switch feature/rejestracja
git rebase -i main

Otworzy się edytor z listą commitów:

pick a1b2c3d dodaj formularz rejestracji
pick d4e5f6g popraw literówkę w walidacji
pick h7i8j9k popraw testy dla rejestracji

Zamiast pick możesz wpisać:

  • reword – zmiana opisu commita,
  • squash (lub s) – połączenie commita z poprzednim,
  • drop – wyrzucenie commita z historii gałęzi.

Przykład – łączenie kilku technicznych poprawek w jeden commit:

pick a1b2c3d dodaj formularz rejestracji
squash d4e5f6g popraw literówkę w walidacji
squash h7i8j9k popraw testy dla rejestracji

Po zapisaniu Git odtworzy historię z Twoimi poprawkami. Efekt końcowy: jeden sensowny commit zamiast trzech „szumu”. Przy przeglądach kodu reviewer widzi czystą intencję, a nie trasę twojej wewnętrznej walki z testami.

Rebase na zdalnym repozytorium – kiedy robi się niebezpiecznie

Najczęstsza mina: rebase na gałęzi, która już poszła do wspólnego repozytorium i z której ktoś inny też korzysta. Rebase przepisuje historię, więc:

  • commity dostają nowe identyfikatory,
  • inni mają „starą” historię, ty „nową” – repozytoria się rozjeżdżają.

Bezpieczna zasada brzmi: nie rebasuj publicznej historii. Jeśli gałąź była tylko twoja i masz pewność, że nikt inny jej nie używał – nic złego się nie stanie. W każdym innym przypadku lepiej użyć merge.

Codzienne scenariusze łączenia zmian w małym zespole

W małych projektach często działa prosty, powtarzalny schemat. Przykład:

  1. Każde zadanie na osobnej gałęzi feature/*.
  2. Przed wypchnięciem: git fetch origin && git rebase origin/main.
  3. Po akceptacji PR: merge do main (czasem squash-merge w narzędziu typu GitHub/GitLab).

W takim układzie:

  • lokalnie masz ładne, liniowe historie gałęzi,
  • na main pojawia się po jednym, sensownym commitcie na zadanie,
  • konflikty rozwiązujesz „u siebie”, zanim w ogóle ktoś zacznie przeglądać zmiany.

Techniczny detal: jeśli po rebase Twojej gałęzi chcesz ją wypchnąć na zdalne repozytorium, zwykłe git push często zostanie odrzucone (bo historia się zmieniła). Wtedy:

git push --force-with-lease

--force-with-lease to bezpieczniejsza wersja --force – nadpisze zdalną gałąź tylko wtedy, gdy po drodze nikt inny jej nie modyfikował.

Cherry-pick – wyciąganie pojedynczych commitów

Typowa sytuacja z produkcji: na gałęzi z dużą funkcją robisz commit, który przy okazji naprawia stary błąd. Ten fix przydałby się już teraz na main, ale cała reszta zmian jeszcze nie jest gotowa. Nie trzeba kopiować kodu ręcznie – tu wchodzi git cherry-pick.

Najpierw sprawdzasz identyfikator commita (np. abc1234), potem:

git switch main
git cherry-pick abc1234

Git weźmie tylko ten jeden commit i zastosuje jego zmiany na aktualnej gałęzi. Jeśli wszystko pójdzie gładko, dostajesz nowy commit z tym samym opisem, ale w historii main.

Cherry-pick bywa też ratunkiem, gdy przez przypadek coś zacommitujesz na złej gałęzi – można wtedy:

  1. zrobić nową gałąź z poprawnej bazy,
  2. przenieść na nią konkretne commity cherry-pickiem,
  3. opcjonalnie wycofać je z pierwotnej gałęzi.

Git stash – odkładanie bałaganu na bok

Drugi klasyk: edytujesz trzy pliki w ramach nowej funkcji, nagle dzwoni product owner z prośbą o szybki hotfix na main. Zmiany są rozgrzebane, za wcześnie na commit. W takim momencie przydaje się git stash – tymczasowy „schowek”.

Najprostsze użycie:

# odkładamy aktualne, niezatwierdzone zmiany
git stash push -m "praca nad filtrem wyników"

# working directory wraca do czystego stanu z ostatniego commita

Teraz możesz spokojnie:

  1. przełączyć się na main,
  2. zrobić hotfix i go zacommitować,
  3. wrócić na swoją gałąź i przywrócić schowane zmiany.

Przywracanie:

# lista wszystkich schowków
git stash list

# przywrócenie najnowszego (i usunięcie z listy)
git stash pop

# przywrócenie bez usuwania z listy (można użyć kilka razy)
git stash apply

stash radzi sobie także z nowymi plikami (jeśli dodasz -u):

git stash push -u -m "WIP: nowy raport"

Po kilku użyciach wychodzi prosty morał: stash to świetne narzędzie awaryjne, ale nie zamiennik commitów. Zbyt wiele schowków szybko robi się nieczytelne.

Reset i revert – dwa sposoby na „ojej, to nie miało tak wyglądać”

Prędzej czy później każdy zrobi commit, którego żałuje. Jedni reagują „usunę folder i sklonuję repo na nowo”, inni uczą się dwóch komend: git reset i git revert. Obie służą do cofania skutków zmian, ale działają inaczej.

Git reset – cofnięcie wskaźnika historii

reset przesuwa gałąź do wcześniejszego commita. Przykład:

# cofnięcie gałęzi do konkretnego commita (zachowanie zmian w working directory)
git reset --soft abc1234

# cofnięcie i wyczyszczenie staging area (working directory zostaje)
git reset --mixed abc1234   # domyślna opcja

# cofnięcie, staging i working directory wracają do stanu z tamtego commita
git reset --hard abc1234

--hard potrafi bezpowrotnie wyrzucić zmiany, jeśli nie ma ich nigdzie indziej. To narzędzie do „lokalnego sprzątania”, a nie do poprawiania publicznej historii, która już poszła na serwer.

Git revert – odwrócenie skutków bez przepisywania historii

revert tworzy nowy commit, który odwraca zmiany wybranego commita. Historia pozostaje liniowa, bez kasowania czegokolwiek.

# odwrócenie ostatniego commita
git revert HEAD

# odwrócenie konkretnego commita
git revert abc1234

To bezpieczna opcja w projektach zespołowych. Widać, że commit istniał, ale też jasno widać, że został wycofany i czym.

Mały rytuał przed pchnięciem zmian: checklist „nie pogubić się”

Po kilku kryzysach z przypadkowym pushowaniem niegotowych commitów wiele osób wyrabia sobie mini-rytuał przed git push. Może wyglądać tak:

  1. git status – czy na pewno jestem na dobrej gałęzi?
  2. git diff – czy nie ma śmieciowych zmian (np. logi, tymczasowe pliki)?
  3. git log --oneline -5 lub alias git lg – czy opisy commitów mają sens?
  4. Opcjonalnie: git rebase -i main na gałęzi funkcjonalnej – kosmetyka historii.

To kilka dodatkowych sekund, ale często ratuje przed wypchnięciem niedokończonej funkcji na produkcyjny branch. Po czasie wchodzi w krew i prawie nie zajmuje uwagi.

Najczęściej zadawane pytania (FAQ)

Po co mi Git, jeśli pracuję sam nad małym projektem?

Scenka jest prosta: piszesz małą aplikację na uczelnię, coś się psuje, więc kopiujesz cały folder jako „backup”. Po kilku dniach masz pięć wersji projektu i żadnego pojęcia, która była „tą dobrą”.

Git rozwiązuje to jednym repozytorium z historią. Każdy commit to konkretna migawka kodu z opisem, co zmieniłeś i kiedy. Możesz bez stresu eksperymentować w osobnym branchu, a gdy pomysł nie wypali – usuwasz gałąź i wracasz do stabilnej wersji bez polowania na foldery „_final_v3_ostateczne”.

Czym Git różni się od Dropboxa, Google Drive czy OneDrive?

Wielu początkujących zaczyna od wrzucania projektu do chmury i liczy, że to „załatwi wersje”. Po chwili okazuje się jednak, że trudno powiedzieć, która wersja pliku wprowadziła błąd i dlaczego coś nagle przestało działać.

Dropbox czy Google Drive przechowują pliki i czasem kilka ostatnich wersji, ale nie wiedzą nic o commitach, branchach ani o tym, które zmiany należą do jednego „kroku” w rozwoju projektu. Git natomiast śledzi historię na poziomie commitów, pozwala rozgałęziać rozwój (branche), łączyć różne linie zmian i pracować całkowicie offline – chmura jest co najwyżej dodatkiem, a nie sednem.

Co to jest repozytorium lokalne i zdalne w Git i jak ze sobą współpracują?

Wyobraź sobie, że piszesz projekt na laptopie w kawiarni, a współpracownik przegląda kod w domu. Ty chcesz pracować offline, on chce zobaczyć najnowsze zmiany bez proszenia o zipa na maila. Tu właśnie wchodzą dwa poziomy repozytorium.

Repozytorium lokalne to katalog na twoim komputerze z ukrytym folderem .git – tam tworzysz commity, branche, mergujesz i możesz to robić bez internetu. Repozytorium zdalne (np. na GitHubie) jest wspólnym punktem odniesienia: ty wysyłasz tam swoje commity (git push), inni pobierają je do siebie (git pull). Cała „prawdziwa praca” odbywa się lokalnie, a serwer to tylko miejsce synchronizacji i współpracy.

Co to jest commit w Git i kiedy powinienem go robić?

Typowy błąd początkujących to pracować godzinami, zmieniać pół projektu i na koniec robić jednego ogromnego commita „poprawki”. Gdy coś się zepsuje, trudno dojść, która poprawka była winna.

Commit to zapis konkretnego stanu projektu z krótkim opisem: co i dlaczego zmieniłeś. Najzdrowiej jest commitować małe, logiczne kroki: „dodano walidację formularza”, „naprawiono błąd w logowaniu”, „dodano testy do modułu X”. Dzięki temu łatwo cofnąć pojedynczą zmianę, znaleźć commit, który wprowadził błąd, i prześledzić historię rozwoju funkcji.

Czym jest branch (gałąź) w Git i jak z niego korzystać jako początkujący?

Wyobraź sobie, że masz działającą aplikację na branchu main, a chcesz dodać duży feature, który może wszystko popsuć. Jeśli dłubiesz bezpośrednio na main, ryzykujesz, że przez kilka dni kod będzie w stanie „pół na pół” – ani nowy, ani stabilny.

Branch to osobna linia historii commitów. Typowy schemat wygląda tak: trzymasz stabilny kod na main, a dla nowej funkcji tworzysz gałąź, np. feature/logowanie. Tam commitujesz wszystkie zmiany, testujesz, poprawiasz, a gdy wszystko działa – łączysz branch z main (merge). Jeśli pomysł się nie sprawdzi, zamykasz gałąź i główny kod pozostaje nietknięty.

Co to jest staging area i po co używać git add przed commit?

Często bywa tak, że w jednym podejściu poprawiasz kilka rzeczy naraz: trochę CSS, drobną literówkę w README i poważny błąd w logice. Niekoniecznie chcesz wrzucać to wszystko do jednego commita.

Staging area (index) to „poczekalnia” zmian przed commitem. Najpierw modyfikujesz pliki w working directory, potem za pomocą git add wybierasz, co ma wejść do konkretnego commita. Dzięki temu możesz spokojnie oddzielić zmiany: jeden commit z poprawką błędu, drugi z poprawioną dokumentacją. Efekt jest prosty: czytelniejsza historia i łatwiejsze cofanie konkretnych modyfikacji.

Czy do pracy z Git potrzebuję internetu i konta na GitHubie?

Wielu początkujących instaluje Git i od razu zakłada, że „bez GitHuba się nie da”. Kończy się na tym, że jak padnie Wi-Fi, praca staje w miejscu, bo „przecież trzeba pushować”.

Git działa w pełni lokalnie: możesz tworzyć repozytorium, robić commity, branche, mergować i cofać zmiany bez jakiegokolwiek połączenia z siecią. Konto na GitHubie, GitLabie czy Bitbuckecie przydaje się do backupu i współpracy w zespole, ale jest dodatkiem. Najpierw ogarnij lokalne repozytorium, a dopiero potem dokładnij zdalne jako warstwę do dzielenia się kodem i przeglądów zmian.

Najważniejsze wnioski

  • Zamiast dziesięciu folderów typu „projekt_final_v3_ostateczne”, jedno repozytorium Git trzyma pełną historię zmian, dzięki czemu można w kilka sekund wrócić do dowolnej wersji plików.
  • Git to system kontroli wersji, a nie „dysk w chmurze” – śledzi historię, rozgałęzienia i łączenie zmian, działa lokalnie i dopiero potem synchronizuje się z serwerem (GitHub, GitLab itp.).
  • Oddzielne branche pozwalają bezpiecznie eksperymentować: rozwijasz funkcję poza główną gałęzią, testujesz, a w razie porażki po prostu usuwasz branch, nie psując działającego kodu.
  • Historia commitów to realne narzędzie debugowania – można sprawdzić, który commit wprowadził błąd, prześledzić jego zawartość i szybko przywrócić wcześniejszy, działający stan projektu.
  • Lokalne repozytorium służy do codziennej pracy (commity, branche, merge), a zdalne jest wspólnym punktem odniesienia dla zespołu i dodatkowym zabezpieczeniem, do którego okresowo wysyła się zmiany.
  • Git nie „robi porządku” sam z siebie – porządek zapewniają dopiero nawyki: sensowne, częste commity, osobne branche na funkcje oraz czytelne komunikaty opisujące, co faktycznie zostało zmienione.
  • Szybka konfiguracja (user.name, user.email, ulubiony edytor, aliasy na komendy) oszczędza czas i nerwy, bo każdy commit jest podpisany, a powtarzalne operacje da się wykonać kilkoma skrótami.