Pre-commit Hooks ile Otomatik Kod Kalite Kontrolü

Geliştirme süreçlerinde “ama bende çalışıyordu” cümlesini kaç kez duydunuz? Ya da bir pull request incelemesinde lint hatalarıyla, yanlış format edilmiş dosyalarla, hatta production’a gidecek kodda hardcode edilmiş API key’lerle karşılaştınız mı? Pre-commit hook’lar tam da bu noktada devreye giriyor. Kod, repository’e ulaşmadan önce bir süzgeçten geçiyor ve çoğu sorun daha commit aşamasında yakalanıyor.

Bu yazıda pre-commit hook framework’ünü, gerçek dünya konfigürasyonlarını ve bir DevOps/sysadmin ekibinin gerçekten işine yarayacak araçları ele alacağım.

Pre-commit Hook Nedir, Neden Önemlidir?

Git, belirli olaylar gerçekleştiğinde otomatik olarak çalıştırılabilecek script mekanizması sağlıyor. Bunlara “git hook” deniyor. pre-commit hook’u, git commit komutu çalıştırıldığında, commit işlemi gerçekleşmeden hemen önce tetikleniyor.

Manuel olarak .git/hooks/pre-commit dosyası oluşturup shell script yazabilirsiniz, ama bu yaklaşımın ciddi dezavantajları var: taşınabilir değil, versiyon kontrolüne dahil etmek zor, ekipteki herkes ayrı ayrı kurmak zorunda.

pre-commit (pypi.org/project/pre-commit) adlı Python tabanlı framework ise bu sorunları çözüyor. YAML tabanlı konfigürasyon dosyasıyla yüzlerce hazır hook’u tek bir yerden yönetebiliyorsunuz ve bu dosya repository ile birlikte versiyonlanıyor.

Kurulum ve Temel Konfigürasyon

Python pip ile kurulum:

pip install pre-commit

# Ya da pipx kullanıyorsanız (önerilen yöntem):
pipx install pre-commit

# Homebrew (macOS/Linux):
brew install pre-commit

# Kurulum doğrulama:
pre-commit --version

Repository’nize hook’ları aktif etmek için önce .pre-commit-config.yaml dosyası oluşturuyorsunuz, ardından hook’ları git’e tanıtıyorsunuz:

# Proje dizininde:
touch .pre-commit-config.yaml

# Hook'ları .git/hooks/ altına kur:
pre-commit install

# Sadece pre-push aşamasında çalışacak hook'lar için:
pre-commit install --hook-type pre-push

Basit bir başlangıç konfigürasyonu:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-json
      - id: check-merge-conflict
      - id: detect-private-key
      - id: check-added-large-files
        args: ['--maxkb=500']

Bu konfigürasyonla bile ciddi bir koruma katmanı elde ediyorsunuz. detect-private-key hook’u özellikle kritik: AWS secret key, RSA private key ve benzeri desenleri tespit ediyor.

Python Projeleri için Kapsamlı Konfigürasyon

Bir Django projesi ya da Python tabanlı otomasyon script’leri yönetiyorsanız şu konfigürasyon iyi bir başlangıç noktası:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-merge-conflict
      - id: detect-private-key
      - id: check-added-large-files
        args: ['--maxkb=1024']
      - id: no-commit-to-branch
        args: ['--branch', 'main', '--branch', 'master', '--branch', 'production']

  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black
        language_version: python3.11

  - repo: https://github.com/PyCQA/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        args: ['--max-line-length=88', '--extend-ignore=E203']
        additional_dependencies:
          - flake8-bugbear
          - flake8-comprehensions

  - repo: https://github.com/PyCQA/isort
    rev: 5.13.2
    hooks:
      - id: isort
        args: ['--profile=black']

  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.7
    hooks:
      - id: bandit
        args: ['-c', 'pyproject.toml']
        additional_dependencies: ['bandit[toml]']

no-commit-to-branch hook’u özellikle değer veririm. Doğrudan main veya production branch’ine commit atmayı engelliyor. Özellikle gece yarısı aceleyle yapılan “küçük düzeltmelerin” production’ı bozduğu senaryoları yaşadıysanız, bu hook hayat kurtarıcı oluyor.

