Git dla początkujących programistów: commit, branch i pull request w praktyce

0
53
1/5 - (1 vote)

Nawigacja:

Po co początkującemu programiście Git i czym różni się od „trzymania plików w folderach”

System kontroli wersji zamiast folderów „projekt_final_ostateczny”

Początkujący programista zwykle zaczyna od prostego podejścia: zapisuje kod w jednym katalogu i co jakiś czas kopiuje cały folder jako kopię bezpieczeństwa. W efekcie na dysku pojawia się ciąg nazw typu: projekt_v1, projekt_v2_ostatni, projekt_final_ostateczny. Działa to tylko do momentu, gdy projekt jest mały i pracuje nad nim jedna osoba, a i tak szybko prowadzi do chaosu.

Git rozwiązuje ten problem u źródła. Jest systemem kontroli wersji, czyli narzędziem, które śledzi historię zmian w plikach tekstowych (najczęściej w kodzie źródłowym), pozwala wrócić do dowolnego punktu w czasie, porównać zmiany oraz pracować równolegle kilku osobom na tym samym kodzie. Zamiast kopiować całe katalogi, zapisujesz w historii tylko różnice, które wprowadzasz. Każda taka porcja zmian to commit.

Z punktu widzenia początkującego najistotniejsze jest to, że Git umożliwia:

  • zapamiętanie konkretnego stanu projektu (np. „działająca wersja przed dużą refaktoryzacją”),
  • wycofanie się z nieudanych eksperymentów bez paniki i kasowania plików na oślep,
  • pracę na kilku równoległych wersjach tego samego projektu dzięki branchom,
  • współpracę z innymi przez serwisy takie jak GitHub czy GitLab i pull requesty.

Dlaczego Git wygrywa z „ręcznymi” kopiami katalogów

Podejście folderowe ma kilka krytycznych wad. Po pierwsze, nie masz spójnej historii. Nie wiesz, w którym katalogu dokładnie naprawiłeś dany błąd i jaka była różnica między „v2” a „v3”. Po drugie, kopiując katalogi powielasz pliki binarne, zależności, buildy – każdy backup zajmuje coraz więcej miejsca. Po trzecie, jeśli pracuje kilka osób, bardzo szybko dochodzi do sytuacji „wysłałem ci wersję final2.zip, ale ty masz final2_poprawki.zip”.

Git podchodzi do tego inaczej:

  • historia zmian jest liniowa (albo drzewiasta, ale spójna) i można ją przeglądać komendą git log,
  • każda zmiana jest podpisana autorem, datą, opisem i unikalnym identyfikatorem,
  • kopie projektu na różnych komputerach mogą się synchronizować przez remote (np. GitHub),
  • nie trzeba wysyłać całych katalogów mailem czy przez komunikator.

W praktyce Git pozwala zapisać to, co w podejściu „folderowym” bywa niemożliwe: precyzyjną odpowiedź na pytanie „kto, kiedy i po co zmienił ten fragment kodu”. Dla programisty, który pracuje z kodem zawodowo, to absolutna podstawa.

Git jako standard branżowy, ale nie zawsze konieczny

W komercyjnych projektach Git jest de facto standardem. Firmy używają GitHuba, GitLaba lub Bitbucketa do hostowania kodu, recenzowania zmian (pull requesty / merge requesty) i automatycznego testowania (CI/CD). Bez znajomości podstaw Gita trudno w ogóle wejść do istniejącego projektu i coś w nim bezpiecznie zmienić. Tak samo w projektach open source – praktycznie każde repozytorium na GitHubie wymaga forka, własnego brancha i pull requesta.

Git sprawdza się również w nauce programowania. Pozwala utrzymać porządek w ćwiczeniach, porównywać własny kod z wcześniejszymi wersjami, wracać do zadań po kilku miesiącach i rozumieć swój sposób myślenia sprzed czasu. Nie chodzi wyłącznie o „ładne historie”, ale o realną możliwość analizy procesu nauki i błędów.

Jest jednak kilka sytuacji, gdzie Git bywa przesadą. Proste skrypty jednorazowe, które powstają w ciągu pięciu minut i nie będą rozwijane – tu system kontroli wersji niewiele wniesie. Podobnie w notatkach prywatnych czy małych „pchełkach” do terminala. Zwykle jednak takie projekty z czasem rosną. Dlatego wiele osób i tak trzyma nawet drobne narzędzia w jednym repozytorium Git (np. dotfiles lub zbiór skryptów), bo później okazuje się to zaskakująco wygodne.

Okulary na tle monitora z odbitym kodem programistycznym
Źródło: Pexels | Autor: Kevin Ku

Podstawowe pojęcia Git bez żargonu: repozytorium, commit, branch, remote

Repozytorium Git – co naprawdę się w nim kryje

Repozytorium Git to nie „magiczny folder w chmurze”, ale zwykły katalog na dysku, w którym znajduje się podkatalog .git. To właśnie ten ukryty katalog przechowuje całą historię, konfigurację oraz metadane projektu. Twoje pliki robocze (kod, dokumentacja, konfiguracja) leżą obok, tak jak w każdym innym folderze.

