Git ile İkili Dosya Yönetimi ve Stratejiler

Versiyon kontrol sistemleriyle geçirdiğim yıllar boyunca öğrendiğim en acı ders şu: Git, metin dosyaları için tasarlanmıştır ve bunu kimse size düzgünce söylemez. İlk büyük projeye başladığınızda, tasarım dosyaları, font paketleri, derleme artifaktları, medya varlıkları hepsi doğal olarak repoya giriyor. Repository zamanla şişiyor, clone işlemleri dakikalar alıyor, CI/CD pipeline’ları yavaşlıyor. Sonunda “bu nasıl böyle oldu?” diye soruyorsunuz. Bu yazıda bu sorunun hem cevabını hem de çözümlerini paylaşacağım.

Git’in İkili Dosyalarla İmtihanı

Git’in diff mekanizması, dosyaların satır bazlı değişikliklerini izler. Bir Python scripti güncellendiğinde, Git sadece değişen satırları saklar. Fakat bir PNG dosyası, bir Excel çalışma kitabı veya bir compiled binary güncellendiğinde, Git’in yapabileceği tek şey dosyanın tamamının yeni bir kopyasını saklamaktır. Her commit, her versiyon değişikliği için tam bir kopya. Bunu birkaç ay boyunca yüzlerce dosyayla yapın, .git klasörünüz dev bir boyuta ulaşır.

Bunun yanında başka pratik problemler de var. İkili dosyalar için git diff hiçbir anlam ifade etmez, merge conflictlerde otomatik birleştirme mümkün değildir ve büyük dosyalar network transferini yavaşlatır.

Önce mevcut durumu analiz etmekle başlayalım:

# Repository'deki en büyük objeleri listele
git rev-list --objects --all | 
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | 
  sed -n 's/^blob //p' | 
  sort --numeric-sort --key=2 | 
  tail -20 | 
  cut -c 1-12,41- | 
  numfmt --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest

# .git klasörünün toplam boyutunu görüntüle
du -sh .git/objects/
du -sh .git/

# Hangi dosya türlerinin ne kadar yer kapladığını analiz et
git ls-tree -r -l HEAD | 
  awk '{print $4, $5}' | 
  sort -k1 -rn | head -30

Bu komutların çıktısına baktığınızda genellikle sürprizlerle karşılaşırsınız. Hiç farkında olmadan repoya girmiş 200MB’lık bir video dosyası, development sürecinde biriktirilen onlarca compiled binary…

.gitignore ile Proaktif Yaklaşım

Sorunları oluştuktan sonra çözmek yerine oluşmadan önlemek çok daha az maliyetlidir. İyi bir .gitignore stratejisi, ikili dosya kirliliğinin önündeki ilk savunma hattıdır.

# Proje genelinde genel bir .gitignore oluştur
cat > .gitignore << 'EOF'
# Derleme çıktıları
*.o
*.a
*.so
*.dylib
*.dll
*.exe
*.out
*.class
*.pyc
*.pyo
__pycache__/
*.egg-info/
dist/
build/

# Medya ve büyük asset dosyaları
*.mp4
*.mov
*.avi
*.mkv
*.mp3
*.wav
*.psd
*.ai
*.sketch

# Ofis belgeleri (genellikle repoya girmemeli)
*.xlsx
*.docx
*.pptx
*.pdf

# Arşiv dosyaları
*.zip
*.tar.gz
*.rar
*.7z

# IDE ve editor çıktıları
.DS_Store
Thumbs.db
*.swp
*.swo
EOF

# Global gitignore ayarla (tüm projeler için geçerli)
git config --global core.excludesfile ~/.gitignore_global

cat > ~/.gitignore_global << 'EOF'
.DS_Store
.idea/
.vscode/settings.json
*.swp
*~
.env
EOF

Global gitignore özelliği genellikle göz ardı edilir ama ekip içinde herkesin kendi IDE ayarlarını sürekli .gitignore‘a eklememesi için mükemmel bir çözümdür. Her geliştirici kendi araçlarına özgü ignore kurallarını global dosyasına alır, proje .gitignore‘u ise gerçekten projeyle ilgili kuralları içerir.

Git LFS: Büyük Dosyalar İçin Doğru Araç

Git Large File Storage (Git LFS), büyük binary dosyaları yönetmenin standart yoluna dönüştü. Temel mantık şu: dosyanın kendisi ayrı bir storage’da tutulur, Git repository’sinde ise sadece o dosyaya işaret eden küçük bir pointer dosyası saklanır.

