Git nie jest tylko narzędziem do wysyłania kodu na GitHub. To dziennik decyzji technicznych, mechanizm współpracy w zespole i ostatnia linia obrony przed utratą pracy. Wielu developerów zna add, commit i push, ale gubi się przy konfliktach, rebase, cofnięciu zmian albo pracy na kilku branchach. W praktyce właśnie te sytuacje odróżniają osobę, która "używa Gita", od osoby, której można zaufać z kodem produkcyjnym.
📋 Spis treści
- 🧠 Mental model: co Git naprawdę zapisuje
- ⚙️ Pierwsza konfiguracja
- 📦 Codzienny workflow
- 🌿 Branche: jak pracować bez chaosu
- ✍️ Commity: małe, opisowe, odwracalne
- 🔀 Merge vs rebase
- 💥 Konflikty: jak je rozwiązywać bez paniki
- ↩️ Cofanie zmian bez niszczenia historii
- 🔐 Czego nigdy nie commitować
- 🧰 Co Fork i GitKraken robią pod spodem
- 🧹 Szum w Git: uprawnienia, końce linii i lokalne pliki
- 🚀 Pull request workflow
- ✅ Podsumowanie
🧠 Mental model: co Git naprawdę zapisuje
Git zapisuje serię snapshotów projektu. Commit to punkt w historii: zestaw zmian, autor, data, wiadomość i odwołanie do poprzedniego commita.
Najważniejsze trzy obszary:
- Working tree - pliki, które aktualnie edytujesz
- Staging area - zmiany przygotowane do commita
- Repository - historia commitów
To dlatego git add i git commit są osobnymi krokami. git add mówi: "te konkretne zmiany mają wejść do następnego commita". git commit zapisuje ten pakiet w historii.
# terminal
git status
git add app/Http/Controllers/PostController.php
git commit -m "feat: add post publishing endpoint"
Dobry developer nie robi commitów przez przypadek. Najpierw sprawdza, co zmienił, potem wybiera logiczny pakiet zmian, dopiero potem zapisuje commit.
⚙️ Pierwsza konfiguracja
Zacznij od ustawienia autora commitów:
# terminal
git config --global user.name "Dominik Jasinski"
git config --global user.email "[email protected]"
Ustaw domyślną nazwę brancha:
# terminal
git config --global init.defaultBranch main
Ustaw sensowny edytor:
# terminal
git config --global core.editor "code --wait"
Warto też włączyć czytelniejsze logi przez alias:
# terminal
git config --global alias.lg "log --oneline --graph --decorate --all"
Od teraz:
# terminal
git lg
pokaże historię branchy w formie, którą da się szybko przeczytać.
📦 Codzienny workflow
Typowy dzień pracy wygląda tak:
# terminal
git switch main
git pull --ff-only
git switch -c feat/post-drafts
git pull --ff-only jest celowo ostrożne. Aktualizuje branch tylko wtedy, gdy można zrobić fast-forward. Jeśli lokalna historia rozjechała się z remote, Git zatrzyma się, zamiast tworzyć przypadkowy merge commit.
Po zmianach:
# terminal
git status
git diff
git add app/ tests/
git diff --staged
git commit -m "feat: add draft status for posts"
git push -u origin feat/post-drafts
Kolejność ma znaczenie:
git status- co jest zmienione?git diff- co dokładnie zmieniłem?git add- które zmiany chcę zapisać?git diff --staged- co trafi do commita?git commit- zapisz logiczny pakietgit push- wyślij branch na remote
Najczęstszy błąd juniorów to commitowanie wszystkiego naraz bez przeczytania diffu. Najczęstszy błąd midów to robienie zbyt dużych commitów, których nie da się łatwo zreviewować ani cofnąć.
🌿 Branche: jak pracować bez chaosu
Branch to wskaźnik na commit. Nie jest kopią projektu. Jest lekkim markerem mówiącym: "tu rozwija się ten kierunek pracy".
Dobre nazwy branchy:
# terminal
feat/post-drafts
fix/login-rate-limit
refactor/order-checkout-action
docs/k3s-ssh-keys
chore/update-dependencies
Złe nazwy:
# terminal
new
fix
changes
dominik
test2
Branch powinien mówić, jaki problem rozwiązuje. Jeśli nie potrafisz nazwać brancha konkretnie, prawdopodobnie zakres pracy jest niejasny.
Podstawowe komendy:
# terminal
git branch
git switch feat/post-drafts
git switch -c fix/api-validation
git branch -d fix/api-validation
git branch -d usuwa branch lokalnie, ale tylko jeśli został zmergowany. To bezpieczniejsza opcja niż -D, które usuwa branch na siłę.
✍️ Commity: małe, opisowe, odwracalne
Dobry commit ma jedną odpowiedzialność. Jeśli zmieniasz walidację formularza, migrację bazy, CSS i konfigurację CI w jednym commicie, reviewer nie widzi intencji.
Dobry commit:
# terminal
git commit -m "fix: validate post slug uniqueness per tenant"
Słaby commit:
# terminal
git commit -m "fix"
git commit -m "changes"
git commit -m "update"
git commit -m "wip"
Praktyczny format:
# commit message
type: short imperative description
Najczęstsze typy:
feat- nowa funkcjafix- poprawka bugarefactor- zmiana struktury bez zmiany zachowaniatest- testydocs- dokumentacjachore- utrzymanie projektu
Przykłady:
# terminal
git commit -m "feat: add article publishing workflow"
git commit -m "fix: prevent duplicate slugs"
git commit -m "refactor: extract order total calculator"
git commit -m "test: cover failed payment retry"
git commit -m "docs: explain SSH key setup for k3s"
Jeśli commit jest mały, można go łatwo:
- zreviewować
- cofnąć przez
git revert - przenieść przez
cherry-pick - zrozumieć po kilku miesiącach
🔀 Merge vs rebase
merge łączy dwie historie i tworzy commit scalający, jeśli jest potrzebny.
# terminal
git switch main
git merge feat/post-drafts
rebase przenosi twoje commity na koniec innego brancha, tworząc prostszą historię.
# terminal
git switch feat/post-drafts
git fetch origin
git rebase origin/main
Praktyczna zasada:
- Merge jest dobry do scalania zaakceptowanego PR-a do
main - Rebase jest dobry do odświeżenia własnego brancha przed PR-em
- Nie rób rebase publicznego brancha, na którym pracują inne osoby
Dlaczego? Rebase przepisuje historię. Jeśli ktoś opiera swoją pracę na twoich starych commitach, po rebase tworzy się chaos.
Bezpieczny workflow:
# terminal
git switch feat/post-drafts
git fetch origin
git rebase origin/main
git push --force-with-lease
--force-with-lease jest bezpieczniejsze niż --force, bo nie nadpisze cudzych zmian, jeśli remote zmienił się od ostatniego fetcha.
💥 Konflikty: jak je rozwiązywać bez paniki
Konflikt oznacza, że Git nie potrafi automatycznie połączyć dwóch zmian w tym samym miejscu pliku.
Przykład:
# app/Services/PostTitleFormatter.php
<<<<<<< HEAD
return Str::headline($title);
=======
return Str::title(trim($title));
>>>>>>> feat/post-drafts
Musisz zdecydować, która wersja jest poprawna albo połączyć obie:
// app/Services/PostTitleFormatter.php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Support\Str;
final class PostTitleFormatter
{
public function format(string $title): string
{
return Str::headline(trim($title));
}
}
Po rozwiązaniu konfliktu:
# terminal
git status
git add app/Services/PostTitleFormatter.php
git rebase --continue
Jeśli konflikt pojawił się podczas merge:
# terminal
git add app/Services/PostTitleFormatter.php
git commit
Jeśli sytuacja wymknęła się spod kontroli:
# terminal
git rebase --abort
albo:
# terminal
git merge --abort
To nie jest porażka. To bezpieczne wyjście awaryjne, które wraca do stanu sprzed operacji.
↩️ Cofanie zmian bez niszczenia historii
Git ma kilka sposobów cofania zmian. Najważniejsze jest dobranie komendy do sytuacji.
Cofnięcie zmian w pliku, które nie są zacommitowane:
# terminal
git restore app/Models/Post.php
Usunięcie pliku ze staging area, ale zostawienie zmian w working tree:
# terminal
git restore --staged app/Models/Post.php
Poprawienie ostatniego commita:
# terminal
git add tests/Feature/PostTest.php
git commit --amend
Używaj --amend tylko zanim ktoś oprze pracę na tym commicie.
Bezpieczne cofnięcie commita, który jest już na remote:
# terminal
git revert <COMMIT_SHA>
revert tworzy nowy commit odwracający zmiany. Nie usuwa historii, więc jest bezpieczny dla współdzielonych branchy.
Przeniesienie jednego commita z innego brancha:
# terminal
git cherry-pick <COMMIT_SHA>
To przydatne, gdy fix z jednego brancha trzeba szybko przenieść na inny, np. z develop na main.
Unikaj git reset --hard, jeśli nie wiesz dokładnie, co robisz. Ta komenda usuwa lokalne zmiany z working tree.
🔐 Czego nigdy nie commitować
W projektach Laravel i Node szczególnie pilnuj sekretów i wygenerowanych katalogów.
Przykładowy .gitignore:
# .gitignore
/vendor
/node_modules
/.env
/.env.*
!.env.example
/storage/*.key
/storage/logs/*.log
/bootstrap/cache/*.php
/public/build
/public/hot
/.phpunit.cache
/.idea
/.vscode
Nigdy nie commituj:
.env- prywatnych kluczy SSH
- tokenów API
- haseł do baz danych
- plików
secret.yaml vendor/node_modules/- buildów, jeśli projekt buduje je w CI
Jeśli sekret już trafił do repo, samo usunięcie go w kolejnym commicie nie wystarczy. Sekret dalej istnieje w historii. Trzeba go natychmiast zrotować.
🧰 Co Fork i GitKraken robią pod spodem
GUI takie jak Fork, GitKraken, Sourcetree czy GitHub Desktop nie robią magii. Większość przycisków to wygodne opakowanie na konkretne komendy Git. Warto znać te komendy, bo gdy GUI pokaże dziwny stan, terminal zwykle szybciej wyjaśnia co się dzieje.
Stage / unstage pliku
# terminal
git add app/Models/Post.php
git restore --staged app/Models/Post.php
Stage wybranego fragmentu pliku
W GUI klikasz pojedynczy hunk. W terminalu:
# terminal
git add -p app/Models/Post.php
To jedna z najważniejszych komend do robienia czystych commitów. Pozwala rozdzielić zmiany z jednego pliku na kilka logicznych commitów.
Discard changes
W GUI to zwykle przycisk "Discard". Pod spodem:
# terminal
git restore app/Models/Post.php
Uwaga: to usuwa lokalne niezacommitowane zmiany w pliku.
Amend ostatniego commita
Gdy w GUI wybierasz "Amend" albo "Modify last commit":
# terminal
git add app/Models/Post.php
git commit --amend
To przepisuje ostatni commit. Bezpieczne przed push, ostrożnie po push.
Stash: odłóż zmiany na bok
stash jest przydatny, gdy musisz szybko zmienić branch, zrobić pull albo sprawdzić coś bez commitowania niedokończonej pracy.
# terminal
git stash push -m "wip: post filters"
Lista stashy:
# terminal
git stash list
Podejrzenie zawartości:
# terminal
git stash show -p stash@{0}
Przywrócenie i usunięcie stash ze stosu:
# terminal
git stash pop
Przywrócenie bez usuwania stash:
# terminal
git stash apply stash@{0}
Usunięcie konkretnego stash:
# terminal
git stash drop stash@{0}
Utworzenie brancha ze stash:
# terminal
git stash branch wip/post-filters stash@{0}
Różnica między pop i apply jest ważna:
popaplikuje stash i usuwa go, jeśli operacja się udaapplyaplikuje stash, ale zostawia go na liście
Jeśli stash może powodować konflikty, bezpieczniej zacząć od apply.
Fetch, pull, push
GUI często pokazuje przyciski "Fetch", "Pull" i "Push" obok siebie, ale to nie to samo:
# terminal
git fetch origin
fetch pobiera informacje z remote, ale nie zmienia twojego brancha.
# terminal
git pull --ff-only
pull to w praktyce fetch + integracja zmian do lokalnego brancha.
# terminal
git push origin feat/post-drafts
push wysyła twoje lokalne commity na remote.
Reset branch do konkretnego commita
GUI często oferuje "Reset current branch to this commit". Pod spodem są trzy różne tryby:
# terminal
git reset --soft <COMMIT_SHA>
git reset --mixed <COMMIT_SHA>
git reset --hard <COMMIT_SHA>
Różnica:
--softcofa historię, ale zostawia zmiany w staging area--mixedcofa historię i staging, ale zostawia pliki zmienione--hardcofa historię i usuwa lokalne zmiany z plików
--hard jest destrukcyjne. Używaj tylko, gdy masz pewność, że nic lokalnego nie jest potrzebne.
Revert commit
W GUI "Revert" zwykle oznacza:
# terminal
git revert <COMMIT_SHA>
To bezpieczny sposób cofania zmian już wypchniętych na remote, bo tworzy nowy commit odwracający poprzedni.
Cherry-pick
W GUI przeciągasz albo wybierasz "Cherry-pick commit". W terminalu:
# terminal
git cherry-pick <COMMIT_SHA>
Przydatne, gdy poprawka z jednego brancha musi trafić na inny, np. hotfix na main.
Tagi
GUI pozwala tworzyć release tagi. Pod spodem:
# terminal
git tag v1.4.0
git push origin v1.4.0
Lepszy tag release'owy to tag adnotowany:
# terminal
git tag -a v1.4.0 -m "Release v1.4.0"
git push origin v1.4.0
Blame: kto zmienił linię i dlaczego
GUI pokazuje autora linii. W terminalu:
# terminal
git blame app/Services/PostPublisher.php
Jeszcze lepiej połączyć to z historią konkretnego pliku:
# terminal
git log --follow -- app/Services/PostPublisher.php
blame nie służy do szukania winnego. Służy do znalezienia kontekstu decyzji.
Bisect: znajdź commit, który wprowadził buga
Mało osób używa bisect, a to jedna z najbardziej praktycznych funkcji Gita. Jeśli wiesz, że bug istnieje teraz, ale nie istniał tydzień temu:
# terminal
git bisect start
git bisect bad
git bisect good <OLD_GOOD_COMMIT_SHA>
Git będzie przełączał cię między commitami. Po każdym sprawdzeniu mówisz:
# terminal
git bisect good
albo:
# terminal
git bisect bad
Na końcu Git wskaże commit, który najprawdopodobniej wprowadził problem. Zakończ:
# terminal
git bisect reset
Worktree: kilka branchy naraz bez stashowania
Jeśli często przeskakujesz między feature, hotfix i review, git worktree bywa lepszy niż stash.
# terminal
git worktree add ../project-hotfix main
To tworzy drugi katalog roboczy tego samego repo, ale na innym branchu. Możesz mieć osobny folder na hotfix bez ruszania aktualnej pracy.
Lista worktree:
# terminal
git worktree list
Usunięcie:
# terminal
git worktree remove ../project-hotfix
Reflog: ratunek po złym rebase/reset
GUI rzadko pokazuje reflog, a to często ratuje dzień. Git zapisuje, gdzie ostatnio wskazywał HEAD.
# terminal
git reflog
Jeśli po rebase albo reset zniknął commit, znajdź go w reflog i wróć:
# terminal
git switch -c rescue-branch <COMMIT_SHA>
Reflog działa lokalnie. Nie traktuj go jak backupu, ale pamiętaj, że istnieje zanim uznasz pracę za straconą.
🧹 Szum w Git: uprawnienia, końce linii i lokalne pliki
Czasem git status pokazuje zmiany, mimo że nie dotykałeś logiki aplikacji. Najczęstsze powody to uprawnienia plików, końce linii, różnice wielkości liter w nazwach plików i lokalna konfiguracja środowiska.
1. Git pokazuje zmiany uprawnień plików
Na macOS, Linuxie, WSL albo przy pracy przez Docker/VM Git może widzieć zmianę trybu pliku, np. 100644 -> 100755, mimo że zawartość pliku się nie zmieniła.
Sprawdź diff:
# terminal
git diff --summary
Jeśli widzisz tylko zmianę mode:
# terminal
mode change 100644 => 100755 app/Services/PostService.php
możesz wyłączyć śledzenie zmian file mode w tym repo:
# terminal
git config core.fileMode false
To ustawia opcję lokalnie dla projektu. Nie używaj tego bezmyślnie globalnie, bo czasem executable bit jest ważny, np. dla skryptów:
# terminal
chmod +x scripts/deploy.sh
git add scripts/deploy.sh
git commit -m "chore: make deploy script executable"
Jeśli plik ma być wykonywalny, commituj tę zmianę. Jeśli Git tylko szumi przez system plików, użyj core.fileMode false.
2. Końce linii: CRLF vs LF
Windows używa zwykle CRLF, Linux/macOS LF. Bez konfiguracji możesz dostać ogromny diff, w którym wygląda jakby zmienił się cały plik.
Dodaj .gitattributes:
# .gitattributes
* text=auto
*.php text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.css text eol=lf
*.md text eol=lf
*.sh text eol=lf
Po dodaniu możesz znormalizować pliki:
# terminal
git add --renormalize .
git status
Taki commit najlepiej zrobić osobno:
# terminal
git commit -m "chore: normalize line endings"
Nie mieszaj normalizacji końców linii z refaktoringiem, bo review stanie się nieczytelne.
3. Zmiana wielkości liter w nazwie pliku
Na macOS domyślny system plików często ignoruje wielkość liter, więc zmiana postservice.php na PostService.php może nie zostać poprawnie wykryta.
Użyj git mv przez nazwę tymczasową:
# terminal
git mv app/Services/postservice.php app/Services/postservice.tmp
git mv app/Services/postservice.tmp app/Services/PostService.php
git commit -m "fix: correct PostService filename casing"
To ważne przy deployu na Linux, gdzie wielkość liter ma znaczenie.
4. Lokalna konfiguracja, której nie chcesz commitować
Czasem musisz zmienić plik konfiguracyjny lokalnie, ale nie chcesz, żeby Git stale pokazywał go w status.
Dla plików, które są już śledzone przez Git, możesz użyć:
# terminal
git update-index --skip-worktree config/local.php
Przywrócenie normalnego śledzenia:
# terminal
git update-index --no-skip-worktree config/local.php
To rozwiązanie awaryjne, nie domyślny workflow. Lepszy wzorzec to commitować plik przykładowy:
# struktura
.env.example
docker-compose.example.yml
a lokalne warianty trzymać poza Gitem.
5. Pliki wygenerowane przez narzędzia
Jeśli po uruchomieniu testów albo IDE pojawiają się nowe pliki, nie dodawaj ich odruchowo. Najpierw sprawdź, czy powinny trafić do .gitignore:
# terminal
git status --ignored
Typowy szum:
.phpunit.cache.pest.idea.vscodestorage/logs/*.lognpm-debug.logcoverage/
Zasada: jeśli plik jest odtwarzalny, lokalny albo zależy od maszyny developera, zwykle nie powinien być commitowany.
🚀 Pull request workflow
Dobry PR zaczyna się przed otwarciem GitHuba.
Przed push:
# terminal
git status
git diff --staged
composer test
npm run build
Jeśli projekt ma Pint/PHPStan/Pest:
# terminal
./vendor/bin/pint --test
./vendor/bin/phpstan analyse
./vendor/bin/pest
Opis PR powinien odpowiadać na trzy pytania:
<!-- .github/pull_request_template.md -->
## Co zmienia ten PR?
## Jak to sprawdziłem?
## Ryzyka / rzeczy do review
Dobry opis:
<!-- .github/pull_request_template.md -->
## Co zmienia ten PR?
Dodaje status draft/published dla artykułów i blokuje publiczny dostęp do draftów.
## Jak to sprawdziłem?
- `./vendor/bin/pest --filter PostPublishingTest`
- ręcznie sprawdzony endpoint `GET /api/v1/posts`
## Ryzyka / rzeczy do review
Migracja dodaje domyślny status `draft`, więc istniejące rekordy muszą zostać opublikowane osobnym skryptem.
Najgorszy opis PR:
<!-- .github/pull_request_template.md -->
fix
Review zaczyna się od kontekstu. Jeśli reviewer musi odgadywać, po co istnieje PR, to tracisz jego czas i zwiększasz ryzyko błędnego review.
✅ Podsumowanie
Git staje się prostszy, gdy traktujesz go jak narzędzie do kontroli jakości, nie tylko transport do GitHuba:
git statusigit diffczytaj przed każdym commitem- commity rób małe, opisowe i odwracalne
- branche nazywaj po problemie, który rozwiązują
- używaj rebase do własnych branchy, merge do scalania pracy zespołu
- konflikty rozwiązuj świadomie, potem uruchom testy
- publiczną historię cofaj przez
git revert, nie przez kasowanie commitów - sekretów nie commituj nigdy; jeśli wyciekną, rotuj je natychmiast
- rozumiej co robi GUI pod spodem: stash, reset, cherry-pick, tagi, blame, bisect i reflog
- eliminuj szum z uprawnień, końców linii i lokalnych plików, zanim trafi do PR-a
- PR powinien tłumaczyć zmianę, testy i ryzyka
Dobry Git workflow nie robi z ciebie seniora automatycznie. Ale zły Git workflow bardzo szybko pokazuje, że zespół nie może ufać twoim zmianom bez dodatkowej kontroli.
Śledź mnie na LinkedIn po więcej praktycznych porad dla developerów! Który temat z Gita sprawia Ci największy problem: rebase, konflikty czy cofanie zmian? Daj znać w komentarzach!