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

Git 2.50–2.56: git history, Config Tabanlı Hook'lar ve git add --resolved

Mert Özen 6 Kas 2026 9 dk 2 görüntülenme
Git 2.50–2.56: git history, Config Tabanlı Hook'lar ve git add --resolved

Git 2.50'den güncel 2.56'ya gelenleri görüyoruz: git history reword ve split (2.55 fixup, 2.56 drop), config tabanlı ve paralel hook'lar, stash export/import, pull.autostash ve conflict durumu, git last-modified, git repo info, reflog drop, branch --delete-merged, add --resolved ve -h çıkış kodu değişikliği.

Sürüm notu: Bu yazı Git 2.50 ile 2.56 arasında gelen değişiklikleri anlatır; komutlar Git 2.54 ile denenmiştir. Yalnızca 2.55 ve 2.56'da olan özellikler release notes'tan aktarılmıştır, onlar için çıktı gösterilmiyor.

Önce Tahmin Et

Bookstore'da iki commit önce "license" kelimesini "licence" diye yazmışsın. Üstünde bir commit daha var ve o commit'i gösteren bir feature/later branch'i de duruyor:

$ git log --oneline
d0bd6b7 Add search and use it in cart
ab20775 Add licence note
ff3f4c5 Add cart total
9a41a96 Add bookstore skeleton
$ git history reword HEAD~1

Komut editörü açtı, sen de mesajı düzeltip kaydettin. Rebase yapmadın, todo listesi görmedin. Sence main dışında başka bir branch değişti mi? Working tree'de bir şey oldu mu?

$ git log --oneline --decorate
d89236c (HEAD -> main, feature/later) Add search and use it in cart
0318329 Add license note
ff3f4c5 Add cart total
9a41a96 Add bookstore skeleton
$ git status -s

İki commit'in hash'i değişti, main ve feature/later birlikte yeni yerlerine taşındı, working tree'ye ise hiç dokunulmadı. Bu, 2.54'te experimental olarak gelen git history. Bu yazıda Git 2.50'den bugünkü güncel sürüm 2.56'ya kadar gelenlere bakıyoruz. Yerelde 2.54 olduğu için 2.55 ve 2.56'ya özgü komutları çalıştırmadan, release notes'a dayanarak anlatacağım.

git history: Rebase'siz Geçmiş Düzeltme

Eskiden eski bir commit'in mesajını değiştirmek için interactive rebase açar, satırı reword yapar, kaydeder, sonra editör yeniden açılınca mesajı düzeltirdin. Arada bir conflict çıkarsa ya da başka branch'ler o commit'in üstünde duruyorsa iş uzardı. git history bu tür tek amaçlı düzeltmeleri doğrudan yapıyor. Man sayfasına göre rebase'ten üç farkı var: working tree'ye ve index'e dokunmadığı için bare repo'da bile çalışır, şimdilik hook çalıştırmaz ve varsayılan olarak düzeltilen commit'in üstündeki bütün branch'leri günceller.

2.54'teki ikinci alt komut split: bir commit'i iki commit'e bölmek. Son commit hem yeni bir dosya ekliyor hem de cart'ta bir satır değiştiriyor; bunları ayırmak istiyoruz:

$ git history split HEAD
diff --git a/src/cart.js b/src/cart.js
index aee2863..b4a0a05 100644
--- a/src/cart.js
+++ b/src/cart.js
@@ -1,3 +1,4 @@
 export function total(items) {
   return items.reduce((sum, item) => sum + item.price, 0);
 }
+// uses search
(1/1) Stage this hunk [y,n,q,a,d,?]? n
diff --git a/src/search.js b/src/search.js
new file mode 100644
index 0000000..2c9809a
--- /dev/null
+++ b/src/search.js
@@ -0,0 +1 @@
+export const search = (q) => q;
(1/1) Stage addition [y,n,q,a,d,?]? y
$ git log --oneline --stat -3
120cbf0 Wire search into cart
 src/cart.js | 1 +
 1 file changed, 1 insertion(+)
