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

Git Pull'u Doğru Anlamak: fetch, merge, rebase ve Divergent Branches

Mert Özen 6 Kas 2026 9 dk 1 görüntülenme
Git Pull'u Doğru Anlamak: fetch, merge, rebase ve Divergent Branches

git pull'un fetch ile merge ya da rebase'den oluştuğunu, Git 2.33.1'den beri ayrışmış branch'lerde çıkan "divergent branches" hatasını, --no-rebase, --rebase ve --ff-only seçeneklerini, pull.rebase ile pull.ff arasındaki tercihi, Git 2.51'deki pull.autostash'i ve fetch --prune'u gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir; pull.autostash Git 2.51'den beri vardır.

Önce Tahmin Et

Bir önceki yazının sonunda Ada'nın push'u reddedilmişti. Alan README'ye test komutunu yazıp push etmişti; Ada ise bundan habersiz Snow Crash'in fiyatını değiştirip commit'lemişti. Git ona git pull önermişti. Ada da öyle yapıyor. Bu repo'da pull ile ilgili hiçbir ayar yok. Sence ne olur: Git bir merge commit mi oluşturur, Ada'nın commit'ini Alan'ınkinin üstüne mi taşır, yoksa hiçbir şey yapmadan durur mu?

Pull = Fetch + Birleştirme

$ git pull
From ../bookstore
   3510c22..871320c  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

Cevap üçüncüsü: durdu. Ama çıktının ilk iki satırına dikkat et. Git durmadan önce bir iş yaptı: fetch. origin/main, 3510c22'den 871320c'ye ilerledi. git pull tek bir komut gibi görünür ama aslında iki adımdır: önce git fetch, sonra indirilenleri kendi branch'ine birleştirmek. Birinci adım her zaman aynıdır. İkinci adımın merge mi rebase mi olacağı ise senin kararın, ve Git bu kararı sana sormadan vermeyi reddediyor.

$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)

nothing to commit, working tree clean
$ git log --oneline --graph --all -4
* d76d45c Raise Snow Crash price
| * 871320c Document how to run tests
|/
| * dee3ed6 Add title search
|/
* 3510c22 Add Foundation

Fetch'ten sonra durum net: Ada'nın main'i ile origin/main aynı commit'ten ayrılmış, ikisinin de birer commit'i var. Merge yazısındaki main ile discount'un durumunun aynısı; tek fark öbür kolun artık başka bir bilgisayardan gelmesi.

"Divergent Branches" Hatası Neden Var?

Eskiden git pull bu durumda hiçbir şey sormadan merge yapardı. Herkes bir gün geçmişinde Merge branch 'main' of ... diye anlamsız merge commit'leri bulur ve nereden geldiklerini anlamazdı. Git 2.27 önce bir uyarı ekledi: pull.rebase ayarlanmamışsa pull her seferinde nasıl birleştireceğini belirtmeni öneriyordu. Git 2.33.1 ile bu uyarı, branch'ler ayrıştığında bir hataya dönüştü. Yukarıdaki fatal: Need to specify how to reconcile divergent branches. satırı o değişikliğin sonucu.

Hata yalnızca branch'ler ayrıştığında çıkar. Fast-forward mümkünse, yani senin yerel commit'in yoksa, ayar olmasa da pull sorunsuz çalışır. İki yol ayrılmışsa Git'in tahmin yürütmesi gerekirdi; yürütmüyor ve seçimi sana bırakıyor. Üç seçenek var ve hint satırları zaten üçünü de listeliyor.

Seçenek 1: Merge

$ git pull --no-rebase
From ../bookstore
   3510c22..871320c  main       -> origin/main
Merge made by the 'ort' strategy.
 README.md | 2 ++
 1 file changed, 2 insertions(+)
$ git log --oneline --graph -4
*   8fb3f33 Merge branch 'main' of ../bookstore
|\
| * 871320c Document how to run tests
* | d76d45c Raise Snow Crash price
|/
* 3510c22 Add Foundation

--no-rebase, pull'un ikinci adımını merge olarak yapar: fetch, ardından git merge origin/main. Sonuç three-way merge ve bir merge commit. Hiçbir commit'in hash'i değişmez, geçmiş olduğu gibi kalır. Bedeli ise grafikteki küçük çatallanma ve Merge branch 'main' of ../bookstore gibi bir mesaj. Gerçek bir özelliğin birleştiğini anlatan bir merge değil bu; sadece iki kişinin aynı branch'e aynı saatlerde commit attığını söylüyor. Ekip büyüdükçe bu tür merge'ler geçmişi okumayı zorlaştırır.

Seçenek 2: Rebase

$ git pull --rebase
From ../bookstore
   3510c22..871320c  main       -> origin/main
Successfully rebased and updated refs/heads/main.
$ git log --oneline --graph -3
* 5e9fee1 Raise Snow Crash price
* 871320c Document how to run tests
* 3510c22 Add Foundation
$ git status -sb
## main...origin/main [ahead 1]
$ git push
To ../bookstore.git
   871320c..5e9fee1  main -> main

--rebase, Ada'nın yerel commit'ini geçici olarak kenara alır, main'i origin/main'e getirir ve Ada'nın commit'ini onun üstüne yeniden uygular. Terminalde bir an Rebasing (1/1) satırını görürsün; bitince yerini sonuç satırı alır. Geçmiş düz bir çizgi oldu, merge commit yok, ve Ada artık sorunsuz push edebiliyor.

Ama hash'e dikkat et: Ada'nın commit'i d76d45c idi, şimdi 5e9fee1. İçerik aynı, parent farklı, bu yüzden yeni bir commit. Rebase commit'leri taşımaz, yeniden yazar. Henüz push etmediğin yerel commit'ler için bu hiçbir sorun yaratmaz. Başkalarının üzerine çalıştığı commit'leri rebase etmek ise ayrı bir konu; onu rebase yazısında "golden rule" ile birlikte ele alacağız.

Seçenek 3: Sadece Fast-forward

$ git pull --ff-only
From ../bookstore
   3510c22..871320c  main       -> origin/main
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.

--ff-only "sadece ileri sarabiliyorsan yap" der. Branch'ler ayrıştığı için durdu ve kararı yine sana bıraktı. Bu bir eksiklik değil, tasarım: hiçbir zaman beklenmedik bir merge ya da rebase yapmaz. Yerel commit'in yoksa ise düz bir ileri sarmadır. Ada push ettikten sonra Alan'ın tarafı tam olarak böyle görünüyor; Alan'ın hiçbir pull ayarı yok ve yine de bir şey sorulmuyor:

# Alan'ın bilgisayarında
$ git pull
From .../bookstore
   871320c..5e9fee1  main       -> origin/main
Updating 871320c..5e9fee1
Fast-forward
 src/books.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Hangisini Seçmeli?

Her pull'da seçeneği yazmak yerine bir kez ayarlarsın. İki yaygın tercih var:

# Seçenek A: yerel commit'leri her zaman remote'un üstüne taşı
$ git config set --global pull.rebase true
# Seçenek B: sadece fast-forward, ayrışınca dur ve karar ver
$ git config set --global pull.ff only

A seçeneği, yani pull.rebase true, günlük işte en az sürtünmeyi verir. Pull her zaman çalışır ve geçmiş düz kalır. Bedeli, rebase sırasında conflict çıkarsa onu merge'den biraz farklı bir akışla çözmen gerekmesi. Feature branch'lerde çalışıp main'e pull request ile merge eden ekipler için iyi bir varsayılan.

B seçeneği, yani pull.ff only, en güvenlisidir. Git hiçbir zaman senin yerine merge ya da rebase yapmaz; ayrışma olduğunda durur ve sen git pull --rebase ya da git pull --no-rebase ile bilinçli olarak seçersin. Geçmişi her zaman kontrol altında tutmak isteyenler ve otomasyon için idealdir. Sunucuda çalışan bir deploy script'inin git pull --ff-only kullanması da aynı mantıktan gelir.

pull.rebase false ise eski merge davranışını açıkça seçmek demek. Ayar branch.main.rebase gibi branch bazında da verilebilir. Git 2.46'dan eski sürümlerde git config set yerine klasik git config --global pull.rebase true biçimini kullanırsın.

pull.autostash ve fetch --prune

Pull'a commit'lenmemiş değişikliklerle girmek bir başka sık durum. Alan README'de küçük bir düzenleme yapmış ve henüz commit'lememiş:

