Git Hooks ile Commit ve Push Kuralları Tanımlama

Ekipte birinin “çalışıyor ya, neden commit mesajı önemli ki?” dediğini duyan her sysadmin bilir bu acıyı. Altı ay sonra o commit’i bulmaya çalışırken “fix stuff” veya “asdfjkl” yazan onlarca satırla karşılaşmak, insanın içini karartan bir deneyim. Git hooks tam da bu noktada devreye giriyor: kuralları koyuyorsun, otomatik olarak uygulanıyor, tartışma bitmiş oluyor.

Git Hooks Nedir ve Nerede Yaşar?

Git hooks, Git’in belirli olaylar gerçekleştiğinde otomatik olarak çalıştırdığı script’lerdir. Her Git reposunda .git/hooks/ dizini bulunur ve içinde örnek dosyalar zaten hazır gelir. Bu dosyaların .sample uzantısını kaldırıp çalıştırılabilir yapman yeterli.

ls -la .git/hooks/
# applypatch-msg.sample
# commit-msg.sample
# pre-commit.sample
# pre-push.sample
# pre-receive.sample
# update.sample

Hook’lar iki ana kategoriye ayrılır:

  • Client-side hooks: Geliştiricinin kendi makinesinde çalışır. pre-commit, commit-msg, pre-push bunların en yaygın örnekleri.
  • Server-side hooks: Uzak repoda çalışır. pre-receive, update, post-receive bu kategoriye girer.

Buradaki kritik nokta şu: client-side hook’lar bypass edilebilir. git commit --no-verify komutuyla geçilebilir. Bu yüzden gerçek anlamda zorlayıcı kurallar için server-side hook’lara ihtiyaç var. İkisini birlikte kullanmak en sağlıklı yaklaşım; client-side hızlı geri bildirim sağlar, server-side ise gerçek güvenceyi.

pre-commit Hook ile Kod Kalitesi Kontrolleri

pre-commit hook, git commit komutu çalıştırıldığında, kullanıcı mesaj yazmadan önce tetiklenir. Eğer hook sıfır dışı bir değer döndürürse commit işlemi iptal edilir.

Basit bir örnekle başlayalım. Diyelim ki Python projesinde print() ifadelerinin production’a gitmesini istemiyorsun:

#!/bin/bash
# .git/hooks/pre-commit

staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep '.py$')

if [ -z "$staged_files" ]; then
    exit 0
fi

found_print=0
for file in $staged_files; do
    if grep -n "print(" "$file" | grep -v "#.*print("; then
        echo "HATA: $file dosyasinda debug print() ifadesi bulundu."
        found_print=1
    fi
done

if [ $found_print -ne 0 ]; then
    echo ""
    echo "Commit iptal edildi. Lutfen print() ifadelerini kaldiriniz."
    echo "Logging kullanmak icin: import logging"
    exit 1
fi

exit 0

Bu hook’u aktif etmek için:

chmod +x .git/hooks/pre-commit

Biraz daha gelişmiş bir senaryo düşünelim. Ekipte hem Python hem JavaScript kodu var, syntax hatası olan dosyaların commit edilmesini engellemek istiyorsun:

#!/bin/bash
# .git/hooks/pre-commit - Syntax kontrol hook'u

RED='33[0;31m'
GREEN='33[0;32m'
NC='33[0m'

error_found=0

# Python syntax kontrolü
python_files=$(git diff --cached --name-only --diff-filter=ACM | grep '.py$')
for file in $python_files; do
    if ! python3 -m py_compile "$file" 2>/dev/null; then
        echo -e "${RED}[SYNTAX HATASI]${NC} $file - Python syntax hatasi"
        python3 -m py_compile "$file"
        error_found=1
    fi
done

# JavaScript syntax kontrolü (node gerektirir)
js_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '.(js|jsx)$')
for file in $js_files; do
    if ! node --check "$file" 2>/dev/null; then
        echo -e "${RED}[SYNTAX HATASI]${NC} $file - JavaScript syntax hatasi"
        error_found=1
    fi
done