Wyróżnia się dwa podstawowe typy repozytorium:

  • lokalne repozytorium – to, co masz na własnym komputerze po git init lub git clone,
  • zdalne repozytorium – najczęściej na GitHubie, GitLabie czy Bitbuckecie, do którego możesz robić push i z którego możesz pobierać zmiany (pull/fetch).

Git jest rozproszony – to ważna cecha odróżniająca go od starszych systemów (jak Subversion). Każde lokalne repozytorium zawiera pełną historię projektu, a nie tylko bieżący stan. Oznacza to, że możesz przeglądać historię, porównywać commity, tworzyć branche i robić większość operacji bez internetu. Dopiero synchronizacja ze zdalnym serwerem wymaga połączenia sieciowego.

Commit – migawka zmian z opisem

Commit to podstawowa jednostka historii w Git. Najprościej traktować go jako migawkę stanu projektu wraz z informacją:

  • jakie pliki i ich fragmenty się zmieniły,
  • kto je zmienił (na podstawie user.name i user.email),
  • kiedy zmiana nastąpiła,
  • dlaczego została wprowadzona (wiadomość commita).

Commit ma unikalny identyfikator (tzw. SHA-1, zwykle skracany do kilku pierwszych znaków, np. a1b2c3d). Ten identyfikator pozwala odwołać się do konkretnej wersji projektu: przejrzeć ją, porównać z inną, a nawet odtworzyć pełny stan plików z tego momentu. Zazwyczaj commit wskazuje też na swojego „rodzica” – poprzedni commit, dzięki czemu powstaje łańcuch historii.

Dla praktyka ważna jest przede wszystkim wiadomość commita. Bez niej historia staje się śmietnikiem typu „update”, „poprawki”, „fix”. Taka historia nie pomaga ani autorowi, ani współpracownikom. Zwięzły, ale jasny opis, np. Fix login form validation for empty password, zaoszczędzi później wiele czasu.

Branch – równoległa linia rozwoju projektu

Branch (gałąź) to nic innego jak nazwana linia historii. W Git nie istnieją kopie projektu w osobnych folderach dla każdego feature’a; zamiast tego bazuje się na wskaźnikach do konkretnych commitów. Branch wskazuje na „aktualny” commit w danej linii rozwoju. Gdy tworzysz nowy commit na tym branchu, wskaźnik przesuwa się do przodu.

Przykładowo:

  • masz gałąź main, która reprezentuje stabilną wersję projektu,
  • tworzysz branch feature/login od aktualnego commita z main,
  • dodajesz kilka commitów na feature/login, testujesz, poprawiasz,
  • po akceptacji łączysz (merge) feature/login z main,
  • branch feature/login można później usunąć – jego commitów i tak nie tracisz, są w historii.

Wizualnie można myśleć o branchach jak o równoległych ścieżkach od wspólnego punktu startowego. To podejście pozwala rozdzielić prace nad różnymi zadaniami, nie psując stabilnej wersji, z której korzystają inni.

Remote i origin – co łączy lokalne repozytorium z GitHubem

Remote to po prostu zdalne repozytorium pod określonym adresem (URL). To może być GitHub, GitLab, Bitbucket, ale także serwer w firmowej sieci. Jedno lokalne repozytorium może mieć wiele remote’ów (np. origin, upstream), choć większość projektów na początku ma jeden – domyślny.

Nazwa origin jest konwencją, nie magią. Gdy klonujesz repozytorium z GitHuba komendą git clone, Git automatycznie dodaje remote o nazwie origin, wskazujący na źródłowe repozytorium. Później możesz:

  • wysłać swoje commity: git push origin main,
  • pobrać nowe commity z serwera: git fetch origin lub git pull origin main,
  • sprawdzić skonfigurowane remote’y: git remote -v.

W projektach open source dochodzi często drugi remote – upstream. Twoje forki na GitHubie mają origin jako twoje własne repozytorium, a upstream to oryginalne źródło projektu, z którego chcesz regularnie pobierać nowe zmiany.

Pułapki uproszczeń: „commit to plik”, „branch to folder”

Na początku łatwo wpaść w zbyt proste analogie:

  • „Commit to taki plik zawierający zmiany” – nie do końca, commit to struktura danych ze stanem repozytorium, odnośnikami i metadanymi.
  • „Branch to folder z osobną kopią projektu” – nie, branch to wskaźnik na commit; fizyczna zawartość roboczego katalogu zmienia się tylko przy przełączaniu brancha.

Jako szybki mentalny model takie porównania są do przełknięcia, ale w praktyce zaczynają przeszkadzać przy operacjach typu merge, rebase czy analizie historii. Dla początkującego lepsze jest myślenie: commit = „stan projektu + opis dlaczego”, branch = „nazwa dla konkretnej ścieżki commitów”. Gdy pojawi się rozumienie tych dwóch pojęć, reszta funkcji Gita staje się znacznie bardziej przewidywalna.

