Git Merge Conflict Çözümü: Araçlar ve Stratejiler

Merge conflict. İki harf, bir kelime, ama bazı günler ekibin tüm ritmini bozabiliyor. Özellikle büyük bir feature branch’i main’e merge etmeye çalışırken terminal sana kırmızı harflerle “CONFLICT” yazdığında, o anlık panik hissi tanıdık geliyordur. Yıllar içinde öğrendim ki conflict çözmek aslında bir teknik beceriden çok bir yaklaşım meselesi. Doğru araçları bilmek, stratejileri içselleştirmek ve paniksiz yaklaşmak işin yüzde seksenini hallediyor.

Conflict Neden Oluşur?

Git, iki branch’i birleştirirken her iki tarafta da değiştirilmiş satırlarla karşılaştığında otomatik çözüm üretemiyor. Bu durumda işi sana bırakıyor. Basit gibi görünse de pratikte birkaç farklı senaryo var:

  • Aynı satır çakışması: İki geliştirici aynı dosyanın aynı satırını değiştirmiş.
  • Silme/değiştirme çakışması: Biri dosyayı silmiş, diğeri düzenlemiş.
  • Yeniden adlandırma çakışması: Dosya iki branch’te farklı isimlerle rename edilmiş.
  • İkili dosya çakışması: Görsel asset’ler, compiled binary’ler gibi merge edilemeyen dosyalar.

Conflict’in tam olarak nerede ve neden çıktığını anlamak, çözüm stratejisini belirlemek açısından kritik. Körü körüne “bizim tarafı alalım” demek çoğu zaman bug üretmekten başka bir şeye yaramıyor.

Temel Conflict Anatomy’si

Git bir conflict yaşandığında dosyanın içine şu formatı yazıyor:

<<<<<<< HEAD
const timeout = 5000;
const retryCount = 3;
=======
const timeout = 10000;
const retryCount = 5;
>>>>>>> feature/retry-logic

Buradaki bölümleri tanıyalım:

  • <<<<<<< HEAD: Mevcut branch’inizdeki (genellikle main veya develop) içerik başlıyor
  • =======: İki tarafı ayıran çizgi
  • >>>>>>> feature/retry-logic: Merge etmeye çalıştığınız branch’teki içerik

Fakat gerçek senaryolarda bu bloklar onlarca satır uzunluğunda olabiliyor ve bir dosyada birden fazla conflict marker görebilirsiniz. git diff komutunu conflict sonrası çalıştırırsanız tüm çakışmaları toplu görebilirsiniz:

git diff --name-only --diff-filter=U

Bu komut sadece unmerged, yani çözüm bekleyen dosyaları listeler. Büyük bir merge operasyonunda hangi dosyalara odaklanmanız gerektiğini hızla görmenizi sağlar.

Komut Satırında Conflict Çözümü

Terminal odaklı çalışmayı sevenler için git checkout ile hızlı bir taraf seçimi mümkün:

# Tüm çakışmalı dosyada mevcut branch'i (ours) tercih et
git checkout --ours src/config/settings.js

# Tüm çakışmalı dosyada gelen branch'i (theirs) tercih et
git checkout --theirs src/config/settings.js

Ancak dikkat: bu komutlar dosyanın tamamı için tek tarafı alıyor. Parçalı çözüm istiyorsanız dosyayı elle düzenlemeniz gerekiyor. Conflict marker’ları temizledikten sonra:

git add src/config/settings.js
git merge --continue

Eğer merge işlemini tamamen iptal etmek istiyorsanız:

git merge --abort

Bu komut sizi merge öncesi temiz duruma geri döndürür. Bir şeyler ters gittiğinde ya da conflict’in boyutunu görünce “dur bir dakika, önce daha iyi anlayayım” demeniz gerektiğinde can simidi oluyor.

git mergetool ile Araç Tabanlı Çözüm

Git’in yerleşik mergetool komutu, harici bir diff/merge editörünü otomatik olarak açıyor. Varsayılan araç genellikle vimdiff oluyor ama bunu değiştirebilirsiniz:

# Mevcut araç konfigürasyonunu gör
git config --global merge.tool

# Tercih ettiğiniz aracı ayarla
git config --global merge.tool vimdiff
git config --global merge.tool meld
git config --global merge.tool kdiff3

# Mergetool'u başlat
git mergetool

vimdiff ile çalışırken dört panel görürsünüz: LOCAL (mevcut branch), BASE (ortak ata), REMOTE (gelen branch) ve alt panelde merge sonucu. Vim kısayollarını biliyorsanız güçlü bir araç, bilmiyorsanız korkutucu.

Grafiksel araçlar tercih edenler için Meld açık kaynak ve oldukça yetenekli bir seçenek. Ubuntu/Debian’da:

sudo apt install meld
git config --global merge.tool meld
git mergetool

VS Code ile Conflict Çözümü