# Büyük dosya kontrolü (5MB üzeri)
large_files=$(git diff --cached --name-only | xargs -I{} sh -c 'test -f "{}" && du -b "{}" | awk -v f="{}" "{if ($1 > 5242880) print f}"')
if [ -n "$large_files" ]; then
    echo -e "${RED}[BUYUK DOSYA]${NC} 5MB'dan büyük dosyalar commit edilemez:"
    echo "$large_files"
    error_found=1
fi

if [ $error_found -ne 0 ]; then
    echo ""
    echo -e "${RED}Commit engellendi.${NC} Yukaridaki hatalari duzeltip tekrar deneyin."
    exit 1
fi

echo -e "${GREEN}Tum kontroller basarili.${NC}"
exit 0

commit-msg Hook ile Mesaj Kuralları

Commit mesajı formatını standart hale getirmek, uzun vadede projenin izlenebilirliği açısından çok değerli. Conventional Commits formatını (feat:, fix:, docs: vb.) zorunlu kılmak, otomatik changelog üretiminin de kapısını aralar.

#!/bin/bash
# .git/hooks/commit-msg

commit_msg_file=$1
commit_msg=$(cat "$commit_msg_file")

# Merge commit'leri atla
if echo "$commit_msg" | grep -q "^Merge"; then
    exit 0
fi

# Conventional Commits pattern
pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)((.+))?: .{1,72}"

if ! echo "$commit_msg" | grep -qE "$pattern"; then
    echo ""
    echo "HATA: Gecersiz commit mesaji formati!"
    echo ""
    echo "Beklenen format:"
    echo "  <tip>(<kapsam>): <aciklama>"
    echo ""
    echo "Gecerli tipler:"
    echo "  feat     - Yeni ozellik"
    echo "  fix      - Bug duzeltme"
    echo "  docs     - Dokumantasyon"
    echo "  style    - Kod formati"
    echo "  refactor - Yeniden yapilandirma"
    echo "  perf     - Performans iyilestirme"
    echo "  test     - Test ekleme/duzeltme"
    echo "  chore    - Bakim isleri"
    echo "  ci       - CI/CD degisiklikleri"
    echo ""
    echo "Ornek gecerli mesajlar:"
    echo "  feat(auth): kullanici girisi JWT ile guncellendi"
    echo "  fix(api): null pointer hatasi duzeltildi"
    echo "  docs: README guncellendi"
    echo ""
    echo "Gonderilen mesaj: '$commit_msg'"
    exit 1
fi

