Geçmişte Arama: git grep, git log -S/-G/-L, blame -w -C, ignoreRevsFile ve "Bu Satır Ne Zaman, Neden Değişti?"
Bir satırı kimin, ne zaman, neden değiştirdiğini bulmak için blame'in ilk cevabı yetmez: git log -S metnin ortaya çıktığı commit'i, -G o satırların her değişimini, -L tek bir method'un geçmişini verir. Bookstore'daki kurumsal indirim hatasını git grep, pickaxe, blame -w -C ve .git-blame-ignore-revs ile Git 2.56'da izliyoruz.
Sürüm notu: Bu yazıdaki komutlar Git 2.56.1 ile denenmiştir; blame.ignoreRevsFile Git 2.23'ten beri vardır. GitHub'ın .git-blame-ignore-revs desteği 15 Kasım 2026 itibarıyla GitHub Docs'tan alınmıştır.
Önce Tahmin Et
Müşteri hizmetlerinden Gül bir kayıt açtı: okul satın alma sorumlusu Nevin Hanım 20 kitaplık bir sipariş vermiş ama %10 kurumsal indirim uygulanmamış. Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) kural belli: kurumsal siparişte en az 20 adet. Emre koda bakıyor ve satırı hemen buluyor:
$ git grep -n 'quantity >'
src/Bookstore.Ordering/Pricing/DiscountCalculator.cs:7: if (isInstitutional && quantity > InstitutionalMinimumQuantity)
>= olması gereken yerde > var. Bu satırı kim, ne zaman, neden değiştirdi? İlk akla gelen git blame:
$ git blame -L '/quantity >/,+1' src/Bookstore.Ordering/Pricing/DiscountCalculator.cs
90981d9 (Ayse 2026-11-18 10:09:00 +0300 7) if (isInstitutional && quantity > InstitutionalMinimumQuantity)
Blame'e göre Ayşe. Sence hatayı Ayşe mi yaptı?
$ git log --oneline
de9bde6 Add campaign calculator
90981d9 Apply dotnet format (tabs)
fb07000 Move pricing code to Pricing folder
0daca3f Move constants below ApplyDiscount
ed84177 Round institutional discount to kurus
ef1d6c7 Add order total rounding
156a0af Add institutional discount
90981d9 bir formatlama commit'i: bütün dosyanın girintisini değiştirdi. Blame satırın son dokunulduğu commit'i gösterir, değişikliğin anlamlı olup olmadığına bakmaz. Bu yazıda geçmişte doğru soruyu sormanın araçlarına bakıyoruz: bugünkü kodda arama, değişiklik arama, tek bir method'un geçmişi ve formatlama commit'lerini görmezden gelen bir blame.
git grep: Bugün Nerede, Dün Neredeydi?
git grep, sıradan grep'in Git'i bilen hâli. Yalnız takip edilen dosyalarda arar (bin/, obj/, node_modules/ gibi ignore edilmiş klasörler hiç taranmaz) ve dosya listesini index'ten aldığı için büyük repo'larda hızlıdır. İki özelliği onu asıl kullanışlı yapıyor. Birincisi -p, eşleşmenin hangi method'un içinde olduğunu söyler:
$ git grep -n -p 'RoundToKurus' -- '*.cs'
src/Bookstore.Ordering/OrderTotals.cs=3=public static class OrderTotals
src/Bookstore.Ordering/OrderTotals.cs:5: public static decimal RoundToKurus(decimal amount)
src/Bookstore.Ordering/Pricing/CampaignCalculator.cs=7= public decimal ApplyCampaign(decimal subtotal, bool isCampaignActive)
src/Bookstore.Ordering/Pricing/CampaignCalculator.cs:12: return OrderTotals.RoundToKurus(discounted);
src/Bookstore.Ordering/Pricing/DiscountCalculator.cs=5= public decimal ApplyDiscount(decimal subtotal, int quantity, bool isInstitutional)
src/Bookstore.Ordering/Pricing/DiscountCalculator.cs:10: return OrderTotals.RoundToKurus(discounted);
= ile biten satırlar bağlam: eşleşmenin üstündeki method imzası. Git bunu bulmak için dile göre bir "function name" regex'i kullanır ve C# için bunun devreye girmesi repo'daki .gitattributes'taki tek satıra bağlı: *.cs diff=csharp. Bu satır olmadan Git varsayılan kuralı kullanır, yani sütun başındaki ilk harfle başlayan satırı alır ve C#'ta çoğu zaman yanlış satırı gösterir. Aynı ayar birazdan git log -L'nin method'u bulmasını da sağlayacak. Bookstore'un .gitattributes'ında bu satır ilk commit'ten beri duruyor; diff= için hazır sürücüler arasında csharp'ın yanında java, python, rust ve html de var.
İkincisi, git grep checkout etmeden geçmişteki bir commit'te ya da tag'de arayabilir:
$ git grep -n 'quantity' v1.0.0 -- '*.cs'
v1.0.0:src/Bookstore.Ordering/DiscountCalculator.cs:8: public decimal ApplyDiscount(decimal subtotal, int quantity, bool isInstitutional)
v1.0.0:src/Bookstore.Ordering/DiscountCalculator.cs:10: if (isInstitutional && quantity >= InstitutionalMinimumQuantity)
$ git grep -n 'quantity >=' $(git rev-list --all --abbrev-commit) -- '*.cs'
ef1d6c7:src/Bookstore.Ordering/DiscountCalculator.cs:10: if (isInstitutional && quantity >= InstitutionalMinimumQuantity)
156a0af:src/Bookstore.Ordering/DiscountCalculator.cs:10: if (isInstitutional && quantity >= InstitutionalMinimumQuantity)
v1.0.0'da kural doğruymuş. İkinci komut bütün commit'lerde arıyor: doğru hâli yalnız ilk iki commit'te var. Hata ikisinden sonra gelmiş. Bu yöntem küçük bir repo'da işe yarar ama her commit'i tek tek taradığı için büyük geçmişte yavaştır. Değişikliği aramanın doğru aracı pickaxe.
Pickaxe: -S ile -G Farkı
$ git log --oneline -S'quantity >='
ed84177 Round institutional discount to kurus
156a0af Add institutional discount
-S<string>, verilen metnin geçme sayısını değiştiren commit'leri listeler. quantity >= ilk commit'te eklendi, ed84177'de kayboldu; aradığımız commit bu. Şimdi bir de sabitin adına bakalım ve -G ile karşılaştıralım:
$ git log --oneline -S'InstitutionalMinimumQuantity'
156a0af Add institutional discount
$ git log --oneline -G'InstitutionalMinimumQuantity'
90981d9 Apply dotnet format (tabs)
0daca3f Move constants below ApplyDiscount
ed84177 Round institutional discount to kurus
156a0af Add institutional discount
-S yalnızca sabitin tanımlandığı commit'i buldu. 0daca3f'te sabit method'un altına taşındı, ed84177'de koşul satırı değişti, 90981d9'da girinti değişti; ama hiçbirinde dosyadaki InstitutionalMinimumQuantity sayısı değişmedi, o yüzden -S onları görmez. -G<regex> ise eklenen ya da silinen satırlardan herhangi biri regex'e uyan her commit'i listeler. Kural basit: "bu metin ne zaman ortaya çıktı ya da kayboldu?" için -S, "bu metnin geçtiği satırlar ne zaman değişti?" için -G. -S regex'i de kabul eder (--pickaxe-regex), ama sayma mantığı aynı kalır.
-S ve -G'ye -p eklersen her commit'in diff'ini, --pickaxe-all eklersen o commit'teki diğer dosyaların değişikliklerini de görürsün.
Tek Bir Method'un Geçmişi: git log -L
Pickaxe metne bakar. Bazen sorunun cevabı "bu method'a ne oldu?" olur. git log -L bir satır aralığının ya da bir method'un geçmişini, yalnızca o kısma ait diff'lerle gösterir:
$ git log -L :ApplyDiscount:src/Bookstore.Ordering/Pricing/DiscountCalculator.cs --format='%h %an %s'
90981d9 Ayse Apply dotnet format (tabs)
...
0daca3f Ayse Move constants below ApplyDiscount
...
ed84177 Emre Round institutional discount to kurus
diff --git a/src/Bookstore.Ordering/DiscountCalculator.cs b/src/Bookstore.Ordering/DiscountCalculator.cs
index ffc2f5d..398b31a 100644
--- a/src/Bookstore.Ordering/DiscountCalculator.cs
+++ b/src/Bookstore.Ordering/DiscountCalculator.cs
@@ -8,10 +8,11 @@
public decimal ApplyDiscount(decimal subtotal, int quantity, bool isInstitutional)
{
- if (isInstitutional && quantity >= InstitutionalMinimumQuantity)
+ if (isInstitutional && quantity > InstitutionalMinimumQuantity)
{
- return subtotal - subtotal * InstitutionalRate;
+ var discounted = subtotal - subtotal * InstitutionalRate;
+ return OrderTotals.RoundToKurus(discounted);
}
return subtotal;
}
}
156a0af Emre Add institutional discount
...
:ApplyDiscount: sözdizimi, .gitattributes'taki diff=csharp sayesinde method'un başını ve sonunu buluyor; -L 7,14:dosya biçiminde satır numarası da verebilirsin. Çıktıda iki şey göze çarpıyor. Birincisi -L, dosyanın Pricing/ klasörüne taşınmasını takip etti: ed84177'nin diff'i eski yolu gösteriyor. İkincisi aradığımız değişiklik orada: Emre yuvarlamayı eklerken >='i > yapmış. Muhtemelen bilerek değil.
Dosyanın bütün geçmişinde taşınmayı takip etmek için --follow gerekir; git log'a yolu verdiğinde Git varsayılan olarak yalnız o yolu izler:
$ git log --oneline -- src/Bookstore.Ordering/DiscountCalculator.cs
fb07000 Move pricing code to Pricing folder
0daca3f Move constants below ApplyDiscount
ed84177 Round institutional discount to kurus
156a0af Add institutional discount
$ git log --oneline --follow -- src/Bookstore.Ordering/Pricing/DiscountCalculator.cs
90981d9 Apply dotnet format (tabs)
fb07000 Move pricing code to Pricing folder
0daca3f Move constants below ApplyDiscount
ed84177 Round institutional discount to kurus
156a0af Add institutional discount
--follow tek bir dosyayla çalışır ve rename'i içerik benzerliğinden tahmin eder; taşıma sırasında dosyanın büyük kısmı da değiştiyse takip kopabilir. Bu yüzden taşımayı ve içerik değişikliğini ayrı commit'lerde yapmak iyi bir alışkanlık.
blame'i Doğru Okumak: -w, -M, -C
Açılıştaki blame'e dönelim ve whitespace değişikliklerini görmezden gelmesini isteyelim:
$ git blame -w -L '/quantity >/,+1' src/Bookstore.Ordering/Pricing/DiscountCalculator.cs
ed84177 src/Bookstore.Ordering/DiscountCalculator.cs (Emre 2026-11-18 10:05:00 +0300 7) if (isInstitutional && quantity > InstitutionalMinimumQuantity)
-w ile blame formatlama commit'inin içinden geçip satırı gerçekten değiştiren commit'e ulaştı ve satırın o zamanki dosya yolunu da yazdı. Bütün method'a bakınca fark daha net:
$ git blame -s -L :ApplyDiscount src/Bookstore.Ordering/Pricing/DiscountCalculator.cs
90981d9b src/Bookstore.Ordering/Pricing/DiscountCalculator.cs 5) public decimal ApplyDiscount(decimal subtotal, int quantity, bool isInstitutional)
90981d9b src/Bookstore.Ordering/Pricing/DiscountCalculator.cs 6) {
90981d9b src/Bookstore.Ordering/Pricing/DiscountCalculator.cs 7) if (isInstitutional && quantity > InstitutionalMinimumQuantity)
...
$ git blame -w -s -L :ApplyDiscount src/Bookstore.Ordering/Pricing/DiscountCalculator.cs
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 5) public decimal ApplyDiscount(decimal subtotal, int quantity, bool isInstitutional)
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 6) {
ed84177d src/Bookstore.Ordering/DiscountCalculator.cs 7) if (isInstitutional && quantity > InstitutionalMinimumQuantity)
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 8) {
ed84177d src/Bookstore.Ordering/DiscountCalculator.cs 9) var discounted = subtotal - subtotal * InstitutionalRate;
ed84177d src/Bookstore.Ordering/DiscountCalculator.cs 10) return OrderTotals.RoundToKurus(discounted);
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 11) }
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 12)
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 13) return subtotal;
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 14) }
0daca3f0 src/Bookstore.Ordering/DiscountCalculator.cs 15)
0daca3f0 src/Bookstore.Ordering/DiscountCalculator.cs 16) public const int InstitutionalMinimumQuantity = 20;
0daca3f0 src/Bookstore.Ordering/DiscountCalculator.cs 17) public const decimal InstitutionalRate = 0.10m;
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 18) }
-s author ve tarihi gizleyerek çıktıyı kısaltıyor; ^ ile başlayan hash, geçmişin ilk commit'i demek. Sabitler hâlâ 0daca3f'e, yani taşıma commit'ine yazılıyor. -M aynı dosya içinde taşınan satırları, -C ise aynı commit'te değişen başka dosyalardan gelen satırları arar. -C iki kez verilirse dosyanın oluşturulduğu commit'teki bütün dosyalara, üç kez verilirse bütün commit'lere bakar. Kampanya hesabı, kurumsal indirimin kopyalanmasıyla yazılmış:
$ git blame -s src/Bookstore.Ordering/Pricing/CampaignCalculator.cs | sed -n '7,14p'
de9bde6 7) public decimal ApplyCampaign(decimal subtotal, bool isCampaignActive)
de9bde6 8) {
de9bde6 9) if (isCampaignActive)
de9bde6 10) {
de9bde6 11) var discounted = subtotal - subtotal * InstitutionalRate;
de9bde6 12) return OrderTotals.RoundToKurus(discounted);
de9bde6 13) }
de9bde6 14)
$ git blame -s -C -C src/Bookstore.Ordering/Pricing/CampaignCalculator.cs | sed -n '7,14p'
de9bde69 src/Bookstore.Ordering/Pricing/CampaignCalculator.cs 7) public decimal ApplyCampaign(decimal subtotal, bool isCampaignActive)
de9bde69 src/Bookstore.Ordering/Pricing/CampaignCalculator.cs 8) {
de9bde69 src/Bookstore.Ordering/Pricing/CampaignCalculator.cs 9) if (isCampaignActive)
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 10) {
ed84177d src/Bookstore.Ordering/DiscountCalculator.cs 11) var discounted = subtotal - subtotal * InstitutionalRate;
ed84177d src/Bookstore.Ordering/DiscountCalculator.cs 12) return OrderTotals.RoundToKurus(discounted);
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 13) }
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 14)
-C -C satırların DiscountCalculator.cs'ten geldiğini buldu. (Bu iki çıktıyı aşağıdaki .git-blame-ignore-revs ayarı açıkken aldık; o yüzden kopyalama tespiti formatlama commit'inde durmadan ilk commit'lere kadar indi.) Pratik sonucu şu: ed84177'deki hata kampanya hesabına da kopyalanmış olabilir ve birinde düzeltilen şey diğerinde unutulabilir.
Formatlama Commit'lerini Atlamak: .git-blame-ignore-revs
Her blame'de -w yazmak yerine, anlamsız commit'leri kalıcı olarak atlamanın bir yolu var. Bu yalnız whitespace değil, dotnet format ya da Prettier gibi bir aracın bütün repo'yu yeniden yazdığı her commit için geçerli:
$ git log -1 --format='%H %s' ':/Apply dotnet format'
90981d9be0b9d1f21c408d153acb677355df0f55 Apply dotnet format (tabs)
$ printf '# dotnet format: tabs\n%s\n' $(git rev-parse ':/Apply dotnet format') > .git-blame-ignore-revs
$ cat .git-blame-ignore-revs
# dotnet format: tabs
90981d9be0b9d1f21c408d153acb677355df0f55
$ git config set blame.ignoreRevsFile .git-blame-ignore-revs
$ git blame -s -L :ApplyDiscount src/Bookstore.Ordering/Pricing/DiscountCalculator.cs
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 5) public decimal ApplyDiscount(decimal subtotal, int quantity, bool isInstitutional)
^156a0af src/Bookstore.Ordering/DiscountCalculator.cs 6) {
ed84177d src/Bookstore.Ordering/DiscountCalculator.cs 7) if (isInstitutional && quantity > InstitutionalMinimumQuantity)
...
$ git -c blame.markIgnoredLines=true blame -s -L '/var discounted/,+1' src/Bookstore.Ordering/Pricing/DiscountCalculator.cs
?ed84177 src/Bookstore.Ordering/DiscountCalculator.cs 9) var discounted = subtotal - subtotal * InstitutionalRate;
:/<metin>, mesajı o metni içeren en yeni commit'i bulur. Dosyada tam hash'ler olmalı, her satıra bir tane; # ile yorum yazabilirsin ve yazmalısın, çünkü altı ay sonra o hash'in neden orada olduğunu kimse hatırlamaz. blame.ignoreRevsFile ayarı yerel config'e yazılır, yani her geliştiricinin bir kez çalıştırması gerekir; komutu README'ye koymak işe yarar. Atlanan bir commit'in değiştirdiği satır, Git'in bir önceki commit'te eşleştirebildiği satıra yazılır; blame.markIgnoredLines açıksa bu satırların başına ? koyar, eşleştiremediklerini de blame.markUnblamableLines ile * olarak işaretleyebilirsin.
GitHub tarafında ayar gerekmiyor. Repo'nun kökünde .git-blame-ignore-revs adında bir dosya varsa GitHub'ın blame görünümü onu kendiliğinden okur ve sayfanın üstünde atlanan revizyonlar olduğunu belirten bir uyarı gösterir. Bu yüzden dosyanın adı ve yeri tam olarak bu olmalı. Formatlama commit'ini attıktan sonra hash'ini bu dosyaya ekleyen ayrı bir commit atmak, toplu formatlamayı ekip için zararsız hâle getirir.
"Neden?" Sorusu ve Sonrası
"Ne zaman" ve "kim" sorusunun cevabını bulduk. "Neden" sorusunun cevabı ise commit mesajında:
$ git show -s --format='%h %an %ad%n%n%B' --date=short ':/Round institutional'
ed84177 Emre 2026-11-18
Round institutional discount to kurus
Totals were shown with four decimals on the institutional order page.
Mesaj, değişikliğin amacının yuvarlama olduğunu söylüyor; koşul değişikliğinden hiç bahsetmiyor. Yani > bilinçli bir karar değil, yan etki. Düzeltme >='e dönmek ve tam 20 adetlik bir test eklemek. Commit'in içi yazısında iyi bir commit mesajının gövdesinin neden önemli olduğunu konuşmuştuk; bu, o gövdenin işe yaradığı an.
Arama her zaman bu kadar kolay olmaz. Elinde aranacak bir metin yoksa ve yalnızca "eskiden çalışıyordu" diyebiliyorsan, doğru araç git bisect: iyi ve kötü bilinen iki commit arasında ikili arama yapar, bisect run ile bir test script'ine bağlanınca kendi kendine ilerler. Ayrıntısı bisect ve worktree yazısında, hata ayıklamadaki yeri Debugging ve Problem Çözme serisinin problemi daraltma yazısında.
Sık Düşülen Tuzaklar
İlki -S'nin her değişikliği yakalayacağını sanmak. -S metnin geçme sayısına bakar: satırı taşıyan, yeniden girintileyen ya da aynı satırın başka yerini değiştiren commit'ler listede çıkmaz. InstitutionalMinimumQuantity örneğindeki gibi. Bir satırın her değişimini istiyorsan -G ya da -L.
İkincisi blame'in ilk satırına güvenmek. Toplu bir format commit'i, dosyadaki her satırın "son değiştireni" olur ve blame'i anlamsızlaştırır. Repo'ya formatlayıcı eklemeden önce .git-blame-ignore-revs'i hazırla; format commit'ini tek başına at, başka değişiklikle karıştırma.
Üçüncüsü .gitattributes'ta diff=csharp olmadan -L :Method: kullanmak. Git method'un başını varsayılan kurala göre arar ve beklemediğin bir aralığı gösterebilir; çıktı hata vermediği için fark etmesi zordur.
Dördüncüsü git grep'in ignore edilmiş dosyalarda aramadığını unutmak. Henüz add'lenmemiş yeni bir dosyada arıyorsan --untracked, build çıktısı gibi ignore edilmiş dosyalarda arıyorsan --untracked --no-exclude-standard ya da düz grep gerekir.
Küçük Bir Deneme
★☆☆ Kendi repo'nda bir sabitin ya da config anahtarının adını seç, git log -S ve git log -G ile ayrı ayrı ara ve iki listenin farkını açıkla.
★★☆ Bir C# ya da TypeScript dosyasında bir method seç, .gitattributes'a uygun diff= satırını ekle ve git log -L :MethodName:dosya çıktısını bu satır olmadan ve onunla karşılaştır.
★★★ Bir deneme repo'sunda bütün dosyaların girintisini değiştiren bir commit at. Önce düz blame, sonra -w, sonra .git-blame-ignore-revs ile aynı satıra bak. Son olarak bir method'u başka bir dosyaya kopyala ve -C, -C -C, -C -C -C farkını gözle.
Bu yazıda geçmişi okuduk. Seriyi GitHub tarafıyla kapatıyoruz: bir sonraki ve son yazıda GitHub CLI ile pull request açmayı, CI'ı terminalden izlemeyi, gh api ile GitHub API'sine erişmeyi ve extension'ları göreceğiz.