Przejdź do treści
·21 min czytania

Git poza podstawami: Workflow'y, które oszczędzają godziny co tydzień

Interactive rebase, cherry-pick, bisect, worktree, reflog rescue i strategie branchowania, które naprawdę działają. Komendy Git, których używam każdego dnia, a o których większość deweloperów nie wie.

Udostępnij:X / TwitterLinkedIn

Większość deweloperów uczy się pięciu komend Git i na tym kończy. add, commit, push, pull, merge. Może checkout i branch, jeśli czują się odważni. To wystarczy na pierwszy rok. Potem twój branch ma 47 commitów z wiadomościami typu "fix" i "wip" i "proszę zadziałaj", przypadkowo resetujesz coś, czego nie powinieneś, i spędzasz 40 minut na Stack Overflow, próbując cofnąć nieudanego merge'a.

Używam Gita od lat. Nie okazjonalnie — intensywnie. Wiele branchy, wiele repozytoriów, wielu współpracowników, cały dzień, każdego dnia. Poniżej znajdziesz komendy i workflow'y, których naprawdę używam. Nie te, które dobrze wyglądają w tutorialu. Te, które oszczędzają mi realny czas, każdego tygodnia.

Interactive Rebase: Posprzątaj bałagan zanim ktokolwiek go zobaczy#

Twój branch ma dwanaście commitów. Połowa to "fix typo". Jeden mówi "undo previous commit". Kolejny mówi "actually fix it this time". Zaraz otworzysz PR. Nikt nie musi widzieć tej historii.

Interactive rebase pozwala przepisać historię na twoim branchu przed udostępnieniem go. Możesz squashować commity razem, zmienić wiadomości, zmienić kolejność albo je całkowicie usunąć.

Podstawowa komenda#

bash
git rebase -i HEAD~5

To otwiera edytor pokazujący twoje 5 ostatnich commitów, od najstarszego:

bash
pick a1b2c3d Add user authentication endpoint
pick d4e5f6g Fix typo in auth middleware
pick h7i8j9k Add rate limiting
pick l0m1n2o Fix rate limit bug
pick p3q4r5s Update auth tests

Każda linia zaczyna się od komendy. Zmień pick na jedno z tych:

  • squash (lub s) — Połącz ten commit z tym powyżej, połącz wiadomości
  • fixup (lub f) — Tak samo jak squash, ale odrzuć wiadomość tego commita
  • reword (lub r) — Zachowaj commit, ale zmień jego wiadomość
  • drop (lub d) — Usuń ten commit całkowicie
  • edit (lub e) — Zatrzymaj rebase na tym commicie, żebyś mógł go zmodyfikować

Prawdziwa sesja porządkowania#

Oto co naprawdę robię. Ta bałaganiarska historia powyżej staje się:

bash
pick a1b2c3d Add user authentication endpoint
fixup d4e5f6g Fix typo in auth middleware
pick h7i8j9k Add rate limiting
fixup l0m1n2o Fix rate limit bug
pick p3q4r5s Update auth tests

Zapisz i zamknij. Teraz masz trzy czyste commity zamiast pięciu. Fix literówki trafia do commita auth. Fix błędu rate limitu trafia do commita rate limitu. Twój recenzent PR widzi czystą, logiczną progresję.

Zmiana kolejności commitów#

Możesz dosłownie przestawić linie. Jeśli commit testów powinien być przed commitem rate limitingu, po prostu przesuń linię:

bash
pick a1b2c3d Add user authentication endpoint
pick p3q4r5s Update auth tests
pick h7i8j9k Add rate limiting

Git odtworzy twoje commity w nowej kolejności. Jeśli będą konflikty, zatrzyma się i pozwoli ci je rozwiązać.

Skrót Autosquash#

Jeśli wiesz, że commit jest fixem dla poprzedniego, zaznacz to w momencie commita:

bash
git commit --fixup=a1b2c3d

To tworzy commit z wiadomością fixup! Add user authentication endpoint. Potem przy rebase:

bash
git rebase -i --autosquash HEAD~5

Git automatycznie przestawia commity fixup zaraz pod ich cele i oznacza je jako fixup. Po prostu zapisujesz i zamykasz. Żadnego ręcznego edytowania.

Używam tego non-stop. To najszybszy sposób na iterowanie na branchu przy zachowaniu czystej historii końcowej.

Złota zasada#

Nigdy nie robiąc rebase na commitach, które zostały wypchnięte na wspólny branch. Jeśli inni ludzie oparli pracę na tych commitach, przepisywanie historii spowoduje realne problemy. Robiąc rebase na swoich feature branchach przed mergem. Nigdy na main.