6a260b8 Add search module
 src/search.js | 1 +
 1 file changed, 1 insertion(+)
0318329 Add license note
 README.md | 2 ++
 1 file changed, 2 insertions(+)

Soru akışı git add -p ile aynı. "y" dediğin hunk'lar yeni bir commit'e gider ve bu commit orijinalin parent'ı olur; geri kalanlar orijinal commit'te kalır. İki commit'in mesajını da editör sorar. Bütün hunk'ları ya da hiçbirini seçmek geçersiz, çünkü o zaman commit'lerden biri boş kalırdı.

Ailenin geri kalanı sonraki sürümlerde geldi. 2.55, staging area'daki değişiklikleri eski bir commit'e katan fixup alt komutunu ekledi; bu, git commit --fixup ve rebase --autosquash ikilisinin tek adımlık hâli. 2.56 ise bir commit'i silip üstündeki commit'leri yeniden uygulayan drop'u getirdi. Geliştirme sürümünde squash da görünüyor. Sınırlar şimdilik net: merge içeren geçmişte çalışmıyor ve conflict çıkarabilecek işlemleri baştan reddediyor. Böyle durumlarda yine interactive rebase'e dönersin.

Hook'lar: Config Tabanlı (2.54) ve Paralel (2.55)

Hook'lar yazısında 2.54'ün config tabanlı hook'larını ayrıntılı denedik: hook.<name>.command ve hook.<name>.event ile bir olaya birden fazla hook bağlamak, git hook list ile görmek, hook.<name>.enabled false ile tek bir repo'da kapatmak. Eskiden aynı iş için ya hepsini tek bir script'te birleştirirdin ya da pre-commit framework, Husky gibi bir araca başvururdun.

2.55 bunun üstüne paralel çalıştırmayı ekledi. Bir hook'un paralel çalışmaya uygun olduğunu hook.<name>.parallel ile işaretliyor, aynı anda kaç tanesinin çalışacağını hook.jobs ile ayarlıyorsun; git hook run da -j seçeneği kazandı. Birbirinden bağımsız lint, format ve gizli anahtar kontrollerinin hepsini sırayla beklemek yerine aynı anda çalıştırmak, pre-commit'i hızlı tutmanın yeni bir yolu.

Stash'i Taşımak: export ve import (2.51)

Stash her zaman yerel bir şeydi; refs/stash push edilmez. Yarım kalmış işini dizüstünden masaüstüne taşımak için ya geçici bir commit atar ya da patch dosyası üretirdin. 2.51 stash'leri sıradan commit'lerden oluşan bir zincire dönüştürüp geri alabilen export ve import'u getirdi:

$ git stash list
stash@{0}: On main: Search draft
stash@{1}: On main: Coupon draft
$ git stash export --to-ref refs/stashes/ada
$ git log --oneline --first-parent refs/stashes/ada
c06de16 git stash: On main: Search draft
a98a124 git stash: On main: Coupon draft
73c9bab 

Artık ortada sıradan bir ref var ve ref'ler push ya da fetch edilebilir. Diğer makinede:

$ git fetch -q origin refs/stashes/ada:refs/stashes/ada
$ git stash import refs/stashes/ada
$ git stash list
stash@{0}: On main: Search draft
stash@{1}: On main: Coupon draft
$ git stash pop -q stash@{1}; git status -s
 M src/cart.js

Burada "diğer makine" aynı diskteki bir clone, origin de ilk repo; GitHub'da ise git push origin refs/stashes/ada ile gönderirsin. Stash'ler sıraları ve mesajlarıyla geldi. 2.52'deki stash.index ayarı da küçük ama faydalı: açık olduğunda stash pop ve stash apply her zaman --index verilmiş gibi davranır, yani stage ettiğin hâl stage edilmiş olarak geri gelir.

pull.autostash (2.51)