bandit Python kodundaki güvenlik açıklarını tarıyor: hardcoded password, SQL injection desenleri, güvensiz hash fonksiyonu kullanımları gibi sorunları yakalıyor.

JavaScript ve Node.js Projeleri

Frontend veya Node.js tabanlı projeler için ESLint ve Prettier entegrasyonu:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-json
      - id: check-yaml
      - id: detect-private-key
      - id: no-commit-to-branch
        args: ['--branch', 'main', '--branch', 'staging']

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v8.56.0
    hooks:
      - id: eslint
        files: .(js|jsx|ts|tsx)$
        types: [file]
        additional_dependencies:
          - [email protected]
          - [email protected]
          - '@typescript-eslint/[email protected]'
          - '@typescript-eslint/[email protected]'

  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v3.1.0
    hooks:
      - id: prettier
        types_or: [javascript, jsx, ts, tsx, css, json, yaml, markdown]

ESLint hook’unu kullanırken dikkat etmeniz gereken bir nokta var: additional_dependencies içindeki paket versiyonları proje package.json‘ınızdakiyle uyumlu olmalı, aksi hâlde garip davranışlar görebiliyorsunuz.

Altyapı Kodu için Hook’lar: Terraform ve Ansible

Sysadmin perspektifinden en keyif aldığım kısım burası. Uygulama kodu yazan arkadaşlar lint ve format konusunu çoktan çözmüş olabilir, ama altyapı kodu (Infrastructure as Code) için aynı disiplin çok daha az uygulanıyor.

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-yaml
        exclude: 'helm/.*'
      - id: detect-private-key
      - id: end-of-file-fixer
      - id: trailing-whitespace

  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.86.0
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tflint
        args:
          - '--args=--only=terraform_deprecated_interpolation'
          - '--args=--only=terraform_deprecated_index'
          - '--args=--only=terraform_unused_declarations'
          - '--args=--only=terraform_comment_syntax'
          - '--args=--only=terraform_documented_outputs'
          - '--args=--only=terraform_documented_variables'
      - id: terraform_trivy
        args:
          - '--args=--severity HIGH,CRITICAL'

  - repo: https://github.com/ansible/ansible-lint
    rev: v24.2.0
    hooks:
      - id: ansible-lint
        files: .(yaml|yml)$
        args: ['--profile=production']

terraform_trivy hook’u Terraform kodundaki güvenlik açıklarını tarıyor. Açık S3 bucket tanımı, şifrelenmemiş disk, geniş IAM permission gibi sorunları commit öncesinde yakalıyorsunuz. Bu hook’u eklemeden önce ne kadar “masum görünen” Terraform kodunun güvenlik açığı içerdiğini görünce şaşırabilirsiniz.

Ansible-lint için --profile=production argümanı biraz agresif olabilir, geliştirme ortamında --profile=basic ile başlamak daha makul.

Özel Hook Yazımı: Gerçek Dünya Senaryosu

Hazır hook’lar çoğu durumu karşılıyor, ama bazen projeye özel kontrollere ihtiyaç duyuyorsunuz. Örneğin: her Python dosyasının başında telif hakkı bilgisi olmasını zorunlu kılmak, ya da belirli bir pattern içeren dosya isimlerini engellemek.

Yerel hook’lar için local repo tipini kullanıyorsunuz:

repos:
  - repo: local
    hooks:
      - id: check-env-file
        name: .env dosyasi commit edilmesin
        entry: bash -c 'if git diff --cached --name-only | grep -q "^.env$"; then echo "HATA: .env dosyasi commit edilemez!"; exit 1; fi'
        language: system
        pass_filenames: false

      - id: check-debug-statements
        name: Debug ifadelerini kontrol et
        entry: bash -c 'if git diff --cached | grep -E "^+.*(console.log|debugger|pdb.set_trace|breakpoint())" | grep -v "^++" | grep -q .; then echo "UYARI: Debug ifadesi bulundu. Lutfen kaldiriniz."; exit 1; fi'
        language: system
        pass_filenames: false

      - id: check-todo-fixme
        name: TODO ve FIXME kontrolu
        entry: bash -c 'git diff --cached | grep -E "^+.*(TODO|FIXME|HACK|XXX)" | grep -v "^++" && echo "Yukaridaki TODO/FIXME ifadelerini issue olarak acin." && exit 1 || exit 0'
        language: system
        pass_filenames: false