Jeśli już wypchnąłeś swój feature branch i musisz zrobić rebase:

bash
git push --force-with-lease

Flaga --force-with-lease jest bezpieczniejsza niż --force. Odmawia wypchnięcia, jeśli ktoś inny wypchnął na ten sam branch od twojego ostatniego fetch. Nie zapobiegnie wszystkim problemom, ale łapie ten najczęstszy.

Cherry-Pick: Chirurgiczne przenoszenie commitów#

Cherry-pick bierze konkretny commit z jednego brancha i aplikuje go na inny. Nie merge. Nie rebase. Tylko jeden commit, czysto zaaplikowany.

Kiedy naprawdę tego używam#

Najczęstszy scenariusz: naprawiłem buga na moim feature branchu, ale fix musi też trafić na main albo branch releasu natychmiast. Nie chcę mergować całego feature brancha. Chcę tylko ten jeden fix.

bash
# Find the commit hash of the fix
git log --oneline feature/user-auth
# a1b2c3d Fix null pointer in session validation
 
# Switch to main and cherry-pick it
git checkout main
git cherry-pick a1b2c3d

Gotowe. Fix jest na main jako nowy commit z tymi samymi zmianami.

Cherry-Pick bez commitowania#

Czasem chcesz zaaplikować zmiany, ale jeszcze ich nie commitować. Może chcesz połączyć kilka cherry-picków w jeden commit albo lekko zmodyfikować zmiany:

bash
git cherry-pick --no-commit a1b2c3d

Zmiany są zastageowane, ale nie commitowane. Możesz je zmodyfikować, dodać więcej zmian, potem commitować gdy gotowe.

Cherry-Pick zakresu#

Potrzebujesz wielu kolejnych commitów? Użyj składni zakresu:

bash
git cherry-pick a1b2c3d..f6g7h8i

To cherry-pickuje wszystko po a1b2c3d aż do i włącznie z f6g7h8i. Zauważ, że sam a1b2c3d jest wykluczony. Jeśli chcesz go włączyć:

bash
git cherry-pick a1b2c3d^..f6g7h8i

Obsługa konfliktów#

Konflikty cherry-pick działają jak konflikty merge. Kiedy jeden wystąpi:

bash
# Git will tell you there's a conflict
# Fix the conflicting files, then:
git add .
git cherry-pick --continue

Albo jeśli zmienisz zdanie:

bash
git cherry-pick --abort

Jedna rzecz do obserwowania: cherry-pickowane commity tworzą nowe hashe commitów. Jeśli później zmergujesz oryginalny branch, Git jest zwykle na tyle sprytny, żeby obsłużyć duplikację. Ale jeśli agresywnie cherry-pickujesz między branchami, które w końcu się zmergują, możesz zobaczyć nieoczekiwane konflikty. Używaj chirurgicznie, nie jako strategii mergowania.

Git Bisect: Wyszukiwanie binarne dla bugów#

Coś się zepsuło. Wiesz, że dwa tygodnie temu działało. Od tego czasu było 200 commitów. Który to zepsuł?

Mógłbyś sprawdzić każdy commit ręcznie. Albo możesz użyć git bisect, które używa wyszukiwania binarnego, żeby znaleźć dokładny commit łamiący w log2(n) krokach. Dla 200 commitów to około 7-8 sprawdzeń zamiast 200.

Sposób ręczny#

bash
# Start bisecting
git bisect start
 
# Mark the current commit as bad (the bug exists here)
git bisect bad
 
# Mark a known good commit (the bug didn't exist here)
git bisect good v2.1.0

Git checkoutuje commit w połowie między dobrym a złym. Testuj go. Potem:

bash
# If the bug exists at this commit:
git bisect bad
 
# If the bug doesn't exist at this commit:
git bisect good

Git zawęża zakres o połowę za każdym razem. Po 7-8 krokach mówi ci:

bash
a1b2c3d4e5f6g7h is the first bad commit
commit a1b2c3d4e5f6g7h
Author: Some Developer <dev@example.com>
Date: Tue Feb 18 14:23:01 2026 +0300
 
    Refactor session handling to use async middleware

Teraz wiesz dokładnie, który commit wprowadził buga. Kiedy skończysz:

bash
git bisect reset

To zabiera cię z powrotem tam, gdzie zacząłeś.

Sposób automatyczny (tu jest prawdziwa siła)#

Jeśli masz test, który potrafi wykryć buga, możesz zautomatyzować cały proces:

bash
git bisect start
git bisect bad HEAD
git bisect good v2.1.0
git bisect run npm test -- --grep "session validation"