Günümüzde pek çok ekip VS Code kullanıyor ve editörün yerleşik conflict çözümü arayüzü gerçekten kullanışlı. Conflict olan dosyayı VS Code’da açtığınızda her marker bloğunun üstüne “Accept Current Change / Accept Incoming Change / Accept Both Changes / Compare Changes” seçenekleri geliyor.

VS Code’u default merge tool olarak tanımlamak için:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

Pratikte çok daha fazla tercih ettiğim yöntem şu: Conflict’i VS Code’da açıp, büyük değişiklikler için “Compare Changes” ile iki versiyon arasındaki farkı tam olarak görmek, sonra hangi kodu alacağıma bilinçli karar vermek. Körü körüne “incoming’i al” yapmak yerine her bloğu okumak, uzun vadede çok daha az bug demek.

Gerçek Dünya Senaryosu: Uzun Yaşayan Branch Felaketi

Şunu defalarca yaşadım: Bir ekip üyesi iki hafta boyunca kendi feature branch’inde çalışıyor, ana branch’e bakmıyor. Sonra merge zamanı geliyor ve yüzlerce conflict çıkıyor. Bu durumda merge yerine rebase stratejisi çok daha temiz sonuç veriyor:

# Feature branch'indesiniz
git fetch origin
git rebase origin/main

Rebase, commit’lerinizi main’in en tepesine yeniden yazıyor. Her commit’in conflict’ini ayrı ayrı çözüyorsunuz, tek seferde dev bir conflict yığınıyla uğraşmıyorsunuz. Conflict çıktığında:

# Conflict'i düzenle, sonra
git add <dosya>
git rebase --continue

# Bir adımı atlamak istersen (dikkatli kullanın)
git rebase --skip

# Her şeyi iptal et
git rebase --abort

Rebase’in altın kuralını da hatırlatayım: Paylaşımlı/public branch’lerde rebase yapmayın. Kendi local feature branch’inizde gayet güvenli, ama başkalarının da üzerine commit attığı bir branch’i rebase ederseniz tarih yeniden yazıldığı için ortam karışıyor.

Interactive Rebase ile Conflict Önleme

Conflict’i çözmek kadar önlemek de önemli. Uzun yaşayan branch’lerde git rebase -i ile commit geçmişini düzenleyerek main’e daha temiz, daha az çakışan bir yapıyla merge açabilirsiniz:

git rebase -i origin/main

Bu komut size commit listesini gösterir. squash ile ilgili commit’leri birleştirmek, reword ile commit mesajlarını düzeltmek mümkün. Küçük, atomik commit’ler conflict çözmeyi de kolaylaştırıyor çünkü neyin neden değiştiğini daha net görüyorsunuz.

git log ile Conflict Bağlamını Anlamak

Conflict’i çözmeden önce o dosyanın geçmişini anlamak bazen hayat kurtarıyor. Özellikle “bu satır neden böyle yazılmış, hangi commit’ten geliyor” sorusunu yanıtlamak için:

# Belirli bir dosyanın commit geçmişi
git log --oneline --follow src/api/auth.js

# Belirli satırların geçmişi (blame)
git blame src/api/auth.js

# İki branch arasındaki farkı göster
git log --oneline main..feature/new-auth

# Conflict olan dosyada ne değişti
git log --merge --oneline src/api/auth.js

git log --merge komutu özellikle değerli: conflict’e yol açan commit’leri doğrudan listeler. “Bu değişiklik neden yapıldı, ticket numarası ne, kim yazdı” sorularını commit mesajlarından bulabilirsiniz. Conflict çözümünü commit geçmişini anlamadan yapmak, bir puzzle’ı kutu resmine bakmadan çözmeye benziyor.

Üç Taraflı Diff: BASE Dosyası

Gelişmiş conflict analizinde three-way merge mantığını kavramak işleri netleştiriyor. Git, conflict sırasında üç versiyon tutuyor:

# Merge sırasında geçici olarak erişilebilen versiyonlar
git show :1:src/config/db.js   # BASE (ortak ata)
git show :2:src/config/db.js   # OURS (mevcut branch)
git show :3:src/config/db.js   # THEIRS (gelen branch)

BASE versiyonu özellikle önemli. İki tarafın ortak ataya göre ne değiştirdiğini görürseniz, kimin haklı olduğunu ya da ikisini nasıl birleştireceğinizi çok daha iyi anlarsınız. Meld ve KDiff3 gibi araçlar bu üç paneli yan yana gösteriyor, bu yüzden karmaşık conflict’lerde grafiksel araçlar ciddi avantaj sağlıyor.

Conflict Sonrası Doğrulama

Conflict çözdükten sonra en sık yapılan hata: marker’ları silip direkt commit atmak. Şunları mutlaka yapın:

# Hala conflict marker var mı kontrol et
grep -r "<<<<<<" src/
grep -r "=======" src/
grep -r ">>>>>>" src/

# Ya da git ile
git diff --check