check-debug-statements hook’u gerçekten can kurtarıyor. Production log’larında console.log çıktıları gören, ya da bir Python servisinin beklenmedik şekilde pdb prompt’unda askıda kaldığını yaşayan varsa bu hook’u anında benimseyecektir.

TODO/FIXME hook’unu biraz daha toleranslı tutmak isteyebilirsiniz, mesela sadece uyarı verip commit’i engellememek gibi. Ekip kültürüne göre ayarlamak lazım.

CI/CD Pipeline ile Entegrasyon

Pre-commit hook’ların yerel makinede çalışması güzel, ama ekipteki herkes mutlaka yükleyip aktif etmeyebilir. Bu yüzden aynı kontrolleri CI/CD pipeline’ına da eklemek iyi bir pratik.

GitHub Actions ile:

# .github/workflows/pre-commit.yml
name: Pre-commit Kontrolleri

on:
  pull_request:
    branches: [main, staging]
  push:
    branches: [main]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Python kur
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: pre-commit cache
        uses: actions/cache@v3
        with:
          path: ~/.cache/pre-commit
          key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}

      - name: pre-commit calistir
        uses: pre-commit/[email protected]

Cache adımı önemli: pre-commit her hook için sanal ortam oluşturuyor ve bu işlem zaman alıyor. Cache olmadan her PR’da 2-3 dakika kaybedebilirsiniz.

GitLab CI için de benzer bir yapı kurabilirsiniz:

# .gitlab-ci.yml içine eklenecek job
pre-commit-check:
  stage: validate
  image: python:3.11-slim
  before_script:
    - pip install pre-commit
  script:
    - pre-commit run --all-files
  cache:
    key: pre-commit-${CI_COMMIT_REF_SLUG}
    paths:
      - .cache/pre-commit
  variables:
    PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit

Ekip Onboarding: Makefile ile Kolaylaştırma

Yeni bir geliştirici projeye katıldığında “pre-commit install çalıştırmayı unutma” demek yetersiz kalıyor. Makefile veya script ile otomatize etmek daha sağlıklı:

# Makefile
.PHONY: setup lint lint-all

setup:
	@echo "Gelistirme ortami kuruluyor..."
	pip install -r requirements-dev.txt
	pre-commit install
	pre-commit install --hook-type commit-msg
	@echo "Kurulum tamamlandi. pre-commit hook'lari aktif."

lint:
	@echo "Staged dosyalar kontrol ediliyor..."
	pre-commit run

lint-all:
	@echo "Tum dosyalar kontrol ediliyor..."
	pre-commit run --all-files

update-hooks:
	@echo "Hook'lar guncelleniyor..."
	pre-commit autoupdate
	@echo "Guncelleme tamamlandi. .pre-commit-config.yaml dosyasini inceleyin."

pre-commit autoupdate komutu .pre-commit-config.yaml içindeki tüm rev değerlerini otomatik olarak en son sürüme güncelliyor. Bunu aylık bir rutine dahil etmek, güvenlik yamalarının zamanında uygulanması açısından önemli.

Performans Optimizasyonu

Büyük projelerde pre-commit çalışma süresi can sıkıcı hâle gelebilir. Birkaç optimizasyon yöntemi:

repos:
  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black
        # Sadece belirli dizinleri kontrol et
        files: ^(src|tests)/
        # Belirli dosyaları hariç tut
        exclude: ^src/migrations/

  - repo: https://github.com/PyCQA/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        # Paralel çalışma için
        args: ['--jobs=auto']
        # Sadece staged dosyaları işle (varsayılan davranış zaten bu)
        stages: [commit]

stages parametresi önemli: bazı ağır hook’ları sadece push aşamasına taşıyabilirsiniz. Örneğin Trivy ile güvenlik taraması her commit’te değil, push öncesinde çalışabilir:

- id: terraform_trivy
  stages: [push]
  args:
    - '--args=--severity HIGH,CRITICAL'

Ayrıca belirli bir commit’te hook’ları geçici olarak atlamak için:

# Tüm hook'ları atla (acil durum için, alışkanlık haline getirilmemeli):
git commit -m "acil fix" --no-verify

# Belirli bir hook'u atla:
SKIP=flake8 git commit -m "WIP: devam edecek"