Git automatycznie checkoutuje commity, uruchamia test i oznacza je jako dobre lub złe na podstawie kodu wyjścia. Zero oznacza dobry, nie-zero oznacza zły. Odejdź, wróć, a powie ci dokładny commit.

Możesz użyć dowolnego skryptu:

bash
git bisect run ./test-regression.sh

Gdzie test-regression.sh to:

bash
#!/bin/bash
npm run build 2>/dev/null || exit 125  # 125 means "skip this commit"
npm test -- --grep "session" || exit 1  # 1 means "bad"
exit 0                                   # 0 means "good"

Kod wyjścia 125 jest specjalny — mówi bisectowi, żeby pominął ten commit (przydatne, jeśli commit się nie kompiluje). To jedna z tych funkcji, która wydaje się niszowa, aż jej potrzebujesz, a wtedy ratuje ci całe popołudnie.

Prawdziwa sesja Bisect#

Oto jak to wygląda w praktyce:

bash
$ git bisect start
$ git bisect bad HEAD
$ git bisect good abc1234
Bisecting: 97 revisions left to test after this (roughly 7 steps)
[def5678...] Commit message here
 
$ npm test -- --grep "login flow"
# Tests fail
 
$ git bisect bad
Bisecting: 48 revisions left to test after this (roughly 6 steps)
[ghi9012...] Another commit message
 
$ npm test -- --grep "login flow"
# Tests pass
 
$ git bisect good
Bisecting: 24 revisions left to test after this (roughly 5 steps)
...
 
# After ~7 iterations:
abc1234def5678ghi is the first bad commit

Siedem kroków, żeby znaleźć igłę w stogu siana z 200 commitów.

Git Worktrees: Wiele branchy, zero stashowania#

To jest najbardziej niedoceniana funkcja Gita. Jestem przekonany, że większość deweloperów nie wie o jej istnieniu.

Problem: jesteś głęboko w feature branchu. Pliki są pozmienianie wszędzie. Wtedy ktoś mówi "możesz zerknąć na tego buga produkcyjnego?" Masz trzy opcje:

  1. Stashujesz wszystko, przełączasz branch, naprawiasz, przełączasz z powrotem, popujesz stash. Mając nadzieję, że nic nie pójdzie źle.
  2. Commitujesz swoją niedokończoną pracę z wiadomością "wip". Brzydkie, ale funkcjonalne.
  3. Klonujesz repo ponownie do innego katalogu.

Albo opcja 4: worktrees.

Czym jest worktree#

Worktree to drugi (albo trzeci, albo czwarty) katalog roboczy podłączony do tego samego repozytorium. Każdy worktree ma swój własny checkout brancha, swoje pliki robocze, swój własny indeks. Ale dzielą te same dane .git, więc nie duplikujesz całego repo.

Dodawanie worktree#

bash
# You're on feature/user-auth, deep in work
# Need to fix a bug on main:
 
git worktree add ../hotfix-session main

To tworzy nowy katalog ../hotfix-session z checkoutowanym main. Twój obecny katalog pozostaje dokładnie taki, jaki był. Nic nie stashowane, nic nie commitowane, nic nie zakłócone.

bash
cd ../hotfix-session
# Fix the bug
git add .
git commit -m "Fix null pointer in session validation"
git push origin main
cd ../my-project
# Continue working on your feature as if nothing happened

Tworzenie nowego brancha w worktree#

bash
git worktree add ../hotfix-nav -b hotfix/nav-crash main

To tworzy worktree ORAZ tworzy nowy branch hotfix/nav-crash oparty na main.

Zarządzanie worktree#

bash
# List all worktrees
git worktree list
# /home/dev/my-project         abc1234 [feature/user-auth]
# /home/dev/hotfix-session     def5678 [main]
 
# Remove a worktree when done
git worktree remove ../hotfix-session
 
# If the directory was already deleted:
git worktree prune

Dlaczego to lepsze niż stashowanie#

Stashowanie jest w porządku na szybkie przełączenia kontekstu. Ale worktrees są lepsze do czegokolwiek, co trwa dłużej niż pięć minut:

  • Twoje IDE zostaje otwarte na feature branchu. Bez reindeksowania, bez utraty pozycji scrolla.
  • Możesz uruchamiać testy w obu katalogach jednocześnie.
  • Żadnego ryzyka konfliktów stasha ani zapomnienia, co zastashowałeś.
  • Możesz mieć długo działający dev server w jednym worktree i czysty build w innym.