git diff --check komutu whitespace hatalarını ve conflict marker’ları tespit eder. CI pipeline’ınıza bu kontrolü eklemek de mantıklı, birisi unutkan davranırsa pipeline yakalasın.

Merge sonrası testleri çalıştırmak da conflict çözümünün ayrılmaz parçası:

# Örnek: Node.js projesi için
npm test

# Python projesi için
python -m pytest

# Go projesi için
go test ./...

Özellikle iki tarafın birbirini etkileyen mantık değişiklikleri yaptığı durumlarda, conflict’i “doğru” çözmüş olsanız bile testler patlayabilir. Bu aslında iyi bir şey: testler olmadan bu durumu ancak production’da fark ederdiniz.

Conflict Stratejileri: merge.conflictstyle

Git’in conflict gösterim biçimini değiştirebilirsiniz. Varsayılan merge stili yerine diff3 stili kullanmak, BASE içeriğini de gösteriyor:

git config --global merge.conflictstyle diff3

Bu ayarla conflict blokları şöyle görünüyor:

<<<<<<< HEAD
const timeout = 5000;
||||||| merged common ancestors
const timeout = 3000;
=======
const timeout = 10000;
>>>>>>> feature/retry-logic

Ortadaki ||||||| bloğu BASE versiyonunu gösteriyor. Şimdi görebiliyorsunuz: base 3000’di, biz 5000 yaptık, onlar 10000 yaptı. Büyük olasılıkla 10000’i almak ya da bir tartışma açmak gerekiyor. Bu bilgi olmadan sadece iki tarafı görüyordunuz.

Otomasyon: rerere ile Tekrarlayan Conflict’leri Hatırlatmak

git rerere (reuse recorded resolution) özelliği, daha önce çözdüğünüz conflict’leri hatırlıyor ve aynı conflict tekrar karşınıza çıktığında otomatik uyguluyor. Uzun süreli feature branch’ler veya düzenli rebase yapılan projelerde büyük zaman kazandırıyor:

# Etkinleştir
git config --global rerere.enabled true

# Kayıtlı çözümleri gör
git rerere status

# Kayıtlı çözümleri temizle
git rerere forget src/config/settings.js

rerere etkin olduğunda Git, çözdüğünüz conflict pattern’ini .git/rr-cache altında saklıyor. Bir sonraki seferde aynı conflict çıktığında otomatik olarak daha önce seçtiğiniz çözümü uyguluyor. Dezavantajı: yanlış bir çözümü de aynı şekilde tekrarlayabilir. Bu yüzden rerere kullanırken sonuçları gözden geçirme alışkanlığını kaybetmemek gerekiyor.

Takım Bazlı Conflict Azaltma Stratejileri

Teknik araçların yanı sıra takım pratikleri conflict sıklığını ve şiddetini ciddi ölçüde etkiliyor:

  • Kısa yaşayan branch’ler: Feature branch’lerinizi küçük tutun. İki haftada bir merge yerine iki üç günde bir merge hedefleyin. Bu mümkün değilse en azından düzenli olarak main’i branch’inize entegre edin.
  • Dosya sahipliği: CODEOWNERS dosyası kullanın. Kim hangi modüle dokunuyor, review süreci bunu netleştirir ve çakışmaları azaltır.
  • Modüler mimari: Monolitik dosyalar conflict fabrikasıdır. Büyük config dosyaları, Büyük utility class’ları herkesin dokunduğu yerler olur. Modülerleştirme conflict’i de azaltır.
  • İletişim: “Ben şu dosyaya büyük refactoring yapacağım” demek, başkasının aynı anda aynı dosyaya dokunmasını engelleyebilir.
  • Merge queue: GitHub ve GitLab’ın merge queue özellikleri, PR’ları sırayla merge ederek conflict ihtimalini düşürüyor.

Sonuç

Merge conflict, Git kullanan herkesin kaçınılmaz olarak yaşayacağı bir durum. Önemli olan paniklemeden, sistematik bir yaklaşımla üzerine gitmek. Önce conflict’in bağlamını anlamak, git log --merge ve git blame ile geçmişe bakmak, sonra uygun araçla çözüme geçmek. VS Code, Meld ya da terminal fark etmez, önemli olan hangi kodu neden aldığınızı bilerek karar vermek.

diff3 conflict stilini açmak, rerere aktif etmek ve düzenli rebase yapmak gibi küçük alışkanlıklar uzun vadede ciddi zaman kazandırıyor. Takım bazlı çözümler ise teknik araçlardan çok daha etkili olabiliyor: kısa yaşayan branch’ler, modüler mimari ve proaktif iletişim, conflict’in çoğunu oluşmadan önce engelliyor.

Conflict’i görmezden gelmeyin, git merge --abort ile kaçmayın. O çakışmayı anlamak, iki ayrı düşüncenin aynı kod üzerinde buluşma noktasını anlamaktır. Ve bazen bu tartışma, en iyi kodu üretir.

Bir yanıt yazın

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