--no-verify kullanımını ekip içinde kural haline getirmemek önemli. Meşru kullanım senaryoları var elbette (üretilmiş kod commit etmek gibi), ama bu seçeneğin varlığı “zahmetli olunca atlıyoruz” kültürünü doğurabilir.

Commit Message Standardizasyonu

Pre-commit sadece kod kalitesiyle sınırlı değil. Commit mesajlarını da standartlaştırabilirsiniz. Conventional Commits formatını zorunlu kılmak için:

repos:
  - repo: https://github.com/compilerla/conventional-pre-commit
    rev: v3.2.0
    hooks:
      - id: conventional-pre-commit
        stages: [commit-msg]
        args: [feat, fix, docs, style, refactor, perf, test, chore, ci, build]

Bu hook’u aktif etmek için ayrı kurulum gerekiyor:

pre-commit install --hook-type commit-msg

Artık git commit -m "bir şeyler yaptım" gibi mesajlar reddedilecek, feat: kullanici girisi eklendi veya fix: session timeout hatasi duzeltildi formatı zorunlu hâle gelecek. Changelog oluşturmayı otomatize ediyorsanız bu standart çok değerli.

Yaygın Sorunlar ve Çözümleri

Pre-commit kullanırken karşılaşılan tipik sorunlar ve çözümleri:

Hook kurulum dizini izin sorunu:

# pre-commit cache dizinini kontrol et:
pre-commit clean

# Yeniden kur:
pre-commit install --overwrite

Belirli bir hook’u güncelleme sonrası sorun çıkartıyor:

# Tüm hook ortamlarını temizle ve yeniden oluştur:
pre-commit clean
pre-commit run --all-files

Docker ortamında pre-commit kullanımı:

# Dockerfile içinde:
RUN pip install pre-commit && 
    pre-commit install-hooks

# Ya da docker-compose ile geliştirme ortamında:
# volumes kısmında .pre-commit-config.yaml mount edilmeli

Windows geliştirici ortamı:

Windows’ta pre-commit kullanıyorsanız WSL2 üzerinde çalışmanızı öneririm. Native Windows’ta bazı shell hook’ları beklendiği gibi çalışmayabiliyor. Git Bash üzerinde de temel hook’lar çalışıyor ama karmaşık shell script’ler sorun çıkartabiliyor.

SonarQube ile Entegrasyon Notu

SonarQube kapsamlı bir statik analiz aracı ama her commit’te çalıştırmak pratik değil. Genellikle SonarQube’u CI/CD pipeline’ına entegre edip pre-commit hook’larıyla tamamlayıcı bir katman oluşturmak daha mantıklı bir yaklaşım.

Pre-commit aşamasında hızlı lokal kontroller (format, basit lint, güvenlik desenleri), CI aşamasında SonarQube ile derin analiz şeklinde bir iş bölümü iyi çalışıyor. SonarQube’un sonar-scanner CLI’sını pre-push hook olarak ekleyebilirsiniz ama analiz süresi uzunsa geliştiricilerin hook’ları devre dışı bırakma riskiyle karşılaşırsınız.

Sonuç

Pre-commit hook’lar, kod kalitesini sol’a kaydırmanın (shift-left) en pratik yollarından biri. Kurulum maliyeti düşük, geri dönüşü yüksek. Bir kere doğru konfigürasyon oluşturulduğunda ekip neredeyse hiç ek yük hissetmeden kalite kontrolünden geçiyor.

Başlamak için şu sırayı öneririm: önce temel pre-commit-hooks paketini kurun ve bir hafta kullanın, ardından projenizin diline özel formatter ve linter ekleyin, sonra güvenlik odaklı hook’ları (bandit, trivy, detect-private-key) dahil edin. Son aşamada CI/CD entegrasyonunu tamamlayın.

En önemli nokta: hook’ları çok katı tutarsanız geliştiriciler --no-verify ile geçmeye başlar ve tüm sistem kağıt üzerinde kalır. Her hook’un neden orada olduğunu ekiple paylaşmak, sahiplenme duygusunu artırıyor. “Bu kural bizi yavaşlatıyor” itirazlarını ciddiye alın, gerekirse konfigürasyonu birlikte gözden geçirin. Araç ekibin önünde değil, arkasında durmalı.

Bir yanıt yazın

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