Git 2.23–2.39: checkout'tan switch ve restore'a, Eski Alışkanlıktan Yenisine
Git 2.23–2.39 arasında gelenleri eskiyle yeniyi yan yana koyarak görüyoruz: checkout'tan switch ve restore'a geçiş, init.defaultBranch, pull'un divergent branches hatası, sparse-checkout, maintenance, fsmonitor, ort ve remerge-diff, rebase --update-refs, force-if-includes, safe.directory ve SSH imzalama.
Sürüm notu: Bu yazı Git 2.23 ile 2.39 arasında gelen değişiklikleri anlatır; komutlar Git 2.54 ile denenmiştir. Eski sürümlerin davranışı release notes'tan aktarılmıştır.
Önce Tahmin Et
Bookstore repo'sunda src/cart.js'e henüz commit'lemediğin bir satır ekledin. StackOverflow'da gördüğün eski bir cevaba bakıp şu komutu yazıyorsun:
$ git checkout src/cart.js
Bu komut seni src/cart.js adında bir branch'e mi geçirir, yoksa dosyaya bir şey mi yapar? Ve ne yaparsa yapsın, sana soru sorar mı?
$ git checkout src/cart.js
Updated 1 path from the index
$ git diff --stat
$ git switch src/cart.js
fatal: invalid reference: src/cart.js
checkout dosyayı index'teki hâline döndürdü ve eklediğin satır hiçbir onay sorulmadan gitti. git diff --stat'ın boş dönmesi bunun kanıtı. Aynı adı switch'e verdiğinde ise Git "böyle bir branch yok" deyip durdu. Bu iki satır, Git 2.23'ten bu yana gelen değişikliklerin ruhunu özetliyor: aynı komutun iki farklı işi yapması yerine her işin kendi komutu, tehlikeli varsayılanlar yerine açık seçimler.
Blok B'nin ilk yazısı bu. Kemik yapıda hep güncel komutları kullandık; burada "eskiden böyle yapıyorduk, Git 2.xx ile şöyle" diye geriye bakacağız. Eski davranışları release notes'tan aktarıyorum, çıktıların hepsi Git 2.54'ten.
checkout'un İki İşi: switch ve restore (2.23)
git checkout yıllarca iki işi birden yaptı: branch değiştirmek ve dosyaları geri yüklemek. Argüman bir branch adıysa birini, bir dosya yoluysa diğerini yapıyordu. Aynı adda bir branch ve bir dosya varsa ne olacağını bilmek için -- ayıracını hatırlaman gerekiyordu. Git 2.23 bu iki işi ayırdı:
$ git restore src/cart.js
$ git switch -c feature/discount
Switched to a new branch 'feature/discount'
$ git switch -
Switched to branch 'main'
$ git switch --detach HEAD~1
HEAD is now at 14693d0 Add bookstore skeleton
$ git switch main
Previous HEAD position was 14693d0 Add bookstore skeleton
Switched to branch 'main'
git checkout -b artık git switch -c, git checkout -- dosya artık git restore dosya, git reset HEAD dosya ise git restore --staged dosya. switch bir commit'e geçerken detached HEAD'e girmeyi sessizce yapmaz, --detach ister. switch'in --guess davranışı varsayılan olarak açık: git switch foo dediğinde yerelde foo yoksa ve tek bir origin/foo varsa onu takip eden bir branch açar. 2.30'dan beri bunu checkout.guess ayarıyla kapatabilirsin.
İki komut 2.23'te man sayfalarında "EXPERIMENTAL" etiketiyle geldi ve bu etiket ancak 2.51'de kalktı. Yani yıllarca resmi olarak deneysel kaldılar, ama pratikte herkes kullandı. git checkout ise kaldırılmıyor; Git 3.0 hazırlık belgesi bunu açıkça yazıyor. Eski script'lerin bozulmayacak, sadece yeni alışkanlıklar için switch ve restore daha güvenli.
init.defaultBranch (2.28)
Eskiden git init ilk branch'e her zaman master adını verirdi ve bunu değiştirmenin tek yolu sonradan git branch -m yapmaktı. GitHub 1 Ekim 2020'de yeni repo'larda varsayılanı main yaptı; Git 2.28 ise bu adı ayarlanabilir hâle getirdi. Hiçbir ayarı olmayan bir makinede bugün git init şunu söylüyor:
$ git init fresh
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
hint:
hint: Disable this message with "git config set advice.defaultBranchName false"
Initialized empty Git repository in .../fresh/.git/
Bu serinin ikinci yazısında init.defaultBranch main ayarını bu yüzden en başta yaptık. Hint'in ikinci satırı da bir sonraki yazıların konusu: varsayılanın Git 3.0'da main olması planlanıyor.
pull: Uyarıdan Hataya (2.27, 2.33.1)
Eskiden git pull, yerel ve uzak branch ayrıştığında hiçbir şey sormadan merge yapar, geçmişe "Merge branch 'main' of ..." commit'leri bırakırdı. Git 2.27 bunu bir uyarıya çevirdi: pull.rebase ayarlı değilse ve komut satırında da seçim yapmadıysan uyarı basıp yine merge ediyordu. 2.33.1'den beri ise branch'ler ayrışmışsa Git hiç tahmin etmiyor:
$ git pull
From ../bookstore
4734313..5d4e711 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.
Dikkat et, fetch yapıldı ve origin/main güncellendi; sadece birleştirme adımı durdu. Hiçbir şey kaybolmadı, bir seçim yapman isteniyor. Pull yazısında bu seçimi ayrıntılı konuşmuştuk.
Büyük Repo'lar İçin: sparse-checkout, maintenance, fsmonitor, scalar
Bu dönemin değişikliklerinin önemli bir kısmı Microsoft ve GitHub'ın dev monorepo'larından geldi. İlki 2.25'teki git sparse-checkout komutu. Daha önce aynı iş core.sparseCheckout ayarı ve elle düzenlenen .git/info/sparse-checkout dosyasıyla yapılıyordu. Şimdi working tree'de yalnızca istediğin klasörleri tutabilirsin:
$ git sparse-checkout set src
$ ls
README.md
src
$ git sparse-checkout list
src
$ git sparse-checkout disable
$ ls
README.md
docs
src
docs klasörü kayboldu ama kökteki dosyalar kaldı. Bu "cone" modu: klasör bazında çalışır ve hızlıdır. 2.37'den beri varsayılan mod bu, eski pattern tabanlı mod ise deprecated.
2.29 git maintenance'ı getirdi. Release notes onu "git gc'nin büyük kardeşi" diye tanıtıyor. Zamanlanmış bakım için git maintenance start var: repo'yu kaydeder ve işletim sisteminin zamanlayıcısına görev ekler. 2.31'den beri cron'un yanında macOS'ta launchd'yi, Windows'ta Task Scheduler'ı da kullanabiliyor. Zamanlayıcıya dokunmamak için burada sadece kayıt kısmını gösteriyorum:
$ git maintenance register
$ git config list --show-scope | grep maintenance
global maintenance.repo=.../bookstore
local maintenance.auto=false
local maintenance.strategy=incremental
$ git maintenance unregister
2.37 Windows ve macOS'a yerleşik bir dosya sistemi izleyicisi ekledi. core.fsmonitor açıkken Git her git status'ta bütün dosyaları taramak yerine bir daemon'a "son seferden beri ne değişti?" diye sorar:
$ git config set core.fsmonitor true
$ git status -s
$ git fsmonitor--daemon status
fsmonitor-daemon is watching '.../bookstore'
$ git fsmonitor--daemon stop
$ git config unset core.fsmonitor
Küçük bir repo'da fark hissetmezsin; yüz binlerce dosyalı bir repo'da ise git status belirgin şekilde hızlanır. 2.38 ise bu ayarların hepsini tek komutta toplayan scalar'ı Git'in içine aldı. Her kurulumda gelmeyebilir; örneğin bu yazıyı denediğim macOS'taki Apple Git'te yok.
Merge ve Rebase Tarafında: ort, remerge-diff, --update-refs
2.34'ten beri varsayılan merge stratejisi ort. Önceki varsayılan recursive'di; eski sürümlerde merge sonrası bu satırda "recursive" yazdığını hatırlayabilirsin. ort aynı sonuçları çok daha hızlı üretmek ve rename'lerle daha iyi başa çıkmak için sıfırdan yazıldı:
$ git merge --no-ff -m "Merge docs/readme" docs/readme
Merge made by the 'ort' strategy.
README.md | 2 ++
1 file changed, 2 insertions(+)
2.36'daki --remerge-diff ise code review'da çok işe yarar. Bir merge commit'inde conflict'in nasıl çözüldüğünü görmek eskiden zordu. Şimdi Git merge'ü arka planda yeniden yapıp, otomatik sonuçla commit'lenmiş hâl arasındaki farkı gösteriyor:
$ git merge feature/vat
Auto-merging src/cart.js
CONFLICT (content): Merge conflict in src/cart.js
Automatic merge failed; fix conflicts and then commit the result.
$ git commit -m "Merge feature/vat"
[main 5ae3eea] Merge feature/vat
$ git log -1 --remerge-diff
commit 5ae3eeae85374b5a38401f24bbedeb80c2878a46
Merge: e95afaf b7165de
Author: Ada Lovelace <ada@example.com>
Date: Sat Nov 7 10:28:00 2026 +0300
Merge feature/vat
diff --git a/src/cart.js b/src/cart.js
remerge CONFLICT (content): Merge conflict in src/cart.js
index b9e845c..7c03358 100644
--- a/src/cart.js
+++ b/src/cart.js
@@ -1,7 +1,3 @@
export function total(items) {
-<<<<<<< e95afaf (Merge docs/readme)
- return items.reduce((sum, item) => sum + item.price * item.qty, 0);
-=======
- return items.reduce((sum, item) => sum + item.price * 1.2, 0);
->>>>>>> b7165de (Add VAT to cart total)
+ return items.reduce((sum, item) => sum + item.price * item.qty * 1.2, 0);
}
Diff'in sol tarafı conflict marker'larıyla otomatik sonuç, sağ tarafı senin çözümün. Conflict çıkmayan bir merge'de bu diff boş gelir; merge sırasında elle eklenmiş bir değişiklik varsa hemen göze batar.
2.38'deki --update-refs, birbirinin üstüne kurulmuş branch'lerle çalışanların hayatını kolaylaştırdı. feature/coupon-ui, feature/coupon-model'in üstünde duruyor ve ikisini birden main'in üstüne taşımak istiyorsun:
$ git log --oneline --graph --all --decorate
* 040e476 (main) Add license note
| * 274649e (HEAD -> feature/coupon-ui) Add coupon form
| * 1e533fd (feature/coupon-model) Add coupon model
|/
* 5fc3d11 Add cart total
* 5f1daed Add bookstore skeleton
$ git rebase --update-refs main
Successfully rebased and updated refs/heads/feature/coupon-ui.
Updated the following refs with --update-refs:
refs/heads/feature/coupon-model
$ git log --oneline --graph --all --decorate
* 49f3940 (HEAD -> feature/coupon-ui) Add coupon form
* e3fa032 (feature/coupon-model) Add coupon model
* 040e476 (main) Add license note
* 5fc3d11 Add cart total
* 5f1daed Add bookstore skeleton
Eskiden alttaki branch eski commit'te kalırdı ve onu git branch -f ile elle yeni yerine taşıman gerekirdi. rebase.updateRefs true ayarıyla bu davranışı varsayılan yapabilirsin.
Daha Güvenli Force Push: --force-if-includes (2.30)
--force-with-lease, "uzaktaki branch benim bildiğim yerdeyse üzerine yaz" demektir. Sorun, "benim bildiğim yer"in origin/main olması. Editörün arka planda git fetch çalıştırırsa Git, Alan'ın yeni commit'ini görmüş gibi davranır ve lease anlamını yitirir. Ada son commit'ini amend etti; bu sırada Alan bir commit push etti ve Ada'nın tarafında bir fetch çalıştı:
$ git commit --amend -q -m "Note discount rules"
$ git fetch
From ../bookstore
b96752d..466788a main -> origin/main
$ git push --force-with-lease --force-if-includes
To ../bookstore.git
! [rejected] main -> main (remote ref updated since checkout)
error: failed to push some refs to '../bookstore.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git push --force-with-lease
To ../bookstore.git
+ 466788a...87c5868 main -> main (forced update)
$ git log --oneline origin/main
87c5868 Note discount rules
5d4e711 Add Ulysses
4734313 Add cart total
14693d0 Add bookstore skeleton
--force-if-includes, uzaktaki ucun senin yerel branch'inin reflog'unda geçip geçmediğine bakar. Alan'ın commit'i Ada'nın branch'ine hiç girmediği için push'u reddetti. Tek başına --force-with-lease ise geçti ve Alan'ın "Add currency" commit'i geçmişten silindi. Bu yüzden push.useForceIfIncludes true ayarını açık tutmak iyi bir alışkanlık.
Güvenlik: safe.directory (2.35.2) ve SSH İmzalama (2.34)
Mart 2022'de çıkan 2.35.2, CVE-2022-24765 için Git'in davranışını değiştirdi. Paylaşılan bir makinede başka bir kullanıcı, senin üst klasörlerinden birine bir .git klasörü koyup oradaki config ile komut çalıştırabiliyordu. O sürümden beri Git, sahibi sen olmayan bir repo'da çalışmayı reddediyor:
$ GIT_TEST_ASSUME_DIFFERENT_OWNER=1 git status
fatal: detected dubious ownership in repository at '.../bookstore'
To add an exception for this directory, call:
git config --global --add safe.directory .../bookstore
Bu çıktıyı görmek için başka bir kullanıcı açman gerekmiyor: GIT_TEST_ASSUME_DIFFERENT_OWNER, Git'in kendi test'lerinde kullandığı ve repo'nun sahibi farklıymış gibi davranmasını sağlayan bir değişken. Gerçek hayatta bu hatayı en sık Docker volume'larında ve CI runner'larında görürsün. Düzeltmesi hatanın kendisinde yazıyor; * değeri her şeye güvenir ve sadece tek kullanıcılı, geçici ortamlarda mantıklıdır. Düzeltme 2.30.3 ile 2.34.2 arasındaki bakım sürümlerine de taşındı.
Önceki yazıda kullandığımız SSH ile imzalama da bu dönemin işi: gpg.format ssh 2.34'te geldi. Aynı dönemde 2.37'deki transfer.credentialsInUrl ayarı, içinde parola yazan URL'lerle fetch ya da push yapıldığında uyarı vermeyi veya durmayı sağladı.
Sık Düşülen Tuzaklar
İlki, eski bir rehberdeki git checkout dosya'yı "branch değiştiriyorum" sanmak. Working tree'deki değişikliğini onay sormadan siler. Dosyaları geri yüklemek için restore, branch için switch kullanırsan bu karışıklık ortadan kalkar.
İkincisi pull hatasını git config --global pull.rebase false ile susturup ne seçtiğini bilmemek. Hint'teki üç seçeneğin üçü de geçerli; önemli olan ekibin aynı seçimi yapması.
Üçüncüsü safe.directory hatasını safe.directory '*' ile kalıcı olarak kapatmak. Kişisel dizüstünde zararsız görünür ama hatanın var olma sebebi tam olarak paylaşılan makineler.
Dördüncüsü --force-with-lease'i tek başına yeterince güvenli sanmak. Arka planda fetch yapan bir IDE kullanıyorsan --force-if-includes olmadan lease'in koruması yok gibidir.
Beşincisi, bu yazıdaki komutları eski bir sunucuda ya da CI image'ında denemek. git --version 2.23'ten eskiyse switch yoktur, 2.34'ten eskiyse SSH imzası doğrulanamaz. CI'da kullandığın Git sürümünü bir kez kontrol etmek, "bende çalışıyordu" tartışmasını baştan bitirir.
Küçük Bir Deneme
★☆☆ Eski bir rehberden kalma şu üç komutu yeni karşılıklarıyla yaz ve dene: git checkout -b hotfix, git checkout -- README.md, git reset HEAD README.md.
★★☆ Bir conflict'i çözerken çözümün dışında, conflict'le ilgisi olmayan bir satırı da değiştirip merge commit'ini oluştur. git log -1 --remerge-diff bu ek değişikliği gösteriyor mu? git show ile karşılaştır.
★★★ Üç katlı bir branch yığını kur (part1, part2, part3), main'e yeni bir commit ekle ve önce --update-refs olmadan, sonra onunla rebase et. Her seferinde git log --oneline --graph --all --decorate çıktısını karşılaştır ve eski commit'lerin hâlâ hangi branch'ler tarafından gösterildiğini bul.
Bir sonraki yazıda Git 2.40–2.49 dönemine geçiyoruz: SHA-256 repo'ları, reftable, git replay, git config'in alt komutları ve git refs migrate. Yani Git 3.0'ın temellerinin atıldığı yıllar.