Pierwsze repozytorium: konfiguracja Git i inicjalizacja projektu krok po kroku

Instalacja Git i pierwszy test

Zanim pojawi się pierwszy commit, Git musi zostać poprawnie zainstalowany. Na popularnych systemach odbywa się to zwykle przez:

  • Windows – instalator ze strony git-scm.com,
  • macOS – brew install git lub Xcode Command Line Tools,
  • Linux – menedżer pakietów, np. sudo apt install git czy sudo dnf install git.

Po instalacji sensownie jest od razu sprawdzić, czy Git działa i jaką ma wersję:

git --version

Jeśli widzisz numer wersji, narzędzie jest dostępne w PATH i można przejść dalej. Brak odpowiedzi lub komunikat o nieznanej komendzie oznacza problem z instalacją lub konfiguracją środowiska.

Podstawowa konfiguracja globalna

Git zapisuje część ustawień globalnie (dla całego użytkownika), a część lokalnie (dla konkretnego repozytorium). Na start konieczne jest ustawienie nazwy użytkownika i adresu e-mail, które będą trafiały do każdego commita:

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

Te dane nie muszą odpowiadać loginowi systemu operacyjnego, ale w środowisku zawodowym zwykle powinny odpowiadać rzeczywistemu imieniu i adresowi firmowemu. W projektach open source część osób używa prywatnych e-maili, inni generują adresy prywatne przez GitHub.

Przydatne jest też ustawienie domyślnego edytora, który Git otworzy np. przy pisaniu wiadomości commita bez parametru -m:

git config --global core.editor "nano"     # przykład dla terminala
git config --global core.editor "code --wait"  # VS Code

Aktualną konfigurację można sprawdzić komendą:

git config --list

Na tym etapie pojawia się pierwsza pułapka: część osób używa jednego zestawu danych na laptopie prywatnym, a innego na komputerze służbowym i miesza te konfiguracje. Potem w historii firmowego repozytorium pojawiają się commity z prywatnym e-mailem. Dobrze jest od razu rozdzielić te światy.

git init vs git clone – dwa sposoby na start projektu

Istnieją dwie podstawowe drogi, by mieć lokalne repozytorium:

  • git init – tworzy nowe, puste repozytorium w bieżącym katalogu,
  • git clone – pobiera istniejące repozytorium z serwera zdalnego.

Przykład nowego projektu od zera:

Tworzenie nowego repozytorium lokalnie

Scenariusz: masz katalog z kodem lub plikami projektu i chcesz zacząć śledzić historię w Git. Idziesz do katalogu projektu i uruchamiasz:

cd /sciezka/do/projektu
git init

W katalogu pojawi się ukryty folder .git. To tam Git trzyma całą historię, referencje do branchy i konfigurację lokalną repozytorium. Samo git init nie zmienia Twoich plików – jedynie dodaje infrastrukturę do śledzenia zmian.

Przy nowym repozytorium, które kiedyś trafi na GitHuba, sensownie jest od razu nazwać główny branch tak, jak używa to serwer. Coraz częściej jest to main, a nie master. Od niedawna wiele wersji Git robi to automatycznie, ale nie wszystkie środowiska są aktualne. Jeśli chcesz mieć pewność, ustaw domyślną nazwę globalnie:

git config --global init.defaultBranch main

Dopiero potem:

git init

Gdy repozytorium już istnieje, zmiana nazwy głównej gałęzi jest możliwa, ale wymaga dodatkowych kroków na lokalnym i zdalnym repozytorium, więc mniej roboty jest na starcie.

Klonowanie istniejącego projektu

Drugi typowy przypadek: projekt już jest na GitHubie, a Ty chcesz zacząć nad nim pracę. Zamiast ściągać ZIP-a, używasz:

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

Ta komenda:

  • tworzy katalog projekt (chyba że podasz własną nazwę jako drugi argument),
  • ściąga całą historię z serwera,
  • ustawia origin na wskazany adres,
  • przełącza Cię automatycznie na domyślny branch (zwykle main).

Pułapka dla początkujących: klonowanie czyjegoś repozytorium firmowego bez uprawnień do zapisu, a potem zdziwienie, że git push zwraca błąd. Klonując repo, zawsze sprawdzaj, czy masz prawo pisać na ten adres; jeśli nie – w publicznych projektach zrób forka, w prywatnych poproś o dostęp.

Sprawdzenie statusu nowego repozytorium

Po git init lub git clone pierwszym odruchem powinno być:

git status

Typowe sytuacje:

  • świeże git init – brak śledzonych plików, wszystko w stanie „untracked”,
  • świeże git clone – czysty stan, „nothing to commit, working tree clean”.

W praktyce git status działa jak kontrolka: jeśli przestajesz rozumieć, co się dzieje w repozytorium, uruchom je i przeczytaj komunikaty bardzo dokładnie, a nie „na pamięć”.