Zwykle trzymam dwa lub trzy aktywne worktrees: mój główny feature branch, worktree z main do szybkich sprawdzeń i czasem worktree do review, gdzie checkout-uję czyjś PR.

Jedno zastrzeżenie#

Nie możesz mieć tego samego brancha checkout-owanego w dwóch worktrees. To celowe — zapobiega dokonywaniu sprzecznych zmian na tym samym branchu w dwóch miejscach. Jeśli spróbujesz, Git odmówi.

Reflog: Przycisk cofnij do wszystkiego#

Zrobiłeś hard reset i straciłeś commity. Usunąłeś branch. Zrobiłeś rebase i coś poszło strasznie źle. Myślisz, że twoja praca przepadła.

Nie przepadła. Git prawie nigdy niczego nie kasuje. Reflog to twoja siatka bezpieczeństwa.

Czym jest reflog#

Za każdym razem, gdy HEAD się przesuwa — każdy commit, checkout, rebase, reset, merge — Git zapisuje to w reflogu. To log każdego miejsca, w którym był twój HEAD, w kolejności.

bash
git reflog

Output:

bash
a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
f4e5d6c HEAD@{1}: commit: Add payment processing
b7a8c9d HEAD@{2}: commit: Update user dashboard
e0f1g2h HEAD@{3}: commit: Fix auth token refresh
i3j4k5l HEAD@{4}: checkout: moving from feature/payments to main

Każdy wpis ma indeks (HEAD@{0}, HEAD@{1} itp.) i opis tego, co się stało.

Odzyskiwanie po hard reset#

Przypadkowo uruchomiłeś git reset --hard HEAD~3 i straciłeś trzy commity. Są tam, w reflogu:

bash
# See what you lost
git reflog
 
# The commit before the reset is HEAD@{1}
git reset --hard f4e5d6c

Wszystkie trzy commity wróciły. Kryzys zażegnany.

Odzyskiwanie usuniętego brancha#

Usunąłeś branch, który miał niezmerowaną pracę:

bash
git branch -D feature/experimental
# Oh no, that had two weeks of work

Commity nadal istnieją. Znajdź je:

bash
git reflog | grep "feature/experimental"
# Or just look through the reflog for the last commit on that branch
 
# Found it. Recreate the branch at that commit:
git branch feature/experimental a1b2c3d

Branch wrócił, ze wszystkimi swoimi commitami.

Odzyskiwanie po złym rebase#

Zrobiłeś rebase i wszystko poszło źle. Konflikty wszędzie, złe commity, chaos:

bash
# The reflog shows where you were before the rebase
git reflog
# a1b2c3d HEAD@{0}: rebase (finish): ...
# ...
# f4e5d6c HEAD@{5}: rebase (start): checkout main
# b7a8c9d HEAD@{6}: commit: Your last good commit
 
# Go back to before the rebase
git reset --hard b7a8c9d

Wracasz dokładnie tam, gdzie byłeś przed rozpoczęciem rebase. Jakby się nigdy nie wydarzył.

30-dniowa siatka bezpieczeństwa#

Domyślnie Git zachowuje wpisy refloga przez 30 dni (90 dni dla osiągalnych commitów). Po tym czasie mogą zostać zebrane przez garbage collector. Więc masz miesiąc na zorientowanie się, że popełniłeś błąd. W praktyce to więcej niż wystarczy.

Możesz sprawdzić termin wygaśnięcia:

bash
git config gc.reflogExpire
# default: 90.days.ago (for reachable)
git config gc.reflogExpireUnreachable
# default: 30.days.ago (for unreachable)

Jeśli jesteś paranoikiem, zwiększ:

bash
git config --global gc.reflogExpireUnreachable "180.days.ago"

Osobista zasada#

Przed każdą destrukcyjną operacją — hard reset, force push, usunięcie brancha — uruchamiam git log --oneline -10 najpierw. Mentalnie notuję obecny HEAD. To zajmuje dwie sekundy i uratowało mnie nie raz od paniki, której nie musiałem mieć.

Stashuj poprawnie: To nie tylko git stash#

Większość ludzi używa stasha tak:

bash
git stash
# do something
git stash pop

To działa, ale to jak wrzucenie wszystkiego do pudełka z etykietą "rzeczy". Kiedy masz trzy stashe, nie masz pojęcia, który jest który.

Nazywaj swoje stashe#

bash
git stash push -m "WIP: user auth form validation"

Teraz kiedy listujesz stashe:

bash
git stash list
# stash@{0}: On feature/auth: WIP: user auth form validation
# stash@{1}: On main: Quick fix attempt for nav bug
# stash@{2}: On feature/payments: Experiment with Stripe webhooks

Widzisz dokładnie, co każdy stash zawiera.

Uwzględniaj nieśledzone pliki#

Domyślnie git stash stashuje tylko śledzone pliki. Nowe pliki, których jeszcze nie dodałeś, zostają:

bash
# Stash everything, including new files
git stash push --include-untracked -m "WIP: new auth components"
 
# Or even include ignored files (rarely needed)
git stash push --all -m "Full workspace snapshot"

Używam --include-untracked prawie za każdym razem. Pozostawienie nowych plików przy przełączaniu branchy powoduje zamieszanie.

Częściowe stashowanie#

To jest to, o czym większość ludzi nie wie. Możesz stashować konkretne pliki:

bash
# Stash only specific files
git stash push -m "Just the auth changes" src/auth/ src/middleware.ts

Albo użyć trybu patch, żeby stashować konkretne kawałki w plikach:

bash
git stash push --patch -m "Partial: only the validation logic"

Git przejdzie przez każdą zmianę interaktywnie i zapyta, czy chcesz ją stashować. y na tak, n na nie, s żeby podzielić kawałek na mniejsze części.

Apply vs Pop#

bash
# Pop: apply and remove from stash list
git stash pop stash@{2}
 
# Apply: apply but keep in stash list
git stash apply stash@{2}

Używam apply, kiedy nie jestem pewien, czy stash zaaplikuje się czysto. Jeśli jest konflikt, stash jest zachowany. Przy pop, jeśli jest konflikt, stash i tak zostaje na liście (wielu ludzi o tym nie wie), ale wolę jawną intencję apply.

Przeglądanie zawartości stasha#

bash
# See what files changed in a stash
git stash show stash@{0}
 
# See the full diff
git stash show -p stash@{0}

Tworzenie brancha ze stasha#

Jeśli twój stash urósł do czegoś poważniejszego:

bash
git stash branch feature/auth-validation stash@{0}

To tworzy nowy branch z commita, na którym pierwotnie stashowałeś, aplikuje stash i go usuwa. Czysto.

Strategie branchowania: Która naprawdę działa#

Są trzy główne strategie branchowania. Każda ma kontekst, w którym świeci, i konteksty, w których powoduje ból.

Gitflow#