Commit'lemediğin değişiklik varken rebase ile pull yapmak hata verir. --autostash seçeneği uzun zamandır vardı, ayarı da rebase.autoStash ve merge.autoStash üzerinden yapılıyordu. 2.51 yalnızca pull için geçerli pull.autostash'i ekledi:

$ git pull
error: cannot pull with rebase: You have unstaged changes.
error: Please commit or stash them.
$ git pull --autostash
From ../bookstore
   ba82313..aef586f  main       -> origin/main
Updating ba82313..aef586f
Created autostash: 54d5401
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)
Applied autostash.
$ git config set pull.autostash true

Bu kolaylığın bir bedeli var. Alan bu kez src/cart.js'in sonuna bir satır ekleyip push etti, sen de aynı dosyanın sonunda çalışıyordun:

$ git pull
From ../bookstore
   aef586f..05cae60  main       -> origin/main
Updating aef586f..05cae60
Created autostash: eb26e42
Fast-forward
 src/cart.js | 1 +
 1 file changed, 1 insertion(+)
Applying autostash resulted in conflicts.
Your changes are safe in the stash.
You can run "git stash pop" or "git stash drop" at any time.
$ git status -s
UU src/cart.js
$ git stash list
stash@{0}: autostash

Pull bitti, ama stash geri uygulanırken conflict çıktı. Değişikliğin kaybolmadı, hem conflict olarak working tree'de hem de stash'te duruyor. Conflict'i çözdükten sonra stash'teki kopyayı git stash drop ile kendin silmen gerekiyor.

Repo'yu Sorgulamak: last-modified ve repo info (2.52)

GitHub'ın dosya listesindeki "son commit" sütununu hiç merak ettin mi? Her dosya için ayrı bir git log -1 -- dosya çalıştırmak büyük repo'larda yavaştır. 2.52'deki git last-modified bunu tek seferde yapar:

$ git last-modified
e48390a0e516c2b0e8d9e2993d06af0c6aae5b4f	README.md
ff3f4c51d3c44e197a4e99ff4c7f879d9e6a4f51	src
9a41a96098c29c9cc79f5213208d140d75a3cd0c	.gitignore
$ git last-modified -r
e48390a0e516c2b0e8d9e2993d06af0c6aae5b4f	README.md
ff3f4c51d3c44e197a4e99ff4c7f879d9e6a4f51	src/cart.js
9a41a96098c29c9cc79f5213208d140d75a3cd0c	.gitignore
9a41a96098c29c9cc79f5213208d140d75a3cd0c	src/books.js

Aynı sürümdeki git repo info ise script'lerin repo hakkında soru sormasının düzgün yolu. Eskiden bu bilgileri git rev-parse'ın farklı seçenekleriyle ya da config'i okuyarak toplardın:

$ git repo info layout.bare object.format references.format
layout.bare=false
object.format=sha1
references.format=files
$ git repo info --keys
layout.bare
layout.shallow
object.format
references.format
$ git repo structure
| Repository structure      | Value    |
| ------------------------- | -------- |
| * References              |          |
|   * Count                 |    1     |
|     * Branches            |    1     |
...

git repo structure ref ve object sayılarını, boyutları ve en büyük object'leri bir tablo olarak verir. Bir repo'nun neden şiştiğini anlamaya çalışırken ilk bakacağın yer burası.

Temizlik: reflog drop, --delete-merged, add --resolved

2.50 bir ref'in bütün reflog'unu silen git reflog drop'u getirdi. Eskiden bunun için git reflog expire'a süre ve ref vermen gerekirdi:

$ git reflog show experiment
e183ba5 experiment@{0}: commit: Try something
4734313 experiment@{1}: branch: Created from HEAD
$ git reflog drop experiment
$ git reflog show experiment; echo $?
0
$ git reflog exists refs/heads/experiment; echo $?
1

Branch duruyor, sadece geçmiş kaydı gitti. Reflog'un bir kurtarma ağı olduğunu hatırla; bu komutu ne yaptığını bilerek kullan.