Dłoń programisty trzymająca naklejkę z logo Git
Źródło: Pexels | Autor: RealToughCandy.com

Od zmian w plikach do commita: staging, status i pierwszy zapis historii

Working directory, staging area i commit – trzy poziomy stanu

Git nie przenosi automatycznie każdej zmiany w plikach do historii. Między edycją a commitem są trzy warstwy:

  • working directory – to, co widzisz w katalogu projektu, bieżące pliki,
  • staging area (indeks) – lista zmian, które mają trafić do kolejnego commita,
  • commit – stan już zapisany w historii, z identyfikatorem SHA i opisem.

Mechanizm stagingu bywa na początku irytujący, bo wydaje się „zbędnym krokiem”. W praktyce pozwala np. podzielić duży zestaw zmian na kilka logicznych commitów, mimo że wszystkie edytowałeś w tym samym czasie.

Dodawanie plików do indeksu: git add

Pierwszy krok przed commitem to wskazanie, które pliki (lub fragmenty) mają trafić do historii:

# dodanie pojedynczego pliku
git add index.html

# dodanie wielu plików
git add index.html styles.css

# dodanie wszystkich nowych i zmodyfikowanych plików w katalogu
git add .

git add . jest wygodne, ale łatwo tam wrzucić coś, co nie powinno trafić do repozytorium (np. pliki z hasłami, duże logi, pliki IDE). Dlatego zanim przyzwyczaisz się do tej komendy, dobrze jest częściej używać git status i dodawać pliki bardziej świadomie:

git status
git add plik_ktory_chcesz_zacommitowac.js

Pierwszy commit: wiadomość, która ma sens

Gdy indeks zawiera to, co ma być zapisane, możesz wykonać commit:

git commit -m "Inicjalna struktura projektu: index.html i styles.css"

Po tym kroku:

  • working directory i staging area są zsynchronizowane z najnowszym commitem,
  • git status pokaże „nothing to commit, working tree clean”.

Regułą, która rzadko się mści, jest zasada: commit opisuje co i po co, nie tylko „co”. Wiadomość Dodanie walidacji formularza rejestracji mówi niewiele. Lepsze będzie np. Add client-side validation for registration form (email pattern, password length). Nie trzeba pisać eseju, wystarczy rzeczowy skrót.

Poprawianie ostatniego commita: git commit –amend

Typowy błąd: zrobiony commit, po chwili refleksja, że:

  • wiadomość jest kiepska lub ma literówki,
  • zapomniałeś dodać jeden plik.

Dopóki commit nie został wypchnięty na zdalne repozytorium, możesz go poprawić:

# dodaj zapomniany plik do indeksu
git add brakujacy_plik.js

# popraw ostatni commit (łącząc stary stan z nowymi zmianami)
git commit --amend

Jeśli nie podasz -m, Git otworzy edytor z obecną wiadomością, którą możesz zmodyfikować. Po zapisaniu i wyjściu powstaje nowy commit z nowym SHA, który zastępuje poprzedni w głównej linii historii.

Po git push taka zabawa --amend przestaje być bezbolesna, bo zmieniasz historię, którą ktoś mógł już pobrać. W zespołach zazwyczaj przyjmuje się prostą zasadę: nie przepisuj publicznej historii, chyba że wszyscy są świadomi konsekwencji.

Przegląd zmian przed commitem: git diff

Zanim zapiszesz commit, dobrym nawykiem jest sprawdzenie, co faktycznie zmieniasz:

# różnice między working directory a indeksem
git diff

# różnice między indeksem a ostatnim commitem (to, co wejdzie w commit)
git diff --cached

Jeśli git diff pokazuje dużo przypadkowych zmian (np. wcięcia, formatowanie, pliki generowane automatycznie), to często sygnał, że warto:

  • ustalić wspólny format (np. Prettier, ESLint, black) i stosować go automatycznie,
  • wyrzucić z indeksu pliki generowane lub konfiguracyjne IDE.

Ignorowanie niepotrzebnych plików: .gitignore bez magii

Po co w ogóle .gitignore

Bez pliku .gitignore Git będzie traktował każdy plik w katalogu jako potencjalnego kandydata do śledzenia. W efekcie w historii pojawiają się:

  • ogromne katalogi z zależnościami (node_modules, vendor),
  • pliki tymczasowe edytora (.idea/, .vscode/, *.swp),
  • artefakty builda (dist/, build/, *.class),
  • logi i pliki z danymi testowymi (*.log, bazy SQLite itp.).

Taka historia rośnie szybko, spowalnia operacje i utrudnia przeglądanie zmian. .gitignore pozwala wstępnie odsiać to, co nie jest źródłem prawdy w projekcie.

Podstawowa składnia .gitignore

.gitignore to zwykły plik tekstowy w katalogu projektu. Każda linia opisuje regułę:

# ignoruj konkretny plik
sekrety.env

# ignoruj wszystkie pliki .log
*.log

