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-folderstoreveya ö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.
