Zaawansowana znajomość Git odróżnia seniorów od juniorów. Na rozmowach rekrutacyjnych pytania o rebase, cherry-pick, czy bisect pokazują czy kandydat rozumie Git jako narzędzie, czy tylko pamięta podstawowe komendy. Umiejętność odzyskiwania z trudnych sytuacji (bad rebase, lost commits) to must-have dla każdego doświadczonego developera.
Odpowiedź w 30 sekund
Kiedy rebase a kiedy merge?
Merge zachowuje pełną historię z merge commitami - używaj dla feature branches i pracy zespołowej. Rebase przepisuje historię na liniową - używaj do aktualizacji lokalnego brancha przed merge. Złota zasada: nigdy nie rebase'uj publicznych branchy (takich które ktoś inny może mieć). Rebase zmienia SHA commitów, co psuje historię innym.
Odpowiedź w 2 minuty
Git oferuje dwa główne sposoby integracji zmian: merge i rebase. Merge tworzy nowy commit który łączy dwie linie historii - zachowuje pełną informację o tym kiedy i jak branch został zmergowany. Rebase "przenosi" commity na nową bazę, przepisując ich SHA - tworzy liniową historię jakby zmiany były robione sekwencyjnie.
Interactive rebase (git rebase -i) to potężne narzędzie do porządkowania historii przed push. Możesz squashować wiele małych commitów w jeden logiczny, zmieniać kolejność, edytować wiadomości, a nawet modyfikować zawartość commitów. To idealne do czyszczenia "WIP" commitów przed code review.
Cherry-pick pozwala wybrać pojedynczy commit z innego brancha i zastosować go na obecnym. Przydatne do backportowania fixów na starsze wersje lub wyciągania konkretnej zmiany bez mergowania całego brancha.
Git bisect to binary search przez historię - pomaga znaleźć commit który wprowadził bug. Podajesz "dobry" i "zły" commit, Git checkout'uje środkowy, testujesz, oznaczasz, powtarzasz. W logarytmicznej liczbie kroków znajdziesz winowajcę.
Worktrees pozwalają mieć wiele working directories dla jednego repo. Zamiast stashować zmiany żeby przełączyć się na hotfix, tworzysz osobny worktree. Każdy worktree może mieć inny branch checked out.
Reflog to "undo history" Git - zapisuje każdą zmianę HEAD nawet po reset --hard czy złym rebase. To ostatnia deska ratunku do odzyskiwania utraconych commitów.
# Interactive rebase - uporządkuj ostatnie 5 commitów
git rebase -i HEAD~5
# Cherry-pick konkretnego commita
git cherry-pick abc1234
# Bisect - znajdź commit z bugiem
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
# ... testuj i oznaczaj ...
git bisect reset
# Worktree - osobny katalog z innym branchem
git worktree add ../hotfix-branch hotfix/critical
# Reflog - historia HEAD
git reflog
git checkout HEAD@{5} # Wróć do stanu sprzed 5 operacji
Rebase vs Merge
Czym różni się rebase od merge?
Merge - tworzy nowy commit łączący dwie historie:
A---B---C feature
/ \
D---E---F---G---M main (M = merge commit)
Rebase - przepisuje commity na nową bazę:
Przed: Po rebase:
A---B---C feature A'--B'--C' feature
/ /
D---E---F---G main D---E---F---G main
| Aspekt | Merge | Rebase |
|---|---|---|
| Historia | Zachowuje pełną, nieliniową | Liniowa, "czysta" |
| Merge commit | Tak | Nie |
| SHA commitów | Zachowane | Zmienione (A→A', B→B'...) |
| Bezpieczeństwo | Bezpieczny | Niebezpieczny dla publicznych branchy |
| Konflikty | Raz, przy merge | Potencjalnie przy każdym commit |
Kiedy używać rebase a kiedy merge?
Używaj merge gdy:
- Mergujesz feature branch do main/develop
- Pracujesz z innymi (branch jest publiczny)
- Chcesz zachować pełną historię
- Branch ma długą historię z wieloma autorami
# Feature branch → main (merge)
git checkout main
git merge feature/user-auth
Używaj rebase gdy:
- Aktualizujesz swój lokalny branch o zmiany z main
- Porządkujesz historię przed push
- Chcesz liniową historię bez merge commitów
- Branch jest tylko twój (nie pushowany lub force push OK)
# Aktualizuj feature branch o zmiany z main
git checkout feature/user-auth
git rebase main
# Porządkuj historię przed push
git rebase -i HEAD~5
Złota zasada: Nigdy nie rebase'uj commitów które są publiczne (pushowane i używane przez innych). Rebase zmienia SHA - inni będą mieli konflikty.
Jak działa interactive rebase?
git rebase -i pozwala modyfikować historię commitów:
git rebase -i HEAD~4 # Ostatnie 4 commity
# Otworzy edytor z listą:
pick abc1234 Add user model
pick def5678 Fix typo in user model
pick ghi9012 Add user validation
pick jkl3456 WIP: testing
# Komendy:
# p, pick = użyj commita
# r, reword = użyj commita, zmień message
# e, edit = użyj commita, zatrzymaj do edycji
# s, squash = połącz z poprzednim, zachowaj oba messages
# f, fixup = połącz z poprzednim, odrzuć message tego commita
# d, drop = usuń commit
# x, exec = uruchom komendę shell
Przykład: Squash WIP commitów:
# Przed:
# abc1234 Add user feature
# def5678 WIP
# ghi9012 WIP 2
# jkl3456 Fix tests
git rebase -i HEAD~4
# Zmień na:
pick abc1234 Add user feature
fixup def5678 WIP
fixup ghi9012 WIP 2
pick jkl3456 Fix tests
# Wynik: 2 commity zamiast 4
Przykład: Zmień kolejność i edytuj:
pick jkl3456 Fix tests # Przenieś wyżej
pick abc1234 Add user feature
reword def5678 WIP # Zmień message
drop ghi9012 WIP 2 # Usuń całkiem
Jak rozwiązywać konflikty podczas rebase?
Podczas rebase konflikty mogą wystąpić przy każdym commicie:
git rebase main
# CONFLICT in file.js
# Rozwiąż konflikt w file.js
git add file.js
git rebase --continue
# Lub przerwij rebase
git rebase --abort
# Lub pomiń problematyczny commit
git rebase --skip
Strategia dla wielu konfliktów:
# Użyj rerere (reuse recorded resolution)
git config rerere.enabled true
# Git zapamięta jak rozwiązałeś konflikt
# Następnym razem zastosuje automatycznie
Rebase onto - zaawansowane przenoszenie branchy
# Scenariusz: feature branch oparty na starym develop
#
# A---B---C feature
# /
# D---E---F---G develop
# \
# H---I main
# Chcesz przenieść feature na aktualny main
git rebase --onto main develop feature
# Wynik:
# A'--B'--C' feature
# /
# D---E---F---G---H---I main
# Usuń środkowe commity
# Masz: A---B---C---D---E (chcesz usunąć B, C)
git rebase --onto A C HEAD
# Wynik: A---D'---E'
Cherry-pick
Jak działa cherry-pick i kiedy go używać?
Cherry-pick kopiuje pojedynczy commit na obecny branch:
git checkout main
git cherry-pick abc1234
# A---B---C feature (abc1234 = C)
# /
# D---E---F---C' main (C' = kopia C)
Use cases:
- Backport hotfixa na starszą wersję:
git checkout release/1.0
git cherry-pick hotfix-commit-sha
- Wyciągnij pojedynczą zmianę z feature brancha:
git checkout main
git cherry-pick feature-commit-sha
- Odzyskaj commit z usuniętego brancha:
git reflog # Znajdź SHA
git cherry-pick lost-commit-sha
Cherry-pick wielu commitów
# Zakres commitów (A do B włącznie)
git cherry-pick A^..B
# Wiele pojedynczych commitów
git cherry-pick abc1234 def5678 ghi9012
# Bez automatycznego commita (staged changes)
git cherry-pick -n abc1234
# Z zachowaniem informacji o autorze
git cherry-pick -x abc1234 # Dodaje "cherry picked from commit..."
Rozwiązywanie konfliktów w cherry-pick
git cherry-pick abc1234
# CONFLICT
# Rozwiąż konflikt...
git add .
git cherry-pick --continue
# Lub przerwij
git cherry-pick --abort
# Lub pomiń ten commit i kontynuuj
git cherry-pick --skip
Git Bisect
Jak działa bisect i jak go używać?
Bisect używa binary search do znajdowania commita który wprowadził bug:
# 1. Rozpocznij bisect
git bisect start
# 2. Oznacz obecny commit jako zły (ma bug)
git bisect bad
# 3. Oznacz stary commit jako dobry (nie ma buga)
git bisect good v1.0.0
# Git checkout'uje środkowy commit
# Bisecting: 50 revisions left to test after this
# 4. Przetestuj, oznacz
git bisect good # lub git bisect bad
# Git checkout'uje następny commit do sprawdzenia
# Powtarzaj aż znajdziesz winowajcę
# 5. Zakończ bisect
git bisect reset
Przykład z 100 commitami:
- Start: 100 commitów do sprawdzenia
- Krok 1: 50 commitów
- Krok 2: 25 commitów
- Krok 3: 12 commitów
- Krok 4: 6 commitów
- Krok 5: 3 commity
- Krok 6: 1 commit
- ~7 kroków zamiast 100!
Automatyczny bisect ze skryptem
# Skrypt testowy: exit 0 = good, exit 1 = bad
git bisect start HEAD v1.0.0
git bisect run npm test
# Lub własny skrypt
git bisect run ./test-for-bug.sh
# Git automatycznie znajdzie commit
#!/bin/bash
# test-for-bug.sh
# Sprawdź czy bug istnieje
if grep -r "buggy code" src/; then
exit 1 # bad
else
exit 0 # good
fi
Bisect z pominięciem commitów
# Commit nie kompiluje się / nie można testować
git bisect skip
# Pomiń zakres commitów
git bisect skip v1.1.0..v1.1.5
Git Worktrees
Czym są worktrees i jak ich używać?
Worktrees pozwalają mieć wiele working directories dla jednego repo:
# Struktura:
# /project (main branch)
# /project-hotfix (hotfix branch - osobny worktree)
# /project-feature (feature branch - osobny worktree)
# Utwórz worktree dla istniejącego brancha
git worktree add ../project-hotfix hotfix/critical
# Utwórz worktree z nowym branchem
git worktree add -b feature/new ../project-feature
# Lista worktrees
git worktree list
# /project abc1234 [main]
# /project-hotfix def5678 [hotfix/critical]
# /project-feature ghi9012 [feature/new]
# Usuń worktree
git worktree remove ../project-hotfix
# Lub ręcznie usuń folder i wyczyść
rm -rf ../project-hotfix
git worktree prune
Kiedy używać worktrees?
1. Szybkie przełączanie kontekstu:
# Zamiast:
git stash
git checkout hotfix
# ... praca ...
git checkout feature
git stash pop
# Użyj worktree:
cd ../project-hotfix
# ... praca ...
cd ../project-feature
# Bez stashowania, bez przełączania
2. Porównywanie kodu między branchami:
git worktree add ../project-v1 v1.0.0
git worktree add ../project-v2 v2.0.0
# Otwórz oba w IDE, porównuj
diff ../project-v1/src ../project-v2/src
3. Równoległe budowanie/testowanie:
# W jednym terminalu
cd ../project-main && npm run build
# W drugim terminalu (równolegle)
cd ../project-feature && npm run build
4. Code review lokalnie:
git worktree add ../review-pr-123 origin/feature/pr-123
cd ../review-pr-123
# Przeglądaj, uruchamiaj, testuj
Ograniczenia worktrees
# Ten sam branch nie może być w dwóch worktrees
git worktree add ../second-main main
# fatal: 'main' is already checked out
# Rozwiązanie: utwórz tymczasowy branch
git worktree add ../second-main -b temp-main main
Reflog i Recovery
Jak działa reflog?
Reflog zapisuje historię gdzie HEAD (i inne refs) wskazywały:
git reflog
# abc1234 HEAD@{0}: commit: Add feature
# def5678 HEAD@{1}: checkout: moving from main to feature
# ghi9012 HEAD@{2}: reset: moving to HEAD~3
# jkl3456 HEAD@{3}: commit: Important work
# ...
# Reflog dla konkretnego brancha
git reflog show main
# Z datami
git reflog --date=relative
# abc1234 HEAD@{5 minutes ago}: commit: Add feature
Odzyskiwanie utraconych commitów
Po reset --hard:
# Ups! Straciłem commity
git reset --hard HEAD~5
# Znajdź utracone commity
git reflog
# abc1234 HEAD@{1}: reset: moving to HEAD~5
# def5678 HEAD@{2}: commit: Important commit <-- ten chcę
# Odzyskaj
git checkout def5678
# lub
git branch recovery def5678
# lub
git reset --hard def5678
Po złym rebase:
# Przed rebase HEAD wskazywał na
git reflog
# abc1234 HEAD@{5}: rebase finished
# def5678 HEAD@{6}: rebase started <-- tu było przed
# Wróć do stanu przed rebase
git reset --hard def5678
Po usunięciu brancha:
git branch -D feature # Ups!
git reflog
# ... znajdź ostatni commit brancha ...
git branch feature abc1234 # Odtwórz branch
ORIG_HEAD - shortcut do poprzedniego stanu
# Po operacjach jak merge, rebase, reset
# ORIG_HEAD wskazuje gdzie HEAD był przed operacją
git reset --hard HEAD~3
# Ups! Za dużo
git reset --hard ORIG_HEAD # Cofnij reset
Git fsck - znajdowanie osieroconych obiektów
# Znajdź obiekty bez referencji (dangling)
git fsck --lost-found
# dangling commit abc1234...
# dangling blob def5678...
# Sprawdź co to za commit
git show abc1234
# Odzyskaj
git branch recovered abc1234
Reset vs Revert
Czym różni się reset od revert?
Reset - przesuwa HEAD (i opcjonalnie index/working directory):
- Zmienia historię
- Niebezpieczny dla pushowanych commitów
- Lokalna operacja
Revert - tworzy nowy commit który cofa zmiany:
- Nie zmienia historii
- Bezpieczny dla publicznych commitów
- Zachowuje ślad że coś było cofnięte
# Reset - usuwa commity z historii
git reset --hard HEAD~3
# A---B---C---D → A (B, C, D "zniknęły")
# Revert - dodaje commit cofający
git revert HEAD~2..HEAD
# A---B---C---D → A---B---C---D---D'---C' (D' cofa D, C' cofa C)
Trzy tryby reset
# --soft: przesuń HEAD, zachowaj index i working directory
git reset --soft HEAD~1
# Commit "znika" ale zmiany są staged
# --mixed (domyślny): przesuń HEAD i index, zachowaj working directory
git reset HEAD~1
# Commit "znika", zmiany są unstaged ale w plikach
# --hard: przesuń HEAD, index i working directory
git reset --hard HEAD~1
# Commit "znika" całkowicie (ale jest w reflog!)
Kiedy używać którego:
# Chcę zmienić ostatni commit message
git reset --soft HEAD~1
git commit -m "Better message"
# Chcę rozdzielić ostatni commit na dwa
git reset HEAD~1
git add file1.js
git commit -m "First part"
git add file2.js
git commit -m "Second part"
# Chcę całkowicie wyrzucić ostatnie zmiany
git reset --hard HEAD~1
Revert pojedynczego i wielu commitów
# Revert ostatniego commita
git revert HEAD
# Revert konkretnego commita
git revert abc1234
# Revert zakresu (od A do B, nie włączając A)
git revert A..B
# Revert bez automatycznego commita
git revert -n abc1234 def5678
git commit -m "Revert features X and Y"
# Revert merge commita (musisz podać parent)
git revert -m 1 merge-commit-sha
# -m 1 = zachowaj zmiany z pierwszego parenta (main)
# -m 2 = zachowaj zmiany z drugiego parenta (feature)
Stash - Zaawansowane Użycie
Podstawy stash
# Schowaj zmiany
git stash
# Schowaj z opisem
git stash push -m "WIP: user authentication"
# Lista stashów
git stash list
# stash@{0}: WIP: user authentication
# stash@{1}: On main: debugging session
# Przywróć i usuń z listy
git stash pop
# Przywróć bez usuwania
git stash apply
# Przywróć konkretny stash
git stash apply stash@{2}
Zaawansowane opcje stash
# Stash włącznie z untracked files
git stash -u
git stash --include-untracked
# Stash wszystko (włącznie z ignored)
git stash -a
git stash --all
# Stash tylko staged changes
git stash --staged
# Stash z zachowaniem index (staged pozostaje staged)
git stash --keep-index
# Interaktywny stash (wybierz co schować)
git stash -p
Zarządzanie stashami
# Zobacz zawartość stasha
git stash show
git stash show -p # Z diffem
git stash show stash@{2}
# Utwórz branch ze stasha
git stash branch new-feature stash@{0}
# Usuń stash
git stash drop stash@{0}
# Usuń wszystkie stashe
git stash clear
Git Hooks
Czym są hooks i jakie są najpopularniejsze?
Hooks to skrypty uruchamiane automatycznie przy operacjach Git:
# Lokalizacja
.git/hooks/
# Popularne hooks:
pre-commit # Przed commit (lint, format)
commit-msg # Walidacja commit message
pre-push # Przed push (testy)
post-merge # Po merge (npm install)
post-checkout # Po checkout
Przykłady hooków
pre-commit - lint i format:
#!/bin/sh
# .git/hooks/pre-commit
# Uruchom lint
npm run lint
if [ $? -ne 0 ]; then
echo "Lint failed. Commit aborted."
exit 1
fi
# Sprawdź czy nie ma console.log
if git diff --cached | grep -E "console\.(log|debug)" > /dev/null; then
echo "Error: console.log found in staged files"
exit 1
fi
exit 0
commit-msg - walidacja formatu:
#!/bin/sh
# .git/hooks/commit-msg
# Conventional Commits format
commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}'
if ! grep -qE "$commit_regex" "$1"; then
echo "Error: Commit message must follow Conventional Commits format"
echo "Example: feat(auth): add login functionality"
exit 1
fi
Husky - zarządzanie hooks w projekcie
# Instalacja
npm install husky --save-dev
npx husky init
# Dodaj hook
echo "npm run lint" > .husky/pre-commit
echo "npm test" > .husky/pre-push
# .husky/commit-msg
npx commitlint --edit $1
// package.json
{
"scripts": {
"prepare": "husky"
}
}
Zaawansowane Komendy
Git blame i log
# Kto zmienił każdą linię
git blame file.js
git blame -L 10,20 file.js # Tylko linie 10-20
# Ignoruj whitespace
git blame -w file.js
# Historia konkretnego pliku
git log -- file.js
git log -p -- file.js # Z diffami
# Znajdź commit który dodał/usunął tekst
git log -S "function oldName" # Pickaxe search
git log -G "regex pattern" # Regex search
# Historia funkcji (jeśli Git rozpoznaje język)
git log -L :functionName:file.js
Git clean
# Pokaż co zostanie usunięte
git clean -n
# Usuń untracked files
git clean -f
# Usuń untracked files i directories
git clean -fd
# Usuń też ignored files
git clean -fdx
# Interaktywnie
git clean -i
Git grep
# Szukaj w tracked files
git grep "pattern"
# Szukaj w konkretnym COMMIT/branchu
git grep "pattern" HEAD~10
git grep "pattern" main
# Z numerami linii
git grep -n "pattern"
# Tylko nazwy plików
git grep -l "pattern"
# Z kontekstem
git grep -C 3 "pattern" # 3 linie przed i po
Git archive
# Eksportuj branch do archiwum
git archive --format=tar main | gzip > release.tar.gz
# Konkretny folder
git archive main src/ | tar -x -C /destination
# ZIP
git archive --format=zip main > release.zip
Workflow Best Practices
Git Flow vs Trunk-Based Development
Git Flow:
main ─────────────────────────────────────
\ /
develop ──●───●───●───●───●──●───●───●───
\ /
feature ──●───●──
Trunk-Based:
main ──●───●───●───●───●───●───●───●───●──
\ / \ / \ /
feature ●───● ●─● ●
Konwencje commit messages
Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer]
feat(auth): add OAuth2 login support
- Implement Google OAuth provider
- Add token refresh mechanism
- Update user model
Closes #123
Typy:
-
feat- nowa funkcjonalność -
fix- naprawa buga -
docs- dokumentacja -
style- formatowanie -
refactor- refaktoryzacja -
test- testy -
chore- maintenance
Aliasy dla produktywności
# ~/.gitconfig
[alias]
st = status -sb
co = checkout
br = branch
ci = commit
unstage = reset HEAD --
last = log -1 HEAD
lg = log --graph --oneline --decorate
undo = reset --soft HEAD~1
amend = commit --amend --no-edit
wip = !git add -A && git commit -m 'WIP'
branches = branch -a
remotes = remote -v
contributors = shortlog -sn
Zadania Praktyczne
1. Cleanup historia przed PR
Masz 10 commitów z "WIP", "fix", "oops". Uporządkuj do 3 logicznych commitów.
git rebase -i HEAD~10
# squash, fixup, reword...
2. Znajdź bug z bisect
Aplikacja działała w v1.0, nie działa w v2.0. Znajdź commit.
git bisect start
git bisect bad HEAD
git bisect good v1.0
# testuj...
3. Hotfix podczas pracy nad feature
Musisz zrobić hotfix na main, ale masz WIP zmiany.
git worktree add ../hotfix main
cd ../hotfix
# ... napraw bug ...
git commit && git push
cd ../project
git worktree remove ../hotfix
4. Odzyskaj po złym rebase
Zrobiłeś git rebase main i wszystko się popsuło.
git reflog
# Znajdź stan przed rebase
git reset --hard HEAD@{N}
Podsumowanie
Zaawansowany Git to kluczowa umiejętność dla każdego doświadczonego developera. Na rozmowach rekrutacyjnych skup się na:
- Rebase vs Merge - kiedy używać, złota zasada
- Interactive rebase - squash, fixup, reword
- Cherry-pick - backporty, selektywne zmiany
- Bisect - binary search przez historię
- Worktrees - równoległa praca na wielu branchach
- Reflog - odzyskiwanie utraconych commitów
- Reset vs Revert - zmiana vs cofanie historii
Pamiętaj: Git to potężne narzędzie które może zarówno uratować jak i zniszczyć twój kod. Reflog jest ostatnią deską ratunku - używaj go mądrze.