# ignoruj katalog i jego zawartość
node_modules/
dist/

# ignoruj pliki tymczasowe edytora
*.swp
*.tmp

Kilka praktycznych zasad:

  • # na początku linii oznacza komentarz,
  • katalog kończy się ukośnikiem (dist/),
  • * zastępuje dowolny ciąg znaków,
  • ? zastępuje pojedynczy znak,
  • ! na początku reguły wyłącza ignorowanie (np. ignorujesz wszystko w katalogu, ale jeden plik chcesz śledzić).

.gitignore nie działa wstecz

Częsty mit: „dopisałem katalog do .gitignore, więc Git przestanie śledzić te pliki”. Niestety, nie. .gitignore dotyczy nowych plików. Jeśli coś już zostało dodane do repozytorium, trzeba to usunąć ze śledzenia:

# dodaj katalog do .gitignore, np.
echo "node_modules/" >> .gitignore

# usuń katalog ze śledzenia, ale nie kasuj z dysku
git rm -r --cached node_modules/

# zapisz commit z aktualizacją
git commit -m "Stop tracking node_modules and update .gitignore"

Po tym kroku Git nie będzie proponował tego katalogu do kolejnych commitów (o ile reguła w .gitignore jest poprawna).

Gotowe szablony .gitignore

Zamiast wymyślać reguły od zera, można skorzystać z gotowych szablonów, zwłaszcza dla popularnych ekosystemów:

  • GitHub utrzymuje repozytorium github/gitignore z plikami dla Node.js, Python, Java, .NET itd.,
  • tworząc nowe repozytorium na GitHubie w interfejsie WWW, można wybrać szablon .gitignore dopasowany do technologii.

Szablony są użyteczne, ale nie bezkrytycznie. Zdarza się, że ignorują pliki, które w konkretnym projekcie warto jednak śledzić (np. niektóre pliki konfiguracyjne). Zawsze trzeba przejrzeć wybrany szablon i dostosować go do własnych potrzeb.

Zbliżenie ekranu z kodem PHP podczas pracy programisty nad repozytorium
Źródło: Pexels | Autor: Pixabay

Branch w praktyce: po co gałęzie i jak ich używać bez teorii grafów

Tworzenie i przełączanie branchy

Standardowa kolejność w pracy nad nową funkcją wygląda tak:

# sprawdź, na jakim branchu jesteś
git branch

# utwórz nowy branch
git branch feature/login-form

# przełącz się na nowy branch
git switch feature/login-form
# lub starsza składnia:
# git checkout feature/login-form

Syntetyczna forma, często używana w praktyce:

git switch -c feature/login-form
# albo:
# git checkout -b feature/login-form

Ta komenda tworzy nową gałąź i od razu na nią przełącza. Nowy branch startuje od commita, na którym byłeś w momencie uruchomienia polecenia (zwykle od main).

Izolowanie prac: kiedy nowy branch ma sens

Nowy branch opłaca się za każdym razem, gdy zmiana:

  • nie jest banalną poprawką literówki w dokumentacji,
  • może wymagać kilku iteracji i poprawek,
  • może wywrócić coś istotnego w istniejącym kodzie.

W małych, jednoosobowych projektach niektórzy pracują tylko na main. Działa to dopóki:

  • nikt inny z repozytorium nie korzysta,
  • zmiany są naprawdę małe i łatwe do cofnięcia.

W każdym projekcie zespołowym albo takim, który ma żyć dłużej niż tydzień, oddzielne branche dla funkcji lub zadań porządkują historię i ułatwiają code review.

Łączenie zmian: merge bez dramatów

Załóżmy, że masz:

  • main – stabilna gałąź projektu,
  • feature/login-form – nową funkcję ukończoną i przetestowaną lokalnie.

Sekwencja do połączenia jest zwykle taka:

# przełącz się na gałąź, do której chcesz włączyć zmiany
git switch main

# upewnij się, że masz aktualny stan (jeśli używasz repo zdalnego)
git pull origin main

# połącz feature z main
git merge feature/login-form

Jeśli od czasu utworzenia brancha nikt istotnie nie zmieniał tych samych fragmentów plików, merge przejdzie „fast-forward” – Git po prostu przesunie wskaźnik main do przodu. W przeciwnym wypadku powstanie commit scalający, który będzie miał dwóch rodziców.

Konflikty merge: co robić, gdy Git się poddaje

Rozwiązywanie konfliktów przy scalaniu

Konflikt to sytuacja, w której Git nie jest w stanie samodzielnie wybrać, która wersja fragmentu pliku jest właściwa. Zwykle dzieje się to, gdy dwie gałęzie zmieniły te same linie tego samego pliku lub jedna gałąź usunęła coś, co druga zmodyfikowała.

Scenariusz jest prosty:

git switch main
git merge feature/login-form

Jeśli pojawią się konflikty, zobaczysz komunikat w stylu:

