Git Zaawansowany - Pytania Rekrutacyjne i Kompletny Przewodnik 2025

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:

  1. Backport hotfixa na starszą wersję:
git checkout release/1.0
git cherry-pick hotfix-commit-sha
  1. Wyciągnij pojedynczą zmianę z feature brancha:
git checkout main
git cherry-pick feature-commit-sha
  1. 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:

  1. Rebase vs Merge - kiedy używać, złota zasada
  2. Interactive rebase - squash, fixup, reword
  3. Cherry-pick - backporty, selektywne zmiany
  4. Bisect - binary search przez historię
  5. Worktrees - równoległa praca na wielu branchach
  6. Reflog - odzyskiwanie utraconych commitów
  7. 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.

Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.