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

Git 2.40–2.49: reftable, SHA-256, git replay ve Yeni git config

Mert Özen 6 Kas 2026 9 dk 1 görüntülenme
Git 2.40–2.49: reftable, SHA-256, git replay ve Yeni git config

Git 3.0'ın temellerinin atıldığı 2.40–2.49 dönemini gerçek çıktılarla görüyoruz: reftable ref deposu ve git refs migrate, SHA-256 repo'larının bugünkü durumu, git config'in get/set/unset/list alt komutları, worktree add --orphan, git replay, safe.directory prefix'i, git backfill ve Rust'ın ilk adımı.

Sürüm notu: Bu yazı Git 2.40 ile 2.49 arasında gelen değişiklikleri anlatır; komutlar Git 2.54 ile denenmiştir.

Önce Tahmin Et

Branch yazısında .git/HEAD dosyasına bakmış ve içinde ref: refs/heads/main yazdığını görmüştük. Şimdi aynı bookstore içeriğiyle yeni bir repo açıyoruz, tek fark git init'e verdiğimiz bir seçenek:

$ git init -q --ref-format=reftable bookstore-reftable
$ git rev-parse --show-ref-format
reftable

İçine bir commit attık ve bir feature/search branch'i açtık. Sence cat .git/HEAD ne yazar? .git/refs/heads/main diye bir dosya var mı?

$ ls .git/reftable
0x000000000001-0x000000000003-360f6241.ref
tables.list
$ cat .git/HEAD
ref: refs/heads/.invalid
$ cat .git/refs/heads
this repository uses the reftable format
$ git branch
  feature/search
* main

HEAD bilerek geçersiz bir yeri gösteriyor ve refs/heads bir klasör bile değil, eski Git'lere "bu repo'yu sen anlayamazsın" diyen bir dosya. Branch'lerin gerçekte nerede durduğunu ise .git/reftable altındaki ikili dosya biliyor. Yine de git branch her zamanki gibi çalışıyor.

Bu yazı Git 2.40–2.49 dönemini anlatıyor. Bu dönemi ilginç yapan, Git 3.0'ın temellerinin burada atılması: ref'ler için yeni bir depolama biçimi, SHA-256 desteğinin olgunlaşması, git config'in yeniden düzenlenmesi ve Rust'ın ilk kez koda girmesi.

Yeni Bir Ref Deposu: reftable (2.45)