# Git LFS kurulumu
# Ubuntu/Debian
curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash
sudo apt-get install git-lfs

# macOS
brew install git-lfs

# LFS'i başlat
git lfs install

# Belirli dosya türlerini LFS ile izle
git lfs track "*.psd"
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.mp4"
git lfs track "*.zip"

# Bu işlem .gitattributes dosyasını günceller, onu da commit'le
git add .gitattributes
git commit -m "chore: configure git lfs tracking"

# Hangi dosyaların LFS ile izlendiğini kontrol et
git lfs ls-files

# LFS durumunu görüntüle
git lfs status

.gitattributes dosyası LFS konfigürasyonunun merkezidir ve repository’e commit edilmelidir. Bu sayede ekipteki herkes aynı LFS kurallarını otomatik olarak kullanır.

# Mevcut büyük dosyaları geriye dönük LFS'e migrate et
# Bu işlem history'yi yeniden yazar, dikkatli kullan!
git lfs migrate import --include="*.psd,*.png" --everything

# Sadece belirli bir branch için migrate
git lfs migrate import --include="*.mp4" --include-ref=refs/heads/main

# Migrate işleminden sonra
git push --force-with-lease origin main

Migrate işlemi konusunda ekibinizi önceden bilgilendirin. History rewrite sonrası herkesin local kopyalarını git pull --rebase veya fresh clone ile güncellemesi gerekir. Bunu birden fazla kişinin aktif olarak üzerinde çalıştığı bir branch üzerinde yapmak ciddi sorunlara yol açabilir.

LFS Storage Limitleri ve Alternatifleri

GitHub’ın ücretsiz planında 1GB LFS storage ve aylık 1GB bandwidth limiti var. Bu limitler kurumsal projelerde hızla dolabilir. Birkaç alternatifi değerlendirmek gerekebilir:

  • Artifactory veya Nexus: Kurumsal ortamlarda binary repository yönetimi için standart çözümler
  • MinIO ile self-hosted LFS: lfs-folderstore veya özel LFS server implementasyonları
  • DVC (Data Version Control): Özellikle ML projeleri için, büyük dataset’leri S3, GCS veya Azure Blob ile entegre yönetir
  • Git Annex: Büyük dosyaları farklı backend’lerde tutmak için esnek bir sistem

BFG Repo Cleaner ile Tarih Temizleme

Bazen iş işten geçmiştir. Repository’e girmiş büyük dosyaları sadece son commit’ten silmek yetmez; tüm tarihten temizlemeniz gerekir.

# BFG Repo Cleaner indir
wget https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar

# 10MB'dan büyük tüm dosyaları sil
java -jar bfg-1.14.0.jar --strip-blobs-bigger-than 10M my-repo.git

# Belirli dosyaları sil (tüm tarihten)
java -jar bfg-1.14.0.jar --delete-files '*.mp4' my-repo.git

# Belirli klasörü sil
java -jar bfg-1.14.0.jar --delete-folders build my-repo.git

# BFG çalıştırma sırası önemli!
# Önce bare clone al
git clone --mirror https://github.com/org/repo.git

# BFG ile temizle
java -jar bfg-1.14.0.jar --strip-blobs-bigger-than 50M repo.git

# Temizlik sonrası
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive

# Force push
git push --force

BFG, Git’in yerleşik git filter-branch komutuna kıyasla çok daha hızlıdır. 5GB’lık bir repository’yi temizlemek için filter-branch saatler alabilirken BFG dakikalar içinde işi bitirir. Fakat yine de bu işlemlerin geri dönüşü yoktur ve tüm ekip üyelerini etkiler.

Özel Diff Driver’ları ile Binary Analiz

Bazı binary formatlar için anlamlı diff üretmek mümkündür. Örneğin PDF içeriği, Office belgeleri veya belirli image metadata’ları. Git’e özel diff driver’ları tanımlayarak bu dosyalar için daha okunabilir diff çıktısı elde edebilirsiniz.

# PDF diff için pdftotext kullan
git config --global diff.pdf.textconv pdftotext

# .gitattributes'a ekle
echo "*.pdf diff=pdf" >> .gitattributes

# Office dosyaları için docx2txt
git config --global diff.docx.textconv docx2txt

echo "*.docx diff=docx" >> .gitattributes