# Mesaj uzunluk kontrolü (ilk satir max 72 karakter)
first_line=$(echo "$commit_msg" | head -1)
if [ ${#first_line} -gt 72 ]; then
    echo "HATA: Commit mesajinin ilk satiri 72 karakteri gecemez."
    echo "Mevcut uzunluk: ${#first_line} karakter"
    exit 1
fi

exit 0

Bu hook sayesinde git commit -m "hızlı fix" yazan birisi anında şu mesajla karşılaşır:

HATA: Gecersiz commit mesaji formati!

İlk başta ekip buna biraz homurdanır, ama iki hafta sonra herkes alışır ve proje geçmişi okunabilir hale gelir.

pre-push Hook ile Branch ve Test Kontrolleri

pre-push hook, git push çalıştırıldığında ağ üzerinden veri gönderilmeden önce devreye girer. Bu hook’a hangi branch’e push yapılacağı bilgisi de gelir, bu yüzden main veya master branch’e doğrudan push’u engellemek için idealdir.

#!/bin/bash
# .git/hooks/pre-push

protected_branches=("main" "master" "production" "staging")
current_branch=$(git symbolic-ref HEAD | sed 's!refs/heads/!!')

for branch in "${protected_branches[@]}"; do
    if [ "$current_branch" = "$branch" ]; then
        echo "HATA: '$branch' branch'ine dogrudan push yapilamaz!"
        echo ""
        echo "Lutfen bir feature branch olusturun:"
        echo "  git checkout -b feature/yeni-ozellik"
        echo ""
        echo "Pull Request acarak merge etmeniz gerekiyor."
        exit 1
    fi
done

# Push öncesi testleri çalıştır
if [ -f "package.json" ] && grep -q '"test"' package.json; then
    echo "Testler calistiriliyor..."
    if ! npm test --silent; then
        echo ""
        echo "HATA: Testler basarisiz oldu. Push iptal edildi."
        echo "Hatalari duzeltip tekrar deneyin."
        exit 1
    fi
    echo "Tum testler gecti."
fi

exit 0

Hook’ları Ekiple Paylaşmak: Git Template ve Araçlar

En büyük sorun şu: .git/hooks/ dizini .gitignore kapsamında değil ama Git tarafından da takip edilmiyor. Bir ekip üyesi repoyu klonladığında hook’lar gelmez. Bunu çözmenin birkaç yolu var.

Yöntem 1: Repo içinde hooks dizini

Proje kökünde bir hooks/ veya .githooks/ dizini oluşturup hook’ları buraya koy, sonra Git’e bu dizini kullanmasını söyle:

# Proje kökünde .githooks/ dizini oluştur
mkdir .githooks
# Hook'ları buraya taşı veya oluştur

# Git config'e kaydet
git config core.hooksPath .githooks

# Bunu tüm ekibin yapması için Makefile veya setup scripti ekle
# Makefile'a ekle:
setup:
    git config core.hooksPath .githooks
    chmod +x .githooks/*
    echo "Git hooks aktif edildi."

Yöntem 2: Husky (Node.js projeleri için)

Node.js projelerinde Husky kullanmak çok yaygın ve pratik:

# Kurulum
npm install --save-dev husky
npx husky init

# .husky/pre-commit dosyası otomatik oluşur
# package.json'a prepare script eklenir

# .husky/commit-msg içeriği:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx commitlint --edit "$1"

Yöntem 3: Git Template Directory

Sistem genelinde tüm yeni klonlanan repolara otomatik uygulanmasını istiyorsan:

# Global template dizini oluştur
mkdir -p ~/.git-templates/hooks

# Global config'e ekle
git config --global init.templateDir ~/.git-templates

# Hook'ları template dizinine koy
cp pre-commit ~/.git-templates/hooks/
chmod +x ~/.git-templates/hooks/pre-commit

# Mevcut repolara uygulamak için
cd /path/to/existing/repo
git init  # Template'i uygular, mevcut dosyaları bozmaz

Server-Side Hook’larla Gerçek Güvenlik

Client-side hook’lar bypass edilebileceğinden, kritik kurallar için server tarafında da kontrol şart. GitLab ve Gitea gibi platformların kendi hook mekanizmaları var, ama bare repo üzerinde doğrudan çalışmak da mümkün.

#!/bin/bash
# /path/to/repo.git/hooks/pre-receive
# Bare repo'nun hooks dizininde bulunur

while read oldrev newrev refname; do
    branch=$(echo "$refname" | sed 's|refs/heads/||')

    # Protected branch kontrolü
    if [[ "$branch" =~ ^(main|master|production)$ ]]; then

        # Force push engeli
        if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
            merge_base=$(git merge-base "$oldrev" "$newrev" 2>/dev/null)
            if [ "$merge_base" != "$oldrev" ]; then
                echo "HATA: '$branch' branch'ine force push yasaktir!"
                exit 1
            fi
        fi

        # Commit mesajı formatı kontrolü
        if [ "$oldrev" = "0000000000000000000000000000000000000000" ]; then
            oldrev=$(git rev-list --max-parents=0 "$newrev")
        fi

        commits=$(git rev-list "$oldrev".."$newrev")
        for commit in $commits; do
            msg=$(git log --format="%s" -n 1 "$commit")
            if ! echo "$msg" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)"; then
                echo "HATA: Gecersiz commit mesaji: '$msg'"
                echo "Commit: $commit"
                echo "Conventional Commits formati kullanilmalidir."
                exit 1
            fi
        done
    fi
done

exit 0

Gerçek Dünya Senaryosu: Finans Uygulamasında Hook Kullanımı

Geçmişte bir finans şirketinin DevOps ekibinde çalışırken karşılaştığım bir durumu aktarayım. Production ortamında kritik bir SQL migration dosyası yanlışlıkla commit edildi, gözden kaçtı ve deploy sırasında felaket oldu. Bunun ardından şu hook’u geliştirdik:

#!/bin/bash
# .git/hooks/pre-commit - Finans projesi güvenlik kontrolleri

RISK_PATTERNS=(
    "DROP TABLE"
    "TRUNCATE TABLE"
    "DELETE FROM.*WHERE.*1=1"
    "DROP DATABASE"
    "passwords*=s*['"][^'"]*['"]"
    "secret_keys*=s*['"][^'"]*['"]"
    "aws_secret_access_key"
    "private_key"
)

staged_files=$(git diff --cached --name-only --diff-filter=ACM)
risk_found=0

for file in $staged_files; do
    [ -f "$file" ] || continue

    for pattern in "${RISK_PATTERNS[@]}"; do
        matches=$(grep -in "$pattern" "$file" 2>/dev/null)
        if [ -n "$matches" ]; then
            echo "GUVENLIK UYARISI: $file dosyasinda riskli pattern bulundu:"
            echo "  Pattern: $pattern"
            echo "  Satirlar:"
            echo "$matches" | head -5 | sed 's/^/    /'
            echo ""
            risk_found=1
        fi
    done
done

# Migration dosyaları için ek kontrol
migration_files=$(echo "$staged_files" | grep -i "migration|migrate")
if [ -n "$migration_files" ]; then
    echo "BILGI: Migration dosyasi commit ediliyor."
    echo "$migration_files"
    echo ""

    # Onay iste
    exec < /dev/tty
    read -p "Migration dosyasini commit etmek istediginizden emin misiniz? (evet/hayir): " confirm
    if [ "$confirm" != "evet" ]; then
        echo "Commit iptal edildi."
        exit 1
    fi
fi

if [ $risk_found -ne 0 ]; then
    echo "Guvenlik kontrolu basarisiz. Lutfen yetkili ile gorusun."
    exit 1
fi

exit 0

Bu hook sayesinde hem credential leak’ler hem de tehlikeli SQL operasyonları commit aşamasında engellendi. Yanlış pozitif durumlar için git commit --no-verify hala mevcut ama bu komutu kullanmak için ekip lideri onayı politikası oluşturduk.

Hook Yazarken Dikkat Edilmesi Gerekenler

Birkaç pratik noktanın altını çizmek istiyorum:

  • Exit kodları kritik: Hook sıfır döndürürse işlem devam eder, sıfır dışı döndürürse iptal edilir. Bu basit kurala uymayan hook’lar beklenmedik davranışlar üretir.
  • Performans önemli: Her commit’te 30 saniye beklemek geliştiricileri hook’ları devre dışı bırakmaya iter. Kontrolleri mümkün olduğunca hızlı tut, sadece staged dosyaları kontrol et.
  • Taşınabilirlik: Hook’larda #!/bin/bash yerine #!/usr/bin/env bash kullanmak daha güvenli. Ayrıca macOS ve Linux arasında sed, grep davranış farklılıklarına dikkat et.
  • Anlamlı hata mesajları: Sadece “HATA” yazmak yetmez. Kullanıcıya neyi yanlış yaptığını ve nasıl düzelteceğini açıkça söyle.
  • Bypass mekanizması: Acil durumlarda --no-verify bayrağı hayat kurtarır. Bunu tamamen kapatmaya çalışma, sadece kötüye kullanımı loglayabilirsin.
  • İdempotent ol: Hook birden fazla çalıştırıldığında aynı sonucu üretmeli, yan etkisi olmamalı.

Sonuç

Git hooks, ekip içi kod kalitesi standartlarını ve güvenlik kurallarını elle tutulur hale getirmenin en pratik yoludur. “Lütfen commit mesajlarını şu formatta yazın” e-postası göndermek yerine, kuralı teknik olarak uygulayan bir hook yazmak hem daha etkili hem de tartışmayı ortadan kaldırır.

Client-side hook’larla hızlı geri bildirim sağla, server-side hook’larla gerçek güvenceyi oluştur. Bu ikisinin kombinasyonu, --no-verify ile bypass edilemeyen bir kalite kapısı yaratır. Hook’ları ekiple paylaşmak için .githooks/ dizini yaklaşımı veya Husky gibi araçlar kullanmak, “bende çalışıyor” sorununu da çözer.

Başlamak için büyük bir sisteme ihtiyacın yok. Mevcut en acı noktandan başla; belki commit mesajları kaostur, belki credential’lar sızdırılıyor, belki testler push’tan sonra kırılıyor. Tek bir küçük hook bile bu sorunların birini tamamen ortadan kaldırabilir. Oradan itibaren zaten kendi kendine büyüyor.

Bir yanıt yazın

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