Git Hooks ile Commit Öncesi Otomatik Kod Kontrolü
Yıllar önce bir üretim ortamında yaşanan küçük bir ihmal, saatlerce süren bir geri alma sürecine yol açmıştı. Bir geliştirici, local’de test etmeden debug console.log satırlarını commit’leyip push etmişti. Ardından CI/CD pipeline devreye girmişti, testler geçmişti, kod production’a çıkmıştı. Müşteri panelinde “DEBUG: user password hash” yazan bir log mesajı, o geceyi herkes için çok uzun bir geceye çevirdi. İşte o günden sonra ekibimizde git hooks konuşulmaya başlandı.
Git Hooks Nedir ve Neden Önemlidir
Git, belirli olaylar gerçekleştiğinde otomatik olarak çalışan betikler tanımlamanıza olanak verir. Bu betiklere “hook” denir. .git/hooks/ dizinine bakarsanız zaten örnek dosyalar göreceksiniz; .sample uzantılıyla gelirler ve aktif değillerdir. Uzantıyı kaldırdığınız anda ilgili olay tetiklendiğinde çalışmaya başlarlar.
Pre-commit hook’u, git commit komutu çalıştırıldığında, commit mesajı yazılmadan hemen önce devreye girer. Eğer hook sıfır dışında bir çıkış kodu döndürürse commit iptal edilir. Bu mekanizma sayesinde kötü kodu repository’ye girmeden önce durdurmak mümkün olur.
Peki neden CI/CD yetmez? Çünkü CI/CD pipeline zaten remote’a push ettikten sonra çalışır. Hata o noktada yakalanmış olsa bile commit tarihi kirlenmiş, branch karışmış, belki başka ekip arkadaşları o branch’i çekmiş olabilir. Pre-commit hook ise sorunu en erken noktada, geliştiricinin kendi makinesinde yakalar. “Shift left” prensibinin en somut uygulaması budur.
Temel Pre-commit Hook Yapısı
Başlamadan önce hook dosyasının çalıştırılabilir olması gerektiğini hatırlatayım. Bu detayı atlayan çok sayıda ekip gördüm; hook çalışmıyor diye saatlerce uğraşılıyor, sonunda chmod meselesi olduğu anlaşılıyor.
touch .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
En basit pre-commit hook şöyle görünür:
#!/bin/bash
echo "Pre-commit kontrolleri başlıyor..."
# Hook'tan çıkarken kullanılacak durum kodu
exit_code=0
# Staged dosyaları al
staged_files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$staged_files" ]; then
echo "Kontrol edilecek staged dosya bulunamadı."
exit 0
fi
echo "Kontrol edilecek dosyalar:"
echo "$staged_files"
exit $exit_code
Bu iskelet üzerine istediğiniz kontrolleri ekleyebilirsiniz. --diff-filter=ACM parametresi sadece eklenen (Added), kopyalanan (Copied) ve değiştirilen (Modified) dosyaları getirir; silinen dosyaları listeye dahil etmez çünkü silinmiş dosyalar üzerinde analiz yapamazsınız.
ESLint Entegrasyonu
JavaScript ve TypeScript projelerde ESLint vazgeçilmez. Hook içinde ESLint’i çalıştırmak için önce staged dosyaları filtreleyin:
#!/bin/bash
set -e
echo "ESLint kontrolü yapılıyor..."
# Sadece JS ve TS dosyalarını filtrele
js_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '.(js|jsx|ts|tsx)$' || true)
if [ -z "$js_files" ]; then
echo "Kontrol edilecek JavaScript/TypeScript dosyası yok."
exit 0
fi
# ESLint'in kurulu olup olmadığını kontrol et
if ! command -v npx &> /dev/null; then
echo "HATA: npx bulunamadı. Node.js kurulu mu?"
exit 1
fi
# ESLint'i çalıştır
echo "Analiz ediliyor: $js_files"
npx eslint --max-warnings=0 $js_files
if [ $? -ne 0 ]; then
echo ""
echo "ESLint hataları bulundu. Lütfen düzeltin ve tekrar deneyin."
echo "Acil durumlarda: git commit --no-verify"
exit 1
fi
echo "ESLint kontrolü başarılı."
Burada --max-warnings=0 parametresi dikkat çekici. Warning’leri de hata gibi işliyoruz. Bunu tartışan ekipler olacaktır; bazıları çok katı bulur. Ben ise şunu gözlemledim: “bu sadece warning, geçsin” mantığıyla başlayan şeyler zamanla kontrolden çıkar. Teknik borç birikir. Warning sayısı 0’dan 50’ye, oradan 200’e çıkar ve artık kimse umursamaz hale gelir. Baştan sıfır tolerans politikası uygulamak daha temiz bir codebase sağlar.
Prettier ile Kod Formatlama Kontrolü
ESLint linting yapar ama formatlama konusunda Prettier daha yaygın kullanılır. İkisini birlikte kullanmak mümkün ve yaygın:
# Prettier kontrolü - formatlama uyumunu test et
check_prettier() {
local prettier_files=$(git diff --cached --name-only --diff-filter=ACM |
grep -E '.(js|jsx|ts|tsx|css|scss|json|md)$' || true)
if [ -z "$prettier_files" ]; then
return 0
fi
echo "Prettier format kontrolü..."
if ! npx prettier --check $prettier_files 2>&1; then
echo ""
echo "HATA: Bazı dosyalar Prettier formatına uygun değil."
echo "Otomatik düzeltme için: npx prettier --write $prettier_files"
echo "Ardından değişiklikleri tekrar stage'leyin: git add $prettier_files"
return 1
fi
echo "Prettier kontrolü geçti."
return 0
}
Burada bir püf nokta var: Prettier’ı otomatik düzeltme modunda çalıştırıp ardından değişiklikleri otomatik stage’lemek cazip görünür. Bunu yapan hook’lar gördüm. Ama önermiyorum çünkü geliştiricinin haberi olmadan dosyalar değişiyor ve stage’leniyor; bu da beklenmedik davranışlara yol açabiliyor. Özellikle kısmi stage yapıldığında, yani bazı değişiklikler kasıtlı olarak dışarıda bırakıldığında, Prettier’ın otomatik çalışması o ayrımı bozuyor. Daha iyi olan, geliştiriciye ne yapması gerektiğini söyleyip bırakmak.
Python Projeleri için Flake8 ve Black Entegrasyonu
Python ekipleri için benzer bir yaklaşım gerekli:
#!/bin/bash
echo "Python kod kalite kontrolleri..."
python_files=$(git diff --cached --name-only --diff-filter=ACM | grep '.py$' || true)
if [ -z "$python_files" ]; then
echo "Python dosyası bulunamadı, atlanıyor."
exit 0
fi
# Virtual environment aktif mi kontrol et
if [ -z "$VIRTUAL_ENV" ] && [ ! -f ".venv/bin/activate" ]; then
echo "UYARI: Virtual environment tespit edilemedi."
fi
# Black format kontrolü
echo "Black format kontrolü yapılıyor..."
if command -v black &> /dev/null; then
black --check --diff $python_files
if [ $? -ne 0 ]; then
echo "Formatlama hatası. Düzeltmek için: black $python_files"
exit 1
fi
else
echo "UYARI: Black bulunamadı. pip install black"
fi
# Flake8 linting
echo "Flake8 analizi yapılıyor..."
if command -v flake8 &> /dev/null; then
flake8 --max-line-length=88 --extend-ignore=E203,W503 $python_files
if [ $? -ne 0 ]; then
echo "Flake8 hataları tespit edildi."
exit 1
fi
else
echo "UYARI: Flake8 bulunamadı. pip install flake8"
fi
echo "Python kontrolleri tamamlandı."
--max-line-length=88 neden 88? Çünkü Black’in varsayılan satır uzunluğu 88 karakter ve ikisini uyumlu çalıştırmak için aynı değeri vermeniz gerekiyor. Flake8 varsayılan olarak 79 karakter kullanır, bu da Black ile sürekli çelişkiye yol açar. Yıllar önce bu uyumsuzlukla boğuşan ekipler gördüm; sebebi anlamak saatler aldı.
SonarQube ile Entegrasyon
SonarQube’u pre-commit hook’a entegre etmek biraz farklı bir yaklaşım gerektirir. SonarQube tam analizi ağır bir işlem olduğundan, hook içinde tüm projeyi analiz ettirmek pratik değildir. Bunun yerine SonarLint CLI veya sadece belirli kuralları uygulayan hafif bir kontrol yapılabilir.
#!/bin/bash
# SonarLint CLI ile pre-commit analizi
# sonar-scanner ve sonar-lint-cli kurulu olmalı
check_sonar_issues() {
local changed_files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$changed_files" ]; then
return 0
fi
# SonarLint CLI kurulu mu?
if ! command -v sonarlint &> /dev/null; then
echo "UYARI: SonarLint CLI bulunamadı. Sonar kontrolü atlanıyor."
echo "Kurulum: https://docs.sonarcloud.io/improving/sonarlint/"
return 0
fi
echo "SonarLint analizi başlatılıyor..."
# Proje konfigürasyonunu oku
if [ ! -f "sonar-project.properties" ]; then
echo "UYARI: sonar-project.properties bulunamadı."
return 0
fi
sonarlint analyze
--src "$changed_files"
--output-format json
--severity BLOCKER,CRITICAL
2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
issues = data.get('issues', [])
if issues:
print(f'SonarLint {len(issues)} kritik sorun buldu:')
for issue in issues:
print(f' [{issue["severity"]}] {issue["message"]} - {issue["file"]}:{issue["line"]}')
sys.exit(1)
else:
print('SonarLint kontrolü geçti.')
sys.exit(0)
"
return $?
}
check_sonar_issues
Pratikte SonarQube’u CI/CD’de tam analizle, pre-commit hook’ta ise SonarLint ile kritik seviye kontrol olarak kullanmak en verimli yaklaşımdır. BLOCKER ve CRITICAL seviyedeki sorunları pre-commit’te durdurup, MAJOR ve altını CI’a bırakmak makul bir denge sağlar.
Hassas Bilgi Tarama Kontrolü
Açıkçası bu benim en çok değer verdiğim kontroldür. Yazının başındaki anekdotu hatırlayın; API key’ler, şifreler, token’lar commit’e sızmadan önce yakalanmalıdır:
#!/bin/bash
check_secrets() {
echo "Hassas bilgi taraması yapılıyor..."
local staged_content=$(git diff --cached)
local found_issues=0
# Yaygın hassas bilgi kalıpları
declare -A patterns
patterns["AWS Access Key"]="AKIA[0-9A-Z]{16}"
patterns["AWS Secret Key"]="[0-9a-zA-Z/+]{40}"
patterns["GitHub Token"]="ghp_[0-9a-zA-Z]{36}"
patterns["Generic API Key"]="api[_-]?key[[:space:]]*=[[:space:]]*['"][^'"]{10,}['"]"
patterns["Generic Password"]="password[[:space:]]*=[[:space:]]*['"][^'"]{6,}['"]"
patterns["Private Key Header"]="-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----"
patterns["Database URL"]="(postgres|mysql|mongodb)://[^:]+:[^@]+@"
for pattern_name in "${!patterns[@]}"; do
if echo "$staged_content" | grep -qP "${patterns[$pattern_name]}" 2>/dev/null; then
echo "UYARI: Potansiyel '$pattern_name' tespit edildi!"
found_issues=1
fi
done
if [ $found_issues -ne 0 ]; then
echo ""
echo "Hassas bilgi içerebilecek satırlar tespit edildi."
echo "Lütfen commit'i inceleyin. Yanlış pozitifse --no-verify kullanın."
return 1
fi
echo "Hassas bilgi taraması temiz."
return 0
}
check_secrets
Bu kontrolü atlatmanın yolu --no-verify bayrağıdır. Bunu kaldıramayız çünkü gerçekten acil durumlar olur. Ama --no-verify kullanıldığında bir audit log tutmak akıllıca olur. Bunu pre-push hook’ta yapabilirsiniz.
Husky ile Takım Genelinde Dağıtım
Kendi hook’unuzu yazdınız, çalışıyor, harika. Ama şu sorun var: .git/hooks/ dizini .gitignore‘da yer alır ve repository’ye commit edilemez. Yani her ekip üyesi aynı hook’u kendisi kurmak zorunda. Bu da pratikte asla tam anlamıyla olmayan bir şey.
Bu sorunu çözmek için iki yaygın yaklaşım var: Husky (Node.js projeleri için) ve pre-commit framework (Python tabanlı, dil bağımsız).
Husky kurulumu:
# Husky kurulumu
npm install --save-dev husky
# Husky'yi başlat
npx husky init
# Bu komut .husky/ dizini oluşturur ve package.json'a prepare script ekler
# .husky/pre-commit dosyasını düzenleyin
cat > .husky/pre-commit << 'EOF'
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo "Pre-commit kontrolleri çalışıyor..."
# ESLint
npx eslint --max-warnings=0 $(git diff --cached --name-only --diff-filter=ACM | grep -E '.(js|ts)$' | xargs)
# Prettier
npx prettier --check $(git diff --cached --name-only --diff-filter=ACM | grep -E '.(js|ts|json)$' | xargs)
EOF
chmod +x .husky/pre-commit
package.json‘da prepare scripti sayesinde ekip arkadaşı npm install yaptığında Husky otomatik kurulur. Bu, hook’ların ekip genelinde standart hale gelmesini sağlar.
Kapsamlı Pre-commit Hook Örneği
Tüm kontrolleri bir araya getiren, modüler bir yapı:
#!/bin/bash
# ============================================
# Ekip Standart Pre-commit Hook'u
# Versiyon: 2.1
# Son güncelleme: 2024
# ============================================
set -e
PASS="33[0;32m✓33[0m"
FAIL="33[0;31m✗33[0m"
WARN="33[1;33m!33[0m"
pass() { echo -e "$PASS $1"; }
fail() { echo -e "$FAIL $1"; }
warn() { echo -e "$WARN $1"; }
echo "================================================"
echo "Pre-commit Kalite Kontrolleri"
echo "================================================"
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
EXIT_CODE=0
# --- 1. Hassas bilgi taraması ---
echo ""
echo "[1/4] Hassas bilgi taraması..."
if echo "$STAGED_FILES" | xargs -I{} git show :"{}" 2>/dev/null |
grep -qiE "(api_key|secret_key|password|private_key)s*=s*['"][^'"]{6,}['"]"; then
fail "Potansiyel hassas bilgi tespit edildi!"
EXIT_CODE=1
else
pass "Hassas bilgi taraması temiz"
fi
# --- 2. ESLint ---
JS_FILES=$(echo "$STAGED_FILES" | grep -E '.(js|ts|jsx|tsx)$' || true)
if [ -n "$JS_FILES" ]; then
echo ""
echo "[2/4] ESLint kontrolü..."
if npx eslint --max-warnings=0 $JS_FILES 2>&1; then
pass "ESLint kontrolü başarılı"
else
fail "ESLint hataları var"
EXIT_CODE=1
fi
else
warn "ESLint: Kontrol edilecek dosya yok"
fi
# --- 3. Unit testler (sadece değişen modüller) ---
echo ""
echo "[3/4] İlgili unit testler çalıştırılıyor..."
TEST_FILES=$(echo "$STAGED_FILES" | grep -E '.(js|ts)$' |
sed 's/src//test//g' | sed 's/.(js|ts)$/.test.$1/g' || true)
if [ -n "$TEST_FILES" ]; then
EXISTING_TESTS=$(echo "$TEST_FILES" | xargs -I{} sh -c 'test -f "{}" && echo "{}"' 2>/dev/null || true)
if [ -n "$EXISTING_TESTS" ]; then
if npx jest $EXISTING_TESTS --passWithNoTests 2>&1; then
pass "Unit testler geçti"
else
fail "Unit testler başarısız"
EXIT_CODE=1
fi
else
warn "Test dosyası bulunamadı, atlanıyor"
fi
fi
# --- 4. Commit mesajı formatı (commit-msg hook'ta da yapılabilir) ---
echo ""
echo "[4/4] Debug kalıntı kontrolü..."
DEBUG_PATTERNS="console.log|debugger|binding.pry|byebug|dd(|var_dump("
if echo "$STAGED_FILES" | xargs -I{} git show :"{}" 2>/dev/null | grep -qE "$DEBUG_PATTERNS"; then
fail "Debug ifadeleri tespit edildi! (console.log, debugger vb.)"
EXIT_CODE=1
else
pass "Debug kontrolü temiz"
fi
echo ""
echo "================================================"
if [ $EXIT_CODE -eq 0 ]; then
echo -e "$PASS Tüm kontroller başarılı. Commit yapılıyor..."
else
echo -e "$FAIL Kontroller başarısız. Commit iptal edildi."
echo ""
echo "Acil durumlarda: git commit --no-verify"
echo "Ancak --no-verify kullanımı takım liderine bildirilmeli!"
fi
echo "================================================"
exit $EXIT_CODE
Performans Optimizasyonu
Pre-commit hook’lar geliştiricinin günlük akışında çalışır. Yavaş hook, geliştirici deneyimini olumsuz etkiler ve zamanla insanlar --no-verify kullanmaya başlar. Bu da tüm sistemi anlamsız kılar.
Birkaç performans ipucu:
- Sadece staged dosyaları analiz edin: Tüm projeyi taramak yerine
git diff --cachedile değişen dosyaları hedefleyin. - Paralel çalıştırın: Bağımsız kontrolleri arka planda paralel başlatıp sonunda bekleyin.
- Önbellekleme kullanın: ESLint’in
--cachebayrağı, değişmeyen dosyaları tekrar analiz etmez. - Zaman aşımı belirleyin: Bir kontrol 30 saniyeden uzun sürüyorsa warning verip geçin, bloklama yapmayın.
# ESLint önbellekleme ile
npx eslint --cache --cache-location .eslintcache --max-warnings=0 $JS_FILES
# .gitignore'a eklemeyi unutmayın
echo ".eslintcache" >> .gitignore
Commit-msg Hook ile Conventional Commits Zorunluluğu
Pre-commit’e ek olarak commit mesajı formatını da otomatik kontrol edebilirsiniz:
#!/bin/bash
# .git/hooks/commit-msg
COMMIT_MSG=$(cat "$1")
# Conventional Commits formatı: type(scope): description
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)((.+))?: .{1,100}$"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "HATA: Commit mesajı Conventional Commits formatına uymuyor."
echo ""
echo "Beklenen format: type(scope): açıklama"
echo "Örnekler:"
echo " feat(auth): kullanıcı girişine 2FA eklendi"
echo " fix(api): null pointer exception düzeltildi"
echo " docs: README güncellendi"
echo ""
echo "Geçerli tipler: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert"
exit 1
fi
exit 0
Bu hook sayesinde git log geçmişiniz okunabilir ve otomatik changelog üretilebilir hale gelir.
Yaygın Sorunlar ve Çözümleri
Ekiplerde hook kurulumu sırasında sık karşılaşılan durumlar:
- Hook çalışmıyor:
chmod +x .git/hooks/pre-commitunutulmuş olabilir. Shebang satırı (#!/bin/bash) eksik olabilir. - PATH sorunları: Hook’lar terminal oturumunuzun PATH’ini miras almaz.
node,npx,pythongibi komutları tam yoluyla belirtin ya daexport PATHsatırı ekleyin. - Windows uyumsuzluğu: Git Bash kullanan Windows geliştiricileri için satır sonu karakterleri (CRLF/LF) sorun yaratabilir.
.gitattributesile yönetin. - Çok yavaş hook: 10 saniyeyi geçen kontroller için CI’a taşıyın. Pre-commit’te sadece hızlı kontroller kalmalı.
- Yanlış pozitifler:
--no-verifykanalını açık tutun ama kullanımı kayıt altına alın.
Sonuç
Git hooks, kod kalitesini sağlamanın en maliyet etkin yollarından biridir. Bir CI/CD döngüsü dakikalar alır; bir pre-commit hook saniyeler. Hatayı ne kadar erken yakalarsanız düzeltme maliyeti o kadar düşer.
Başlamak için karmaşık bir kurulum şart değil. Bugün tek bir kontrolle başlayın: sadece debug console.log’larını yakalayan beş satırlık bir hook bile fark yaratır. Zaman içinde kontrolleri büyütün, Husky veya pre-commit framework ile standart hale getirin, ekip alışınca daha kapsamlı analizler ekleyin.
En önemli nokta şu: hook’lar engel değil, koltuk değneği olmalı. Geliştiriciye “bunu yapma” değil, “bunu şöyle yap” diyen, yol gösteren mesajlar yazın. İyi bir hook başarısız olduğunda tam olarak ne yapılması gerektiğini söyler. Geliştirici süreç içinde öğrenir, zamanla hataları commit etmeden önce kendisi fark etmeye başlar. Bu noktaya gelindiğinde hook’lar bir güvenlik ağına dönüşür; her zaman orada, ama nadiren tetiklenir.
