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 --cached ile 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 --cache bayrağı, 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-commit unutulmuş olabilir. Shebang satırı (#!/bin/bash) eksik olabilir.
  • PATH sorunları: Hook’lar terminal oturumunuzun PATH’ini miras almaz. node, npx, python gibi komutları tam yoluyla belirtin ya da export PATH satırı ekleyin.
  • Windows uyumsuzluğu: Git Bash kullanan Windows geliştiricileri için satır sonu karakterleri (CRLF/LF) sorun yaratabilir. .gitattributes ile 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-verify kanalı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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir