Mert Özen Her Satırda Daha İleri
DevOps Git & GitHub — #09

Git Merge: Fast-forward, Three-way Merge ve Merge Commit

Mert Özen 6 Kas 2026 8 dk 1 görüntülenme
Git Merge: Fast-forward, Three-way Merge ve Merge Commit

Fast-forward ile three-way merge arasındaki farkı, ortak atanın (git merge-base) rolünü, iki parent'lı merge commit'in içini, --no-ff ve --ff-only seçeneklerini, Git 2.34'ten beri varsayılan olan ort stratejisini ve merge sonrası branch temizliğini gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir; ort Git 2.34'ten beri varsayılan merge stratejisidir.

Önce Tahmin Et

Bir önceki yazıda bıraktığımız yerdeyiz. Bookstore repo'sunun grafiği şöyle:

$ git log --oneline --graph --all
* 593811b Try a lower price
| * a170771 Add wishlist
| * 5bcf046 Add Snow Crash
| | * 86303f4 Add discount to cart total
| |/
| * 6651492 Reword README
|/
* a5a7a57 Update book prices
* 6dde20b Add cart total
* f9b9660 Add .gitignore
* 02ea975 Add book list
* 7ff0565 Add README

main şu an 5bcf046'yı gösteriyor. wishlist branch'i onun bir commit önünde, discount ise 6651492'de yolunu ayırmış. Şimdi main'deyken önce git merge wishlist, ardından git merge discount yazacağız. Soru: bu iki komuttan hangisi yeni bir commit oluşturur? İkisi de mi, biri mi, hiçbiri mi?

Fast-forward: Pointer'ı İleri Kaydırmak

$ git switch main
Already on 'main'
$ git merge wishlist
Updating 5bcf046..a170771
Fast-forward
 src/wishlist.js | 9 +++++++++
 1 file changed, 9 insertions(+)
 create mode 100644 src/wishlist.js
$ git branch -v
  discount   86303f4 Add discount to cart total
* main       a170771 Add wishlist
  price-test 593811b Try a lower price
  wishlist   a170771 Add wishlist

Yeni commit yok. main'in gösterdiği commit, wishlist'in doğrudan atasıydı; yani main'den wishlist'e giden yol düz bir çizgi. Bu durumda birleştirilecek bir şey kalmaz. Git sadece main pointer'ını ileri kaydırır ve iki branch aynı commit'i göstermeye başlar. Çıktıdaki Fast-forward kelimesi ve Updating 5bcf046..a170771 satırı bunu anlatıyor.

Bunu bir video oynatıcıdaki ileri sarma gibi düşün. Film zaten çekilmiş; sen sadece okun durduğu yeri ileri alıyorsun. Hiçbir sahne yeniden çekilmiyor.

Three-way Merge: Ortak Ata ve İki Uç

İkinci merge farklı. main ile discount ortak bir atadan sonra ayrı yollara gitmiş, ikisinin de kendine ait commit'leri var. Bu kez pointer'ı kaydırmak yetmez; iki tarafın değişikliğini birleştiren yeni bir snapshot gerekir. Git bunun için önce ortak atayı bulur:

$ git merge-base main discount
66514929b4fe53a14d1a73a9334836979ca13ccf
$ git merge discount
Merge made by the 'ort' strategy.
 src/cart.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

Adın neden "three-way" olduğu burada belli oluyor. Git üç commit'e bakar: ortak ata 6651492, senin tarafın a170771 ve öbür taraf 86303f4. Her dosya için şunu sorar: atadan bu yana kim neyi değiştirdi? src/cart.js yalnızca discount tarafında değişmiş, o hâli alınır. src/books.js ve src/wishlist.js yalnızca main tarafında değişmiş, onlar da korunur. İki taraf aynı dosyanın aynı yerine dokunmadığı sürece Git bu birleştirmeyi kendisi yapar. Dokunduklarında ise conflict çıkar; o bir sonraki yazının konusu.

Ortak ata olmadan bu karar verilemezdi. Sadece iki ucu karşılaştırsaydın, discount'ta Snow Crash olmadığını görür ama "biri ekledi mi, öbürü sildi mi?" sorusunu cevaplayamazdın. Ata, hangi tarafın neyi değiştirdiğini ayırt etmeni sağlayan referans noktası. Diff yazısındaki üç noktalı git diff main...discount de tam olarak bu atadan hesaplanıyordu.

Gerçek hayatta bu komut bir editör açar ve Merge branch 'discount' varsayılan mesajını önüne koyar. Mesajı olduğu gibi bırakıp kaydetmen yeterli. Editör açılsın istemiyorsan --no-edit ekleyebilirsin.

Merge Commit'in İçi

$ git log --oneline --graph
*   c3d85ee Merge branch 'discount'
|\
| * 86303f4 Add discount to cart total
* | a170771 Add wishlist
* | 5bcf046 Add Snow Crash
|/
* 6651492 Reword README
* a5a7a57 Update book prices
* 6dde20b Add cart total
* f9b9660 Add .gitignore
* 02ea975 Add book list
* 7ff0565 Add README
$ git cat-file -p HEAD
tree e5f4d0645109bec6c2e8144808e14cd66b1897a0
parent a170771d07f6cf4f7a1d038a3808bcb3c008eab2
parent 86303f4e515d8b599a9f7f53706b526f80f79d74
author Ada Lovelace <ada@example.com> 1794035520 +0300
committer Ada Lovelace <ada@example.com> 1794035520 +0300

Merge branch 'discount'

Merge commit, sıradan bir commit'ten tek bir farkla ayrılır: iki parent'ı vardır. İlki merge yaptığın sırada üzerinde durduğun branch'in ucu (a170771), ikincisi merge ettiğin branch'in ucu (86303f4). Tree satırı ise birleştirilmiş snapshot'ı gösterir. Commit'in içini anlattığımız yazıdaki yapının aynısı; sadece bir parent satırı fazla.

Parent sırası önemsiz bir ayrıntı gibi görünse de ileride işine yarayacak. git log --first-parent yalnızca ilk parent'ları takip ederek main'in kendi çizgisini gösterir. Bir merge commit'i revert ederken kullanılan -m 1 de "birinci parent'ı ana çizgi say" demektir.

--no-ff: Fast-forward'a Rağmen Merge Commit

Fast-forward temiz bir geçmiş bırakır ama bir bilgiyi kaybettirir: o commit'lerin bir branch'te geliştirildiği artık grafikte görünmez. Bazı ekipler her özelliğin sınırını geçmişte görmek ister. Bunun için --no-ff var. Dune kaydına bir ISBN alanı ekleyen küçük bir branch açıp deneyelim:

$ git switch -c isbn
Switched to a new branch 'isbn'
$ git commit -am "Add ISBN to Dune"
[isbn 0c535da] Add ISBN to Dune
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
$ git merge --no-ff isbn
Merge made by the 'ort' strategy.
 src/books.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph -4
*   7111256 Merge branch 'isbn'
|\
| * 0c535da Add ISBN to Dune
|/
*   c3d85ee Merge branch 'discount'
|\
| * 86303f4 Add discount to cart total

main hiç ilerlemediği için burada fast-forward mümkündü. --no-ff buna rağmen bir merge commit oluşturdu ve isbn'in küçük kolu grafikte kaldı. GitHub'daki pull request ekranında "Create a merge commit" seçeneği de bu davranışla çalışır. Hangisinin doğru olduğu ekibin tercihi; bu konuya branch stratejileri yazısında döneceğiz.

--ff-only: Sadece İleri Sarılabiliyorsa

Tersini istediğin durumlar da var: "Fast-forward olabiliyorsa yap, olamıyorsa hiçbir şey yapma." Detached HEAD'den kurtardığımız price-test branch'i main'den çok önce ayrılmıştı, ileri sarılamaz:

$ git merge --ff-only price-test
hint: Diverging branches can't be fast-forwarded, you need to either:
hint:
hint: 	git merge --no-ff
hint:
hint: or:
hint:
hint: 	git rebase
hint:
hint: Disable this message with "git config set advice.diverging false"
fatal: Not possible to fast-forward, aborting.

Git hiçbir şeye dokunmadan durdu ve iki seçenek önerdi. --ff-only özellikle otomasyonda değerlidir: bir deploy script'i sunucudaki kopyayı güncellerken beklenmedik bir merge commit üretmemeli. CI/CD tarafında "sadece ileri sar, olmuyorsa hata ver" kuralı, sunucuda kimsenin fark etmeden geçmişi dallandırmasını engeller. Aynı fikir git pull için de geçerli; pull yazısında pull.ff only ayarıyla tekrar karşılaşacağız.

ort: Varsayılan Strateji

Merge çıktılarındaki Merge made by the 'ort' strategy. satırı, işi hangi merge stratejisinin yaptığını söylüyor. Git 2.34'ten önce varsayılan strateji recursive idi ve eski yazılarda bu çıktıyı 'recursive' strategy diye görürsün. ort aynı işi baştan yazılmış, daha hızlı bir kodla yapar ve özellikle yeniden adlandırılmış dosyaların olduğu büyük merge'lerde fark yaratır. Git 2.34 sürüm notları bunu açıkça söyler: varsayılan olarak recursive yerine ort kullanılır.

Günlük işte stratejiyi seçmen gerekmez. -s ort ile açıkça belirtebilirsin, ama bugünkü bir Git'te zaten varsayılan budur. Stratejiden farklı olarak -X ours ya da -X theirs gibi strateji seçenekleri vardır; bunlar conflict'lerde tek tarafı otomatik seçer ve dikkatsiz kullanıldığında öbür tarafın değişikliğini sessizce yutar. Onları conflict'i elle çözmeyi öğrendikten sonra kullan.

Merge Sonrası Temizlik

Merge'den sonra feature branch'leri tutmanın bir anlamı yok; commit'leri artık main'den ulaşılabilir durumda. Hangilerinin güvenle silinebileceğini Git'e sorabilirsin:

$ git branch --merged
  discount
  isbn
* main
  wishlist
$ git branch --no-merged
  price-test
$ git branch -d wishlist discount isbn
Deleted branch wishlist (was a170771).
Deleted branch discount (was 86303f4).
Deleted branch isbn (was 0c535da).
$ git branch -v
* main       7111256 Merge branch 'isbn'
  price-test 593811b Try a lower price

--merged, ucu şu anki branch'ten ulaşılabilen branch'leri listeler; --no-merged ise tersini. -d zaten merge edilmemiş bir branch'i silmeyi reddettiği için bu temizlik güvenlidir. GitHub'da da bir pull request merge edildikten sonra branch'i silme düğmesi çıkar; GitHub flow'un son adımı budur.

Git 2.56 bu işe yeni bir kısayol ekledi: git branch --delete-merged, upstream'ine zaten merge edilmiş yerel branch'leri topluca siler ve --dry-run ile önce ne sileceğini gösterir. Bu yazıdaki komutlar 2.54 ile denendiği için çıktısını göstermiyoruz; upstream kavramını da remote yazısında açacağız.

Sık Düşülen Tuzaklar

Birincisi, merge'ü ters yönde yapmak. git merge discount, discount'u üzerinde bulunduğun branch'e alır. discount'tayken git merge main yazarsan main'in değişikliklerini feature branch'e çekmiş olursun. Bu bazen istenen bir şey, ama genelde değil. Merge'den önce git status'un ilk satırına bak.

İkincisi, working tree temiz değilken merge başlatmak. Git, merge'ün dokunacağı dosyalarda commit'lenmemiş değişiklik varsa durur. Dokunmayacağı dosyalarda ise devam eder, ve conflict çıkarsa kendi yarım işinle merge'ün yarım işi birbirine karışır. Merge'den önce commit'le ya da stash'e al.

Üçüncüsü, fast-forward'dan sonra "branch kayboldu" diye endişelenmek. Hiçbir commit kaybolmadı; wishlist'in commit'i main'in çizgisine oturdu. Kaybolan tek şey, bu commit'in ayrı bir branch'te yazıldığı bilgisi. O bilgi senin için önemliyse --no-ff kullan.

Dördüncüsü, git pull'un da arka planda merge yaptığını unutmak. "Ben merge yapmadım, bu merge commit nereden çıktı?" sorusunun cevabı çoğunlukla pull'dur. Pull yazısında bunu ayrıntılı göreceğiz.

Küçük Bir Deneme

★☆☆ main'den yeni bir branch aç, iki commit at ve main'e dönüp merge et. Çıktıda Fast-forward görüp görmeyeceğini önceden tahmin et. Sonra git log --oneline --graph ile kontrol et.

★★☆ Aynı denemeyi bu sefer main'e de bir commit ekleyerek tekrarla, farklı bir dosyaya dokun. Merge'den önce git merge-base ile ortak atayı bul, merge'den sonra git cat-file -p HEAD ile iki parent'ı kontrol et.

★★★ git log --oneline --first-parent ile düz git log --oneline çıktısını karşılaştır. main'in "kendi" commit'leri hangileri? --no-ff ile yapılan merge'lerin bu görünümde ne işe yaradığını açıkla.

Bu yazıdaki merge'lerin hepsi sorunsuz geçti, çünkü iki taraf hiçbir zaman aynı satıra dokunmadı. Gerçek hayatta ise Ada ile Alan aynı kitabın fiyatını aynı anda değiştirir. Bir sonraki yazıda conflict'in neden çıktığını, marker'ları nasıl okuyacağını ve onu adım adım nasıl çözeceğini göreceğiz.