# Alan'ın bilgisayarında
$ git pull --rebase
error: cannot pull with rebase: You have unstaged changes.
error: Please commit or stash them.
$ git config set pull.autostash true
$ git pull --rebase
From .../bookstore
   871320c..5e9fee1  main       -> origin/main
Updating 871320c..5e9fee1
Created autostash: 36d6e05
Fast-forward
 src/books.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Applied autostash.
$ git status -s
 M README.md

pull.autostash Git 2.51 ile geldi ve yalnızca git pull için geçerlidir: pull'dan önce yerel değişiklikleri stash'e alır, pull bittikten sonra geri koyar. rebase.autostash ve merge.autostash ayarlarını pull için geçersiz kılar. Geri koyarken conflict çıkabileceğini unutma; stash yazısında bu durumu ayrıca göreceğiz.

Son temizlik konusu: remote'ta silinen branch'ler. Ada, merge edilen arama branch'ini remote'tan sildi. Alan'ın repo'sunda ise bundan haberi olmayan bir remote-tracking branch kaldı:

$ git push origin --delete feature/search
To ../bookstore.git
 - [deleted]         feature/search
# Alan'ın bilgisayarında
$ git fetch
$ git branch -a
  feature/search
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/feature/search
  remotes/origin/main
$ git fetch --prune
From .../bookstore
 - [deleted]         (none)     -> origin/feature/search
$ git branch -vv
  feature/search dee3ed6 [origin/feature/search: gone] Add title search
* main           5e9fee1 [origin/main] Raise Snow Crash price

Düz git fetch yeni şeyleri indirir ama remote'ta silinmiş branch'lerin yerel kopyasını silmez. --prune bunu yapar. Alan'ın yerel feature/search branch'i ise yerinde duruyor; gone etiketi, upstream'inin artık olmadığını söylüyor. Onu silmek yine Alan'ın kararı. Her fetch'te prune istiyorsan git config set --global fetch.prune true yeterli.

Sık Düşülen Tuzaklar

Birincisi, hint'teki komutu okumadan kopyalamak. git config pull.rebase false satırı listede ilk sırada diye "doğru cevap" değildir; eski merge davranışını geri getirir. Üç seçeneğin ne yaptığını bilerek birini seç.

İkincisi, pull'u körlemesine çalıştırmak. Pull, fetch ile birleştirmeyi tek adımda yapar ve gelenlere bakma fırsatını elinden alır. Emin olmadığın durumlarda önce git fetch, sonra git log --oneline main..origin/main ile gelenleri gör, sonra karar ver.

Üçüncüsü, rebase sonrası hash değişti diye paniklemek ya da tam tersine push edilmiş commit'leri rebase'lemek. Yerel, paylaşılmamış commit'lerin hash'inin değişmesi normaldir. Başkalarının elindeki commit'leri yeniden yazmak ise --force push gerektirir ve onların işini bozar.

Dördüncüsü, rebase sırasında çıkan conflict'te git merge --abort yazmak. Pull'un ikinci adımı rebase ise yarım kalan iş de bir rebase'dir; vazgeçmek için git rebase --abort, çözüp devam etmek için git rebase --continue gerekir. git status hangisinin içinde olduğunu her zaman söyler.

Küçük Bir Deneme

★☆☆ Bir bare repo ve iki clone ile Ada'nın durumunu yeniden kur: iki clone'da da main'e birer commit at, birinden push et. Öbüründe hiçbir ayar yokken git pull çalıştır ve hatayı oku.

★★☆ Aynı başlangıçtan iki kez ilerle: bir kez git pull --no-rebase, bir kez git pull --rebase. İki sonucun git log --oneline --graph çıktısını ve yerel commit'in hash'ini karşılaştır.

★★★ pull.ff only ayarla, sonra ayrışmış bir branch'te önce git pull, ardından git pull --rebase çalıştır. Komut satırındaki seçeneğin ayarı nasıl geçersiz kıldığını açıkla. Bonus: pull.autostash açıkken commit'lenmemiş değişiklik, gelen commit'le aynı satıra dokunuyorsa ne olur?

Artık Ada ile Alan aynı repo üzerinden düzgün çalışabiliyor, ama remote'umuz hâlâ yerel bir bare repo. Bir sonraki yazıda gerçek GitHub'a bağlanıyoruz: repo oluşturmak, HTTPS ve personal access token, credential helper ve SSH anahtarlarıyla kimlik doğrulama.