# Image dosyaları için exiftool ile metadata diff
git config --global diff.image.textconv "exiftool"
echo "*.jpg diff=image" >> .gitattributes
echo "*.png diff=image" >> .gitattributes

# SQLite veritabanları için
git config diff.sqlite3.textconv "sqlite3 $1 .dump"
# veya
cat >> ~/.gitconfig << 'EOF'
[diff "sqlite3"]
    textconv = "sqlite3 $1 .dump"
    binary = true
EOF
echo "*.sqlite diff=sqlite3" >> .gitattributes

Bu yaklaşım özellikle konfigürasyon verilerini SQLite’ta tutan uygulamalar veya döküman tabanlı iş akışlarında gerçekten işe yarıyor. Tam binary diff yerine en azından “bu PDF’te şu paragraf değişti” bilgisini görebilmek, code review süreçlerini anlamlı kılıyor.

Merge Strategy ve Binary Conflict Çözümü

Binary dosyalarda merge conflict oluştuğunda Git otomatik birleştirme yapamaz ve her zaman bir tarafı tercih etmek zorunda kalırsınız. Bu süreci yönetmek için bazı stratejiler geliştirebilirsiniz.

# Conflict sırasında belirli bir tarafı seçmek
# "ours" stratejisi - mevcut branch versiyonunu kullan
git checkout --ours -- assets/logo.png

# "theirs" stratejisi - gelen versiyonu kullan
git checkout --theirs -- assets/logo.png

git add assets/logo.png
git commit -m "resolve: binary conflict resolved by keeping theirs"

# Belirli dosya türleri için otomatik merge strategy tanımla
# .gitattributes içine
echo "*.png merge=ours" >> .gitattributes

# Bu, PNG conflict'lerinde her zaman mevcut branch'i tercih eder
# Production config dosyaları için kullanışlı

# Custom merge driver tanımla
git config --global merge.ours.driver true

Büyük frontend projelerinde compiled CSS ve JavaScript bundle’ları için “ours” merge stratejisi oldukça kullanışlıdır. Bu dosyalar zaten build sürecinde yeniden üretileceği için conflict çözümü yerine her zaman tek bir tarafı seçmek mantıklıdır.

Sparse Checkout ile Seçici Clone

Büyük repository’lerde çalışırken herkese her şeyi clone ettirmek zorunda değilsiniz. Sparse checkout, sadece ihtiyaç duyulan klasörleri local’e çeker.

# Yeni sparse checkout başlat
git clone --no-checkout https://github.com/org/large-repo.git
cd large-repo

# Sparse checkout'u etkinleştir
git sparse-checkout init --cone

# Sadece belirli klasörleri al
git sparse-checkout set src/backend docs/api

# Büyük assets klasörünü atlayarak checkout yap
git checkout main

# Mevcut sparse checkout konfigürasyonunu görüntüle
git sparse-checkout list

# Yeni klasör ekle
git sparse-checkout add src/frontend

# Tüm dosyalara geri dön
git sparse-checkout disable

Mono-repo stratejisi benimseyen ama farklı ekiplerin farklı parçalar üzerinde çalıştığı organizasyonlarda sparse checkout büyük fark yaratıyor. Frontend geliştiricilerin tüm backend binary’lerini, ML modellerini çekmesine gerek yok.

Partial Clone ile Akıllı Download

Git 2.22 ile gelen partial clone, sparse checkout’un bir adım ötesine geçer. Binary blob’ları gerektiğinde lazy loading ile çeker.

# Blob'ları olmadan clone et (sadece history ve tree objects)
git clone --filter=blob:none https://github.com/org/repo.git

# Belirli boyutun altındaki blob'ları al, büyükleri lazy yükle
git clone --filter=blob:limit=1m https://github.com/org/repo.git

# Tree objects hariç clone (çok agresif, nadiren kullanılır)
git clone --filter=tree:0 https://github.com/org/repo.git

# Partial clone sonrası eksik blob'ları fetch et
git fetch --filter=blob:none origin

# Belirli bir dosyayı explicit olarak çek
git cat-file -e HEAD:path/to/large/file.bin || 
  git fetch origin $(git rev-parse HEAD:path/to/large/file.bin)

CI/CD pipeline’larında partial clone özellikle değerlidir. Test runner’ınız sadece source code’a ihtiyaç duyuyorsa büyük asset dosyalarını çekmeden build başlatabilirsiniz. Pipeline süreniz ciddi miktarda kısalır.

Repository Boyutunu Sürekli İzleme

Sorunların tekrar oluşmaması için monitoring şart.

