Git Conflict Çözmek: Marker'lar, merge --abort ve zdiff3
Conflict'in neden çıktığını, marker'ların nasıl okunduğunu, git status, git log --merge ve git ls-files -u ile durumu görmeyi, adım adım çözümü, git merge --abort'u, merge.conflictStyle zdiff3'ü, --ours/--theirs, mergetool ve rerere'yi gerçek çıktılarla görüyoruz; Git 2.56'daki git add --resolved'a da kısaca değiniyoruz.
Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir; git add --resolved Git 2.56 ile gelir ve burada çıktısız anlatılır.
Önce Tahmin Et
Bahar indirimi yaklaşıyor. Alan, spring-sale branch'inde Dune'un fiyatını 14'ten 12'ye düşürüyor ve README'ye kampanya için bir satır ekliyor. Aynı saatlerde Ada, main'de Dune'un fiyatını 16'ya çıkarıyor. Alan'ın commit'ini bu örnekte aynı repo'da, onun adıyla attık:
$ git switch -c spring-sale
Switched to a new branch 'spring-sale'
$ git commit -am "Discount Dune for the spring sale"
[spring-sale 76a5e12] Discount Dune for the spring sale
2 files changed, 3 insertions(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
$ git commit -am "Raise Dune price"
[main 552f98c] Raise Dune price
1 file changed, 1 insertion(+), 1 deletion(-)
Şimdi Ada git merge spring-sale yazacak. Soru: README değişikliği ne olur? Dune'un fiyatı hangi değerde kalır? Git hiç durmadan bir merge commit oluşturur mu?
Conflict Neden Çıkar?
$ git merge spring-sale
Auto-merging src/books.js
CONFLICT (content): Merge conflict in src/books.js
Automatic merge failed; fix conflicts and then commit the result.
Bir önceki yazıdaki three-way merge'ü hatırla: Git ortak atadan bu yana her iki tarafın neyi değiştirdiğine bakar. README yalnızca Alan'ın tarafında değişti, sorun yok. Ama Dune satırı iki tarafta da, birbirinden farklı biçimde değişti. Atada 14 yazıyordu; bir taraf 16, öbür taraf 12 diyor. Hangisinin doğru olduğunu Git bilemez, çünkü bu bir metin sorunu değil, bir iş kararı. Bu yüzden durur ve kararı sana bırakır.
Conflict bir hata değil, bir soru. Git "ikiniz aynı yere farklı şeyler yazdınız, hangisi kalsın?" diye soruyor. Merge yarım kaldı ama hiçbir şey kaybolmadı; iki tarafın commit'leri yerinde duruyor.
Marker'ları Okumak
$ cat src/books.js
const books = [
<<<<<<< HEAD
{ id: 1, title: "Dune", price: 16, isbn: "9780441013593" },
=======
{ id: 1, title: "Dune", price: 12, isbn: "9780441013593" },
>>>>>>> spring-sale
{ id: 2, title: "Neuromancer", price: 11 },
{ id: 3, title: "Snow Crash", price: 10 },
];
function findBook(id) {
return books.find((b) => b.id === id);
}
module.exports = { books, findBook };
Git dosyaya conflict marker'ları yazdı. <<<<<<< HEAD ile ======= arası senin tarafın, yani merge'ü başlattığın branch'teki hâli. ======= ile >>>>>>> spring-sale arası ise merge ettiğin branch'teki hâli. Marker'ların dışındaki her şey zaten sorunsuz birleştirilmiş durumda. Senin işin yalnızca bu bloğu, marker'larla birlikte, olması gereken son hâliyle değiştirmek.
Bunu bir sözleşme taslağı gibi düşün. İki avukat aynı maddeyi farklı yazmış; editör iki versiyonu alt alta koymuş ve "hangisi?" diye kenara not düşmüş. Maddenin son hâlini sen yazarsın: birini seçebilir, ikisini birleştirebilir ya da tamamen yeni bir cümle kurabilirsin.
Conflict Sırasında Durumu Görmek
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: README.md
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/books.js
git status sana ne yapman gerektiğini adım adım söylüyor. README temiz birleşti ve index'e alındı. src/books.js ise "both modified" olarak bekliyor. Birden fazla dosyada conflict varsa hepsi burada listelenir; çözdükçe listeden düşer.
Conflict sırasında git diff da farklı davranır ve combined diff denen biçimi gösterir. İki sütunlu başlık (@@@) ve satır başlarındaki iki işaret, satırın hangi taraftan geldiğini anlatır. Bir örneğini çözüm adımında göreceğiz. Hangi commit'lerin bu conflict'e yol açtığını görmek istersen git log --merge işe yarar:
$ git log --merge --oneline
552f98c Raise Dune price
76a5e12 Discount Dune for the spring sale
$ git ls-files -u
100644 7c367681de29936ec0c92ec9f899077690c3a3d3 1 src/books.js
100644 8319e439b375fc21a5cc14dfbedc01367530186a 2 src/books.js
100644 b2cbcae16fd76c65f7a2790afc87db7ca097a0b4 3 src/books.js
git log --merge, iki tarafta da conflict'li dosyalara dokunan commit'leri listeler; uzun ömürlü branch'lerde "bu satırı kim, neden değiştirdi?" sorusunun hızlı cevabıdır. git ls-files -u ise perde arkasını gösterir. Conflict sırasında index'te aynı dosyanın üç hâli durur: 1 ortak ata, 2 senin tarafın, 3 öbür taraf. Index'in üç kat dolması, merge'ün neden "yarım" sayıldığını da açıklıyor.
Adım Adım Çözüm
Ada ile Alan konuşup ortak bir karara varıyor: normal fiyat 16 olacak, kampanya süresince 12'ye satılacak. Ada marker'ları silip satırı ikisini de içeren yeni hâliyle yazıyor. Kaydettikten sonra git diff çözümün iki taraftan da farklı olduğunu gösteriyor:
$ git diff
diff --cc src/books.js
index 8319e43,b2cbcae..0000000
--- a/src/books.js
+++ b/src/books.js
@@@ -1,5 -1,5 +1,5 @@@
const books = [
- { id: 1, title: "Dune", price: 16, isbn: "9780441013593" },
- { id: 1, title: "Dune", price: 12, isbn: "9780441013593" },
++ { id: 1, title: "Dune", price: 16, salePrice: 12, isbn: "9780441013593" },
{ id: 2, title: "Neuromancer", price: 11 },
{ id: 3, title: "Snow Crash", price: 10 },
];
Dosyanın çözüldüğünü Git'e git add ile söylersin. Burada add'in anlamı biraz genişliyor: "bu dosyanın son hâli bu, conflict bitti" demek. Sonra merge'ü git commit ile tamamlarsın. Mesaj vermezsen Git editörü Merge branch 'spring-sale' mesajıyla açar:
$ git add src/books.js
$ git status
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: README.md
modified: src/books.js
$ git commit
[main 85ae714] Merge branch 'spring-sale'
$ git log --oneline --graph -4
* 85ae714 Merge branch 'spring-sale'
|\
| * 76a5e12 Discount Dune for the spring sale
* | 552f98c Raise Dune price
|/
* 7111256 Merge branch 'isbn'
|\
Adımları toparlarsak: git status ile conflict'li dosyaları bul, her birini açıp marker'ları temizle, kodun gerçekten çalıştığını kontrol et, git add ile işaretle, git commit ile bitir. Üçüncü adımı atlama. Marker'sız bir dosya her zaman doğru dosya demek değildir; iki tarafın mantığını birleştirirken bir parantezi, bir virgülü kaçırmak çok kolay. Testleri merge commit'ten önce çalıştırmak bu yüzden önemli.
Vazgeçmek: git merge --abort
Bazen conflict'i o an çözmek istemezsin: yanlış branch'i merge etmişsindir ya da önce takım arkadaşınla konuşman gerekiyordur. Merge'ü hiç başlamamış gibi geri almak için:
$ git merge --abort
$ git status
On branch main
nothing to commit, working tree clean
--abort, repo'yu merge'den hemen önceki hâline döndürür. Bu yüzden merge'e temiz bir working tree ile başlamak önemli. Commit'lenmemiş değişikliklerin varken başlattığın bir merge'ü abort edersen, Git o değişiklikleri her zaman eksiksiz geri getiremeyebilir.
zdiff3: Ortak Atayı da Göster
Varsayılan marker'larda bir bilgi eksik: atada ne yazıyordu? 16 ile 12'yi görüyorsun ama orijinal değerin 14 olduğunu bilmiyorsun. Basit bir fiyatta sorun değil; ama iki tarafın aynı fonksiyonu farklı yönlerde değiştirdiği bir conflict'te ata, "kim neyi değiştirdi" sorusunun anahtarıdır. merge.conflictStyle ayarı bunu çözer:
$ git config set --global merge.conflictStyle zdiff3
$ git merge spring-sale
Auto-merging src/books.js
CONFLICT (content): Merge conflict in src/books.js
Automatic merge failed; fix conflicts and then commit the result.
$ head -9 src/books.js
const books = [
<<<<<<< HEAD
{ id: 1, title: "Dune", price: 16, isbn: "9780441013593" },
||||||| 7111256
{ id: 1, title: "Dune", price: 14, isbn: "9780441013593" },
=======
{ id: 1, title: "Dune", price: 12, isbn: "9780441013593" },
>>>>>>> spring-sale
{ id: 2, title: "Neuromancer", price: 11 },
Ortadaki ||||||| 7111256 bölümü ortak atanın hâlini gösterir. Artık okuması kolay: Ada 14'ü 16 yaptı, Alan 14'ü 12 yaptı. diff3 stili de aynı bölümü ekler; zdiff3 ise iki tarafta ortak olan baş ve son satırları blok dışına alarak daha sıkı bir çıktı üretir. Bu ayarı bir kez global olarak açıp unutmanı öneririz. Git 2.46'dan eski bir sürümde git config set yoksa klasik biçim git config --global merge.conflictStyle zdiff3 aynı işi görür.
Tek Tarafı Seçmek ve Yardımcı Araçlar
Conflict'li bir dosyada bir tarafı olduğu gibi almak istiyorsan elle düzenlemene gerek yok:
$ git restore --theirs src/books.js
$ head -3 src/books.js
const books = [
{ id: 1, title: "Dune", price: 12, isbn: "9780441013593" },
{ id: 2, title: "Neuromancer", price: 11 },
--theirs merge ettiğin branch'in hâlini, --ours kendi tarafını yazar. Ardından yine git add gerekir. Eski yazılarda bunun git checkout --theirs biçimini görürsün; aynı işi yapar.
git mergetool, conflict'li dosyaları sırayla yapılandırdığın görsel bir araçta (vimdiff, Meld, Beyond Compare, VS Code gibi) açar; araç merge.tool ayarıyla seçilir. Çoğu editör bugün marker'ları zaten kendisi tanıyıp "Accept Current / Accept Incoming" düğmeleri sunduğu için birçok geliştirici mergetool'a hiç ihtiyaç duymaz.
rerere ("reuse recorded resolution") ise aynı conflict'i tekrar tekrar çözenler için. git config set rerere.enabled true ile açtığında Git conflict'li hâli ve senin çözümünü kaydeder; merge sırasında Recorded preimage for 'src/books.js', commit'te Recorded resolution for 'src/books.js'. satırlarını görürsün. Aynı conflict tekrar çıktığında Resolved 'src/books.js' using previous resolution. der ve çözümü kendisi uygular. Dosyayı yine kontrol edip git add yapmak sana kalır. Uzun ömürlü branch'leri sık sık rebase edenler için büyük zaman kazancıdır.
Git 2.56 bu akışa küçük ama yerinde bir komut ekledi: git add --resolved, yalnızca conflict'i çözülmüş dosyaları stage'ler ve içinde hâlâ conflict marker'ı kalan bir dosya varsa durur. Bu yazıdaki komutlar 2.54 ile denendiği için çıktısını göstermiyoruz.
Sık Düşülen Tuzaklar
Birincisi, marker'ı dosyada unutup commit'lemek. git add marker'lara bakmaz, ne verirsen kabul eder. Diff yazısında gördüğümüz --check burada da işe yarar; stage'lenmiş içerikte kalmış marker'ları satır numarasıyla gösterir:
$ git add src/books.js
$ git diff --cached --check
src/books.js:2: leftover conflict marker
src/books.js:4: leftover conflict marker
src/books.js:6: leftover conflict marker
Bu kontrolü bir pre-commit hook'una koymak ya da CI'da lint ve test adımlarını çalıştırmak, bu tür kazaları ana branch'e ulaşmadan yakalar.
İkincisi, conflict'i "benimki doğru" diye tek taraflı çözmek. --ours ile öbür tarafı sildiğinde Alan'ın değişikliği sessizce kaybolur ve merge commit'te bunun izi pek görünmez. Emin değilsen değişikliği yapan kişiye sor.
Üçüncüsü, conflict sırasında git status'a bakmadan devam etmek. Birden fazla dosya conflict'liyse birini çözüp commit'lemeye çalışırsın ve Git Committing is not possible because you have unmerged files. diyerek durur.
Dördüncüsü, conflict'ten korkup büyük merge'leri ertelemek. İki branch ne kadar uzun süre ayrı kalırsa conflict o kadar büyür. Küçük ve sık merge, büyük ve nadir merge'den her zaman daha az acıtır.
Küçük Bir Deneme
★☆☆ İki branch'te aynı satırı farklı değiştirip merge et. Marker'ları okuyup elle çöz, git add ve git commit ile bitir. Sonra git show ile merge commit'e bak.
★★☆ Aynı conflict'i merge.conflictStyle ayarı zdiff3 iken tekrar oluştur ve ortak ata bölümünü bul. Ardından git merge --abort ile vazgeç ve git status'un temiz olduğunu doğrula.
★★★ rerere.enabled'ı aç, bir conflict çöz ve commit'le. Aynı conflict'i yeniden üretmenin bir yolunu bul (ipucu: aynı iki branch'ten yeni bir branch çiftiyle). Git'in çözümü kendisi uyguladığını gör.
Şimdiye kadar her şey tek bir bilgisayarda, tek bir repo'da oldu. Alan'ın değişikliğini bile aynı repo'da, onun adıyla commit'ledik. Bir sonraki yazıda gerçek ekip çalışmasına geçiyoruz: git clone, origin, remote-tracking branch'ler, fetch ve push.