Klasik "files" biçiminde her branch ve tag .git/refs/ altında ayrı bir dosyadır; sayı artınca bunlar packed-refs adında tek bir metin dosyasına toplanır. Bu basit tasarımın bilinen sorunları var. Büyük-küçük harf duyarsız dosya sistemlerinde (macOS ve Windows'ta varsayılan) Feature ve feature adlı iki branch çakışır. Yüz binlerce ref'i olan repo'larda tek bir ref'i silmek bile koca packed-refs dosyasını yeniden yazmak demektir. Birden fazla ref'i aynı anda güncellemek de tam anlamıyla atomik değildir.

reftable aslında JGit için tasarlanmış ikili bir biçim. Ref'leri sıralı ve sıkıştırılmış tablolarda tutar, yeni güncellemeleri yeni bir tablo olarak ekler ve arada bunları birleştirir. Reflog'lar da aynı tablolara girer. Git 2.45 ile git init --ref-format=reftable geldi. Bunun için bir ayar da var: init.defaultRefFormat reftable dersen yeni repo'ların hepsi reftable ile açılır.

Dosya isimlerindeki sayılar her çalıştırmada aynı olmayabilir; asıl önemli olan, artık ref'lerin içini cat ile okuyamaman. Bunun yerine Git'e sorarsın: git branch, git for-each-ref ya da 2.52'deki git refs list. Ref dosyalarını doğrudan okuyan eski script'lerin varsa, reftable'a geçmeden önce onları düzeltmen gerekir.

Var Olan Repo'yu Taşımak: git refs migrate (2.46)

Yeni repo açmak kolay; peki yıllardır kullandığın bir repo? 2.46 bunun için git refs migrate'i getirdi. Önce kuru bir deneme yapıp sonra gerçek dönüşümü yapalım:

$ git rev-parse --show-ref-format
files
$ git refs migrate --ref-format=reftable --dry-run
Finished dry-run migration of refs, the result can be found at '.git/ref_migration.q51NvX'
$ git refs migrate --ref-format=reftable
$ git rev-parse --show-ref-format
reftable
$ git refs list
b26b14b302b91e7a187d7e8a313124df92c6c4b3 commit	refs/heads/feature/search
b26b14b302b91e7a187d7e8a313124df92c6c4b3 commit	refs/heads/main
b26b14b302b91e7a187d7e8a313124df92c6c4b3 commit	refs/tags/v0.1.0
$ git reflog -2
b26b14b HEAD@{0}: commit: Add cart total
bfffdfa HEAD@{1}: commit (initial): Add bookstore skeleton

Branch'ler, tag ve reflog yerinde. Reflog'un taşınması sonradan geldi: 2.46'daki ilk sürüm reflog'ları taşımıyordu, 2.48 taşımaya başladı, 2.49 ise istemeyenler için --no-reflog seçeneğini ekledi. Dönüşüm sadece yerel repo'yu etkiler; remote'a, clone'lara ya da GitHub'a hiçbir şey olmaz. Ref biçimi her makinenin kendi meselesidir, push ve fetch sırasında aynı protokol konuşulur.

Dikkat etmen gereken tek yer araçlar. JGit, libgit2 ya da başka bir Git kütüphanesi kullanan bir IDE veya CI aracı reftable'ı tanımıyorsa repo'nu açamaz. Git 3.0'ın bu varsayılanı değiştirme planı da tam bu yüzden "ekosistem hazır olduğunda" koşuluna bağlı.

SHA-256 Repo'ları: Durum Ne?

Git'in object'leri adlandırmak için kullandığı SHA-1'e karşı pratik bir çakışma saldırısı 2017'de gösterildi. Git bunun üzerine hem SHA-1'in sertleştirilmiş bir sürümüne geçti hem de SHA-256'ya hazırlanmaya başladı. SHA-256 repo desteği 2.29'da deneysel olarak belgelendi, 2.42'de ise o sert "experimental" uyarısının tonu düşürüldü:

$ git init -q --object-format=sha256 bookstore-sha256
$ git commit -m "Add README"
[main (root-commit) d5f0e71] Add README
 1 file changed, 3 insertions(+)
 create mode 100644 README.md
$ git rev-parse HEAD
d5f0e71ed8a1737eca1841c6e357e472406eeca7bfbe903904a6af89a36c3cf9
$ git rev-parse --show-object-format
sha256
$ git log --oneline
d5f0e71 Add README

Kısa hash yine yedi karakter, tam hash ise 40 yerine 64 karakter. Günlük kullanımda fark bu kadar. Asıl sorun başka yerde: SHA-1 ve SHA-256 repo'ları arasında hâlâ birlikte çalışma yok. Bir SHA-256 repo'yu SHA-1 bir remote'a push edemezsin, ikisi arasında fetch de yapamazsın. Barındırma tarafında LWN'in Eylül 2026 tarihli yazısına göre GitLab ve Forgejo SHA-256 repo'larını destekliyor, GitHub'ın durumu ise belirsiz. Yani bugün yerelde deneyebilirsin, ama ekibin repo'sunu SHA-256'ya taşımak için daha erken.

git config'in Alt Komutları (2.46)

git config yıllarca davranışı seçeneklerle değişen tek bir komuttu: --get, --get-all, --get-regexp, --unset, --list. Hiçbir seçenek vermezsen ne olacağı argüman sayısına bağlıydı. 2.46 bunu alt komutlara çevirdi ve eski seçenekleri deprecated ilan etti. İkisi de bugün çalışıyor:

$ git config --get init.defaultBranch
main
$ git config --get-regexp "^core\.(bare|filemode)"
core.filemode true
core.bare false
$ git config get init.defaultBranch
main
$ git config get --all --show-names --regexp "^core\.(bare|filemode)"
core.filemode true
core.bare false

Okumak get, yazmak set, silmek unset, listelemek list. Çok değerli bir ayara yeni değer eklemek için eskiden --add vardı, şimdi set --append:

$ git config set pull.rebase true
$ git config set --global --append safe.directory /srv/git/bookstore
$ git config set --global --append safe.directory /srv/git/tools
$ git config get --global --all safe.directory
/srv/git/bookstore
/srv/git/tools
$ git config unset --global --all safe.directory
$ git config list --show-scope | tail -3
local	core.precomposeunicode=true
local	extensions.refstorage=reftable
local	pull.rebase=true
$ git config get --show-origin --show-scope pull.rebase
local	file:.git/config	true

Listede extensions.refstorage=reftable satırına dikkat et: biraz önce taşıdığımız repo'nun ref biçimi bu ayarda yazıyor. Bu seride ikinci yazıdan beri yeni biçimi kullanıyoruz. Eski biçimi 2.46'dan eski bir Git'in çalışacağı script'lerde tutmak mantıklı; yeni yazdığın her şeyde alt komutları tercih et.

Worktree ve Replay: --orphan (2.42), git replay (2.44)

Proje sitesi gibi geçmişi ana koddan tamamen ayrı bir branch'i ayrı bir klasörde açmak istiyorsun. Eskiden önce detached bir worktree açar, içine girip git switch --orphan ile yeni bir geçmiş başlatırdın. 2.42'den beri tek komut:

$ git worktree add --orphan -b gh-pages ../bookstore-pages
Preparing worktree (new branch 'gh-pages')
$ cd ../bookstore-pages
$ git status
On branch gh-pages

No commits yet

nothing to commit (create/copy files and use "git add" to track)
$ echo '<h1>Bookstore</h1>' > index.html
$ git add index.html
$ git commit -m "Start project site"
[gh-pages (root-commit) d13ad76] Start project site
 1 file changed, 1 insertion(+)
 create mode 100644 index.html
$ git log --oneline --all --decorate
d13ad76 (HEAD -> gh-pages) Start project site
4734313 (main) Add cart total
14693d0 Add bookstore skeleton

2.44'te gelen git replay ise rebase'in working tree'ye hiç dokunmayan bir kuzeni. Commit'leri yeni bir tabanın üstüne yeniden oluşturur ama checkout yapmaz, index'e dokunmaz. Bu yüzden asıl hedefi sunucular: geçmişi working tree olmadan yeniden yazması gereken barındırma platformları. Hâlâ experimental:

$ git log --oneline --graph --all --decorate
* 7318512 (HEAD -> main) Add license note
| * 8699687 (feature/search) Note fuzzy search
| * e5517df Add search stub
|/
* ba82313 Add cart total
* e9375b7 Add bookstore skeleton
$ git replay --ref-action=print --onto main main..feature/search
update refs/heads/feature/search 3881e4ac3201fa1c262cae263243d0688b8f6a0e 8699687aa6eed33bfac8155b1a99e7538b73d1d8
$ git replay --onto main main..feature/search
$ git log --oneline --graph --all --decorate
* 3881e4a (feature/search) Note fuzzy search
* f024b7b Add search stub
* 7318512 (HEAD -> main) Add license note
* ba82313 Add cart total
* e9375b7 Add bookstore skeleton

İlk hâliyle git replay ref'leri kendisi güncellemezdi; yukarıdaki update satırlarını basardı, sen de bunları git update-ref --stdin'e verirdin. 2.53'ten beri ref'leri kendisi atomik olarak güncelliyor; eski davranış --ref-action=print ile duruyor. Bu arada HEAD hiç kıpırdamadı, hâlâ main'deyiz. Conflict çıkarsa replay durup sana çözdürmez; hata koduyla çıkar ve hiçbir ref'i değiştirmez. Günlük işte rebase'in yerini almıyor.

safe.directory Prefix'i, git backfill ve Rust

Önceki yazıda gördüğümüz safe.directory'nin can sıkıcı bir yanı vardı: her repo'yu tek tek eklemen gerekiyordu. 2.46'dan beri sonu /* ile biten bir değer, o klasörün altındaki her şeyi kapsıyor:

$ export GIT_TEST_ASSUME_DIFFERENT_OWNER=1
$ git status -s; echo $?
fatal: detected dubious ownership in repository at '.../bookstore'
To add an exception for this directory, call:

	git config --global --add safe.directory .../bookstore
128
$ git config set --global --append safe.directory "$(dirname "$PWD")/*"
$ git status -s; echo $?
0
$ git -C ../bookstore-reftable branch --show-current
main

2.49'daki git backfill kısmi clone kullananlar için. --filter=blob:none ile clone'ladığında commit ve tree'ler gelir, dosya içerikleri ise ihtiyaç oldukça tek tek indirilir. git blame gibi geçmişin tamamına bakan bir komut bu yüzden yüzlerce küçük istek atabilir. backfill eksik blob'ları toplu olarak indirir:

$ git clone -q --filter=blob:none "file://$PWD/bookstore.git" partial
$ cd partial
$ git rev-list --objects --all --missing=print | grep -c "^?"
3
$ git backfill
$ git rev-list --objects --all --missing=print | grep -c "^?"
0

Burada remote yerel bir bare repo olduğu için uploadpack.allowFilter ayarını sunucu tarafında kendimiz açtık; GitHub ve GitLab kısmi clone'u zaten destekliyor.

Son olarak 2.49'da sessiz ama tarihi bir adım atıldı: Git'in kaynak koduna Rust ile kullanılabilecek bir arayüz eklendi. O an için tamamen isteğe bağlıydı ve kimsenin derlemesini etkilemiyordu. Sonraki yazıda bu yolun 2.55'te varsayılan açık Rust'a, Git 3.0'da ise zorunlu Rust'a nasıl uzandığını göreceğiz.

Sık Düşülen Tuzaklar

İlki .git/refs/heads/ altındaki dosyaları okuyan script'ler. Reftable'a geçen bir repo'da bu dosyalar yok ve script'in sessizce boş sonuç döndürür. Ref okumak için her zaman Git'in kendisine sor: git rev-parse, git for-each-ref, git refs list.

İkincisi reftable'a geçip IDE'yi unutmak. Komut satırında her şey çalışırken IDE'nin Git paneli repo'yu açamayabilir. Geçmeden önce kullandığın araçların reftable desteğini kontrol et; geri dönmen gerekirse git refs migrate --ref-format=files var.

Üçüncüsü SHA-256 repo'yu ekip repo'su olarak açmak. Yerelde her şey çalışır, ilk push'ta SHA-1 bir remote'la karşılaşınca duvara toslarsın. SHA-256 bugün deneme ve hazırlık için.

Dördüncüsü deprecated git config biçiminden kaçarken eski makineleri unutmak. git config get 2.46'dan eski bir Git'te çalışmaz; CI image'ın eskiyse script'in orada kırılır.

Küçük Bir Deneme

★☆☆ Kendi makinende denemelik bir repo'yu git refs migrate --ref-format=reftable ile taşı, sonra git config list --show-scope çıktısında hangi satırın eklendiğini bul. Ardından --ref-format=files ile geri taşı.

★★☆ Sık kullandığın üç git config komutunu (--get, --unset, --list gibi) yeni alt komut biçimiyle yeniden yaz. Kendi alias'larında ya da dotfile'larında eski biçim geçiyor mu?

★★★ git replay ile bir branch'i main'in üstüne taşımayı dene, ama bu kez main'de aynı satırı değiştiren bir commit olsun. Replay ne yapıyor, rebase'ten farkı ne?

Bir sonraki yazıda Git 2.50'den bugünkü 2.56'ya geliyoruz: git history, config tabanlı ve paralel hook'lar, git last-modified, stash'i başka makineye taşımak ve git add --resolved.