2.56'dan iki küçük ama sevilecek komut daha. git branch --delete-merged, upstream'ine zaten merge edilmiş yerel branch'leri topluca siler; önce --dry-run ile neyi sileceğini görebilirsin. Eskiden bunun için git branch --merged | grep -v main | xargs git branch -d gibi kırılgan bir boru hattı yazılırdı. git add --resolved ise conflict çözerken yalnızca çözdüğün dosyaları stage eder ve içinde hâlâ conflict marker'ı kalmış bir dosya varsa işlemi durdurur. git add . ile yarım çözülmüş bir dosyayı yanlışlıkla commit'lemenin önüne geçer.

Script'ler ve Build İçin Değişenler

Bu dönemin bazı değişiklikleri günlük komutlardan çok script'leri ve paketleyicileri ilgilendiriyor. En sinsi olanı yardım çıktısının çıkış kodu:

$ git status -h >/dev/null; echo $?
129
$ git version
git version 2.54.0 (Apple Git-157)

2.54 dahil eski sürümlerde -h 129 ile çıkar. 2.56'dan beri -h ve --help 0 döndürüyor. "Bu alt komut var mı?" sorusunu git foo -h'ın çıkış koduyla cevaplayan bir script, sürüme göre farklı davranabilir.

2.51'de git switch ve git restore resmen experimental olmaktan çıktı; 2.54'ün man sayfasında o etiket artık yok. 2.55'te Rust, Git'in iki build sisteminde de varsayılan olarak açıldı; paketleyiciler hâlâ kapatabiliyor, ama Git 3.0'da bu seçeneğin kalkması planlanıyor. Aynı sürüm fsmonitor'u inotify ile Linux'a getirdi ve remote'tan gelen sideband mesajlarındaki terminal kontrol karakterlerini, ANSI renkleri dışında, maskelemeye başladı. 2.54'te ise git maintenance'ın varsayılan stratejisi geometric oldu; eski davranış için maintenance.strategy=gc var.

Sık Düşülen Tuzaklar

İlki git history'yi paylaşılmış commit'lerde kullanmak. Adı ne olursa olsun bu bir geçmiş yeniden yazımı; hash'ler değişir. Push ettiğin bir commit'i reword edersen yine force push gerekir, ve ekip arkadaşların yine eski commit'lerle kalır.

İkincisi git history'nin bütün branch'leri güncellediğini unutmak. Önce Tahmin Et'teki feature/later sessizce taşındı. Yalnız HEAD'in taşınmasını istiyorsan --update-refs=head var; emin değilsen önce --dry-run ile hangi ref'lerin değişeceğine bak.

Üçüncüsü pull.autostash'i açıp conflict'te paniğe kapılmak. Çıktı açıkça söylüyor: değişiklik stash'te güvende. Çözdükten sonra stash'i silmeyi unutursan listende anlamsız autostash girdileri birikir.

Dördüncüsü bu yazıdaki 2.55 ve 2.56 özelliklerini her makinede var saymak. macOS'un Xcode ile gelen Git'i, Linux dağıtımlarının paketleri ve CI image'ları çoğu zaman birkaç sürüm geriden gelir. Bu yazıyı yazdığım makinede de durum bu.

Küçük Bir Deneme

★☆☆ Kendi repo'nda mesajında yazım hatası olan bir commit bul ve önce git history reword --dry-run ile hangi ref'lerin değişeceğine bak, sonra gerçekten düzelt.

★★☆ İki stash oluştur, git stash export --to-ref ile bir ref'e aktar, ref'i yerel bir bare repo'ya push et, başka bir clone'da fetch edip git stash import ile geri al. Stash mesajları ve sırası korunuyor mu?

★★★ git history split ile bir commit'i bölerken ilk soruda "y", ikincisinde de "y" de. Git ne diyor? Neden?

Bir sonraki yazı serinin şimdilik son yazısı: Git 3.0. Neler değişecek, neler kalkacak, git checkout'a ne olacak ve bugünden nasıl hazırlanırsın.