Auto-merging src/LoginForm.jsx
CONFLICT (content): Merge conflict in src/LoginForm.jsx
Automatic merge failed; fix conflicts and then commit the result.

Git oznaczy miejsca konfliktu w pliku:

<<<<<<< HEAD
// wersja z gałęzi main
=======
 // wersja z feature/login-form
>>>>>>> feature/login-form

Twoja rola:

  1. Otworzyć plik w edytorze.
  2. Świadomie wybrać właściwą wersję (czasem jedną z dwóch, czasem miks obu).
  3. Usunąć znaczniki <<<<<<<, =======, >>>>>>>.
  4. Sprawdzić, czy kod się kompiluje / testy przechodzą.

Gdy wszystkie konflikty są ręcznie poprawione:

# oznacz poprawione pliki jako gotowe
git add src/LoginForm.jsx

# dokończ merge (commit scalający)
git commit -m "Merge branch 'feature/login-form' into main"

Większość IDE (VS Code, IntelliJ, WebStorm) ma wbudowane narzędzia do obsługi konfliktów, pokazujące „lewo/prawo” i wersję wynikową. Sam mechanizm Gita pozostaje jednak ten sam – narzędzie tylko ułatwia wybór.

Unikanie zbędnych konfliktów

Nie da się wyeliminować wszystkich konfliktów, ale da się ograniczyć ich liczbę. Kilka praktyk, które zwykle robią różnicę:

  • krótsze żywoty branchy – im szybciej łączysz funkcję z main, tym mniejsza szansa, że ktoś zdąży gruntownie przebudować ten sam fragment kodu,
  • częste aktualizowanie brancha – regularne git pull i git merge main (lub rebase, jeśli zespół tak pracuje) zmniejsza rozjazd między gałęziami,
  • mniejsze commity i lepsze podziały plików – jeśli jedna klasa/plik ma 1000 linii, kilka osób prędzej czy później wejdzie sobie w drogę.

W praktyce najwięcej konfliktów pojawia się nie z powodu Gita, ale przez chaotyczne zmiany: masowe formatowanie w jednym commicie razem z logiką, przenoszenie plików bez czytelnego podziału albo refaktoryzacje robione „przy okazji”.

Sprzątanie po branchu funkcji

Gdy funkcja jest zmergowana do main i wypchnięta na zdalne repozytorium, branch funkcji zwykle jest zbędny. Lokalne porządki:

# usuń lokalny branch
git branch -d feature/login-form

Jeśli Git protestuje, że gałąź nie jest w pełni połączona, można użyć wersji „siłowej”:

git branch -D feature/login-form

Na zdalnym repozytorium:

git push origin --delete feature/login-form

Porządkowanie branchy zmniejsza bałagan w widoku repozytorium i utrudnia przypadkowe odświeżanie dawno zamkniętych funkcji.

Praca ze zdalnym repozytorium: push, pull, fetch i pierwsza współpraca

Dodanie zdalnego repozytorium

Repozytorium zdalne to po prostu kopia projektu na serwerze (GitHub, GitLab, Bitbucket, własny serwer). Lokalny projekt można podpiąć do istniejącego zdalnego repozytorium:

# sprawdź aktualne zdalne repozytoria
git remote -v

# dodaj nowe zdalne repo jako "origin"
git remote add origin git@github.com:twoj-login/twoj-projekt.git

Po podpięciu można wypchnąć aktualny stan:

git push -u origin main

Opcja -u ustawia tzw. upstream – dzięki temu kolejne wypchnięcia sprowadzą się do:

git push

Wypychanie zmian: git push

Lokalne commity są tylko na twojej maszynie. git push przesyła je na serwer:

# wypchnij aktualny branch na zdalne repozytorium
git push origin feature/login-form

Typowy błąd po stronie początkujących: zakładanie, że push zadziała zawsze. Jeśli ktoś w międzyczasie wypchnął nowe commity na ten sam branch, zobaczysz:

To https://github.com/...
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs...

To sygnał, że najpierw trzeba pobrać zmiany i je włączyć:

git pull origin main
# rozwiąż ewentualne konflikty
git push origin main

Pobieranie i łączenie zmian: git pull

git pull to wygodny skrót dla dwóch kroków: git fetch + git merge. Używa się go, żeby zaktualizować lokalną gałąź o zdalne commity:

# zaktualizuj aktualny branch z zadanego zdalnego
git pull origin main

Domyślnie pull zrobi merge. Niektóre zespoły wymagają używania rebase przy pobieraniu (żeby historia była liniowa), wtedy konfiguracja wygląda np. tak:

git config --global pull.rebase true

To zmienia sposób zapisu historii, ale zasada biznesowa zostaje ta sama: twoje lokalne zmiany muszą jakoś dogadać się z tym, co już jest na serwerze.

Bezpieczne sprawdzanie, co jest na serwerze: git fetch

git fetch tylko pobiera informacje o nowych commitach ze zdalnego repozytorium, ale nie łączy ich automatycznie z twoim aktualnym branchem:

# pobierz aktualny stan zdalnego repo
git fetch origin

Po fetch możesz podejrzeć zdalną gałąź bez ingerencji w lokalny kod:

# porównaj lokalny main ze zdalnym
git log --oneline main..origin/main

# zobacz, jakie commity ma zdalny main, których lokalnie brakuje
git diff main..origin/main

To podejście jest bezpieczniejsze, gdy chcesz zrozumieć, co się zadziało na serwerze, zanim włączysz to do swojej pracy.

Śledzone i nieśledzone gałęzie zdalne

Kiedy klonujesz repozytorium:

git clone git@github.com:twoj-login/twoj-projekt.git

Git automatycznie tworzy:

  • lokalny branch (zwykle main lub master),
  • zdalny odpowiednik origin/main,
  • relację śledzenia między nimi.

Przy nowych branchach trzeba zrobić to ręcznie:

# utwórz lokalny branch i od razu go wypchnij, ustawiając śledzenie
git switch -c feature/profile
git push -u origin feature/profile

Po tym zabiegu:

git status

pokaże m.in. informację, o ile commitów twój branch jest „do przodu” lub „do tyłu” względem zdalnego.

Typowy przepływ pracy z pull requestem

W projektach zespołowych najczęściej przyjęty jest schemat: branch funkcji → push → pull request → code review → merge. Minimalna ścieżka dla nowego zadania:

# 1. Aktualizujesz main
git switch main
git pull origin main

# 2. Tworzysz branch na funkcję
git switch -c feature/login-validation

# 3. Kodujesz, commitujesz zmiany
git add src/LoginForm.jsx
git commit -m "Add basic client-side login validation"

# 4. Wypychasz branch na serwer
git push -u origin feature/login-validation

Następny krok dzieje się już w interfejsie GitHuba czy GitLaba – tworzysz pull request (PR) z feature/login-validation do main. PR to nie osobny byt w Gicie, tylko „prośba o merge” plus komentarze, recenzje, checki CI itp.

Po zaakceptowaniu PR:

  • gałąź funkcji jest scalana z main (merge albo squash – zależnie od ustawień),
  • na zdalnym repozytorium często gałąź funkcji jest od razu usuwana.

Lokalnie możesz się zsynchronizować:

git switch main
git pull origin main

# ewentualne sprzątanie lokalnego brancha
git branch -d feature/login-validation

Schematy strategii merge w pull requestach

Platformy typu GitHub pozwalają wybrać sposób scalania PR-ów. Z punktu widzenia początkującego programisty najczęściej w grze są trzy tryby:

  • Merge commit – zachowuje wszystkie commity z brancha funkcji i generuje dodatkowy commit scalający; historia jest pełna, ale może być gęsta,
  • Squash and merge – łączy wszystkie commity z PR w jeden commit na main; czytelne, jeśli w branchu było dużo „roboczych” commitów,
  • Rebase and merge – przepisuje commity z brancha tak, jakby powstały bezpośrednio na main; historia liniowa, za to wymaga większej dyscypliny przy cofnięciach.

Jeśli dopiero zaczynasz, najłatwiej czytać historię, gdy zespół używa merge commit lub squash. Rebase bywa wygodny, ale źle użyty potrafi wywołać więcej zamieszania niż pożytku.

Minimalne zasady współpracy w zespole

Nawet w małym zespole dobrze jest od początku przyjąć kilka prostych zasad wokół Gita i PR-ów. Przykładowy zestaw, który zazwyczaj się sprawdza:

  • wszystkie zmiany do main idą przez pull requesty (bez bezpośrednich pushy),
  • branch per zadanie – jeden branch dla konkretnego issue/ticketa,
  • przed otwarciem PR: aktualizacja z main i zielone testy lokalne,
  • commit opisuje sens zmiany, PR opisuje kontekst (dlaczego to robimy).

Takie minimum redukuje liczbę niespodzianek typu: „Dlaczego produkcja nie wstaje po merge’u?” albo „Kto nadpisał mój kod sprzed dwóch dni?”.

Najczęściej zadawane pytania (FAQ)

Po co mi Git jako początkującemu programiście, skoro mogę robić kopie folderów?

Kopie folderów dają złudne poczucie bezpieczeństwa. Przy kilku wersjach projektu jeszcze da się to ogarnąć, ale przy większej liczbie zmian szybko tracisz orientację: nie wiesz, gdzie jest która poprawka i czym dokładnie różni się „v2_ostateczna” od „v3_final”. Trudniej też wycofać pojedynczą błędną zmianę, bo wszystko jest wymieszane.

Git śledzi historię na poziomie zmian w plikach, a nie całych katalogów. Każdy commit ma autora, datę i opis, więc możesz sprawdzić „kto, kiedy i po co” zmienił dany fragment kodu. Do tego większość operacji (przegląd historii, tworzenie branchy) działa lokalnie bez internetu, a zdalne repozytorium służy tylko do synchronizacji, a nie jako jedyne źródło prawdy.