Klasyka. main, develop, feature/*, release/*, hotfix/*. Stworzone przez Vincenta Driessena w 2010.

bash
# Feature branch
git checkout -b feature/user-auth develop
# ... work ...
git checkout develop
git merge --no-ff feature/user-auth
 
# Release branch
git checkout -b release/2.1.0 develop
# ... final fixes ...
git checkout main
git merge --no-ff release/2.1.0
git tag -a v2.1.0 -m "Release 2.1.0"
git checkout develop
git merge --no-ff release/2.1.0
 
# Hotfix
git checkout -b hotfix/session-fix main
# ... fix ...
git checkout main
git merge --no-ff hotfix/session-fix
git checkout develop
git merge --no-ff hotfix/session-fix

Kiedy działa: Aplikacje mobilne, oprogramowanie desktopowe, cokolwiek z nazwanymi wersjonowanymi releasy i wieloma wersjami utrzymywanymi jednocześnie. Jeśli wysyłasz v2.1 i v3.0 i musisz łatać obie, Gitflow to obsługuje.

Kiedy nie działa: Aplikacje webowe z ciągłym wdrażaniem. Jeśli deployujesz na produkcję 5 razy dziennie, ceremonia release branchy i develop branchy to czysty narzut. Większość zespołów webowych, które adoptują Gitflow, kończy z branchem develop, który jest wiecznie zepsuty, i release branchami, których nikt nie rozumie.

GitHub Flow#

Proste. Masz main. Tworzysz feature branche. Otwierasz PR-y. Mergujesz do main. Deployujesz main.

bash
git checkout -b feature/user-auth main
# ... work ...
git push origin feature/user-auth
# Open PR, get reviewed, merge
# main is always deployable

Kiedy działa: Małe i średnie zespoły wysyłające aplikacje webowe. Ciągłe wdrażanie. Jeśli main jest zawsze deployowany, to wszystko, czego potrzebujesz. Z tego korzysta ta strona.

Kiedy nie działa: Kiedy musisz utrzymywać wiele wersji release, albo kiedy masz długi cykl QA przed wdrożeniem. GitHub Flow zakłada, że main szybko trafia na produkcję.

Trunk-Based Development#

Wszyscy commitują do main ("trunk") bezpośrednio albo przez bardzo krótko żyjące branche (krócej niż dzień). Bez długo żyjących feature branchy.

bash
# Short-lived branch (merged same day)
git checkout -b fix/auth-token main
# ... small, focused change ...
git push origin fix/auth-token
# PR reviewed and merged within hours, not days

Kiedy działa: Wysoko wydajne zespoły z dobrym CI/CD, kompleksowymi zestawami testów i feature flagami. Google, Meta i większość dużych firm technologicznych używa trunk-based development. Wymusza to małe, przyrostowe zmiany i eliminuje merge hell.

Kiedy nie działa: Zespoły bez dobrego pokrycia testami ani CI. Jeśli merge do trunk oznacza deployowanie nietestowanego kodu, będziesz łamać produkcję stale. Potrzebujesz też feature flag do czegokolwiek, co trwa dłużej niż dzień:

bash
# Feature flag in code
if (featureFlags.isEnabled('new-checkout-flow')) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}

Moja rekomendacja#

Dla większości zespołów web developmentowych: zacznij od GitHub Flow. Jest prosty, działa i nie wymaga toolingu poza tym, co GitHub/GitLab już zapewnia.

Jeśli twój zespół urośnie powyżej 15-20 inżynierów i deployujecie wiele razy dziennie, rozważ trunk-based development z feature flagami. Inwestycja w infrastrukturę feature flag zwraca się w zmniejszonej liczbie merge konfliktów i szybszej iteracji.

Jeśli wysyłasz wersjonowane oprogramowanie (aplikacje mobilne, narzędzia CLI, biblioteki): Gitflow albo jego uproszczona wersja. Faktycznie potrzebujesz tych release branchy.

Nie wybieraj strategii, bo post na blogu powiedział, że jest najlepsza. Wybierz tę, która pasuje do tego, jak naprawdę wysyłasz.

Git Hooks: Automatyzuj rzeczy, o których ciągle zapominasz#

Git hooks to skrypty, które uruchamiają się automatycznie w określonych momentach workflow'u Git. Są lokalne na twojej maszynie (nie są pushowane na remote), co oznacza, że potrzebujesz sposobu na dzielenie się nimi z zespołem.

Hooki, które naprawdę mają znaczenie#

pre-commit — Uruchamia się przed każdym commitem. Użyj do lintingu i formatowania:

bash
#!/bin/bash
# .git/hooks/pre-commit
 
# Run ESLint on staged files only
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|tsx)$')
 
if [ -n "$STAGED_FILES" ]; then
  echo "Running ESLint on staged files..."
  npx eslint $STAGED_FILES --quiet
  if [ $? -ne 0 ]; then
    echo "ESLint failed. Fix errors before committing."
    exit 1
  fi
fi
 
# Run Prettier on staged files
if [ -n "$STAGED_FILES" ]; then
  echo "Running Prettier..."
  npx prettier --check $STAGED_FILES
  if [ $? -ne 0 ]; then
    echo "Prettier check failed. Run 'npx prettier --write' first."
    exit 1
  fi
fi

commit-msg — Waliduje format wiadomości commita. Idealny do wymuszania conventional commits:

bash
#!/bin/bash
# .git/hooks/commit-msg
 
COMMIT_MSG=$(cat "$1")
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,72}"
 
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
  echo "Invalid commit message format."
  echo "Expected: type(scope): description"
  echo "Example: feat(auth): add session refresh endpoint"
  echo ""
  echo "Valid types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert"
  exit 1
fi

pre-push — Uruchamia się przed pushem. Użyj do testów:

bash
#!/bin/bash
# .git/hooks/pre-push
 
echo "Running tests before push..."
npm test
if [ $? -ne 0 ]; then
  echo "Tests failed. Push aborted."
  exit 1
fi

Husky vs natywne hooki#

Natywne hooki żyją w .git/hooks/. Problem: katalog .git nie jest śledzony przez Git, więc nie możesz dzielić się hookami przez repo. Każdy musi je skonfigurować ręcznie.

Husky rozwiązuje to. Przechowuje konfiguracje hooków w repo i automatycznie ustawia .git/hooks przy npm install:

bash
npx husky init

To tworzy katalog .husky/. Dodawaj hooki jako pliki:

bash
# .husky/pre-commit
npx lint-staged

W połączeniu z lint-staged dostajesz szybkie, celowane sprawdzenia pre-commit:

json
{
  "lint-staged": {
    "*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.css": ["prettier --write"],
    "*.json": ["prettier --write"]
  }
}

To uruchamia ESLint i Prettier tylko na plikach, które faktycznie commitujesz. Nie na całym codebase. Szybko.

Kiedy pomijać hooki#

Czasem musisz commitować bez uruchamiania hooków. Awaryjne hotfixy, commity work-in-progress na swoim branchu:

bash
git commit --no-verify -m "WIP: debugging production issue"

Używaj tego oszczędnie. Jeśli regularnie pomijasz hooki, twoje hooki są prawdopodobnie zbyt wolne albo zbyt restrykcyjne.

Aliasy, które oszczędzają czas#

Mój .gitconfig ewoluował przez lata. To są aliasy, które przetrwały — te, których faktycznie używam codziennie, nie te, które dodałem bo wyglądały sprytnie.

Ładny log#

Domyślny git log jest gadatliwy. To daje ci czysty, kolorowy, grafowy widok:

bash
git config --global alias.lg "log --oneline --graph --all --decorate"

Użycie:

bash
git lg
# * a1b2c3d (HEAD -> main) Fix session validation
# | * d4e5f6g (feature/payments) Add Stripe integration
# | * h7i8j9k Update payment models
# |/
# * l0m1n2o Merge PR #42
# * p3q4r5s Add user dashboard

Uruchamiam to 20 razy dziennie. To najszybszy sposób na zrozumienie stanu repozytorium.

Cofnij ostatni commit#

Zachowaj zmiany, po prostu cofnij commit:

bash
git config --global alias.undo "reset HEAD~1 --mixed"

Użycie:

bash
git undo
# Commit is gone, but all changes are still in your working directory

Używam tego, kiedy commitnę za wcześnie, zapomnę o pliku albo chcę zrestrukturyzować zmiany.

Unstage'uj wszystko#

bash
git config --global alias.unstage "reset HEAD --"

Użycie:

bash
git unstage src/auth/session.ts
# File is unstaged but changes are preserved

Lista wszystkich aliasów#

Bo zapomnisz, co ustawiłeś:

bash
git config --global alias.aliases "config --get-regexp ^alias\\."

Więcej aliasów, których używam#

bash
# Show what you're about to commit
git config --global alias.staged "diff --staged"
 
# Short status
git config --global alias.st "status -sb"
 
# Amend without changing the message
git config --global alias.amend "commit --amend --no-edit"
 
# Show the last commit
git config --global alias.last "log -1 HEAD --stat"
 
# Pull with rebase instead of merge
git config --global alias.up "pull --rebase --autostash"
 
# Delete branches that have been merged to main
git config --global alias.cleanup "!git branch --merged main | grep -v '^[ *]*main$' | xargs git branch -d"

Alias up jest szczególnie dobry. pull --rebase utrzymuje twoją historię liniową zamiast tworzenia merge commitów przy każdym pull. --autostash automatycznie stashuje i przywraca twoje zmiany, jeśli masz brudne pliki. To jest to, czym pull powinien być domyślnie.

Alias cleanup kasuje lokalne branche, które zostały zmergowane do main. Z czasem gromadzisz dziesiątki nieaktualnych branchy. Uruchamiaj to co tydzień.

Komendy, których używam codziennie#

To nie są aliasy ani zaawansowane funkcje. To po prostu komendy, które uruchamiam ciągle, a o których wielu deweloperów wydaje się nie wiedzieć.

Najlepsza komenda logu#

bash
git log --oneline --graph --all

To pokazuje każdy branch, każdy merge, całą topologię twojego repo w kompaktowym widoku. To pierwsza rzecz, którą uruchamiam po pullowaniu zmian. Odpowiada na "co się dzieje w tym repo teraz?"

Diff zmian na stage#

bash
git diff --staged

To pokazuje, co ma zostać commitowane. Nie co się zmieniło w twoim katalogu roboczym — co jest faktycznie na stage. Zawsze uruchamiam to przed commitem. Zawsze. To łapie przypadkowe włączenia, debug statementy, console.logi, których tam nie powinno być.

bash
# See unstaged changes
git diff
 
# See staged changes
git diff --staged
 
# See both
git diff HEAD

Pokaż konkretny commit#

bash
git show a1b2c3d

Pokazuje pełny diff pojedynczego commita. Przydatne przy przeglądaniu historii, rozumieniu, co commit naprawdę zmienił.

bash
# Show just the files that changed
git show --stat a1b2c3d
 
# Show a specific file at a specific commit
git show a1b2c3d:src/auth/session.ts

Ta ostatnia jest niesamowicie przydatna. Możesz przeglądać dowolny plik w dowolnym punkcie historii bez checkout-owania tego commita.

Blame z zakresami linii#

bash
git blame -L 42,60 src/auth/session.ts

Pokazuje, kto ostatnio zmodyfikował linie 42-60. Bardziej przydatne niż blame na całym pliku, co jest zwykle przytłaczające.

bash
# Show the commit message too, not just the hash
git blame -L 42,60 --show-name src/auth/session.ts
 
# Ignore whitespace changes (very useful)
git blame -w -L 42,60 src/auth/session.ts
 
# Show the commit before the blamed one (dig deeper)
git log --follow -p -- src/auth/session.ts

Flaga -w jest ważna. Bez niej blame przypisze linie osobie, która ostatnio przeformatowała plik, co rzadko jest osobą, której szukasz.

Znajdź string w całej historii#

bash
# Find when a function was added or removed
git log -S "validateSession" --oneline
 
# Find when a regex pattern appeared
git log -G "session.*timeout" --oneline

-S ("pickaxe") znajduje commity, w których zmieniła się liczba wystąpień stringa. -G znajduje commity, w których diff pasuje do regexa. Oba są potężne do archeologii — ustalania, kiedy coś zostało wprowadzone lub usunięte.

Pokaż, co się zmieniło między dwoma punktami#

bash
# What changed between two branches
git diff main..feature/auth
 
# What changed on this branch since it diverged from main
git diff main...feature/auth
 
# List just the changed files
git diff main...feature/auth --name-only
 
# Stat view (files + insertions/deletions)
git diff main...feature/auth --stat

Dwie kropki vs trzy kropki ma znaczenie. Dwie kropki pokazują różnicę między wierzchołkami obu branchy. Trzy kropki pokazują, co się zmieniło po prawej stronie od momentu, gdy rozeszła się z lewą stroną. Trzy kropki to zwykle to, czego chcesz przy przeglądaniu feature brancha.

Posprzątaj nieśledzone pliki#

bash
# See what would be deleted (dry run)
git clean -n
 
# Delete untracked files
git clean -f
 
# Delete untracked files and directories
git clean -fd
 
# Delete untracked and ignored files (nuclear option)
git clean -fdx

Zawsze uruchamiaj z -n najpierw. git clean -fdx usunie twoje node_modules, .env, artefakty builda — wszystko, co nie jest śledzone przez Git. Przydatne do naprawdę świeżego startu, ale destrukcyjne.

Przywróć pojedynczy plik z innego brancha#

bash
# Get the main branch version of a file without switching branches
git restore --source main -- src/config/database.ts

Albo z konkretnego commita:

bash
git restore --source a1b2c3d -- src/config/database.ts

To jest czystsze niż stara składnia git checkout main -- path/to/file i nie wpływa na HEAD.

Składając wszystko razem: prawdziwy workflow#

Oto jak wygląda typowy dzień z tymi narzędziami:

bash
# Morning: check what's happening
git lg
git fetch --all
 
# Start a feature
git checkout -b feature/session-refresh main
 
# Work, commit incrementally
git add -p                        # Stage specific hunks, not entire files
git commit -m "Add token refresh endpoint"
git commit -m "Add refresh token rotation"
git commit -m "Fix: handle expired refresh tokens"
git commit -m "Add integration tests"
 
# Interrupted: need to fix a production bug
git worktree add ../hotfix main
cd ../hotfix
# ... fix, commit, push, PR merged ...
cd ../my-project
git worktree remove ../hotfix
 
# Back to feature work
git commit -m "Fix edge case in token validation"
 
# Ready for PR: clean up history
git rebase -i main
# Squash the fix commits, reword for clarity
 
# Push and open PR
git push -u origin feature/session-refresh
 
# Something broke in staging? Find out which commit:
git bisect start
git bisect bad HEAD
git bisect good main
git bisect run npm test
 
# Oops, I hard-reset the wrong thing
git reflog
git reset --hard HEAD@{2}

Każda z tych komend zajmuje sekundy. Razem oszczędzają godziny. Nie hipotetyczne godziny — prawdziwe godziny, każdego tygodnia, które w innym przypadku spędziłbym na rozplątywaniu bałaganu w Git, ręcznym szukaniu bugów albo traceniu pracy na przełączeniach kontekstu.

Git to narzędzie, które nagradza głębokość. Podstawy wystarczą na codzień. Ale komendy w tym poście to to, co oddziela "używam Gita" od "Git faktycznie mnie przyspiesza". Ucz się ich przyrostowo. Wybierz jedną nową technikę w tym tygodniu i używaj jej, aż stanie się pamięcią mięśniową. Potem wybierz następną.

Twoje przyszłe ja, wpatrujące się w buga produkcyjnego o 23, podziękuje ci za znajomość git bisect.

Powiązane wpisy