# Repository büyüklük raporlama scripti
cat > repo-size-report.sh << 'SCRIPT'
#!/bin/bash

REPO_PATH="${1:-.}"
cd "$REPO_PATH"

echo "=== Repository Size Report ==="
echo "Total .git size: $(du -sh .git | cut -f1)"
echo ""
echo "=== Top 10 Largest Files in HEAD ==="
git ls-tree -r -l HEAD | 
  sort -k4 -rn | 
  head -10 | 
  awk '{printf "%-60s %sn", $5, $4}' | 
  numfmt --field=2 --to=iec-i --suffix=B

echo ""
echo "=== File Type Distribution ==="
git ls-tree -r HEAD | 
  awk '{print $4}' | 
  sed 's/.*.//' | 
  sort | uniq -c | sort -rn | head -15

echo ""
echo "=== Objects Database Size ==="
git count-objects -vH
SCRIPT

chmod +x repo-size-report.sh

# pre-commit hook ile büyük dosyaları engelle
cat > .git/hooks/pre-commit << 'HOOK'
#!/bin/bash

MAX_FILE_SIZE=5242880  # 5MB

# Staged dosyaları kontrol et
while IFS= read -r -d '' file; do
  filesize=$(git cat-file -s ":$file" 2>/dev/null || echo 0)
  if [ "$filesize" -gt "$MAX_FILE_SIZE" ]; then
    echo "ERROR: $file is $(numfmt --to=iec-i --suffix=B $filesize)"
    echo "Files larger than 5MB should use Git LFS"
    echo "Run: git lfs track "$file""
    exit 1
  fi
done < <(git diff --cached --name-only -z)
HOOK

chmod +x .git/hooks/pre-commit

Bu pre-commit hook’u ekip genelinde dağıtmak için pre-commit framework veya Husky kullanabilirsiniz. Yoksa herkes kendi .git/hooks/ klasörüne kopyalamak zorunda kalır ki bu sürdürülebilir değildir.

Gerçek Dünya Senaryo: Oyun Projesi Repo Kurtarma

Birkaç ay önce bir Unity projesi üzerinde çalışan ekiple ilgili şu durumla karşılaştım: Repository 8GB boyutuna ulaşmış, clone işlemi 20-25 dakika alıyordu ve CI build’leri başlamadan zaman aşımına giriyordu.

# Önce hasarı analiz et
git rev-list --all --objects | 
  git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' | 
  grep blob | sort -k2 -rn | head -20

# Sonuç: .fbx, .png, .wav dosyaları tarih boyunca onlarca versiyonla birikmiş

# LFS migration planı
git lfs migrate import 
  --include="*.fbx,*.png,*.jpg,*.wav,*.mp3,*.tga,*.psd" 
  --everything 
  --yes

# Sonuç: Repository 8GB'tan 340MB'a düştü
# Clone süresi: 22 dakikadan 90 saniyeye

# CI/CD konfigürasyonunu güncelle
# LFS pull sadece gerekli build adımlarında yapılmalı

Bu tür bir kurtarma operasyonu sonrası ekibi bilgilendirmek kritik. Herkesin fresh clone alması gerekiyor ve bu geçiş dönemini iyi yönetmezseniz insanlar yanlışlıkla eski büyük dosyaları tekrar push edebilir.

Sonuç

Git’te binary dosya yönetimi, “çalışıyor, neden değiştirelim?” mantığıyla ertelenebilecek bir konu değil. Repository’niz büyüdükçe bu sorunlar katlanarak büyür ve bir noktada ciddi verimlilik kaybına dönüşür.

Başlangıç için pratik öneri: Her yeni projede önce .gitignore ve .gitattributes dosyalarını oluşturun, binary asset’ler varsa LFS kurulumunu ilk commit’ten önce yapın. Mevcut projeler için git count-objects -vH ve yukarıdaki analiz scriptleriyle durumu ölçün, sonra bir temizlik planı yapın.

En önemli nokta şu: Bu konular sadece teknik değil, aynı zamanda ekip kültürüne dair. “Repoya ne girer ne girmez” sorusunun cevabı yazılı bir kural olarak belirlenmeli ve herkes tarafından bilinmelidir. Pre-commit hook’ları bu kuralları otomatik olarak uygular ve kod inceleme sürecindeki tartışmaları azaltır. Teknik çözüm kadar önemli olan, ekibin bu çözümü sahiplenmesidir.

Bir yanıt yazın

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