Czym praktycznie różni się commit od zwykłego zapisu pliku?

Zapis pliku w edytorze dotyczy wyłącznie bieżącej wersji na dysku. System plików nie wie, co było wcześniej i nie pozwala wrócić do konkretnego stanu sprzed godziny czy tygodnia. Commit w Git jest natomiast migawką zmian z opisem, która trafia do trwałej historii projektu.

Każdy commit:

  • ma unikalny identyfikator (skrót SHA),
  • zawiera różnice względem poprzedniego stanu,
  • jest połączony z rodzicem, więc tworzy spójną linię historii.

Dzięki temu możesz porównać dwie wersje, odtworzyć stan z konkretnego momentu albo stwierdzić, który commit wprowadził błąd. Sam zapis pliku takich możliwości nie daje.

Co to jest branch w Git i kiedy faktycznie powinienem go używać?

Branch to nazwana linia historii, czyli wskaźnik na określony commit i jego dalsze commity. Wbrew częstemu mitowi branch nie jest osobną kopią katalogu, tylko innym „szlakiem” w tym samym drzewie zmian. Tworzysz go zwykle po to, by rozwijać nową funkcję lub robić większy refactoring bez psucia stabilnej wersji.

Typowy schemat wygląda tak: stabilny kod trzymasz na main, dla nowej funkcji robisz feature/nazwa, tam dodajesz commity, a po przetestowaniu łączysz go z main przez merge lub pull request. Przy małych, jednorazowych skryptach branch może być przesadą, ale w każdym projekcie, który ma rosnąć i w którym kod dotyka więcej niż jedna osoba, osobne gałęzie to standard, a nie fanaberia.

Czym jest pull request i czy naprawdę jest mi potrzebny na początku?

Pull request (w GitLabie częściej „merge request”) to prośba o włączenie zmian z jednego brancha do innego w zdalnym repozytorium, np. z feature/login do main. Technicznie to tylko „opakowanie” dla zwykłego merge’a, ale z dodatkiem:

  • przeglądu zmian (diff),
  • komentarzy do konkretnych linii kodu,
  • ewentualnych automatycznych testów (CI).

Jeśli uczysz się sam i masz jedno repo tylko dla siebie, pull request nie jest konieczny. Natomiast w komercyjnym projekcie lub w open source jest praktycznie obowiązkiem – bez niego trudno prowadzić sensowny code review i pilnować jakości zmian. Lepiej oswoić się z tym narzędziem wcześnie, niż później nadrabiać pod presją czasu.

Czy każdy mały projekt powinien być w Git? Kiedy Git to przesada?

Nie każdy skrypt wymaga repozytorium. Jednorazowe eksperymenty, notatki dla siebie, krótkie „pchełki” do terminala – tam Git często nie wniesie dużej wartości, o ile rzeczywiście pozostaną jednorazowe. Problem w tym, że wiele takich „drobiazgów” po kilku miesiącach nagle zaczyna być rozwijane dalej.

Rozsądną praktyką jest trzymanie wszystkiego, co ma szansę rosnąć lub być używane ponownie, w jakiejś formie repozytorium: nawet jeśli to jedno wspólne repo na skrypty czy konfiguracje (tzw. dotfiles). Wyjątkiem są sytuacje, gdzie koszt ogarniania repo (nawet minimalny) jest większy niż potencjalny zysk z historii zmian.

Czym różni się lokalne repozytorium Git od zdalnego (np. na GitHubie)?

Lokalne repozytorium to katalog na twoim dysku wraz z podkatalogiem .git. Masz tam pełną historię projektu, więc możesz przeglądać commity, tworzyć branche, robić merge – wszystko bez połączenia z siecią. Git jest rozproszony, więc lokalna kopia nie jest „półproduktem”, tylko pełnoprawnym repozytorium.

Zdalne repozytorium (remote), np. na GitHubie czy GitLabie, to dodatkowa kopia do synchronizacji i współpracy. Typowe komendy to:

  • git push origin main – wysłanie lokalnych commitów na serwer,
  • git fetch origin lub git pull – pobranie commitów innych osób.

Bez zdalnego repo trudno pracować zespołowo, ale do nauki podstaw logiki Gita w zupełności wystarczy repozytorium lokalne utworzone przez git init.

Czy Git jest jedynym systemem kontroli wersji, jakiego muszę się nauczyć?

Na rynku istnieją inne systemy kontroli wersji (np. Subversion, Mercurial), ale w praktyce komercyjnej Git jest dziś standardem. Większość firm, projektów open source i materiałów edukacyjnych zakłada właśnie Git oraz serwis typu GitHub/GitLab.

Zdarzają się wyjątki – starsze projekty potrafią używać SVN, a niektóre organizacje mają własne rozwiązania. Znajomość zasad działania Gita (commit, branch, remote) dobrze „przekłada się” na inne systemy, ale to Git daje największy zwrot z inwestycji czasu, bo z nim spotkasz się najczęściej.